From 48f7dcc464ac1b0b765d99bd86d5f699f26349bf Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Mon, 26 Sep 2011 09:27:09 +0000 Subject: [PATCH] POLISH CREATION OF DATA BINDERS FOR @RequestMapping METHODS Make it possible to hook in custom ServletRequestDataBinderFactory by overriding RequestMappingHandlerAdapter. Create ExtendedServletRequestDataBinder to add URI template vars to the binding values taking advantage of a new extension hook in ServletRequestDataBinder to provide additional values to bind. --- .../ExtendedServletRequestDataBinder.java | 67 +++++++++++++++++++ .../RequestMappingHandlerAdapter.java | 17 ++++- .../ServletRequestDataBinderFactory.java | 47 ++----------- ...xtendedServletRequestDataBinderTests.java} | 32 +++------ .../web/bind/ServletRequestDataBinder.java | 11 +++ .../support/DefaultDataBinderFactory.java | 14 ++-- .../InitBinderDataBinderFactory.java | 15 ++--- 7 files changed, 123 insertions(+), 80 deletions(-) create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ExtendedServletRequestDataBinder.java rename org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/method/annotation/{ServletRequestDataBinderFactoryTests.java => ExtendedServletRequestDataBinderTests.java} (68%) diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ExtendedServletRequestDataBinder.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ExtendedServletRequestDataBinder.java new file mode 100644 index 00000000000..fd83e2be922 --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ExtendedServletRequestDataBinder.java @@ -0,0 +1,67 @@ +/* + * Copyright 2002-2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.web.servlet.mvc.method.annotation; + +import java.util.Map; + +import javax.servlet.ServletRequest; + +import org.springframework.beans.MutablePropertyValues; +import org.springframework.web.bind.ServletRequestDataBinder; +import org.springframework.web.servlet.HandlerMapping; + +/** + * Subclass of {@link ServletRequestDataBinder} that adds URI template variables + * to the values used for data binding. + * + * @author Rossen Stoyanchev + * @since 3.1 + */ +public class ExtendedServletRequestDataBinder extends ServletRequestDataBinder { + + /** + * Create a new instance, with default object name. + * @param target the target object to bind onto (or null + * if the binder is just used to convert a plain parameter value) + * @see #DEFAULT_OBJECT_NAME + */ + public ExtendedServletRequestDataBinder(Object target) { + super(target); + } + + /** + * Create a new instance. + * @param target the target object to bind onto (or null + * if the binder is just used to convert a plain parameter value) + * @param objectName the name of the target object + * @see #DEFAULT_OBJECT_NAME + */ + public ExtendedServletRequestDataBinder(Object target, String objectName) { + super(target, objectName); + } + + /** + * Add URI template variables to the property values used for data binding. + */ + @Override + @SuppressWarnings("unchecked") + protected void addBindValues(MutablePropertyValues mpvs, ServletRequest request) { + String attr = HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE; + mpvs.addPropertyValues((Map) request.getAttribute(attr)); + } + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapter.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapter.java index 46ebe008287..6b48574531e 100644 --- a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapter.java +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapter.java @@ -676,7 +676,7 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter i return modelFactory; } - private WebDataBinderFactory getDataBinderFactory(HandlerMethod handlerMethod) { + private WebDataBinderFactory getDataBinderFactory(HandlerMethod handlerMethod) throws Exception { Class handlerType = handlerMethod.getBeanType(); WebDataBinderFactory binderFactory = this.dataBinderFactoryCache.get(handlerType); if (binderFactory == null) { @@ -688,12 +688,25 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter i binderMethod.setParameterNameDiscoverer(this.parameterNameDiscoverer); binderMethods.add(binderMethod); } - binderFactory = new ServletRequestDataBinderFactory(binderMethods, this.webBindingInitializer); + binderFactory = createDataBinderFactory(binderMethods); this.dataBinderFactoryCache.put(handlerType, binderFactory); } return binderFactory; } + /** + * Template method to create a new ServletRequestDataBinderFactory instance. + *

The default implementation creates a ServletRequestDataBinderFactory. + * This can be overridden for custom ServletRequestDataBinder subclasses. + * @param binderMethods {@code @InitBinder} methods + * @return the ServletRequestDataBinderFactory instance to use + * @throws Exception in case of invalid state or arguments + */ + protected ServletRequestDataBinderFactory createDataBinderFactory(List binderMethods) + throws Exception { + return new ServletRequestDataBinderFactory(binderMethods, getWebBindingInitializer()); + } + /** * MethodFilter that matches {@link InitBinder @InitBinder} methods. */ diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ServletRequestDataBinderFactory.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ServletRequestDataBinderFactory.java index d807cb55e99..f43a7c9f8ad 100644 --- a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ServletRequestDataBinderFactory.java +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ServletRequestDataBinderFactory.java @@ -17,21 +17,15 @@ package org.springframework.web.servlet.mvc.method.annotation; import java.util.List; -import java.util.Map; -import org.springframework.beans.MutablePropertyValues; import org.springframework.web.bind.ServletRequestDataBinder; -import org.springframework.web.bind.WebDataBinder; import org.springframework.web.bind.support.WebBindingInitializer; import org.springframework.web.context.request.NativeWebRequest; -import org.springframework.web.context.request.RequestAttributes; import org.springframework.web.method.annotation.InitBinderDataBinderFactory; import org.springframework.web.method.support.InvocableHandlerMethod; -import org.springframework.web.servlet.HandlerMapping; /** - * Creates a WebDataBinder of type {@link ServletRequestDataBinder} that can - * also use URI template variables values for data binding purposes. + * Creates a {@code ServletRequestDataBinder}. * * @author Rossen Stoyanchev * @since 3.1 @@ -43,45 +37,16 @@ public class ServletRequestDataBinderFactory extends InitBinderDataBinderFactory * @param binderMethods one or more {@code @InitBinder} methods * @param initializer provides global data binder initialization */ - public ServletRequestDataBinderFactory(List binderMethods, - WebBindingInitializer initializer) { + public ServletRequestDataBinderFactory(List binderMethods, WebBindingInitializer initializer) { super(binderMethods, initializer); } - + /** - * Create a WebDataBinder of type {@link ServletRequestDataBinder} that can - * also use URI template variables values for data binding purposes. + * Returns an instance of {@link ExtendedServletRequestDataBinder}. */ @Override - protected WebDataBinder createBinderInstance(Object target, String objectName, final NativeWebRequest request) { - return new ServletRequestDataBinder(target, objectName) { - @Override - protected void doBind(MutablePropertyValues mpvs) { - mergeUriTemplateVariables(mpvs, request); - super.doBind(mpvs); - } - }; - } - - /** - * Merge URI variable values into the given PropertyValues. - * @param mpvs the PropertyValues to add to - * @param request the current request - */ - @SuppressWarnings("unchecked") - protected final void mergeUriTemplateVariables(MutablePropertyValues mpvs, NativeWebRequest request) { - - Map uriTemplateVars = - (Map) request.getAttribute( - HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, RequestAttributes.SCOPE_REQUEST); - - if (uriTemplateVars != null){ - for (String variableName : uriTemplateVars.keySet()) { - if (!mpvs.contains(variableName)) { - mpvs.addPropertyValue(variableName, uriTemplateVars.get(variableName)); - } - } - } + protected ServletRequestDataBinder createBinderInstance(Object target, String objectName, NativeWebRequest request) { + return new ExtendedServletRequestDataBinder(target, objectName); } } diff --git a/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ServletRequestDataBinderFactoryTests.java b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ExtendedServletRequestDataBinderTests.java similarity index 68% rename from org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ServletRequestDataBinderFactoryTests.java rename to org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ExtendedServletRequestDataBinderTests.java index 10da357019e..f65e034af71 100644 --- a/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ServletRequestDataBinderFactoryTests.java +++ b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ExtendedServletRequestDataBinderTests.java @@ -21,42 +21,26 @@ import static org.junit.Assert.assertEquals; import java.util.HashMap; import java.util.Map; -import org.junit.After; import org.junit.Before; import org.junit.Test; import org.springframework.beans.TestBean; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.web.bind.ServletRequestDataBinder; import org.springframework.web.bind.WebDataBinder; -import org.springframework.web.context.request.NativeWebRequest; -import org.springframework.web.context.request.RequestContextHolder; -import org.springframework.web.context.request.ServletWebRequest; import org.springframework.web.servlet.HandlerMapping; /** - * Test fixture with {@link ServletRequestDataBinderFactory}. + * Test fixture for {@link ExtendedServletRequestDataBinder}. * * @author Rossen Stoyanchev */ -public class ServletRequestDataBinderFactoryTests { +public class ExtendedServletRequestDataBinderTests { - private ServletRequestDataBinderFactory binderFactory; - private MockHttpServletRequest request; - private NativeWebRequest webRequest; - @Before public void setup() { - binderFactory = new ServletRequestDataBinderFactory(null, null); - request = new MockHttpServletRequest(); - webRequest = new ServletWebRequest(request); - RequestContextHolder.setRequestAttributes(webRequest); - } - - @After - public void teardown() { - RequestContextHolder.resetRequestAttributes(); + this.request = new MockHttpServletRequest(); } @Test @@ -67,7 +51,7 @@ public class ServletRequestDataBinderFactoryTests { request.setAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, uriTemplateVars); TestBean target = new TestBean(); - WebDataBinder binder = binderFactory.createBinder(webRequest, target, ""); + WebDataBinder binder = new ExtendedServletRequestDataBinder(target, ""); ((ServletRequestDataBinder) binder).bind(request); assertEquals("nameValue", target.getName()); @@ -75,7 +59,7 @@ public class ServletRequestDataBinderFactoryTests { } @Test - public void requestParamsOverrideUriTemplateVars() throws Exception { + public void uriTemplateVarAndRequestParam() throws Exception { request.addParameter("age", "35"); Map uriTemplateVars = new HashMap(); @@ -84,17 +68,17 @@ public class ServletRequestDataBinderFactoryTests { request.setAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, uriTemplateVars); TestBean target = new TestBean(); - WebDataBinder binder = binderFactory.createBinder(webRequest, target, ""); + WebDataBinder binder = new ExtendedServletRequestDataBinder(target, ""); ((ServletRequestDataBinder) binder).bind(request); assertEquals("nameValue", target.getName()); - assertEquals(35, target.getAge()); + assertEquals(25, target.getAge()); } @Test public void noUriTemplateVars() throws Exception { TestBean target = new TestBean(); - WebDataBinder binder = binderFactory.createBinder(webRequest, target, ""); + WebDataBinder binder = new ExtendedServletRequestDataBinder(target, ""); ((ServletRequestDataBinder) binder).bind(request); assertEquals(null, target.getName()); diff --git a/org.springframework.web/src/main/java/org/springframework/web/bind/ServletRequestDataBinder.java b/org.springframework.web/src/main/java/org/springframework/web/bind/ServletRequestDataBinder.java index f488024d9b6..838cc633b2a 100644 --- a/org.springframework.web/src/main/java/org/springframework/web/bind/ServletRequestDataBinder.java +++ b/org.springframework.web/src/main/java/org/springframework/web/bind/ServletRequestDataBinder.java @@ -108,9 +108,20 @@ public class ServletRequestDataBinder extends WebDataBinder { if (multipartRequest != null) { bindMultipart(multipartRequest.getMultiFileMap(), mpvs); } + addBindValues(mpvs, request); doBind(mpvs); } + /** + * Extension point that subclasses can use to add extra bind values for a + * request. Invoked before {@link #doBind(MutablePropertyValues)}. + * The default implementation is empty. + * @param mpvs the property values that will be used for data binding + * @param request the current request + */ + protected void addBindValues(MutablePropertyValues mpvs, ServletRequest request) { + } + /** * Treats errors as fatal. *

Use this method only if it's an error if the input isn't valid. diff --git a/org.springframework.web/src/main/java/org/springframework/web/bind/support/DefaultDataBinderFactory.java b/org.springframework.web/src/main/java/org/springframework/web/bind/support/DefaultDataBinderFactory.java index 55e7d2db0ab..a0ddc9713fc 100644 --- a/org.springframework.web/src/main/java/org/springframework/web/bind/support/DefaultDataBinderFactory.java +++ b/org.springframework.web/src/main/java/org/springframework/web/bind/support/DefaultDataBinderFactory.java @@ -41,10 +41,12 @@ public class DefaultDataBinderFactory implements WebDataBinderFactory { /** * Create a new {@link WebDataBinder} for the given target object and * initialize it through a {@link WebBindingInitializer}. + * @throws Exception in case of invalid state or arguments */ - public final WebDataBinder createBinder(NativeWebRequest webRequest, Object target, String objectName) throws Exception { + public final WebDataBinder createBinder(NativeWebRequest webRequest, Object target, String objectName) + throws Exception { WebDataBinder dataBinder = createBinderInstance(target, objectName, webRequest); - if (initializer != null) { + if (this.initializer != null) { this.initializer.initBinder(dataBinder, webRequest); } initBinder(dataBinder, webRequest); @@ -52,13 +54,15 @@ public class DefaultDataBinderFactory implements WebDataBinderFactory { } /** - * Extension point to create the WebDataBinder instance, which is - * {@link WebRequestDataBinder} by default. + * Extension point to create the WebDataBinder instance. + * By default this is {@code WebRequestDataBinder}. * @param target the binding target or {@code null} for type conversion only * @param objectName the binding target object name * @param webRequest the current request + * @throws Exception in case of invalid state or arguments */ - protected WebDataBinder createBinderInstance(Object target, String objectName, NativeWebRequest webRequest) { + protected WebDataBinder createBinderInstance(Object target, String objectName, NativeWebRequest webRequest) + throws Exception { return new WebRequestDataBinder(target, objectName); } diff --git a/org.springframework.web/src/main/java/org/springframework/web/method/annotation/InitBinderDataBinderFactory.java b/org.springframework.web/src/main/java/org/springframework/web/method/annotation/InitBinderDataBinderFactory.java index a85fda6fa4d..991ab9ff862 100644 --- a/org.springframework.web/src/main/java/org/springframework/web/method/annotation/InitBinderDataBinderFactory.java +++ b/org.springframework.web/src/main/java/org/springframework/web/method/annotation/InitBinderDataBinderFactory.java @@ -58,12 +58,11 @@ public class InitBinderDataBinderFactory extends DefaultDataBinderFactory { @Override public void initBinder(WebDataBinder binder, NativeWebRequest request) throws Exception { for (InvocableHandlerMethod binderMethod : this.binderMethods) { - if (!invokeInitBinderMethod(binderMethod, binder)) { - continue; - } - Object returnValue = binderMethod.invokeForRequest(request, null, binder); - if (returnValue != null) { - throw new IllegalStateException("@InitBinder methods should return void: " + binderMethod); + if (isBinderMethodApplicable(binderMethod, binder)) { + Object returnValue = binderMethod.invokeForRequest(request, null, binder); + if (returnValue != null) { + throw new IllegalStateException("@InitBinder methods should return void: " + binderMethod); + } } } } @@ -74,8 +73,8 @@ public class InitBinderDataBinderFactory extends DefaultDataBinderFactory { *

The default implementation checks if target object name is included * in the attribute names specified in the {@code @InitBinder} annotation. */ - protected boolean invokeInitBinderMethod(HandlerMethod binderMethod, WebDataBinder binder) { - InitBinder annot = binderMethod.getMethodAnnotation(InitBinder.class); + protected boolean isBinderMethodApplicable(HandlerMethod initBinderMethod, WebDataBinder binder) { + InitBinder annot = initBinderMethod.getMethodAnnotation(InitBinder.class); Collection names = Arrays.asList(annot.value()); return (names.size() == 0 || names.contains(binder.getObjectName())); }