From 61b54282110bf37827c4b1bb74d677c44dfe9455 Mon Sep 17 00:00:00 2001 From: Scott Andrews Date: Fri, 23 Jan 2009 00:03:33 +0000 Subject: [PATCH] SPR-2733 Improvement for handling checkboxes in web forms (patch included) Introduced default field prefix of '!', which can be overridden with WebDataBinder#setFieldDefaultPrefix. If a field is otherwise not present, the default value is used for the field. Field markers for the same field are ignored. --- .../web/bind/WebDataBinder.java | 69 ++++++++++++++++++- .../bind/ServletRequestDataBinderTests.java | 54 +++++++++++++++ 2 files changed, 121 insertions(+), 2 deletions(-) diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/bind/WebDataBinder.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/bind/WebDataBinder.java index ba575a95ba7..f4359bd46d5 100644 --- a/org.springframework.web.servlet/src/main/java/org/springframework/web/bind/WebDataBinder.java +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/bind/WebDataBinder.java @@ -34,14 +34,17 @@ import org.springframework.web.multipart.MultipartFile; * HTML checkboxes and select options: detecting that a field was part of * the form, but did not generate a request parameter because it was empty. * A field marker allows to detect that state and reset the corresponding - * bean property accordingly. + * bean property accordingly. Default values, for parameters that are otherwise + * not present, can specify a value for the field other then empty. * * @author Juergen Hoeller + * @author Scott Andrews * @since 1.2 * @see #registerCustomEditor * @see #setAllowedFields * @see #setRequiredFields * @see #setFieldMarkerPrefix + * @see #setFieldDefaultPrefix * @see ServletRequestDataBinder */ public class WebDataBinder extends DataBinder { @@ -58,9 +61,19 @@ public class WebDataBinder extends DataBinder { */ public static final String DEFAULT_FIELD_MARKER_PREFIX = "_"; + /** + * Default prefix that field default parameters start with, followed by the field + * name: e.g. "!subscribeToNewsletter" for a field "subscribeToNewsletter". + *

Default parameters differ from field markers in that they provide a default + * value instead of an empty value. + * @see #setFieldDefaultPrefix + */ + public static final String DEFAULT_FIELD_DEFAULT_PREFIX = "!"; private String fieldMarkerPrefix = DEFAULT_FIELD_MARKER_PREFIX; + private String fieldDefaultPrefix = DEFAULT_FIELD_DEFAULT_PREFIX; + private boolean bindEmptyMultipartFiles = true; @@ -118,6 +131,32 @@ public class WebDataBinder extends DataBinder { return this.fieldMarkerPrefix; } + /** + * Specify a prefix that can be used for parameters that indicate default + * value fields, having "prefix + field" as name. The value of the default + * field is used when the field is not provided. + *

Default is "!", for "!FIELD" parameters (e.g. "!subscribeToNewsletter"). + * Set this to null if you want to turn off the field defaults completely. + *

HTML checkboxes only send a value when they're checked, so it is not + * possible to detect that a formerly checked box has just been unchecked, + * at least not with standard HTML means. A default field is especially + * useful when a checkbox represents a non-boolean value. + *

The presence of a default parameter preempts the behavior of a field + * marker for the given field. + * @see #DEFAULT_FIELD_DEFAULT_PREFIX + * @see org.springframework.web.servlet.mvc.BaseCommandController#onBind + */ + public void setFieldDefaultPrefix(String fieldDefaultPrefix) { + this.fieldDefaultPrefix = fieldDefaultPrefix; + } + + /** + * Return the prefix for parameters that mark default fields. + */ + public String getFieldDefaultPrefix() { + return this.fieldDefaultPrefix; + } + /** * Set whether to bind empty MultipartFile parameters. Default is "true". *

