Skip to content

Commit 9aa369f

Browse files
committed
Data class construction supports field default/marker parameters
Issue: SPR-15871
1 parent 18f42f9 commit 9aa369f

File tree

3 files changed

+122
-42
lines changed

3 files changed

+122
-42
lines changed

spring-web/src/main/java/org/springframework/web/bind/WebDataBinder.java

Lines changed: 41 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -206,8 +206,8 @@ protected void doBind(MutablePropertyValues mpvs) {
206206
* @see #getFieldDefaultPrefix
207207
*/
208208
protected void checkFieldDefaults(MutablePropertyValues mpvs) {
209-
if (getFieldDefaultPrefix() != null) {
210-
String fieldDefaultPrefix = getFieldDefaultPrefix();
209+
String fieldDefaultPrefix = getFieldDefaultPrefix();
210+
if (fieldDefaultPrefix != null) {
211211
PropertyValue[] pvArray = mpvs.getPropertyValues();
212212
for (PropertyValue pv : pvArray) {
213213
if (pv.getName().startsWith(fieldDefaultPrefix)) {
@@ -233,8 +233,8 @@ protected void checkFieldDefaults(MutablePropertyValues mpvs) {
233233
* @see #getEmptyValue(String, Class)
234234
*/
235235
protected void checkFieldMarkers(MutablePropertyValues mpvs) {
236-
if (getFieldMarkerPrefix() != null) {
237-
String fieldMarkerPrefix = getFieldMarkerPrefix();
236+
String fieldMarkerPrefix = getFieldMarkerPrefix();
237+
if (fieldMarkerPrefix != null) {
238238
PropertyValue[] pvArray = mpvs.getPropertyValues();
239239
for (PropertyValue pv : pvArray) {
240240
if (pv.getName().startsWith(fieldMarkerPrefix)) {
@@ -251,47 +251,60 @@ protected void checkFieldMarkers(MutablePropertyValues mpvs) {
251251

252252
/**
253253
* Determine an empty value for the specified field.
254-
* <p>Default implementation returns:
254+
* <p>The default implementation delegates to {@link #getEmptyValue(Class)}
255+
* if the field type is known, otherwise falls back to {@code null}.
256+
* @param field the name of the field
257+
* @param fieldType the type of the field
258+
* @return the empty value (for most fields: {@code null})
259+
*/
260+
@Nullable
261+
protected Object getEmptyValue(String field, @Nullable Class<?> fieldType) {
262+
return (fieldType != null ? getEmptyValue(fieldType) : null);
263+
}
264+
265+
/**
266+
* Determine an empty value for the specified field.
267+
* <p>The default implementation returns:
255268
* <ul>
256269
* <li>{@code Boolean.FALSE} for boolean fields
257270
* <li>an empty array for array types
258271
* <li>Collection implementations for Collection types
259272
* <li>Map implementations for Map types
260273
* <li>else, {@code null} is used as default
261274
* </ul>
262-
* @param field the name of the field
263275
* @param fieldType the type of the field
264-
* @return the empty value (for most fields: null)
276+
* @return the empty value (for most fields: {@code null})
277+
* @since 5.0
265278
*/
266279
@Nullable
267-
protected Object getEmptyValue(String field, @Nullable Class<?> fieldType) {
268-
if (fieldType != null) {
269-
try {
270-
if (boolean.class == fieldType || Boolean.class == fieldType) {
271-
// Special handling of boolean property.
272-
return Boolean.FALSE;
273-
}
274-
else if (fieldType.isArray()) {
275-
// Special handling of array property.
276-
return Array.newInstance(fieldType.getComponentType(), 0);
277-
}
278-
else if (Collection.class.isAssignableFrom(fieldType)) {
279-
return CollectionFactory.createCollection(fieldType, 0);
280-
}
281-
else if (Map.class.isAssignableFrom(fieldType)) {
282-
return CollectionFactory.createMap(fieldType, 0);
283-
}
284-
} catch (IllegalArgumentException exc) {
285-
return null;
280+
public Object getEmptyValue(Class<?> fieldType) {
281+
try {
282+
if (boolean.class == fieldType || Boolean.class == fieldType) {
283+
// Special handling of boolean property.
284+
return Boolean.FALSE;
285+
}
286+
else if (fieldType.isArray()) {
287+
// Special handling of array property.
288+
return Array.newInstance(fieldType.getComponentType(), 0);
289+
}
290+
else if (Collection.class.isAssignableFrom(fieldType)) {
291+
return CollectionFactory.createCollection(fieldType, 0);
292+
}
293+
else if (Map.class.isAssignableFrom(fieldType)) {
294+
return CollectionFactory.createMap(fieldType, 0);
286295
}
287296
}
288-
// Default value: try null.
297+
catch (IllegalArgumentException ex) {
298+
logger.debug("Failed to create default value - falling back to null: " + ex.getMessage());
299+
}
300+
// Default value: null.
289301
return null;
290302
}
291303

304+
292305
/**
293306
* Bind all multipart files contained in the given request, if any
294-
* (in case of a multipart request).
307+
* (in case of a multipart request). To be called by subclasses.
295308
* <p>Multipart files will only be added to the property values if they
296309
* are not empty or if we're configured to bind empty multipart files too.
297310
* @param multipartFiles Map of field name String to MultipartFile object

spring-web/src/main/java/org/springframework/web/method/annotation/ModelAttributeMethodProcessor.java

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -241,14 +241,30 @@ protected Object constructAttribute(Constructor<?> ctor, String attributeName,
241241
Class<?>[] paramTypes = ctor.getParameterTypes();
242242
Assert.state(paramNames.length == paramTypes.length,
243243
() -> "Invalid number of parameter names: " + paramNames.length + " for constructor " + ctor);
244+
244245
Object[] args = new Object[paramTypes.length];
245246
WebDataBinder binder = binderFactory.createBinder(webRequest, null, attributeName);
247+
String fieldDefaultPrefix = binder.getFieldDefaultPrefix();
248+
String fieldMarkerPrefix = binder.getFieldMarkerPrefix();
246249
boolean bindingFailure = false;
250+
247251
for (int i = 0; i < paramNames.length; i++) {
248-
String[] paramValues = webRequest.getParameterValues(paramNames[i]);
252+
String paramName = paramNames[i];
253+
Class<?> paramType = paramTypes[i];
254+
Object value = webRequest.getParameterValues(paramName);
255+
if (value == null) {
256+
if (fieldDefaultPrefix != null) {
257+
value = webRequest.getParameter(fieldDefaultPrefix + paramName);
258+
}
259+
if (value == null && fieldMarkerPrefix != null) {
260+
if (webRequest.getParameter(fieldMarkerPrefix + paramName) != null) {
261+
value = binder.getEmptyValue(paramType);
262+
}
263+
}
264+
}
249265
try {
250-
args[i] = (paramValues != null ?
251-
binder.convertIfNecessary(paramValues, paramTypes[i], new MethodParameter(ctor, i)) : null);
266+
args[i] = (value != null ?
267+
binder.convertIfNecessary(value, paramType, new MethodParameter(ctor, i)) : null);
252268
}
253269
catch (TypeMismatchException ex) {
254270
bindingFailure = true;
@@ -257,6 +273,7 @@ protected Object constructAttribute(Constructor<?> ctor, String attributeName,
257273
new String[] {ex.getErrorCode()}, null, ex.getLocalizedMessage()));
258274
}
259275
}
276+
260277
if (bindingFailure) {
261278
throw new BindException(binder.getBindingResult());
262279
}

spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ServletAnnotationControllerHandlerMethodTests.java

Lines changed: 61 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1729,10 +1729,10 @@ public void dataClassBinding() throws Exception {
17291729

17301730
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/bind");
17311731
request.addParameter("param1", "value1");
1732-
request.addParameter("param2", "2");
1732+
request.addParameter("param2", "true");
17331733
MockHttpServletResponse response = new MockHttpServletResponse();
17341734
getServlet().service(request, response);
1735-
assertEquals("value1-2-0", response.getContentAsString());
1735+
assertEquals("value1-true-0", response.getContentAsString());
17361736
}
17371737

17381738
@Test
@@ -1741,11 +1741,11 @@ public void dataClassBindingWithAdditionalSetter() throws Exception {
17411741

17421742
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/bind");
17431743
request.addParameter("param1", "value1");
1744-
request.addParameter("param2", "2");
1744+
request.addParameter("param2", "true");
17451745
request.addParameter("param3", "3");
17461746
MockHttpServletResponse response = new MockHttpServletResponse();
17471747
getServlet().service(request, response);
1748-
assertEquals("value1-2-3", response.getContentAsString());
1748+
assertEquals("value1-true-3", response.getContentAsString());
17491749
}
17501750

17511751
@Test
@@ -1754,11 +1754,11 @@ public void dataClassBindingWithResult() throws Exception {
17541754

17551755
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/bind");
17561756
request.addParameter("param1", "value1");
1757-
request.addParameter("param2", "2");
1757+
request.addParameter("param2", "true");
17581758
request.addParameter("param3", "3");
17591759
MockHttpServletResponse response = new MockHttpServletResponse();
17601760
getServlet().service(request, response);
1761-
assertEquals("value1-2-3", response.getContentAsString());
1761+
assertEquals("value1-true-3", response.getContentAsString());
17621762
}
17631763

17641764
@Test
@@ -1777,7 +1777,7 @@ public void dataClassBindingWithValidationError() throws Exception {
17771777
initServletWithControllers(ValidatedDataClassController.class);
17781778

17791779
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/bind");
1780-
request.addParameter("param2", "2");
1780+
request.addParameter("param2", "true");
17811781
request.addParameter("param3", "3");
17821782
MockHttpServletResponse response = new MockHttpServletResponse();
17831783
getServlet().service(request, response);
@@ -1790,11 +1790,11 @@ public void dataClassBindingWithOptional() throws Exception {
17901790

17911791
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/bind");
17921792
request.addParameter("param1", "value1");
1793-
request.addParameter("param2", "2");
1793+
request.addParameter("param2", "true");
17941794
request.addParameter("param3", "3");
17951795
MockHttpServletResponse response = new MockHttpServletResponse();
17961796
getServlet().service(request, response);
1797-
assertEquals("value1-2-3", response.getContentAsString());
1797+
assertEquals("value1-true-3", response.getContentAsString());
17981798
}
17991799

18001800
@Test
@@ -1808,6 +1808,56 @@ public void dataClassBindingWithOptionalAndConversionError() throws Exception {
18081808
assertTrue(response.getContentAsString().contains("field 'param2'"));
18091809
}
18101810

1811+
@Test
1812+
public void dataClassBindingWithFieldMarker() throws Exception {
1813+
initServletWithControllers(DataClassController.class);
1814+
1815+
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/bind");
1816+
request.addParameter("param1", "value1");
1817+
request.addParameter("param2", "true");
1818+
request.addParameter("_param2", "on");
1819+
MockHttpServletResponse response = new MockHttpServletResponse();
1820+
getServlet().service(request, response);
1821+
assertEquals("value1-true-0", response.getContentAsString());
1822+
}
1823+
1824+
@Test
1825+
public void dataClassBindingWithFieldMarkerFallback() throws Exception {
1826+
initServletWithControllers(DataClassController.class);
1827+
1828+
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/bind");
1829+
request.addParameter("param1", "value1");
1830+
request.addParameter("_param2", "on");
1831+
MockHttpServletResponse response = new MockHttpServletResponse();
1832+
getServlet().service(request, response);
1833+
assertEquals("value1-false-0", response.getContentAsString());
1834+
}
1835+
1836+
@Test
1837+
public void dataClassBindingWithFieldDefault() throws Exception {
1838+
initServletWithControllers(DataClassController.class);
1839+
1840+
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/bind");
1841+
request.addParameter("param1", "value1");
1842+
request.addParameter("param2", "true");
1843+
request.addParameter("!param2", "false");
1844+
MockHttpServletResponse response = new MockHttpServletResponse();
1845+
getServlet().service(request, response);
1846+
assertEquals("value1-true-0", response.getContentAsString());
1847+
}
1848+
1849+
@Test
1850+
public void dataClassBindingWithFieldDefaultFallback() throws Exception {
1851+
initServletWithControllers(DataClassController.class);
1852+
1853+
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/bind");
1854+
request.addParameter("param1", "value1");
1855+
request.addParameter("!param2", "false");
1856+
MockHttpServletResponse response = new MockHttpServletResponse();
1857+
getServlet().service(request, response);
1858+
assertEquals("value1-false-0", response.getContentAsString());
1859+
}
1860+
18111861

18121862
@Controller
18131863
static class ControllerWithEmptyValueMapping {
@@ -3336,12 +3386,12 @@ public static class DataClass {
33363386
@NotNull
33373387
public final String param1;
33383388

3339-
public final int param2;
3389+
public final boolean param2;
33403390

33413391
public int param3;
33423392

33433393
@ConstructorProperties({"param1", "param2"})
3344-
public DataClass(String param1, int p2) {
3394+
public DataClass(String param1, boolean p2) {
33453395
this.param1 = param1;
33463396
this.param2 = p2;
33473397
}

0 commit comments

Comments
 (0)