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.
This commit is contained in:
Rossen Stoyanchev 2011-09-26 09:27:09 +00:00
parent 6bc4ea058c
commit 48f7dcc464
7 changed files with 123 additions and 80 deletions

View File

@ -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 <code>null</code>
* 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 <code>null</code>
* 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<String, String>) request.getAttribute(attr));
}
}

View File

@ -676,7 +676,7 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter i
return modelFactory; return modelFactory;
} }
private WebDataBinderFactory getDataBinderFactory(HandlerMethod handlerMethod) { private WebDataBinderFactory getDataBinderFactory(HandlerMethod handlerMethod) throws Exception {
Class<?> handlerType = handlerMethod.getBeanType(); Class<?> handlerType = handlerMethod.getBeanType();
WebDataBinderFactory binderFactory = this.dataBinderFactoryCache.get(handlerType); WebDataBinderFactory binderFactory = this.dataBinderFactoryCache.get(handlerType);
if (binderFactory == null) { if (binderFactory == null) {
@ -688,12 +688,25 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter i
binderMethod.setParameterNameDiscoverer(this.parameterNameDiscoverer); binderMethod.setParameterNameDiscoverer(this.parameterNameDiscoverer);
binderMethods.add(binderMethod); binderMethods.add(binderMethod);
} }
binderFactory = new ServletRequestDataBinderFactory(binderMethods, this.webBindingInitializer); binderFactory = createDataBinderFactory(binderMethods);
this.dataBinderFactoryCache.put(handlerType, binderFactory); this.dataBinderFactoryCache.put(handlerType, binderFactory);
} }
return binderFactory; return binderFactory;
} }
/**
* Template method to create a new ServletRequestDataBinderFactory instance.
* <p>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<InvocableHandlerMethod> binderMethods)
throws Exception {
return new ServletRequestDataBinderFactory(binderMethods, getWebBindingInitializer());
}
/** /**
* MethodFilter that matches {@link InitBinder @InitBinder} methods. * MethodFilter that matches {@link InitBinder @InitBinder} methods.
*/ */

View File

@ -17,21 +17,15 @@
package org.springframework.web.servlet.mvc.method.annotation; package org.springframework.web.servlet.mvc.method.annotation;
import java.util.List; import java.util.List;
import java.util.Map;
import org.springframework.beans.MutablePropertyValues;
import org.springframework.web.bind.ServletRequestDataBinder; import org.springframework.web.bind.ServletRequestDataBinder;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.support.WebBindingInitializer; import org.springframework.web.bind.support.WebBindingInitializer;
import org.springframework.web.context.request.NativeWebRequest; 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.annotation.InitBinderDataBinderFactory;
import org.springframework.web.method.support.InvocableHandlerMethod; import org.springframework.web.method.support.InvocableHandlerMethod;
import org.springframework.web.servlet.HandlerMapping;
/** /**
* Creates a WebDataBinder of type {@link ServletRequestDataBinder} that can * Creates a {@code ServletRequestDataBinder}.
* also use URI template variables values for data binding purposes.
* *
* @author Rossen Stoyanchev * @author Rossen Stoyanchev
* @since 3.1 * @since 3.1
@ -43,45 +37,16 @@ public class ServletRequestDataBinderFactory extends InitBinderDataBinderFactory
* @param binderMethods one or more {@code @InitBinder} methods * @param binderMethods one or more {@code @InitBinder} methods
* @param initializer provides global data binder initialization * @param initializer provides global data binder initialization
*/ */
public ServletRequestDataBinderFactory(List<InvocableHandlerMethod> binderMethods, public ServletRequestDataBinderFactory(List<InvocableHandlerMethod> binderMethods, WebBindingInitializer initializer) {
WebBindingInitializer initializer) {
super(binderMethods, initializer); super(binderMethods, initializer);
} }
/** /**
* Create a WebDataBinder of type {@link ServletRequestDataBinder} that can * Returns an instance of {@link ExtendedServletRequestDataBinder}.
* also use URI template variables values for data binding purposes.
*/ */
@Override @Override
protected WebDataBinder createBinderInstance(Object target, String objectName, final NativeWebRequest request) { protected ServletRequestDataBinder createBinderInstance(Object target, String objectName, NativeWebRequest request) {
return new ServletRequestDataBinder(target, objectName) { return new ExtendedServletRequestDataBinder(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<String, String> uriTemplateVars =
(Map<String, String>) 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));
}
}
}
} }
} }

