Skip to content

Commit 53b8d73

Browse files
encircledsbrannen
authored andcommitted
Support single-value request param resolution for data class constructors
Prior to this commit, when Web MVC attempted to resolve a constructor argument for a data class constructor with @ModelAttribute binding, ModelAttributeMethodProcessor failed to unwrap the array returned by WebRequest.getParameter(String). According to the Javadoc for WebRequest.getParameter(String), "a single-value parameter will be exposed as an array with a single element." This commit fixes this issue by extracting the single value from such an array and using that as the constructor argument (potentially converted by the WebDataBinder). Closes gh-25200
1 parent 3ab270e commit 53b8d73

File tree

2 files changed

+47
-3
lines changed

2 files changed

+47
-3
lines changed

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
import java.beans.ConstructorProperties;
2020
import java.lang.annotation.Annotation;
21+
import java.lang.reflect.Array;
2122
import java.lang.reflect.Constructor;
2223
import java.lang.reflect.Field;
2324
import java.util.ArrayList;
@@ -71,6 +72,7 @@
7172
* @author Rossen Stoyanchev
7273
* @author Juergen Hoeller
7374
* @author Sebastien Deleuze
75+
* @author Vladislav Kisel
7476
* @since 3.1
7577
*/
7678
public class ModelAttributeMethodProcessor implements HandlerMethodArgumentResolver, HandlerMethodReturnValueHandler {
@@ -285,6 +287,12 @@ protected Object constructAttribute(Constructor<?> ctor, String attributeName, M
285287
}
286288
}
287289
}
290+
291+
// Singular web parameters are wrapped with array, extract it so it can be picked up by conversion service later on
292+
if (value != null && value.getClass().isArray() && Array.getLength(value) == 1) {
293+
value = Array.get(value, 0);
294+
}
295+
288296
try {
289297
MethodParameter methodParam = new FieldAwareConstructorParameter(ctor, i, paramName);
290298
if (value == null && methodParam.isOptional()) {

spring-web/src/test/java/org/springframework/web/method/annotation/ModelAttributeMethodProcessorTests.java

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2019 the original author or authors.
2+
* Copyright 2002-2020 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -19,13 +19,16 @@
1919
import java.lang.annotation.Retention;
2020
import java.lang.annotation.Target;
2121
import java.lang.reflect.Method;
22+
import java.util.Arrays;
23+
import java.util.List;
2224

2325
import org.junit.jupiter.api.BeforeEach;
2426
import org.junit.jupiter.api.Test;
2527

2628
import org.springframework.beans.testfixture.beans.TestBean;
2729
import org.springframework.core.MethodParameter;
2830
import org.springframework.core.annotation.SynthesizingMethodParameter;
31+
import org.springframework.format.support.DefaultFormattingConversionService;
2932
import org.springframework.validation.BindException;
3033
import org.springframework.validation.BindingResult;
3134
import org.springframework.validation.Errors;
@@ -58,6 +61,7 @@
5861
* Test fixture with {@link ModelAttributeMethodProcessor}.
5962
*
6063
* @author Rossen Stoyanchev
64+
* @author Vladislav Kisel
6165
*/
6266
public class ModelAttributeMethodProcessorTests {
6367

@@ -73,6 +77,7 @@ public class ModelAttributeMethodProcessorTests {
7377
private MethodParameter paramModelAttr;
7478
private MethodParameter paramBindingDisabledAttr;
7579
private MethodParameter paramNonSimpleType;
80+
private MethodParameter beanWithConstructorArgs;
7681

7782
private MethodParameter returnParamNamedModelAttr;
7883
private MethodParameter returnParamNonSimpleType;
@@ -86,14 +91,15 @@ public void setup() throws Exception {
8691

8792
Method method = ModelAttributeHandler.class.getDeclaredMethod("modelAttribute",
8893
TestBean.class, Errors.class, int.class, TestBean.class,
89-
TestBean.class, TestBean.class);
94+
TestBean.class, TestBean.class, TestBeanWithConstructorArgs.class);
9095

9196
this.paramNamedValidModelAttr = new SynthesizingMethodParameter(method, 0);
9297
this.paramErrors = new SynthesizingMethodParameter(method, 1);
9398
this.paramInt = new SynthesizingMethodParameter(method, 2);
9499
this.paramModelAttr = new SynthesizingMethodParameter(method, 3);
95100
this.paramBindingDisabledAttr = new SynthesizingMethodParameter(method, 4);
96101
this.paramNonSimpleType = new SynthesizingMethodParameter(method, 5);
102+
this.beanWithConstructorArgs = new SynthesizingMethodParameter(method, 6);
97103

98104
method = getClass().getDeclaredMethod("annotatedReturnValue");
99105
this.returnParamNamedModelAttr = new MethodParameter(method, -1);
@@ -264,6 +270,26 @@ public void handleNotAnnotatedReturnValue() throws Exception {
264270
assertThat(this.container.getModel().get("testBean")).isSameAs(testBean);
265271
}
266272

273+
@Test // gh-25182
274+
public void testResolveConstructorListParameter() throws Exception {
275+
MockHttpServletRequest mockRequest = new MockHttpServletRequest();
276+
mockRequest.addParameter("listOfStrings", "1,2");
277+
ServletWebRequest requestWithParam = new ServletWebRequest(mockRequest);
278+
279+
WebDataBinderFactory factory = mock(WebDataBinderFactory.class);
280+
given(factory.createBinder(any(), any(), eq("testBeanWithConstructorArgs")))
281+
.willAnswer(invocation -> {
282+
WebRequestDataBinder binder = new WebRequestDataBinder(invocation.getArgument(1));
283+
284+
// Add conversion service which will convert "1,2" to List of size 2
285+
binder.setConversionService(new DefaultFormattingConversionService());
286+
return binder;
287+
});
288+
289+
Object resolved = this.processor.resolveArgument(this.beanWithConstructorArgs, this.container, requestWithParam, factory);
290+
assertThat(resolved).isNotNull();
291+
assertThat(((TestBeanWithConstructorArgs) resolved).listOfStrings).isEqualTo(Arrays.asList("1", "2"));
292+
}
267293

268294
private void testGetAttributeFromModel(String expectedAttrName, MethodParameter param) throws Exception {
269295
Object target = new TestBean();
@@ -330,10 +356,20 @@ public void modelAttribute(
330356
int intArg,
331357
@ModelAttribute TestBean defaultNameAttr,
332358
@ModelAttribute(name="noBindAttr", binding=false) @Valid TestBean noBindAttr,
333-
TestBean notAnnotatedAttr) {
359+
TestBean notAnnotatedAttr,
360+
TestBeanWithConstructorArgs beanWithConstructorArgs) {
334361
}
335362
}
336363

364+
static class TestBeanWithConstructorArgs {
365+
366+
final List<String> listOfStrings;
367+
368+
public TestBeanWithConstructorArgs(List<String> listOfStrings) {
369+
this.listOfStrings = listOfStrings;
370+
}
371+
372+
}
337373

338374
@ModelAttribute("modelAttrName") @SuppressWarnings("unused")
339375
private String annotatedReturnValue() {

0 commit comments

Comments
 (0)