diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/annotation/support/ServletModelAttributeMethodProcessor.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/annotation/support/ServletModelAttributeMethodProcessor.java index b435d5f2ca..66e3aa3adf 100644 --- a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/annotation/support/ServletModelAttributeMethodProcessor.java +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/annotation/support/ServletModelAttributeMethodProcessor.java @@ -22,6 +22,10 @@ import java.util.Map; import javax.servlet.ServletRequest; import org.springframework.core.MethodParameter; +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.core.convert.converter.Converter; +import org.springframework.util.StringUtils; import org.springframework.validation.DataBinder; import org.springframework.web.bind.ServletRequestDataBinder; import org.springframework.web.bind.WebDataBinder; @@ -36,9 +40,9 @@ import org.springframework.web.servlet.mvc.method.annotation.ServletRequestDataB * A Servlet-specific {@link ModelAttributeMethodProcessor} that applies data * binding through a WebDataBinder of type {@link ServletRequestDataBinder}. * - *

Adds a fall-back strategy to instantiate a model attribute from a - * URI template variable combined with type conversion, if the model attribute - * name matches to a URI template variable name. + *

Also adds a fall-back strategy to instantiate the model attribute from a + * URI template variable or from a request parameter if the name matches the + * model attribute name and there is an appropriate type conversion strategy. * * @author Rossen Stoyanchev * @since 3.1 @@ -56,36 +60,89 @@ public class ServletModelAttributeMethodProcessor extends ModelAttributeMethodPr } /** - * Add a fall-back strategy to instantiate the model attribute from a URI - * template variable with type conversion, if the model attribute name - * matches to a URI variable name. + * Instantiate the model attribute from a URI template variable or from a + * request parameter if the name matches to the model attribute name and + * if there is an appropriate type conversion strategy. If none of these + * are true delegate back to the base class. + * @see #createAttributeFromUriValue */ @Override - protected Object createAttribute(String attributeName, - MethodParameter parameter, - WebDataBinderFactory binderFactory, - NativeWebRequest request) throws Exception { - - Map uriVariables = getUriTemplateVariables(request); - - if (uriVariables.containsKey(attributeName)) { - DataBinder binder = binderFactory.createBinder(request, null, attributeName); - return binder.convertIfNecessary(uriVariables.get(attributeName), parameter.getParameterType()); + protected final Object createAttribute(String attributeName, + MethodParameter parameter, + WebDataBinderFactory binderFactory, + NativeWebRequest request) throws Exception { + + String value = getRequestValueForAttribute(attributeName, request); + if (value != null) { + Object attribute = createAttributeFromRequestValue(value, attributeName, parameter, binderFactory, request); + if (attribute != null) { + return attribute; + } } - + return super.createAttribute(attributeName, parameter, binderFactory, request); } + + /** + * Obtain a value from the request that may be used to instantiate the + * model attribute through type conversion from String to the target type. + *

The default implementation looks for the attribute name to match + * a URI variable first and then a request parameter. + * @param attributeName the model attribute name + * @param request the current request + * @return the request value to try to convert or {@code null} + */ + protected String getRequestValueForAttribute(String attributeName, NativeWebRequest request) { + Map variables = getUriTemplateVariables(request); + if (StringUtils.hasText(variables.get(attributeName))) { + return variables.get(attributeName); + } + else if (StringUtils.hasText(request.getParameter(attributeName))) { + return request.getParameter(attributeName); + } + else { + return null; + } + } @SuppressWarnings("unchecked") - private Map getUriTemplateVariables(NativeWebRequest request) { - - Map uriTemplateVars = + protected final Map getUriTemplateVariables(NativeWebRequest request) { + Map variables = (Map) request.getAttribute( HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, RequestAttributes.SCOPE_REQUEST); - - return (uriTemplateVars != null) ? uriTemplateVars : Collections.emptyMap(); + return (variables != null) ? variables : Collections.emptyMap(); } + /** + * Create a model attribute from a String request value (e.g. URI template + * variable, request parameter) using type conversion. + *

The default implementation converts only if there a registered + * {@link Converter} that can perform the conversion. + * @param sourceValue the source value to create the model attribute from + * @param attributeName the name of the attribute, never {@code null} + * @param parameter the method parameter + * @param binderFactory for creating WebDataBinder instance + * @param request the current request + * @return the created model attribute, or {@code null} + * @throws Exception + */ + protected Object createAttributeFromRequestValue(String sourceValue, + String attributeName, + MethodParameter parameter, + WebDataBinderFactory binderFactory, + NativeWebRequest request) throws Exception { + DataBinder binder = binderFactory.createBinder(request, null, attributeName); + ConversionService conversionService = binder.getConversionService(); + if (conversionService != null) { + TypeDescriptor source = TypeDescriptor.valueOf(String.class); + TypeDescriptor target = new TypeDescriptor(parameter); + if (conversionService.canConvert(source, target)) { + return binder.convertIfNecessary(sourceValue, parameter.getParameterType(), parameter); + } + } + return null; + } + /** * {@inheritDoc} *

Downcast {@link WebDataBinder} to {@link ServletRequestDataBinder} before binding. diff --git a/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/method/annotation/SerlvetModelAttributeMethodProcessorTests.java b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/method/annotation/SerlvetModelAttributeMethodProcessorTests.java index abc85ab3d9..64732552f5 100644 --- a/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/method/annotation/SerlvetModelAttributeMethodProcessorTests.java +++ b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/method/annotation/SerlvetModelAttributeMethodProcessorTests.java @@ -27,8 +27,10 @@ import org.junit.Before; import org.junit.Test; import org.springframework.beans.TestBean; import org.springframework.core.MethodParameter; +import org.springframework.core.convert.support.DefaultConversionService; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.bind.support.ConfigurableWebBindingInitializer; import org.springframework.web.bind.support.WebDataBinderFactory; import org.springframework.web.context.request.NativeWebRequest; import org.springframework.web.context.request.ServletWebRequest; @@ -60,44 +62,72 @@ public class SerlvetModelAttributeMethodProcessorTests { @Before public void setUp() throws Exception { - processor = new ServletModelAttributeMethodProcessor(false); + this.processor = new ServletModelAttributeMethodProcessor(false); Method method = getClass().getDeclaredMethod("modelAttribute", TestBean.class, TestBeanWithoutStringConstructor.class); - testBeanModelAttr = new MethodParameter(method, 0); - testBeanWithoutStringConstructorModelAttr = new MethodParameter(method, 1); + this.testBeanModelAttr = new MethodParameter(method, 0); + this.testBeanWithoutStringConstructorModelAttr = new MethodParameter(method, 1); - binderFactory = new ServletRequestDataBinderFactory(null, null); - mavContainer = new ModelAndViewContainer(); + ConfigurableWebBindingInitializer initializer = new ConfigurableWebBindingInitializer(); + initializer.setConversionService(new DefaultConversionService()); - request = new MockHttpServletRequest(); - webRequest = new ServletWebRequest(request); + this.binderFactory = new ServletRequestDataBinderFactory(null, initializer ); + this.mavContainer = new ModelAndViewContainer(); + + this.request = new MockHttpServletRequest(); + this.webRequest = new ServletWebRequest(request); } @Test - public void createAttributeViaPathVariable() throws Exception { + public void createAttributeUriTemplateVar() throws Exception { Map uriTemplateVars = new HashMap(); - uriTemplateVars.put("testBean1", "pathy"); - request.setAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, uriTemplateVars); + uriTemplateVars.put("testBean1", "Patty"); + this.request.setAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, uriTemplateVars); - // Type conversion from "pathy" to TestBean via TestBean(String) constructor + // Type conversion from "Patty" to TestBean via TestBean(String) constructor TestBean testBean = - (TestBean) processor.resolveArgument(testBeanModelAttr, mavContainer, webRequest, binderFactory); + (TestBean) this.processor.resolveArgument( + this.testBeanModelAttr, this.mavContainer, this.webRequest, this.binderFactory); - assertEquals("pathy", testBean.getName()); + assertEquals("Patty", testBean.getName()); } @Test - public void createAttributeAfterPathVariableConversionError() throws Exception { + public void createAttributeUriTemplateVarCannotConvert() throws Exception { Map uriTemplateVars = new HashMap(); - uriTemplateVars.put("testBean1", "pathy"); + uriTemplateVars.put("testBean2", "Patty"); request.setAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, uriTemplateVars); TestBeanWithoutStringConstructor testBean = - (TestBeanWithoutStringConstructor) processor.resolveArgument( - testBeanWithoutStringConstructorModelAttr, mavContainer, webRequest, binderFactory); + (TestBeanWithoutStringConstructor) this.processor.resolveArgument( + this.testBeanWithoutStringConstructorModelAttr, this.mavContainer, this.webRequest, this.binderFactory); + + assertNotNull(testBean); + } + + @Test + public void createAttributeRequestParameter() throws Exception { + this.request.addParameter("testBean1", "Patty"); + + // Type conversion from "Patty" to TestBean via TestBean(String) constructor + + TestBean testBean = + (TestBean) this.processor.resolveArgument( + this.testBeanModelAttr, this.mavContainer, this.webRequest, this.binderFactory); + + assertEquals("Patty", testBean.getName()); + } + + @Test + public void createAttributeRequestParameterCannotConvert() throws Exception { + this.request.addParameter("testBean1", "Patty"); + + TestBeanWithoutStringConstructor testBean = + (TestBeanWithoutStringConstructor) this.processor.resolveArgument( + this.testBeanWithoutStringConstructorModelAttr, this.mavContainer, this.webRequest, this.binderFactory); assertNotNull(testBean); } diff --git a/org.springframework.web/src/test/java/org/springframework/web/method/annotation/InitBinderDataBinderFactoryTests.java b/org.springframework.web/src/test/java/org/springframework/web/method/annotation/InitBinderDataBinderFactoryTests.java index f535cc6377..3b69854f05 100644 --- a/org.springframework.web/src/test/java/org/springframework/web/method/annotation/InitBinderDataBinderFactoryTests.java +++ b/org.springframework.web/src/test/java/org/springframework/web/method/annotation/InitBinderDataBinderFactoryTests.java @@ -99,6 +99,14 @@ public class InitBinderDataBinderFactoryTests { assertNull(dataBinder.getDisallowedFields()); } + @Test + public void createBinderNullAttrName() throws Exception { + WebDataBinderFactory factory = createBinderFactory("initBinderWithAttributeName", WebDataBinder.class); + WebDataBinder dataBinder = factory.createBinder(webRequest, null, null); + + assertNull(dataBinder.getDisallowedFields()); + } + @Test(expected=IllegalStateException.class) public void returnValueNotExpected() throws Exception { WebDataBinderFactory factory = createBinderFactory("initBinderReturnValue", WebDataBinder.class); diff --git a/spring-framework-reference/src/mvc.xml b/spring-framework-reference/src/mvc.xml index 4bb84eaea2..3f75114cae 100644 --- a/spring-framework-reference/src/mvc.xml +++ b/spring-framework-reference/src/mvc.xml @@ -1607,9 +1607,8 @@ public String save(@ModelAttribute("account") Account account) { In this example the name of the model attribute (i.e. "account") matches the name of a URI template variable. If you register - Converter<String, Account> - (or PropertyEditor) that can turn the - String-based account into an Account + Converter<String, Account> that can turn the + String account value into an Account instance, then the above example will work without the need for an @ModelAttribute method.