View File

@ -21,42 +21,26 @@ import static org.junit.Assert.assertEquals;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import org.junit.After;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.springframework.beans.TestBean; import org.springframework.beans.TestBean;
import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.web.bind.ServletRequestDataBinder; import org.springframework.web.bind.ServletRequestDataBinder;
import org.springframework.web.bind.WebDataBinder; 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; import org.springframework.web.servlet.HandlerMapping;
/** /**
* Test fixture with {@link ServletRequestDataBinderFactory}. * Test fixture for {@link ExtendedServletRequestDataBinder}.
* *
* @author Rossen Stoyanchev * @author Rossen Stoyanchev
*/ */
public class ServletRequestDataBinderFactoryTests { public class ExtendedServletRequestDataBinderTests {
private ServletRequestDataBinderFactory binderFactory;
private MockHttpServletRequest request; private MockHttpServletRequest request;
private NativeWebRequest webRequest;
@Before @Before
public void setup() { public void setup() {
binderFactory = new ServletRequestDataBinderFactory(null, null); this.request = new MockHttpServletRequest();
request = new MockHttpServletRequest();
webRequest = new ServletWebRequest(request);
RequestContextHolder.setRequestAttributes(webRequest);
}
@After
public void teardown() {
RequestContextHolder.resetRequestAttributes();
} }
@Test @Test
@ -67,7 +51,7 @@ public class ServletRequestDataBinderFactoryTests {
request.setAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, uriTemplateVars); request.setAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, uriTemplateVars);
TestBean target = new TestBean(); TestBean target = new TestBean();
WebDataBinder binder = binderFactory.createBinder(webRequest, target, ""); WebDataBinder binder = new ExtendedServletRequestDataBinder(target, "");
((ServletRequestDataBinder) binder).bind(request); ((ServletRequestDataBinder) binder).bind(request);
assertEquals("nameValue", target.getName()); assertEquals("nameValue", target.getName());
@ -75,7 +59,7 @@ public class ServletRequestDataBinderFactoryTests {
} }
@Test @Test
public void requestParamsOverrideUriTemplateVars() throws Exception { public void uriTemplateVarAndRequestParam() throws Exception {
request.addParameter("age", "35"); request.addParameter("age", "35");
Map<String, String> uriTemplateVars = new HashMap<String, String>(); Map<String, String> uriTemplateVars = new HashMap<String, String>();
@ -84,17 +68,17 @@ public class ServletRequestDataBinderFactoryTests {
request.setAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, uriTemplateVars); request.setAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, uriTemplateVars);
TestBean target = new TestBean(); TestBean target = new TestBean();
WebDataBinder binder = binderFactory.createBinder(webRequest, target, ""); WebDataBinder binder = new ExtendedServletRequestDataBinder(target, "");
((ServletRequestDataBinder) binder).bind(request); ((ServletRequestDataBinder) binder).bind(request);
assertEquals("nameValue", target.getName()); assertEquals("nameValue", target.getName());
assertEquals(35, target.getAge()); assertEquals(25, target.getAge());
} }
@Test @Test
public void noUriTemplateVars() throws Exception { public void noUriTemplateVars() throws Exception {
TestBean target = new TestBean(); TestBean target = new TestBean();
WebDataBinder binder = binderFactory.createBinder(webRequest, target, ""); WebDataBinder binder = new ExtendedServletRequestDataBinder(target, "");
((ServletRequestDataBinder) binder).bind(request); ((ServletRequestDataBinder) binder).bind(request);
assertEquals(null, target.getName()); assertEquals(null, target.getName());

View File

@ -108,9 +108,20 @@ public class ServletRequestDataBinder extends WebDataBinder {
if (multipartRequest != null) { if (multipartRequest != null) {
bindMultipart(multipartRequest.getMultiFileMap(), mpvs); bindMultipart(multipartRequest.getMultiFileMap(), mpvs);
} }
addBindValues(mpvs, request);
doBind(mpvs); 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. * Treats errors as fatal.
* <p>Use this method only if it's an error if the input isn't valid. * <p>Use this method only if it's an error if the input isn't valid.

View File

@ -41,10 +41,12 @@ public class DefaultDataBinderFactory implements WebDataBinderFactory {
/** /**
* Create a new {@link WebDataBinder} for the given target object and * Create a new {@link WebDataBinder} for the given target object and
* initialize it through a {@link WebBindingInitializer}. * 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); WebDataBinder dataBinder = createBinderInstance(target, objectName, webRequest);
if (initializer != null) { if (this.initializer != null) {
this.initializer.initBinder(dataBinder, webRequest); this.initializer.initBinder(dataBinder, webRequest);
} }
initBinder(dataBinder, webRequest); initBinder(dataBinder, webRequest);
@ -52,13 +54,15 @@ public class DefaultDataBinderFactory implements WebDataBinderFactory {
} }
/** /**
* Extension point to create the WebDataBinder instance, which is * Extension point to create the WebDataBinder instance.
* {@link WebRequestDataBinder} by default. * By default this is {@code WebRequestDataBinder}.
* @param target the binding target or {@code null} for type conversion only * @param target the binding target or {@code null} for type conversion only
* @param objectName the binding target object name * @param objectName the binding target object name
* @param webRequest the current request * @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); return new WebRequestDataBinder(target, objectName);
} }

View File

@ -58,12 +58,11 @@ public class InitBinderDataBinderFactory extends DefaultDataBinderFactory {
@Override @Override
public void initBinder(WebDataBinder binder, NativeWebRequest request) throws Exception { public void initBinder(WebDataBinder binder, NativeWebRequest request) throws Exception {
for (InvocableHandlerMethod binderMethod : this.binderMethods) { for (InvocableHandlerMethod binderMethod : this.binderMethods) {
if (!invokeInitBinderMethod(binderMethod, binder)) { if (isBinderMethodApplicable(binderMethod, binder)) {
continue; Object returnValue = binderMethod.invokeForRequest(request, null, binder);
} if (returnValue != null) {
Object returnValue = binderMethod.invokeForRequest(request, null, binder); throw new IllegalStateException("@InitBinder methods should return void: " + binderMethod);
if (returnValue != null) { }
throw new IllegalStateException("@InitBinder methods should return void: " + binderMethod);
} }
} }
} }
@ -74,8 +73,8 @@ public class InitBinderDataBinderFactory extends DefaultDataBinderFactory {
* <p>The default implementation checks if target object name is included * <p>The default implementation checks if target object name is included
* in the attribute names specified in the {@code @InitBinder} annotation. * in the attribute names specified in the {@code @InitBinder} annotation.
*/ */
protected boolean invokeInitBinderMethod(HandlerMethod binderMethod, WebDataBinder binder) { protected boolean isBinderMethodApplicable(HandlerMethod initBinderMethod, WebDataBinder binder) {
InitBinder annot = binderMethod.getMethodAnnotation(InitBinder.class); InitBinder annot = initBinderMethod.getMethodAnnotation(InitBinder.class);
Collection<String> names = Arrays.asList(annot.value()); Collection<String> names = Arrays.asList(annot.value());
return (names.size() == 0 || names.contains(binder.getObjectName())); return (names.size() == 0 || names.contains(binder.getObjectName()));
} }