diff --git a/spring-web/src/main/java/org/springframework/web/bind/WebDataBinder.java b/spring-web/src/main/java/org/springframework/web/bind/WebDataBinder.java index 0a928c4f16..7685be3fcb 100644 --- a/spring-web/src/main/java/org/springframework/web/bind/WebDataBinder.java +++ b/spring-web/src/main/java/org/springframework/web/bind/WebDataBinder.java @@ -206,8 +206,8 @@ public class WebDataBinder extends DataBinder { * @see #getFieldDefaultPrefix */ protected void checkFieldDefaults(MutablePropertyValues mpvs) { - if (getFieldDefaultPrefix() != null) { - String fieldDefaultPrefix = getFieldDefaultPrefix(); + String fieldDefaultPrefix = getFieldDefaultPrefix(); + if (fieldDefaultPrefix != null) { PropertyValue[] pvArray = mpvs.getPropertyValues(); for (PropertyValue pv : pvArray) { if (pv.getName().startsWith(fieldDefaultPrefix)) { @@ -233,8 +233,8 @@ public class WebDataBinder extends DataBinder { * @see #getEmptyValue(String, Class) */ protected void checkFieldMarkers(MutablePropertyValues mpvs) { - if (getFieldMarkerPrefix() != null) { - String fieldMarkerPrefix = getFieldMarkerPrefix(); + String fieldMarkerPrefix = getFieldMarkerPrefix(); + if (fieldMarkerPrefix != null) { PropertyValue[] pvArray = mpvs.getPropertyValues(); for (PropertyValue pv : pvArray) { if (pv.getName().startsWith(fieldMarkerPrefix)) { @@ -251,7 +251,20 @@ public class WebDataBinder extends DataBinder { /** * Determine an empty value for the specified field. - *

Default implementation returns: + *

The default implementation delegates to {@link #getEmptyValue(Class)} + * if the field type is known, otherwise falls back to {@code null}. + * @param field the name of the field + * @param fieldType the type of the field + * @return the empty value (for most fields: {@code null}) + */ + @Nullable + protected Object getEmptyValue(String field, @Nullable Class fieldType) { + return (fieldType != null ? getEmptyValue(fieldType) : null); + } + + /** + * Determine an empty value for the specified field. + *

The default implementation returns: *

- * @param field the name of the field * @param fieldType the type of the field - * @return the empty value (for most fields: null) + * @return the empty value (for most fields: {@code null}) + * @since 5.0 */ @Nullable - protected Object getEmptyValue(String field, @Nullable Class fieldType) { - if (fieldType != null) { - try { - if (boolean.class == fieldType || Boolean.class == fieldType) { - // Special handling of boolean property. - return Boolean.FALSE; - } - else if (fieldType.isArray()) { - // Special handling of array property. - return Array.newInstance(fieldType.getComponentType(), 0); - } - else if (Collection.class.isAssignableFrom(fieldType)) { - return CollectionFactory.createCollection(fieldType, 0); - } - else if (Map.class.isAssignableFrom(fieldType)) { - return CollectionFactory.createMap(fieldType, 0); - } - } catch (IllegalArgumentException exc) { - return null; + public Object getEmptyValue(Class fieldType) { + try { + if (boolean.class == fieldType || Boolean.class == fieldType) { + // Special handling of boolean property. + return Boolean.FALSE; + } + else if (fieldType.isArray()) { + // Special handling of array property. + return Array.newInstance(fieldType.getComponentType(), 0); + } + else if (Collection.class.isAssignableFrom(fieldType)) { + return CollectionFactory.createCollection(fieldType, 0); + } + else if (Map.class.isAssignableFrom(fieldType)) { + return CollectionFactory.createMap(fieldType, 0); } } - // Default value: try null. + catch (IllegalArgumentException ex) { + logger.debug("Failed to create default value - falling back to null: " + ex.getMessage()); + } + // Default value: null. return null; } + /** * Bind all multipart files contained in the given request, if any - * (in case of a multipart request). + * (in case of a multipart request). To be called by subclasses. *

Multipart files will only be added to the property values if they * are not empty or if we're configured to bind empty multipart files too. * @param multipartFiles Map of field name String to MultipartFile object diff --git a/spring-web/src/main/java/org/springframework/web/method/annotation/ModelAttributeMethodProcessor.java b/spring-web/src/main/java/org/springframework/web/method/annotation/ModelAttributeMethodProcessor.java index 5114cbd0c2..1b5872d8fe 100644 --- a/spring-web/src/main/java/org/springframework/web/method/annotation/ModelAttributeMethodProcessor.java +++ b/spring-web/src/main/java/org/springframework/web/method/annotation/ModelAttributeMethodProcessor.java @@ -241,14 +241,30 @@ public class ModelAttributeMethodProcessor implements HandlerMethodArgumentResol Class[] paramTypes = ctor.getParameterTypes(); Assert.state(paramNames.length == paramTypes.length, () -> "Invalid number of parameter names: " + paramNames.length + " for constructor " + ctor); + Object[] args = new Object[paramTypes.length]; WebDataBinder binder = binderFactory.createBinder(webRequest, null, attributeName); + String fieldDefaultPrefix = binder.getFieldDefaultPrefix(); + String fieldMarkerPrefix = binder.getFieldMarkerPrefix(); boolean bindingFailure = false; + for (int i = 0; i < paramNames.length; i++) { - String[] paramValues = webRequest.getParameterValues(paramNames[i]); + String paramName = paramNames[i]; + Class paramType = paramTypes[i]; + Object value = webRequest.getParameterValues(paramName); + if (value == null) { + if (fieldDefaultPrefix != null) { + value = webRequest.getParameter(fieldDefaultPrefix + paramName); + } + if (value == null && fieldMarkerPrefix != null) { + if (webRequest.getParameter(fieldMarkerPrefix + paramName) != null) { + value = binder.getEmptyValue(paramType); + } + } + } try { - args[i] = (paramValues != null ? - binder.convertIfNecessary(paramValues, paramTypes[i], new MethodParameter(ctor, i)) : null); + args[i] = (value != null ? + binder.convertIfNecessary(value, paramType, new MethodParameter(ctor, i)) : null); } catch (TypeMismatchException ex) { bindingFailure = true; @@ -257,6 +273,7 @@ public class ModelAttributeMethodProcessor implements HandlerMethodArgumentResol new String[] {ex.getErrorCode()}, null, ex.getLocalizedMessage())); } } + if (bindingFailure) { throw new BindException(binder.getBindingResult()); } diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ServletAnnotationControllerHandlerMethodTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ServletAnnotationControllerHandlerMethodTests.java index 5f4653f411..88f5a6bfe6 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ServletAnnotationControllerHandlerMethodTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ServletAnnotationControllerHandlerMethodTests.java @@ -1729,10 +1729,10 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl MockHttpServletRequest request = new MockHttpServletRequest("GET", "/bind"); request.addParameter("param1", "value1"); - request.addParameter("param2", "2"); + request.addParameter("param2", "true"); MockHttpServletResponse response = new MockHttpServletResponse(); getServlet().service(request, response); - assertEquals("value1-2-0", response.getContentAsString()); + assertEquals("value1-true-0", response.getContentAsString()); } @Test @@ -1741,11 +1741,11 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl MockHttpServletRequest request = new MockHttpServletRequest("GET", "/bind"); request.addParameter("param1", "value1"); - request.addParameter("param2", "2"); + request.addParameter("param2", "true"); request.addParameter("param3", "3"); MockHttpServletResponse response = new MockHttpServletResponse(); getServlet().service(request, response); - assertEquals("value1-2-3", response.getContentAsString()); + assertEquals("value1-true-3", response.getContentAsString()); } @Test @@ -1754,11 +1754,11 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl MockHttpServletRequest request = new MockHttpServletRequest("GET", "/bind"); request.addParameter("param1", "value1"); - request.addParameter("param2", "2"); + request.addParameter("param2", "true"); request.addParameter("param3", "3"); MockHttpServletResponse response = new MockHttpServletResponse(); getServlet().service(request, response); - assertEquals("value1-2-3", response.getContentAsString()); + assertEquals("value1-true-3", response.getContentAsString()); } @Test @@ -1777,7 +1777,7 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl initServletWithControllers(ValidatedDataClassController.class); MockHttpServletRequest request = new MockHttpServletRequest("GET", "/bind"); - request.addParameter("param2", "2"); + request.addParameter("param2", "true"); request.addParameter("param3", "3"); MockHttpServletResponse response = new MockHttpServletResponse(); getServlet().service(request, response); @@ -1790,11 +1790,11 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl MockHttpServletRequest request = new MockHttpServletRequest("GET", "/bind"); request.addParameter("param1", "value1"); - request.addParameter("param2", "2"); + request.addParameter("param2", "true"); request.addParameter("param3", "3"); MockHttpServletResponse response = new MockHttpServletResponse(); getServlet().service(request, response); - assertEquals("value1-2-3", response.getContentAsString()); + assertEquals("value1-true-3", response.getContentAsString()); } @Test @@ -1808,6 +1808,56 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl assertTrue(response.getContentAsString().contains("field 'param2'")); } + @Test + public void dataClassBindingWithFieldMarker() throws Exception { + initServletWithControllers(DataClassController.class); + + MockHttpServletRequest request = new MockHttpServletRequest("GET", "/bind"); + request.addParameter("param1", "value1"); + request.addParameter("param2", "true"); + request.addParameter("_param2", "on"); + MockHttpServletResponse response = new MockHttpServletResponse(); + getServlet().service(request, response); + assertEquals("value1-true-0", response.getContentAsString()); + } + + @Test + public void dataClassBindingWithFieldMarkerFallback() throws Exception { + initServletWithControllers(DataClassController.class); + + MockHttpServletRequest request = new MockHttpServletRequest("GET", "/bind"); + request.addParameter("param1", "value1"); + request.addParameter("_param2", "on"); + MockHttpServletResponse response = new MockHttpServletResponse(); + getServlet().service(request, response); + assertEquals("value1-false-0", response.getContentAsString()); + } + + @Test + public void dataClassBindingWithFieldDefault() throws Exception { + initServletWithControllers(DataClassController.class); + + MockHttpServletRequest request = new MockHttpServletRequest("GET", "/bind"); + request.addParameter("param1", "value1"); + request.addParameter("param2", "true"); + request.addParameter("!param2", "false"); + MockHttpServletResponse response = new MockHttpServletResponse(); + getServlet().service(request, response); + assertEquals("value1-true-0", response.getContentAsString()); + } + + @Test + public void dataClassBindingWithFieldDefaultFallback() throws Exception { + initServletWithControllers(DataClassController.class); + + MockHttpServletRequest request = new MockHttpServletRequest("GET", "/bind"); + request.addParameter("param1", "value1"); + request.addParameter("!param2", "false"); + MockHttpServletResponse response = new MockHttpServletResponse(); + getServlet().service(request, response); + assertEquals("value1-false-0", response.getContentAsString()); + } + @Controller static class ControllerWithEmptyValueMapping { @@ -3336,12 +3386,12 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl @NotNull public final String param1; - public final int param2; + public final boolean param2; public int param3; @ConstructorProperties({"param1", "param2"}) - public DataClass(String param1, int p2) { + public DataClass(String param1, boolean p2) { this.param1 = param1; this.param2 = p2; }