SPR-6464 Add 'setAlwaysUseRedirectAttributes' flag.

When set to 'true' the flag makes RedirectAttributes the only way to add 
attributes for a redirect thus ignoring the content of the default model 
even if RedirectAttributes is not in the list of controller method args.
This commit is contained in:
Rossen Stoyanchev 2011-09-13 07:53:17 +00:00
parent a456a1a0e3
commit 2799e710bc
8 changed files with 227 additions and 80 deletions

View File

@ -40,6 +40,7 @@ import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.http.converter.xml.SourceHttpMessageConverter;
import org.springframework.http.converter.xml.XmlAwareFormHttpMessageConverter;
import org.springframework.ui.Model;
import org.springframework.ui.ModelMap;
import org.springframework.util.ReflectionUtils.MethodFilter;
import org.springframework.validation.DataBinder;
@ -92,6 +93,7 @@ import org.springframework.web.servlet.mvc.method.annotation.support.ServletRequ
import org.springframework.web.servlet.mvc.method.annotation.support.ServletResponseMethodArgumentResolver;
import org.springframework.web.servlet.mvc.method.annotation.support.ViewMethodReturnValueHandler;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import org.springframework.web.servlet.mvc.support.RedirectAttributesModelMap;
import org.springframework.web.servlet.support.RequestContextUtils;
import org.springframework.web.util.WebUtils;
@ -145,6 +147,8 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter i
private SessionAttributeStore sessionAttributeStore = new DefaultSessionAttributeStore();
private boolean alwaysUseRedirectAttributes;
private final Map<Class<?>, SessionAttributesHandler> sessionAttributesHandlerCache =
new ConcurrentHashMap<Class<?>, SessionAttributesHandler>();
@ -329,6 +333,22 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter i
this.parameterNameDiscoverer = parameterNameDiscoverer;
}
/**
* By default a controller uses {@link Model} to select attributes for
* rendering and for redirecting. However, a controller can also use
* {@link RedirectAttributes} to select attributes before a redirect.
* <p>When this flag is set to {@code true}, {@link RedirectAttributes}
* becomes the only way to select attributes for a redirect.
* In other words, for a redirect a controller must use
* {@link RedirectAttributes} or no attributes will be used.
* <p>The default value is {@code false}, meaning the {@link Model} is
* used unless {@link RedirectAttributes} is used.
* @see RedirectAttributes
*/
public void setAlwaysUseRedirectAttributes(boolean alwaysUseRedirectAttributes) {
this.alwaysUseRedirectAttributes = alwaysUseRedirectAttributes;
}
public void setBeanFactory(BeanFactory beanFactory) {
if (beanFactory instanceof ConfigurableBeanFactory) {
this.beanFactory = (ConfigurableBeanFactory) beanFactory;
@ -510,13 +530,19 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter i
HandlerMethod handlerMethod) throws Exception {
ServletWebRequest webRequest = new ServletWebRequest(request, response);
ServletInvocableHandlerMethod requestMappingMethod = createRequestMappingMethod(handlerMethod);
ModelFactory modelFactory = getModelFactory(handlerMethod);
WebDataBinderFactory binderFactory = getDataBinderFactory(handlerMethod);
ServletInvocableHandlerMethod requestMappingMethod = createRequestMappingMethod(handlerMethod, binderFactory);
ModelFactory modelFactory = getModelFactory(handlerMethod, binderFactory);
ModelAndViewContainer mavContainer = new ModelAndViewContainer();
mavContainer.addAllAttributes(RequestContextUtils.getInputFlashMap(request));
modelFactory.initModel(webRequest, mavContainer, requestMappingMethod);
if (this.alwaysUseRedirectAttributes) {
DataBinder dataBinder = binderFactory.createBinder(webRequest, null, null);
mavContainer.setRedirectModel(new RedirectAttributesModelMap(dataBinder));
}
SessionStatus sessionStatus = new SimpleSessionStatus();
@ -536,23 +562,23 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter i
Map<String, ?> flashAttributes = ((RedirectAttributes) model).getFlashAttributes();
RequestContextUtils.getOutputFlashMap(request).putAll(flashAttributes);
}
return mav;
return mav;
}
}
private ServletInvocableHandlerMethod createRequestMappingMethod(HandlerMethod handlerMethod) {
ServletInvocableHandlerMethod requestMappingMethod =
new ServletInvocableHandlerMethod(handlerMethod.getBean(), handlerMethod.getMethod());
requestMappingMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers);
requestMappingMethod.setHandlerMethodReturnValueHandlers(this.returnValueHandlers);
requestMappingMethod.setDataBinderFactory(getDataBinderFactory(handlerMethod));
requestMappingMethod.setParameterNameDiscoverer(this.parameterNameDiscoverer);
return requestMappingMethod;
private ServletInvocableHandlerMethod createRequestMappingMethod(HandlerMethod handlerMethod,
WebDataBinderFactory binderFactory) {
ServletInvocableHandlerMethod requestMethod;
requestMethod = new ServletInvocableHandlerMethod(handlerMethod.getBean(), handlerMethod.getMethod());
requestMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers);
requestMethod.setHandlerMethodReturnValueHandlers(this.returnValueHandlers);
requestMethod.setDataBinderFactory(binderFactory);
requestMethod.setParameterNameDiscoverer(this.parameterNameDiscoverer);
return requestMethod;
}
private ModelFactory getModelFactory(HandlerMethod handlerMethod) {
private ModelFactory getModelFactory(HandlerMethod handlerMethod, WebDataBinderFactory binderFactory) {
SessionAttributesHandler sessionAttrHandler = getSessionAttributesHandler(handlerMethod);
WebDataBinderFactory binderFactory = getDataBinderFactory(handlerMethod);
Class<?> handlerType = handlerMethod.getBeanType();
ModelFactory modelFactory = this.modelFactoryCache.get(handlerType);
if (modelFactory == null) {

View File

@ -49,10 +49,16 @@ public class RedirectAttributesMethodArgumentResolver implements HandlerMethodAr
ModelAndViewContainer mavContainer,
NativeWebRequest webRequest,
WebDataBinderFactory binderFactory) throws Exception {
DataBinder dataBinder = binderFactory.createBinder(webRequest, null, null);
ModelMap attributes = new RedirectAttributesModelMap(dataBinder);
mavContainer.setRedirectModel(attributes);
return attributes;
if (mavContainer.getRedirectModel() != null) {
return mavContainer.getRedirectModel();
}
else {
DataBinder dataBinder = binderFactory.createBinder(webRequest, null, null);
ModelMap redirectAttributes = new RedirectAttributesModelMap(dataBinder);
mavContainer.setRedirectModel(redirectAttributes);
return redirectAttributes;
}
}
}

View File

@ -61,14 +61,14 @@ public class ViewMethodReturnValueHandler implements HandlerMethodReturnValueHan
String viewName = (String) returnValue;
mavContainer.setViewName(viewName);
if (isRedirectViewName(viewName)) {
mavContainer.setRedirectModelEnabled();
mavContainer.setUseRedirectModel();
}
}
else if (returnValue instanceof View){
View view = (View) returnValue;
mavContainer.setView(view);
if (isRedirectView(view)) {
mavContainer.setRedirectModelEnabled();
mavContainer.setUseRedirectModel();
}
}
else {

View File

@ -66,13 +66,14 @@ public class RedirectAttributesModelMap extends ModelMap implements RedirectAttr
* <p>Formats the attribute value as a String before adding it.
*/
public RedirectAttributesModelMap addAttribute(String attributeName, Object attributeValue) {
if (attributeValue != null) {
super.addAttribute(attributeName, formatValue(attributeValue));
}
super.addAttribute(attributeName, formatValue(attributeValue));
return this;
}
private String formatValue(Object value) {
if (value == null) {
return null;
}
return (dataBinder != null) ? dataBinder.convertIfNecessary(value, String.class) : value.toString();
}
@ -126,6 +127,28 @@ public class RedirectAttributesModelMap extends ModelMap implements RedirectAttr
return this;
}
/**
* {@inheritDoc}
* <p>The value is formatted as a String before being added.
*/
@Override
public Object put(String key, Object value) {
return super.put(key, formatValue(value));
}
/**
* {@inheritDoc}
* <p>Each value is formatted as a String before being added.
*/
@Override
public void putAll(Map<? extends String, ? extends Object> map) {
if (map != null) {
for (String key : map.keySet()) {
put(key, formatValue(map.get(key)));
}
}
}
public RedirectAttributes addFlashAttribute(String attributeName, Object attributeValue) {
this.flashAttributes.addAttribute(attributeName, attributeValue);
return this;

View File

@ -30,6 +30,7 @@ import org.springframework.beans.DirectFieldAccessor;
import org.springframework.core.MethodParameter;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.SessionAttributes;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
@ -42,19 +43,21 @@ import org.springframework.web.method.support.HandlerMethodReturnValueHandler;
import org.springframework.web.method.support.HandlerMethodReturnValueHandlerComposite;
import org.springframework.web.method.support.InvocableHandlerMethod;
import org.springframework.web.method.support.ModelAndViewContainer;
import org.springframework.web.servlet.FlashMap;
import org.springframework.web.servlet.FlashMapManager;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.method.annotation.support.RedirectAttributesMethodArgumentResolver;
import org.springframework.web.servlet.mvc.method.annotation.support.ServletRequestMethodArgumentResolver;
import org.springframework.web.servlet.mvc.method.annotation.support.ViewMethodReturnValueHandler;
/**
* Fine-grained {@link RequestMappingHandlerAdapter} unit tests.
*
* <p>For higher-level adapter tests see:
* <ul>
* <li>{@link ServletAnnotationControllerHandlerMethodTests}
* <li>{@link HandlerMethodAnnotationDetectionTests}
* <li>{@link RequestMappingHandlerAdapterIntegrationTests}
* </ul>
* Unit tests for {@link RequestMappingHandlerAdapter}.
*
* @author Rossen Stoyanchev
*
* @see ServletAnnotationControllerHandlerMethodTests
* @see HandlerMethodAnnotationDetectionTests
* @see RequestMappingHandlerAdapterIntegrationTests
*/
public class RequestMappingHandlerAdapterTests {
@ -68,35 +71,56 @@ public class RequestMappingHandlerAdapterTests {
public void setup() throws Exception {
this.handlerAdapter = new RequestMappingHandlerAdapter();
this.handlerAdapter.setApplicationContext(new GenericWebApplicationContext());
this.request = new MockHttpServletRequest();
this.response = new MockHttpServletResponse();
}
@Test
public void cacheControlWithoutSessionAttributes() throws Exception {
SimpleHandler handler = new SimpleHandler();
handlerAdapter.afterPropertiesSet();
handlerAdapter.setCacheSeconds(100);
handlerAdapter.handle(request, response, handlerMethod(new SimpleHandler(), "handle"));
handlerAdapter.handle(request, response, handlerMethod(handler, "handle"));
assertTrue(response.getHeader("Cache-Control").toString().contains("max-age"));
}
@Test
public void cacheControlWithSessionAttributes() throws Exception {
SessionAttributeHandler handler = new SessionAttributeHandler();
handlerAdapter.afterPropertiesSet();
handlerAdapter.setCacheSeconds(100);
handlerAdapter.handle(request, response, handlerMethod(new SessionAttributeHandler(), "handle"));
handlerAdapter.handle(request, response, handlerMethod(handler, "handle"));
assertEquals("no-cache", response.getHeader("Cache-Control"));
}
@Test
public void setAlwaysUseRedirectAttributes() throws Exception {
HandlerMethodArgumentResolver redirectAttributesResolver = new RedirectAttributesMethodArgumentResolver();
HandlerMethodArgumentResolver modelResolver = new ModelMethodProcessor();
HandlerMethodReturnValueHandler viewHandler = new ViewMethodReturnValueHandler();
handlerAdapter.setArgumentResolvers(Arrays.asList(redirectAttributesResolver, modelResolver));
handlerAdapter.setReturnValueHandlers(Arrays.asList(viewHandler));
handlerAdapter.setAlwaysUseRedirectAttributes(true);
handlerAdapter.afterPropertiesSet();
request.setAttribute(FlashMapManager.OUTPUT_FLASH_MAP_ATTRIBUTE, new FlashMap());
HandlerMethod handlerMethod = handlerMethod(new RedirectAttributeHandler(), "handle", Model.class);
ModelAndView mav = handlerAdapter.handle(request, response, handlerMethod);
assertTrue("No redirect attributes added, model should be empty", mav.getModel().isEmpty());
}
@Test
@SuppressWarnings("unchecked")
public void setArgumentResolvers() {
List<HandlerMethodArgumentResolver> expected = new ArrayList<HandlerMethodArgumentResolver>();
expected.add(new ServletRequestMethodArgumentResolver());
handlerAdapter.setArgumentResolvers(expected);
List<HandlerMethodArgumentResolver> argumentResolvers = new ArrayList<HandlerMethodArgumentResolver>();
argumentResolvers.add(new ServletRequestMethodArgumentResolver());
handlerAdapter.setArgumentResolvers(argumentResolvers);
handlerAdapter.afterPropertiesSet();
HandlerMethodArgumentResolverComposite composite = (HandlerMethodArgumentResolverComposite)
@ -105,15 +129,16 @@ public class RequestMappingHandlerAdapterTests {
List<HandlerMethodArgumentResolver> actual = (List<HandlerMethodArgumentResolver>)
new DirectFieldAccessor(composite).getPropertyValue("argumentResolvers");
assertEquals(expected, actual);
assertEquals(argumentResolvers, actual);
}
@Test
@SuppressWarnings("unchecked")
public void setInitBinderArgumentResolvers() {
List<HandlerMethodArgumentResolver> expected = new ArrayList<HandlerMethodArgumentResolver>();
expected.add(new ServletRequestMethodArgumentResolver());
handlerAdapter.setInitBinderArgumentResolvers(expected);
List<HandlerMethodArgumentResolver> argumentResolvers = new ArrayList<HandlerMethodArgumentResolver>();
argumentResolvers.add(new ServletRequestMethodArgumentResolver());
handlerAdapter.setInitBinderArgumentResolvers(argumentResolvers);
handlerAdapter.afterPropertiesSet();
HandlerMethodArgumentResolverComposite composite = (HandlerMethodArgumentResolverComposite)
@ -122,15 +147,16 @@ public class RequestMappingHandlerAdapterTests {
List<HandlerMethodArgumentResolver> actual = (List<HandlerMethodArgumentResolver>)
new DirectFieldAccessor(composite).getPropertyValue("argumentResolvers");
assertEquals(expected, actual);
assertEquals(argumentResolvers, actual);
}
@Test
@SuppressWarnings("unchecked")
public void setReturnValueHandlers() {
List<HandlerMethodReturnValueHandler> expected = new ArrayList<HandlerMethodReturnValueHandler>();
expected.add(new ModelMethodProcessor());
handlerAdapter.setReturnValueHandlers(expected);
HandlerMethodReturnValueHandler handler = new ModelMethodProcessor();
List<HandlerMethodReturnValueHandler> handlers = Arrays.asList(handler);
handlerAdapter.setReturnValueHandlers(handlers);
handlerAdapter.afterPropertiesSet();
HandlerMethodReturnValueHandlerComposite composite = (HandlerMethodReturnValueHandlerComposite)
@ -139,14 +165,14 @@ public class RequestMappingHandlerAdapterTests {
List<HandlerMethodReturnValueHandler> actual = (List<HandlerMethodReturnValueHandler>)
new DirectFieldAccessor(composite).getPropertyValue("returnValueHandlers");
assertEquals(expected, actual);
assertEquals(handlers, actual);
}
@Test
@SuppressWarnings("unchecked")
public void setCustomArgumentResolvers() {
TestHanderMethodArgumentResolver resolver = new TestHanderMethodArgumentResolver();
handlerAdapter.setCustomArgumentResolvers(Arrays.<HandlerMethodArgumentResolver>asList(resolver));
HandlerMethodArgumentResolver resolver = new TestHanderMethodArgumentResolver();
handlerAdapter.setCustomArgumentResolvers(Arrays.asList(resolver));
handlerAdapter.afterPropertiesSet();
HandlerMethodArgumentResolverComposite composite = (HandlerMethodArgumentResolverComposite)
@ -181,13 +207,14 @@ public class RequestMappingHandlerAdapterTests {
assertTrue(actual.contains(handler));
}
private HandlerMethod handlerMethod(Object handler, String methodName, Class<?>... paramTypes) throws Exception {
Method method = handler.getClass().getDeclaredMethod(methodName, paramTypes);
return new InvocableHandlerMethod(handler, method);
}
private final class TestHanderMethodArgumentResolver implements HandlerMethodArgumentResolver {
public boolean supportsParameter(MethodParameter parameter) {
return false;
}
@ -209,19 +236,23 @@ public class RequestMappingHandlerAdapterTests {
}
}
private static class SimpleHandler {
@SuppressWarnings("unused")
static class SimpleHandler {
public void handle() {
}
}
@SessionAttributes("attr1")
private static class SessionAttributeHandler {
@SuppressWarnings("unused")
static class SessionAttributeHandler {
public void handle() {
}
}
static class RedirectAttributeHandler {
public String handle(Model model) {
model.addAttribute("someAttr", "someAttrValue");
return "redirect:/path";
}
}
}

View File

@ -1456,8 +1456,8 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl
}
@Test
public void flashAttribute() throws Exception {
initServletWithControllers(MessageController.class);
public void redirectAttribute() throws Exception {
initServletWithControllers(RedirectAttributesController.class);
MockHttpServletRequest request = new MockHttpServletRequest("POST", "/messages");
HttpSession session = request.getSession();
@ -2803,7 +2803,7 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl
}
@Controller
static class MessageController {
static class RedirectAttributesController {
@InitBinder
public void initBinder(WebDataBinder dataBinder) {
@ -2827,8 +2827,7 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl
}
}
}
// Test cases deleted from the original SevletAnnotationControllerTests:
// @Ignore("Controller interface => no method-level @RequestMapping annotation")

View File

@ -119,6 +119,24 @@ public class RedirectAttributesModelMapTests {
assertEquals("33", this.redirectAttributes.get("age"));
}
@Test
public void put() {
this.redirectAttributes.put("testBean", new TestBean("Fred"));
assertEquals("Fred", this.redirectAttributes.get("testBean"));
}
@Test
public void putAll() {
Map<String, Object> map = new HashMap<String, Object>();
map.put("person", new TestBean("Fred"));
map.put("age", 33);
this.redirectAttributes.putAll(map);
assertEquals("Fred", this.redirectAttributes.get("person"));
assertEquals("33", this.redirectAttributes.get("age"));
}
public static class TestBeanConverter implements Converter<TestBean, String> {
public String convert(TestBean source) {

View File

@ -23,9 +23,19 @@ import org.springframework.ui.ModelMap;
import org.springframework.validation.support.BindingAwareModelMap;
/**
* Record model and view related decisions made by {@link HandlerMethodArgumentResolver}s
* and {@link HandlerMethodReturnValueHandler}s during the course of invocation of a
* request-handling method.
* Records model and view related decisions made by
* {@link HandlerMethodArgumentResolver}s and
* {@link HandlerMethodReturnValueHandler}s during the course of invocation of
* a controller method.
*
* <p>The {@link #setResolveView(boolean)} flag can be used to indicate that
* view resolution is not required (e.g. {@code @ResponseBody} method).
*
* <p>A default {@link Model} is created at instantiation and used thereafter.
* The {@link #setRedirectModel(ModelMap)} method can be used to provide a
* separate model to use potentially in case of a redirect.
* The {@link #setUseRedirectModel()} can be used to enable use of the
* redirect model if the controller decides to redirect.
*
* @author Rossen Stoyanchev
* @since 3.1
@ -40,7 +50,7 @@ public class ModelAndViewContainer {
private ModelMap redirectModel;
private boolean redirectModelEnabled;
private boolean useRedirectModel = false;
/**
* Create a new instance.
@ -89,12 +99,15 @@ public class ModelAndViewContainer {
}
/**
* Whether view resolution is required or not. The default value is "true".
* <p>When set to "false" by a {@link HandlerMethodReturnValueHandler}, the response
* is considered complete and view resolution is not be performed.
* <p>When set to "false" by {@link HandlerMethodArgumentResolver}, the response is
* considered complete only in combination with the request mapping method
* returning {@code null} or void.
* Whether view resolution is required or not.
* <p>A {@link HandlerMethodReturnValueHandler} may use this flag to
* indicate the response has been fully handled and view resolution
* is not required (e.g. {@code @ResponseBody}).
* <p>A {@link HandlerMethodArgumentResolver} may also use this flag
* to indicate the presence of an argument (e.g.
* {@code ServletResponse} or {@code OutputStream}) that may lead to
* a complete response depending on the method return value.
* <p>The default value is {@code true}.
*/
public void setResolveView(boolean resolveView) {
this.resolveView = resolveView;
@ -108,32 +121,42 @@ public class ModelAndViewContainer {
}
/**
* Return the model to use, never {@code null}.
* Return the default model created at instantiation or the one provided
* via {@link #setRedirectModel(ModelMap)} as long as it has been enabled
* via {@link #setUseRedirectModel()}.
*/
public ModelMap getModel() {
if (this.redirectModelEnabled && (this.redirectModel != null)) {
if ((this.redirectModel != null) && this.useRedirectModel) {
return this.redirectModel;
}
else {
return this.model;
}
}
/**
* Provide an alternative model that may be prepared for a specific redirect
* case. To enable use of this model, {@link #setRedirectModelEnabled()}
* must also be called.
* Provide a model instance to use in case the controller redirects.
* Note that {@link #setUseRedirectModel()} must also be called in order
* to enable use of the redirect model.
*/
public void setRedirectModel(ModelMap redirectModel) {
this.redirectModel = redirectModel;
}
/**
* Signals that a redirect model provided via {@link #setRedirectModel}
* may be used if it was provided.
* Return the redirect model provided via
* {@link #setRedirectModel(ModelMap)} or {@code null} if not provided.
*/
public void setRedirectModelEnabled() {
this.redirectModelEnabled = true;
public ModelMap getRedirectModel() {
return this.redirectModel;
}
/**
* Indicate that the redirect model provided via
* {@link #setRedirectModel(ModelMap)} should be used.
*/
public void setUseRedirectModel() {
this.useRedirectModel = true;
}
/**
@ -180,5 +203,26 @@ public class ModelAndViewContainer {
public boolean containsAttribute(String name) {
return getModel().containsAttribute(name);
}
/**
* Return diagnostic information.
*/
@Override
public String toString() {
StringBuilder sb = new StringBuilder("ModelAndViewContainer: ");
if (isResolveView()) {
if (isViewReference()) {
sb.append("reference to view with name '").append(this.view).append("'");
}
else {
sb.append("View is [").append(this.view).append(']');
}
sb.append("; model is ").append(getModel());
}
else {
sb.append("View resolution not required");
}
return sb.toString();
}
}