Turn this off if you want to keep an already bound MultipartFile @@ -139,16 +178,42 @@ public class WebDataBinder extends DataBinder { /** - * This implementation performs a field marker check + * This implementation performs a field default and marker check * before delegating to the superclass binding process. + * @see #checkFieldDefaults * @see #checkFieldMarkers */ @Override protected void doBind(MutablePropertyValues mpvs) { + checkFieldDefaults(mpvs); checkFieldMarkers(mpvs); super.doBind(mpvs); } + /** + * Check the given property values for field defaults, + * i.e. for fields that start with the field default prefix. + *

The existence of a field defaults indicates that the specified + * value should be used if the field is otherwise not present. + * @param mpvs the property values to be bound (can be modified) + * @see #getFieldDefaultPrefix + */ + protected void checkFieldDefaults(MutablePropertyValues mpvs) { + if (getFieldDefaultPrefix() != null) { + String fieldDefaultPrefix = getFieldDefaultPrefix(); + PropertyValue[] pvArray = mpvs.getPropertyValues(); + for (PropertyValue pv : pvArray) { + if (pv.getName().startsWith(fieldDefaultPrefix)) { + String field = pv.getName().substring(fieldDefaultPrefix.length()); + if (getPropertyAccessor().isWritableProperty(field) && !mpvs.contains(field)) { + mpvs.addPropertyValue(field, pv.getValue()); + } + mpvs.removePropertyValue(pv); + } + } + } + } + /** * Check the given property values for field markers, * i.e. for fields that start with the field marker prefix. diff --git a/org.springframework.web.servlet/src/test/java/org/springframework/web/bind/ServletRequestDataBinderTests.java b/org.springframework.web.servlet/src/test/java/org/springframework/web/bind/ServletRequestDataBinderTests.java index ceaf1870bf7..aa7bd4f9de0 100644 --- a/org.springframework.web.servlet/src/test/java/org/springframework/web/bind/ServletRequestDataBinderTests.java +++ b/org.springframework.web.servlet/src/test/java/org/springframework/web/bind/ServletRequestDataBinderTests.java @@ -34,6 +34,7 @@ import org.springframework.mock.web.MockHttpServletRequest; * @author Rod Johnson * @author Juergen Hoeller * @author Chris Beams + * @author Scott Andrews */ public class ServletRequestDataBinderTests { @@ -90,6 +91,59 @@ public class ServletRequestDataBinderTests { assertFalse(target.isPostProcessed()); } + @Test + public void testFieldDefault() throws Exception { + TestBean target = new TestBean(); + ServletRequestDataBinder binder = new ServletRequestDataBinder(target); + + MockHttpServletRequest request = new MockHttpServletRequest(); + request.addParameter("!postProcessed", "off"); + request.addParameter("postProcessed", "on"); + binder.bind(request); + assertTrue(target.isPostProcessed()); + + request.removeParameter("postProcessed"); + binder.bind(request); + assertFalse(target.isPostProcessed()); + } + + @Test + public void testFieldDefaultPreemptsFieldMarker() throws Exception { + TestBean target = new TestBean(); + ServletRequestDataBinder binder = new ServletRequestDataBinder(target); + + MockHttpServletRequest request = new MockHttpServletRequest(); + request.addParameter("!postProcessed", "on"); + request.addParameter("_postProcessed", "visible"); + request.addParameter("postProcessed", "on"); + binder.bind(request); + assertTrue(target.isPostProcessed()); + + request.removeParameter("postProcessed"); + binder.bind(request); + assertTrue(target.isPostProcessed()); + + request.removeParameter("!postProcessed"); + binder.bind(request); + assertFalse(target.isPostProcessed()); + } + + @Test + public void testFieldDefaultNonBoolean() throws Exception { + TestBean target = new TestBean(); + ServletRequestDataBinder binder = new ServletRequestDataBinder(target); + + MockHttpServletRequest request = new MockHttpServletRequest(); + request.addParameter("!name", "anonymous"); + request.addParameter("name", "Scott"); + binder.bind(request); + assertEquals("Scott", target.getName()); + + request.removeParameter("name"); + binder.bind(request); + assertEquals("anonymous", target.getName()); + } + @Test public void testWithCommaSeparatedStringArray() throws Exception { TestBean target = new TestBean();