SPR-6464 Drop @FlashAttributes, add ResponseContext, ViewResponse, and RedirectResponse types for annotated controllers to use to prepare a redirect response with flash attributes; Add FlashMap and FlashMapManager and update DispatcherServlet to discover and invoke the FlashMapManager.

This commit is contained in:
Rossen Stoyanchev 2011-08-08 14:00:07 +00:00
parent 11597c906d
commit 1df0cd9f20
28 changed files with 1171 additions and 724 deletions

View File

@ -29,6 +29,7 @@ import java.util.Map;
import java.util.Properties;
import java.util.Set;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@ -52,6 +53,7 @@ import org.springframework.web.context.request.ServletWebRequest;
import org.springframework.web.multipart.MultipartException;
import org.springframework.web.multipart.MultipartHttpServletRequest;
import org.springframework.web.multipart.MultipartResolver;
import org.springframework.web.servlet.support.RequestContextUtils;
import org.springframework.web.util.NestedServletException;
import org.springframework.web.util.UrlPathHelper;
import org.springframework.web.util.WebUtils;
@ -178,6 +180,11 @@ public class DispatcherServlet extends FrameworkServlet {
*/
public static final String VIEW_RESOLVER_BEAN_NAME = "viewResolver";
/**
* Well-known name for the FlashMapManager object in the bean factory for this namespace.
*/
public static final String FLASH_MAP_MANAGER_BEAN_NAME = "flashMapManager";
/**
* Request attribute to hold the current web application context.
* Otherwise only the global web app context is obtainable by tags etc.
@ -202,7 +209,7 @@ public class DispatcherServlet extends FrameworkServlet {
* @see org.springframework.web.servlet.support.RequestContextUtils#getThemeSource
*/
public static final String THEME_SOURCE_ATTRIBUTE = DispatcherServlet.class.getName() + ".THEME_SOURCE";
/** Log category to use when no mapped handler is found for a request. */
public static final String PAGE_NOT_FOUND_LOG_CATEGORY = "org.springframework.web.servlet.PageNotFound";
@ -269,6 +276,9 @@ public class DispatcherServlet extends FrameworkServlet {
/** RequestToViewNameTranslator used by this servlet */
private RequestToViewNameTranslator viewNameTranslator;
/** FlashMapManager used by this servlet */
private FlashMapManager flashMapManager;
/** List of ViewResolvers used by this servlet */
private List<ViewResolver> viewResolvers;
@ -414,6 +424,7 @@ public class DispatcherServlet extends FrameworkServlet {
initHandlerExceptionResolvers(context);
initRequestToViewNameTranslator(context);
initViewResolvers(context);
initFlashMapManager(context);
}
/**
@ -659,6 +670,28 @@ public class DispatcherServlet extends FrameworkServlet {
}
}
/**
* Initialize the {@link FlashMapManager} used by this servlet instance.
* <p>If no implementation is configured then we default to DefaultFlashMapManager.
*/
private void initFlashMapManager(ApplicationContext context) {
try {
this.flashMapManager =
context.getBean(FLASH_MAP_MANAGER_BEAN_NAME, FlashMapManager.class);
if (logger.isDebugEnabled()) {
logger.debug("Using FlashMapManager [" + this.flashMapManager + "]");
}
}
catch (NoSuchBeanDefinitionException ex) {
// We need to use the default.
this.flashMapManager = getDefaultStrategy(context, FlashMapManager.class);
if (logger.isDebugEnabled()) {
logger.debug("Unable to locate FlashMapManager with name '" +
FLASH_MAP_MANAGER_BEAN_NAME + "': using default [" + this.flashMapManager + "]");
}
}
}
/**
* Return this servlet's ThemeSource, if any; else return <code>null</code>.
* <p>Default is to return the WebApplicationContext as ThemeSource,
@ -782,6 +815,8 @@ public class DispatcherServlet extends FrameworkServlet {
}
}
boolean flashInitialized = this.flashMapManager.requestStarted(request);
// Make framework objects available to handlers and view objects.
request.setAttribute(WEB_APPLICATION_CONTEXT_ATTRIBUTE, getWebApplicationContext());
request.setAttribute(LOCALE_RESOLVER_ATTRIBUTE, this.localeResolver);
@ -792,6 +827,9 @@ public class DispatcherServlet extends FrameworkServlet {
doDispatch(request, response);
}
finally {
if (flashInitialized) {
this.flashMapManager.requestCompleted(request);
}
// Restore the original attribute snapshot, in case of an include.
if (attributesSnapshot != null) {
restoreAttributesAfterInclude(request, attributesSnapshot);

View File

@ -20,3 +20,5 @@ org.springframework.web.servlet.HandlerExceptionResolver=org.springframework.web
org.springframework.web.servlet.RequestToViewNameTranslator=org.springframework.web.servlet.view.DefaultRequestToViewNameTranslator
org.springframework.web.servlet.ViewResolver=org.springframework.web.servlet.view.InternalResourceViewResolver
org.springframework.web.servlet.FlashMapManager=org.springframework.web.servlet.support.DefaultFlashMapManager

View File

@ -0,0 +1,97 @@
/*
* 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;
import org.springframework.ui.ModelMap;
import org.springframework.util.Assert;
/**
* Stores attributes that need to be made available in the next request.
*
* @author Rossen Stoyanchev
* @since 3.1
*/
public class FlashMap extends ModelMap {
private static final long serialVersionUID = 1L;
private final String key;
private final String keyParameterName;
private long expirationStartTime;
private int timeToLive;
/**
* Create a FlashMap with a unique key.
*/
public FlashMap(String key, String keyParameterName) {
Assert.notNull("The key is required", key);
Assert.notNull("The key parameter name is required", keyParameterName);
this.key = key;
this.keyParameterName = keyParameterName;
}
/**
* Create a FlashMap without a key.
*/
public FlashMap() {
this.key = null;
this.keyParameterName = null;
}
/**
* Return the key assigned to this FlashMap instance;
* or {@code null} if a unique key has not been assigned.
*/
public String getKey() {
return this.key;
}
/**
* Return the name of the request parameter to use when appending the flash
* key to a redirect URL.
*/
public String getKeyParameterName() {
return keyParameterName;
}
/**
* Start the expiration period for this instance. After the given number of
* seconds calls to {@link #isExpired()} will return "true".
* @param timeToLive the number of seconds before flash map expires
*/
public void startExpirationPeriod(int timeToLive) {
this.expirationStartTime = System.currentTimeMillis();
this.timeToLive = timeToLive;
}
/**
* Whether the flash map has expired depending on the number of seconds
* elapsed since the call to {@link #startExpirationPeriod}.
*/
public boolean isExpired() {
if (this.expirationStartTime != 0) {
return (System.currentTimeMillis() - this.expirationStartTime) > this.timeToLive * 1000;
}
else {
return false;
}
}
}

View File

@ -0,0 +1,72 @@
/*
* 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;
import javax.servlet.http.HttpServletRequest;
import org.springframework.web.servlet.support.RequestContextUtils;
/**
* A strategy interface for maintaining {@link FlashMap} instances in some
* underlying storage between two requests. This is typically used when
* redirecting from one URL to another.
*
* TODO ...
*
* @author Rossen Stoyanchev
* @since 3.1
*
* @see FlashMap
*/
public interface FlashMapManager {
/**
* Request attribute to hold the current request FlashMap.
* @see RequestContextUtils#getFlashMap
*/
public static final String CURRENT_FLASH_MAP_ATTRIBUTE = DispatcherServlet.class.getName() + ".CURRENT_FLASH_MAP";
/**
* Request attribute to hold the FlashMap from the previous request.
*/
public static final String PREVIOUS_FLASH_MAP_ATTRIBUTE = DispatcherServlet.class.getName() + ".PREVIOUS_FLASH_MAP";
/**
* Perform flash storage tasks at the start of a new request:
* <ul>
* <li>Create a new FlashMap and make it available to the current request
* under the request attribute {@link #CURRENT_FLASH_MAP_ATTRIBUTE}.
* <li>Locate the FlashMap saved on the previous request and expose its
* contents as attributes in the current request.
* <li>Remove expired flash map instances.
* </ul>
*
* @param request the current request
*
* @return "true" if flash storage tasks were performed; "false" otherwise
* if the {@link #CURRENT_FLASH_MAP_ATTRIBUTE} request attribute exists.
*/
boolean requestStarted(HttpServletRequest request);
/**
* Access the current FlashMap through the {@link #CURRENT_FLASH_MAP_ATTRIBUTE}
* request attribute and if not empty, save it in the underlying storage. This
* method should be invoked after {@link #requestStarted} and if it returned "true".
*/
void requestCompleted(HttpServletRequest request);
}

View File

@ -60,6 +60,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.ServletWebArgumentResolverAdapter;
import org.springframework.web.servlet.mvc.method.annotation.support.ViewMethodReturnValueHandler;
import org.springframework.web.servlet.mvc.method.support.ResponseContext;
/**
* An {@link AbstractHandlerMethodExceptionResolver} that supports using {@link ExceptionHandler}-annotated methods
@ -231,7 +232,9 @@ public class ExceptionHandlerExceptionResolver extends AbstractHandlerMethodExce
}
ModelAndViewContainer mavContainer = new ModelAndViewContainer();
exceptionHandler.invokeAndHandle(webRequest, mavContainer, ex);
ResponseContext responseContext = new ResponseContext(webRequest, mavContainer);
exceptionHandler.invokeAndHandle(webRequest, mavContainer, ex, responseContext);
if (!mavContainer.isResolveView()) {
return new ModelAndView();
@ -239,7 +242,7 @@ public class ExceptionHandlerExceptionResolver extends AbstractHandlerMethodExce
else {
ModelAndView mav = new ModelAndView().addAllObjects(mavContainer.getModel());
mav.setViewName(mavContainer.getViewName());
if (mavContainer.getView() != null) {
if (!mavContainer.isViewReference()) {
mav.setView((View) mavContainer.getView());
}
return mav;

View File

@ -20,7 +20,6 @@ import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import javax.servlet.http.HttpServletRequest;
@ -47,13 +46,10 @@ import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.InitBinder;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.SessionAttributes;
import org.springframework.web.bind.support.DefaultDataBinderFactory;
import org.springframework.web.bind.support.DefaultSessionAttributeStore;
import org.springframework.web.bind.support.FlashStatus;
import org.springframework.web.bind.support.SessionAttributeStore;
import org.springframework.web.bind.support.SessionStatus;
import org.springframework.web.bind.support.SimpleFlashStatus;
import org.springframework.web.bind.support.SimpleSessionStatus;
import org.springframework.web.bind.support.WebBindingInitializer;
import org.springframework.web.bind.support.WebDataBinderFactory;
@ -61,7 +57,6 @@ import org.springframework.web.context.request.ServletWebRequest;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.method.HandlerMethodSelector;
import org.springframework.web.method.annotation.FlashAttributesHandler;
import org.springframework.web.method.annotation.ModelFactory;
import org.springframework.web.method.annotation.SessionAttributesHandler;
import org.springframework.web.method.annotation.support.ErrorsMethodArgumentResolver;
@ -78,6 +73,8 @@ 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.View;
import org.springframework.web.servlet.mvc.LastModified;
@ -94,6 +91,7 @@ import org.springframework.web.servlet.mvc.method.annotation.support.ServletMode
import org.springframework.web.servlet.mvc.method.annotation.support.ServletRequestMethodArgumentResolver;
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.method.support.ResponseContext;
import org.springframework.web.util.WebUtils;
/**
@ -149,18 +147,16 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter i
private final Map<Class<?>, SessionAttributesHandler> sessionAttributesHandlerCache =
new ConcurrentHashMap<Class<?>, SessionAttributesHandler>();
private final Map<Class<?>, FlashAttributesHandler> flashAttributesHandlerCache =
new ConcurrentHashMap<Class<?>, FlashAttributesHandler>();
private HandlerMethodArgumentResolverComposite argumentResolvers;
private HandlerMethodArgumentResolverComposite initBinderArgumentResolvers;
private HandlerMethodReturnValueHandlerComposite returnValueHandlers;
private final Map<Class<?>, Set<Method>> initBinderMethodCache = new ConcurrentHashMap<Class<?>, Set<Method>>();
private final Map<Class<?>, WebDataBinderFactory> dataBinderFactoryCache =
new ConcurrentHashMap<Class<?>, WebDataBinderFactory>();
private final Map<Class<?>, Set<Method>> modelAttributeMethodCache = new ConcurrentHashMap<Class<?>, Set<Method>>();
private final Map<Class<?>, ModelFactory> modelFactoryCache = new ConcurrentHashMap<Class<?>, ModelFactory>();
/**
* Create a {@link RequestMappingHandlerAdapter} instance.
@ -372,7 +368,7 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter i
argumentResolvers.addResolver(new HttpEntityMethodProcessor(messageConverters));
argumentResolvers.addResolver(new ModelMethodProcessor());
argumentResolvers.addResolver(new ErrorsMethodArgumentResolver());
// Default-mode resolution
argumentResolvers.addResolver(new RequestParamMethodArgumentResolver(beanFactory, true));
argumentResolvers.addResolver(new ServletModelAttributeMethodProcessor(true));
@ -462,8 +458,8 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter i
protected final ModelAndView handleInternal(HttpServletRequest request,
HttpServletResponse response,
HandlerMethod handlerMethod) throws Exception {
if (hasSessionAttributes(handlerMethod.getBeanType())) {
if (getSessionAttributesHandler(handlerMethod).hasSessionAttributes()) {
// Always prevent caching in case of session attribute management.
checkAndPrepare(request, response, this.cacheSecondsForSessionAttributeHandlers, true);
}
@ -487,118 +483,106 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter i
}
/**
* Whether the given handler type defines any handler-specific session attributes
* via {@link SessionAttributes}.
* Return the {@link SessionAttributesHandler} instance for the given
* handler type, never {@code null}.
*/
private boolean hasSessionAttributes(Class<?> handlerType) {
SessionAttributesHandler sessionAttrsHandler = null;
synchronized(this.sessionAttributesHandlerCache) {
sessionAttrsHandler = this.sessionAttributesHandlerCache.get(handlerType);
if (sessionAttrsHandler == null) {
sessionAttrsHandler = new SessionAttributesHandler(handlerType, sessionAttributeStore);
this.sessionAttributesHandlerCache.put(handlerType, sessionAttrsHandler);
private SessionAttributesHandler getSessionAttributesHandler(HandlerMethod handlerMethod) {
Class<?> handlerType = handlerMethod.getBeanType();
SessionAttributesHandler sessionAttrHandler = this.sessionAttributesHandlerCache.get(handlerType);
if (sessionAttrHandler == null) {
synchronized(this.sessionAttributesHandlerCache) {
sessionAttrHandler = this.sessionAttributesHandlerCache.get(handlerType);
if (sessionAttrHandler == null) {
sessionAttrHandler = new SessionAttributesHandler(handlerType, sessionAttributeStore);
this.sessionAttributesHandlerCache.put(handlerType, sessionAttrHandler);
}
}
}
FlashAttributesHandler flashAttrsHandler = null;
synchronized(this.flashAttributesHandlerCache) {
flashAttrsHandler = this.flashAttributesHandlerCache.get(handlerType);
if (flashAttrsHandler == null) {
flashAttrsHandler = new FlashAttributesHandler(handlerType);
this.flashAttributesHandlerCache.put(handlerType, flashAttrsHandler);
}
}
return sessionAttrsHandler.hasSessionAttributes() || flashAttrsHandler.hasFlashAttributes();
return sessionAttrHandler;
}
/**
* Invoke the {@link RequestMapping} handler method preparing a {@link ModelAndView} if view resolution is required.
*/
private ModelAndView invokeHandlerMethod(HttpServletRequest request,
HttpServletResponse response,
HandlerMethod handlerMethod) throws Exception {
private ModelAndView invokeHandlerMethod(HttpServletRequest request, HttpServletResponse response,
HandlerMethod handlerMethod) throws Exception {
WebDataBinderFactory binderFactory = createDataBinderFactory(handlerMethod);
ModelFactory modelFactory = createModelFactory(handlerMethod, binderFactory);
ServletInvocableHandlerMethod requestMethod = createRequestMappingMethod(handlerMethod, binderFactory);
ServletWebRequest webRequest = new ServletWebRequest(request, response);
SessionStatus sessionStatus = new SimpleSessionStatus();
FlashStatus flashStatus = new SimpleFlashStatus();
ServletInvocableHandlerMethod requestMappingMethod = createRequestMappingMethod(handlerMethod);
ModelFactory modelFactory = getModelFactory(handlerMethod);
FlashMap flashMap = (FlashMap) request.getAttribute(FlashMapManager.PREVIOUS_FLASH_MAP_ATTRIBUTE);
ModelAndViewContainer mavContainer = new ModelAndViewContainer();
modelFactory.initModel(webRequest, mavContainer, requestMethod);
mavContainer.addAllAttributes(flashMap);
modelFactory.initModel(webRequest, mavContainer, requestMappingMethod);
requestMethod.invokeAndHandle(webRequest, mavContainer, sessionStatus, flashStatus);
modelFactory.updateModel(webRequest, mavContainer, sessionStatus, flashStatus);
SessionStatus sessionStatus = new SimpleSessionStatus();
ResponseContext responseContext = new ResponseContext(webRequest, mavContainer);
requestMappingMethod.invokeAndHandle(webRequest, mavContainer, sessionStatus, responseContext);
modelFactory.updateModel(webRequest, mavContainer, sessionStatus);
if (!mavContainer.isResolveView()) {
return null;
}
else {
ModelAndView mav = new ModelAndView().addAllObjects(mavContainer.getModel());
mav.setViewName(mavContainer.getViewName());
if (mavContainer.getView() != null) {
if (!mavContainer.isViewReference()) {
mav.setView((View) mavContainer.getView());
}
return mav;
}
}
private WebDataBinderFactory createDataBinderFactory(HandlerMethod handlerMethod) {
List<InvocableHandlerMethod> initBinderMethods = new ArrayList<InvocableHandlerMethod>();
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 ModelFactory getModelFactory(HandlerMethod handlerMethod) {
SessionAttributesHandler sessionAttrHandler = getSessionAttributesHandler(handlerMethod);
WebDataBinderFactory binderFactory = getDataBinderFactory(handlerMethod);
Class<?> handlerType = handlerMethod.getBeanType();
Set<Method> binderMethods = initBinderMethodCache.get(handlerType);
if (binderMethods == null) {
binderMethods = HandlerMethodSelector.selectMethods(handlerType, INIT_BINDER_METHODS);
initBinderMethodCache.put(handlerType, binderMethods);
ModelFactory modelFactory = this.modelFactoryCache.get(handlerType);
if (modelFactory == null) {
List<InvocableHandlerMethod> attrMethods = new ArrayList<InvocableHandlerMethod>();
for (Method method : HandlerMethodSelector.selectMethods(handlerType, MODEL_ATTRIBUTE_METHODS)) {
InvocableHandlerMethod attrMethod = new InvocableHandlerMethod(handlerMethod.getBean(), method);
attrMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers);
attrMethod.setParameterNameDiscoverer(this.parameterNameDiscoverer);
attrMethod.setDataBinderFactory(binderFactory);
attrMethods.add(attrMethod);
}
modelFactory = new ModelFactory(attrMethods, binderFactory, sessionAttrHandler);
this.modelFactoryCache.put(handlerType, modelFactory);
}
for (Method method : binderMethods) {
Object bean = handlerMethod.getBean();
InvocableHandlerMethod binderMethod = new InvocableHandlerMethod(bean, method);
binderMethod.setHandlerMethodArgumentResolvers(this.initBinderArgumentResolvers);
binderMethod.setDataBinderFactory(new DefaultDataBinderFactory(this.webBindingInitializer));
binderMethod.setParameterNameDiscoverer(this.parameterNameDiscoverer);
initBinderMethods.add(binderMethod);
}
return new ServletRequestDataBinderFactory(initBinderMethods, this.webBindingInitializer);
return modelFactory;
}
private ModelFactory createModelFactory(HandlerMethod handlerMethod, WebDataBinderFactory binderFactory) {
List<InvocableHandlerMethod> modelAttrMethods = new ArrayList<InvocableHandlerMethod>();
private WebDataBinderFactory getDataBinderFactory(HandlerMethod handlerMethod) {
Class<?> handlerType = handlerMethod.getBeanType();
Set<Method> attributeMethods = modelAttributeMethodCache.get(handlerType);
if (attributeMethods == null) {
attributeMethods = HandlerMethodSelector.selectMethods(handlerType, MODEL_ATTRIBUTE_METHODS);
modelAttributeMethodCache.put(handlerType, attributeMethods);
WebDataBinderFactory binderFactory = this.dataBinderFactoryCache.get(handlerType);
if (binderFactory == null) {
List<InvocableHandlerMethod> binderMethods = new ArrayList<InvocableHandlerMethod>();
for (Method method : HandlerMethodSelector.selectMethods(handlerType, INIT_BINDER_METHODS)) {
InvocableHandlerMethod binderMethod = new InvocableHandlerMethod(handlerMethod.getBean(), method);
binderMethod.setHandlerMethodArgumentResolvers(this.initBinderArgumentResolvers);
binderMethod.setDataBinderFactory(new DefaultDataBinderFactory(this.webBindingInitializer));
binderMethod.setParameterNameDiscoverer(this.parameterNameDiscoverer);
binderMethods.add(binderMethod);
}
binderFactory = new ServletRequestDataBinderFactory(binderMethods, this.webBindingInitializer);
this.dataBinderFactoryCache.put(handlerType, binderFactory);
}
for (Method method : attributeMethods) {
InvocableHandlerMethod attrMethod = new InvocableHandlerMethod(handlerMethod.getBean(), method);
attrMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers);
attrMethod.setDataBinderFactory(binderFactory);
attrMethod.setParameterNameDiscoverer(this.parameterNameDiscoverer);
modelAttrMethods.add(attrMethod);
}
SessionAttributesHandler sessionAttrsHandler = sessionAttributesHandlerCache.get(handlerType);
FlashAttributesHandler flashAttrsHandler = flashAttributesHandlerCache.get(handlerType);
return new ModelFactory(modelAttrMethods, binderFactory, sessionAttrsHandler, flashAttrsHandler);
}
private ServletInvocableHandlerMethod createRequestMappingMethod(HandlerMethod handlerMethod,
WebDataBinderFactory binderFactory) {
Method method = handlerMethod.getMethod();
ServletInvocableHandlerMethod requestMethod = new ServletInvocableHandlerMethod(handlerMethod.getBean(), method);
requestMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers);
requestMethod.setHandlerMethodReturnValueHandlers(this.returnValueHandlers);
requestMethod.setDataBinderFactory(binderFactory);
requestMethod.setParameterNameDiscoverer(this.parameterNameDiscoverer);
return requestMethod;
return binderFactory;
}
/**

View File

@ -17,7 +17,6 @@
package org.springframework.web.servlet.mvc.method.annotation.support;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import org.springframework.beans.BeanUtils;
@ -84,9 +83,11 @@ public class DefaultMethodReturnValueHandler implements HandlerMethodReturnValue
ExtendedModelMap model = (ExtendedModelMap) mavContainer.getModel();
ModelAndView mav = resolver.resolveModelAndView(method, handlerType, returnValue, model, request);
if (mav != ModelAndViewResolver.UNRESOLVED) {
mavContainer.setView(mav.getView());
mavContainer.setViewName(mav.getViewName());
mavContainer.addAllAttributes(mav.getModel());
mavContainer.setViewName(mav.getViewName());
if (!mav.isReference()) {
mavContainer.setView(mav.getView());
}
return;
}
}

View File

@ -0,0 +1,115 @@
/*
* 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.support;
import javax.servlet.http.HttpServletRequest;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.ModelAndViewContainer;
import org.springframework.web.servlet.FlashMap;
import org.springframework.web.servlet.support.RequestContextUtils;
/**
* Provides annotated controller methods with convenience methods for setting
* up response with a view name that will result in a redirect.
*
* <p>An instance of this class is obtained via {@link ResponseContext#redirect}.
*
* @author Keith Donald
* @author Rossen Stoyanchev
*
* @since 3.1
*/
public class RedirectResponse {
private final NativeWebRequest webRequest;
private final ModelAndViewContainer mavContainer;
RedirectResponse(NativeWebRequest webRequest, ModelAndViewContainer mavContainer) {
this.webRequest = webRequest;
this.mavContainer = mavContainer;
}
/**
* Add a URI template variable to use to expand the URI template into a URL.
* <p><strong>Note:</strong> URI template variables from the current
* request are automatically used when expanding the redirect URI template.
* They don't need to be added explicitly here.
*/
public RedirectResponse uriVariable(String name, Object value) {
this.mavContainer.addAttribute(name, value);
return this;
}
/**
* Add a URI template variable to use to expand the URI template into a URL.
* The name of the variable is selected using a
* {@link org.springframework.core.Conventions#getVariableName generated name}.
* <p><strong>Note:</strong> URI template variables from the current
* request are automatically used when expanding the redirect URI template.
* They don't need to be added explicitly here.
*/
public RedirectResponse uriVariable(Object value) {
this.mavContainer.addAttribute(value);
return this;
}
/**
* Add a query parameter to append to the redirect URL.
*/
public RedirectResponse queryParam(String name, Object value) {
this.mavContainer.addAttribute(name, value);
return this;
}
/**
* Add a query parameter to append to the redirect URL.
* The name of the parameter is selected using a
* {@link org.springframework.core.Conventions#getVariableName generated name}.
*/
public RedirectResponse queryParam(Object value) {
this.mavContainer.addAttribute(value);
return this;
}
/**
* Add a flash attribute to save and make available in the model of the
* target controller method after the redirect.
*/
public RedirectResponse flashAttribute(String name, Object value) {
getFlashMap().addAttribute(name, value);
return this;
}
/**
* Add a flash attribute to save and make available in the model of the
* target controller method after the redirect.
* The name of the attribute is selected using a
* {@link org.springframework.core.Conventions#getVariableName generated name}.
*/
public RedirectResponse flashAttribute(Object value) {
getFlashMap().addAttribute(value);
return this;
}
private FlashMap getFlashMap() {
HttpServletRequest servletRequest = webRequest.getNativeRequest(HttpServletRequest.class);
return RequestContextUtils.getFlashMap(servletRequest);
}
}

View File

@ -0,0 +1,68 @@
/*
* 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.support;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.ModelAndViewContainer;
/**
* Provides annotated controllers with convenience methods for setting up view
* resolution.
*
* @author Keith Donald
* @author Rossen Stoyanchev
*
* @since 3.1
*/
public class ResponseContext {
private final NativeWebRequest webRequest;
private final ModelAndViewContainer mavContainer;
public ResponseContext(NativeWebRequest webRequest, ModelAndViewContainer mavContainer) {
this.webRequest = webRequest;
this.mavContainer = mavContainer;
}
/**
* Set up view resolution based on the given view name and the implicit model
* of the current request.
*/
public ViewResponse view(String viewName) {
this.mavContainer.setViewName(viewName);
return new ViewResponse(this.mavContainer);
}
/**
* Set up view resolution for a redirect. This method clears the implicit
* model. Use convenience methods on the returned {@link RedirectResponse}
* instance to add URI variables, query parameters, and flash attributes
* as necessary.
* @param redirectUri a URI template either relative to the current URL or
* absolute; do not prefix with "redirect:"
*/
public RedirectResponse redirect(String redirectUri) {
if (!redirectUri.startsWith("redirect:")) {
redirectUri = "redirect:" + redirectUri;
}
this.mavContainer.getModel().clear();
this.mavContainer.setViewName(redirectUri);
return new RedirectResponse(this.webRequest, this.mavContainer);
}
}

View File

@ -0,0 +1,50 @@
/*
* 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.support;
import org.springframework.web.method.support.ModelAndViewContainer;
/**
* Provides annotated controller methods with convenience methods for setting
* up a response with a view name that does not have the "redirect:" prefix .
*
* <p>An instance of this class is obtained via {@link ResponseContext#view}.
*
* @author Keith Donald
* @author Rossen Stoyanchev
*
* @since 3.1
*/
public class ViewResponse {
private final ModelAndViewContainer mavContainer;
ViewResponse(ModelAndViewContainer mavContainer) {
this.mavContainer = mavContainer;
}
public ViewResponse attribute(String attributeName, Object attributeValue) {
this.mavContainer.addAttribute(attributeName, attributeValue);
return this;
}
public ViewResponse attribute(Object attributeValue) {
this.mavContainer.addAttribute(attributeValue);
return this;
}
}

View File

@ -0,0 +1,200 @@
/*
* 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.support;
import java.util.Iterator;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.ConcurrentHashMap;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import org.springframework.web.servlet.FlashMap;
import org.springframework.web.servlet.FlashMapManager;
/**
* A default implementation that saves and retrieves FlashMap instances to and
* from the HTTP session.
*
* @author Rossen Stoyanchev
* @since 3.1
*/
public class DefaultFlashMapManager implements FlashMapManager {
static final String FLASH_MAPS_SESSION_ATTRIBUTE = DefaultFlashMapManager.class + ".FLASH_MAPS";
private boolean useUniqueFlashKey = true;
private String flashKeyParameterName = "_flashKey";
private int flashTimeout = 180;
private static final Random random = new Random();
/**
* Whether each FlashMap instance should be stored with a unique key.
* The unique key needs to be passed as a parameter in the redirect URL
* and then used to look up the FlashMap instance avoiding potential
* issues with concurrent requests.
* <p>The default setting is "true".
* <p>When set to "false" only one FlashMap is maintained making it
* possible for a second concurrent request (e.g. via Ajax) to "consume"
* the FlashMap inadvertently.
*/
public void setUseUniqueFlashKey(boolean useUniqueFlashKey) {
this.useUniqueFlashKey = useUniqueFlashKey;
}
/**
* Customize the name of the request parameter to be appended to the
* redirect URL when using a unique flash key.
* <p>The default value is "_flashKey".
*/
public void setFlashKeyParameterName(String parameterName) {
this.flashKeyParameterName = parameterName;
}
/**
* The amount of time in seconds after a request has completed processing
* and before a FlashMap is considered expired.
* The default value is 180.
*/
public void setFlashMapTimeout(int flashTimeout) {
this.flashTimeout = flashTimeout;
}
/**
* {@inheritDoc}
*
* <p>This method never creates an HTTP session. The current FlashMap is
* exposed as a request attribute only and is not saved in the session
* until {@link #requestCompleted}.
*/
public boolean requestStarted(HttpServletRequest request) {
if (request.getAttribute(CURRENT_FLASH_MAP_ATTRIBUTE) != null) {
return false;
}
FlashMap currentFlashMap =
this.useUniqueFlashKey ?
new FlashMap(createFlashKey(request), this.flashKeyParameterName) : new FlashMap();
request.setAttribute(CURRENT_FLASH_MAP_ATTRIBUTE, currentFlashMap);
FlashMap previousFlashMap = lookupPreviousFlashMap(request);
if (previousFlashMap != null) {
for (String name : previousFlashMap.keySet()) {
if (request.getAttribute(name) == null) {
request.setAttribute(name, previousFlashMap.get(name));
}
}
// For exposing flash attributes in other places (e.g. annotated controllers)
request.setAttribute(PREVIOUS_FLASH_MAP_ATTRIBUTE, previousFlashMap);
}
// Check and remove expired instances
Map<String, FlashMap> allFlashMaps = retrieveAllFlashMaps(request, false);
if (allFlashMaps != null && !allFlashMaps.isEmpty()) {
Iterator<FlashMap> iterator = allFlashMaps.values().iterator();
while (iterator.hasNext()) {
if (iterator.next().isExpired()) {
iterator.remove();
}
}
}
return true;
}
/**
* Create a unique flash key. The default implementation uses {@link Random}.
* @return the unique key; never {@code null}.
*/
protected String createFlashKey(HttpServletRequest request) {
return String.valueOf(random.nextInt());
}
/**
* Return the FlashMap from the previous request, if available.
* If {@link #useUniqueFlashKey} is "true", a flash key parameter is
* expected to be in the request. Otherwise there can be only one
* FlashMap instance to return.
*
* @return the FlashMap from the previous request; or {@code null} if none.
*/
private FlashMap lookupPreviousFlashMap(HttpServletRequest request) {
Map<String, FlashMap> flashMaps = retrieveAllFlashMaps(request, false);
if (flashMaps != null && !flashMaps.isEmpty()) {
if (this.useUniqueFlashKey) {
String key = request.getParameter(this.flashKeyParameterName);
if (key != null) {
return flashMaps.remove(key);
}
}
else {
String key = flashMaps.keySet().iterator().next();
return flashMaps.remove(key);
}
}
return null;
}
/**
* Retrieve all FlashMap instances from the HTTP session in a thread-safe way.
* @param request the current request
* @param allowCreate whether to create and the FlashMap container if not found
* @return a Map with all stored FlashMap instances; or {@code null}
*/
@SuppressWarnings("unchecked")
private Map<String, FlashMap> retrieveAllFlashMaps(HttpServletRequest request, boolean allowCreate) {
HttpSession session = request.getSession(allowCreate);
if (session == null) {
return null;
}
Map<String, FlashMap> result = (Map<String, FlashMap>) session.getAttribute(FLASH_MAPS_SESSION_ATTRIBUTE);
if (result == null && allowCreate) {
synchronized (DefaultFlashMapManager.class) {
result = (Map<String, FlashMap>) session.getAttribute(FLASH_MAPS_SESSION_ATTRIBUTE);
if (result == null) {
result = new ConcurrentHashMap<String, FlashMap>(5);
session.setAttribute(FLASH_MAPS_SESSION_ATTRIBUTE, result);
}
}
}
return result;
}
/**
* {@inheritDoc}
*
* <p>The HTTP session is not created if the current FlashMap instance is empty.
*/
public void requestCompleted(HttpServletRequest request) {
FlashMap flashMap = (FlashMap) request.getAttribute(CURRENT_FLASH_MAP_ATTRIBUTE);
if (flashMap == null) {
throw new IllegalStateException(
"Did not find current FlashMap exposed as request attribute " + CURRENT_FLASH_MAP_ATTRIBUTE);
}
if (!flashMap.isEmpty()) {
Map<String, FlashMap> allFlashMaps = retrieveAllFlashMaps(request, true);
flashMap.startExpirationPeriod(this.flashTimeout);
String key = this.useUniqueFlashKey ? flashMap.getKey() : "flashMap";
allFlashMaps.put(key, flashMap);
}
}
}

View File

@ -17,6 +17,7 @@
package org.springframework.web.servlet.support;
import java.util.Locale;
import javax.servlet.ServletContext;
import javax.servlet.ServletRequest;
import javax.servlet.http.HttpServletRequest;
@ -26,6 +27,8 @@ import org.springframework.ui.context.ThemeSource;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.support.WebApplicationContextUtils;
import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.FlashMapManager;
import org.springframework.web.servlet.FlashMap;
import org.springframework.web.servlet.LocaleResolver;
import org.springframework.web.servlet.ThemeResolver;
@ -152,4 +155,13 @@ public abstract class RequestContextUtils {
}
}
/**
* Retrieves the flash map to use for the current request.
* @param request current HTTP request
* @return the flash map for the current request; never {@code null}.
*/
public static FlashMap getFlashMap(HttpServletRequest request) {
return (FlashMap) request.getAttribute(FlashMapManager.CURRENT_FLASH_MAP_ATTRIBUTE);
}
}

View File

@ -30,14 +30,19 @@ import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.BeanUtils;
import org.springframework.http.HttpStatus;
import org.springframework.ui.ModelMap;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.web.servlet.FlashMap;
import org.springframework.web.servlet.HandlerMapping;
import org.springframework.web.servlet.View;
import org.springframework.web.servlet.support.RequestContextUtils;
import org.springframework.web.util.UriTemplate;
import org.springframework.web.util.UriUtils;
import org.springframework.web.util.WebUtils;
@ -76,6 +81,7 @@ import org.springframework.web.util.WebUtils;
* @author Colin Sampaleanu
* @author Sam Brannen
* @author Arjen Poutsma
* @author Rossen Stoyanchev
* @see #setContextRelative
* @see #setHttp10Compatible
* @see #setExposeModelAttributes
@ -262,6 +268,14 @@ public class RedirectView extends AbstractUrlBasedView {
appendQueryProperties(targetUrl, model, enc);
}
FlashMap flashMap = RequestContextUtils.getFlashMap(request);
if (flashMap != null && !flashMap.isEmpty()) {
if (flashMap.getKey() != null) {
ModelMap queryParam = new ModelMap(flashMap.getKeyParameterName(), flashMap.getKey());
appendQueryProperties(targetUrl, queryParam, enc);
}
}
return targetUrl.toString();
}
@ -323,7 +337,7 @@ public class RedirectView extends AbstractUrlBasedView {
}
// If there aren't already some parameters, we need a "?".
boolean first = (getUrl().indexOf('?') < 0);
boolean first = (targetUrl.toString().indexOf('?') < 0);
for (Map.Entry<String, Object> entry : queryProperties(model).entrySet()) {
Object rawValue = entry.getValue();
Iterator<Object> valueIter;

View File

@ -1,215 +0,0 @@
/*
* 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 static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import java.util.Locale;
import java.util.Map;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.junit.Test;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.context.ApplicationContextInitializer;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.FlashAttributes;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.support.FlashStatus;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.support.GenericWebApplicationContext;
import org.springframework.web.method.annotation.FlashAttributesHandler;
import org.springframework.web.servlet.View;
import org.springframework.web.servlet.ViewResolver;
/**
* Test controllers with @{@link FlashAttributes} through the DispatcherServlet.
*
* @author Rossen Stoyanchev
*/
public class FlashAttributesServletTests extends AbstractServletHandlerMethodTests {
private static final String MESSAGE_KEY = "message";
@Test
public void successMessage() throws Exception {
initServletWithModelExposingViewResolver(MessageController.class);
MockHttpServletRequest req = new MockHttpServletRequest("GET", "/message");
MockHttpServletResponse res = new MockHttpServletResponse();
getServlet().service(req, res);
assertEquals(200, res.getStatus());
assertNull(getModelAttribute(req, MESSAGE_KEY));
assertNull(getFlashAttribute(req, MESSAGE_KEY));
req.setMethod("POST");
getServlet().service(req, res);
assertEquals(200, res.getStatus());
assertEquals("Yay!", ((Message) getModelAttribute(req, MESSAGE_KEY)).getText());
assertEquals("Yay!", ((Message) getFlashAttribute(req, MESSAGE_KEY)).getText());
req.setMethod("GET");
getServlet().service(req, res);
assertEquals(200, res.getStatus());
assertEquals("Yay!", ((Message) getModelAttribute(req, MESSAGE_KEY)).getText());
assertNull(getFlashAttribute(req, MESSAGE_KEY));
}
@Test
public void successMessageAcrossControllers() throws Exception {
initServletWithModelExposingViewResolver(MessageController.class, SecondMessageController.class);
MockHttpServletRequest req = new MockHttpServletRequest("POST", "/message");
MockHttpServletResponse res = new MockHttpServletResponse();
getServlet().service(req, res);
req.setParameter("another", "true");
getServlet().service(req, res);
assertEquals(200, res.getStatus());
assertEquals("Nay!", ((Message) getModelAttribute(req, MESSAGE_KEY)).getText());
assertEquals("Nay!", ((Message) getFlashAttribute(req, MESSAGE_KEY)).getText());
req.setMethod("GET");
req.setRequestURI("/second/message");
getServlet().service(req, res);
assertEquals(200, res.getStatus());
assertEquals("Nay!", ((Message) getModelAttribute(req, MESSAGE_KEY)).getText());
assertNull(getFlashAttribute(req, MESSAGE_KEY));
}
@Controller
@FlashAttributes("message")
static class MessageController {
@RequestMapping(value="/message", method=RequestMethod.GET)
public void message(Model model) {
}
@RequestMapping(value="/message", method=RequestMethod.POST)
public String sendMessage(Model model, FlashStatus status) {
status.setActive();
model.addAttribute(Message.success("Yay!"));
return "redirect:/message";
}
@RequestMapping(value="/message", method=RequestMethod.POST, params="another")
public String sendMessageToSecondController(Model model, FlashStatus status) {
status.setActive();
model.addAttribute(Message.error("Nay!"));
return "redirect:/second/message";
}
}
@Controller
static class SecondMessageController {
@RequestMapping(value="/second/message", method=RequestMethod.GET)
public void message(Model model) {
}
}
private static class Message {
private final MessageType type;
private final String text;
private Message(MessageType type, String text) {
this.type = type;
this.text = text;
}
public static Message success(String text) {
return new Message(MessageType.success, text);
}
public static Message error(String text) {
return new Message(MessageType.error, text);
}
public MessageType getType() {
return type;
}
public String getText() {
return text;
}
public String toString() {
return type + ": " + text;
}
}
private static enum MessageType {
info, success, warning, error
}
@SuppressWarnings("unchecked")
private Object getModelAttribute(MockHttpServletRequest req, String key) {
Map<String, ?> model = (Map<String, ?>) req.getAttribute(ModelExposingViewResolver.REQUEST_ATTRIBITE_MODEL);
return model.get(key);
}
@SuppressWarnings("unchecked")
private Object getFlashAttribute(MockHttpServletRequest req, String key) {
String flashAttributesKey = FlashAttributesHandler.FLASH_ATTRIBUTES_SESSION_KEY;
Map<String, Object> attrs = (Map<String, Object>) req.getSession().getAttribute(flashAttributesKey);
return (attrs != null) ? attrs.get(key) : null;
}
private WebApplicationContext initServletWithModelExposingViewResolver(Class<?>... controllerClasses)
throws ServletException {
return initServlet(new ApplicationContextInitializer<GenericWebApplicationContext>() {
public void initialize(GenericWebApplicationContext wac) {
wac.registerBeanDefinition("viewResolver", new RootBeanDefinition(ModelExposingViewResolver.class));
}
}, controllerClasses);
}
static class ModelExposingViewResolver implements ViewResolver {
static String REQUEST_ATTRIBITE_MODEL = "ModelExposingViewResolver.model";
public View resolveViewName(final String viewName, Locale locale) throws Exception {
return new View() {
public void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) throws Exception {
request.setAttribute(REQUEST_ATTRIBITE_MODEL, model);
}
public String getContentType() {
return null;
}
};
}
}
}

View File

@ -133,11 +133,14 @@ import org.springframework.web.context.support.GenericWebApplicationContext;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.multipart.support.StringMultipartFileEditor;
import org.springframework.web.servlet.FlashMap;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.View;
import org.springframework.web.servlet.ViewResolver;
import org.springframework.web.servlet.mvc.annotation.ModelAndViewResolver;
import org.springframework.web.servlet.mvc.method.annotation.support.ServletWebArgumentResolverAdapter;
import org.springframework.web.servlet.mvc.method.support.ResponseContext;
import org.springframework.web.servlet.support.RequestContextUtils;
import org.springframework.web.servlet.view.InternalResourceViewResolver;
/**
@ -1452,7 +1455,46 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl
assertEquals("application/json", response.getHeader("Content-Type"));
assertEquals("homeJson", response.getContentAsString());
}
@Test
public void flashAttribute() throws Exception {
initServletWithControllers(MessageController.class);
MockHttpServletRequest request = new MockHttpServletRequest("POST", "/messages");
HttpSession session = request.getSession();
MockHttpServletResponse response = new MockHttpServletResponse();
getServlet().service(request, response);
// POST -> bind error
getServlet().service(request, response);
assertEquals(200, response.getStatus());
assertEquals("messages/new", response.getForwardedUrl());
assertTrue(RequestContextUtils.getFlashMap(request).isEmpty());
// POST -> success
request = new MockHttpServletRequest("POST", "/messages");
request.setSession(session);
request.addParameter("name", "Jeff");
response = new MockHttpServletResponse();
getServlet().service(request, response);
FlashMap flashMap = RequestContextUtils.getFlashMap(request);
assertNotNull(flashMap);
assertEquals(200, response.getStatus());
assertEquals("/messages/1?name=value&_flashKey=" + flashMap.getKey(), response.getRedirectedUrl());
// GET after POST
request = new MockHttpServletRequest("GET", "/messages/1");
request.setSession(session);
request.setParameter("_flashKey", String.valueOf(flashMap.getKey()));
response = new MockHttpServletResponse();
getServlet().service(request, response);
assertEquals(200, response.getStatus());
assertEquals("Got: yay!", response.getContentAsString());
}
/*
* Controllers
@ -2761,6 +2803,32 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl
return "homeJson";
}
}
@Controller
static class MessageController {
@InitBinder
public void initBinder(WebDataBinder dataBinder) {
dataBinder.setRequiredFields("name");
}
@RequestMapping(value = "/messages/{id}", method = RequestMethod.GET)
public void message(ModelMap model, Writer writer) throws IOException {
writer.write("Got: " + model.get("successMessage"));
}
@RequestMapping(value = "/messages", method = RequestMethod.POST)
public void sendMessage(TestBean testBean, BindingResult result, ResponseContext responseContext) {
if (result.hasErrors()) {
responseContext.view("messages/new");
}
else {
responseContext.redirect("/messages/{id}").uriVariable("id", "1").queryParam("name", "value")
.flashAttribute("successMessage", "yay!");
}
}
}
// Test cases deleted from the original SevletAnnotationControllerTests:

View File

@ -56,7 +56,7 @@ public class DefaultMethodReturnValueHandlerTests {
public void setUp() {
mavResolvers = new ArrayList<ModelAndViewResolver>();
handler = new DefaultMethodReturnValueHandler(mavResolvers);
mavContainer = new ModelAndViewContainer(new ExtendedModelMap());
mavContainer = new ModelAndViewContainer();
request = new ServletWebRequest(new MockHttpServletRequest());
}
@ -69,7 +69,7 @@ public class DefaultMethodReturnValueHandlerTests {
handler.handleReturnValue(testBean, testBeanType, mavContainer, request);
assertEquals("viewName", mavContainer.getViewName());
assertSame(testBean, mavContainer.getAttribute("modelAttrName"));
assertSame(testBean, mavContainer.getModel().get("modelAttrName"));
assertTrue(mavContainer.isResolveView());
}

View File

@ -26,12 +26,9 @@ import org.junit.Before;
import org.junit.Test;
import org.springframework.core.MethodParameter;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.ui.ExtendedModelMap;
import org.springframework.web.context.request.ServletWebRequest;
import org.springframework.web.method.support.ModelAndViewContainer;
import org.springframework.web.servlet.View;
import org.springframework.web.servlet.mvc.method.annotation.support.DefaultMethodReturnValueHandler;
import org.springframework.web.servlet.mvc.method.annotation.support.ViewMethodReturnValueHandler;
import org.springframework.web.servlet.view.InternalResourceView;
/**
@ -50,7 +47,7 @@ public class ViewMethodReturnValueHandlerTests {
@Before
public void setUp() {
handler = new ViewMethodReturnValueHandler();
mavContainer = new ModelAndViewContainer(new ExtendedModelMap());
mavContainer = new ModelAndViewContainer();
webRequest = new ServletWebRequest(new MockHttpServletRequest());
}

View File

@ -0,0 +1,174 @@
/*
* 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.support;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
import java.util.HashMap;
import java.util.Map;
import org.junit.Before;
import org.junit.Test;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.web.servlet.FlashMap;
import org.springframework.web.servlet.FlashMapManager;
/**
* Test fixture for {@link DefaultFlashMapManager} tests.
*
* @author Rossen Stoyanchev
*/
public class DefaultFlashMapManagerTests {
private DefaultFlashMapManager flashMapManager;
private MockHttpServletRequest request;
@Before
public void setup() {
this.flashMapManager = new DefaultFlashMapManager();
this.request = new MockHttpServletRequest();
}
@Test
public void requestAlreadyStarted() {
request.setAttribute(FlashMapManager.CURRENT_FLASH_MAP_ATTRIBUTE, new FlashMap());
boolean actual = this.flashMapManager.requestStarted(this.request);
assertFalse(actual);
}
@Test
public void createFlashMap() {
boolean actual = this.flashMapManager.requestStarted(this.request);
FlashMap flashMap = RequestContextUtils.getFlashMap(this.request);
assertTrue(actual);
assertNotNull(flashMap);
assertNotNull(flashMap.getKey());
assertEquals("_flashKey", flashMap.getKeyParameterName());
}
@Test
public void createFlashMapWithoutKey() {
this.flashMapManager.setUseUniqueFlashKey(false);
boolean actual = this.flashMapManager.requestStarted(this.request);
FlashMap flashMap = RequestContextUtils.getFlashMap(this.request);
assertTrue(actual);
assertNotNull(flashMap);
assertNull(flashMap.getKey());
assertNull(flashMap.getKeyParameterName());
}
@Test
public void lookupPreviousFlashMap() {
FlashMap flashMap = new FlashMap("key", "_flashKey");
flashMap.put("name", "value");
Map<String, FlashMap> allFlashMaps = new HashMap<String, FlashMap>();
allFlashMaps.put(flashMap.getKey(), flashMap);
this.request.getSession().setAttribute(DefaultFlashMapManager.FLASH_MAPS_SESSION_ATTRIBUTE, allFlashMaps);
this.request.addParameter("_flashKey", flashMap.getKey());
this.flashMapManager.requestStarted(this.request);
assertSame(flashMap, request.getAttribute(DefaultFlashMapManager.PREVIOUS_FLASH_MAP_ATTRIBUTE));
assertEquals("value", request.getAttribute("name"));
}
@Test
public void lookupPreviousFlashMapWithoutKey() {
Map<String, FlashMap> allFlashMaps = new HashMap<String, FlashMap>();
request.getSession().setAttribute(DefaultFlashMapManager.FLASH_MAPS_SESSION_ATTRIBUTE, allFlashMaps);
FlashMap flashMap = new FlashMap();
flashMap.put("name", "value");
allFlashMaps.put("key", flashMap);
this.flashMapManager.setUseUniqueFlashKey(false);
this.flashMapManager.requestStarted(this.request);
assertSame(flashMap, this.request.getAttribute(DefaultFlashMapManager.PREVIOUS_FLASH_MAP_ATTRIBUTE));
assertEquals("value", this.request.getAttribute("name"));
}
@SuppressWarnings("static-access")
@Test
public void removeExpired() throws InterruptedException {
FlashMap[] flashMapArray = new FlashMap[5];
flashMapArray[0] = new FlashMap("key0", "_flashKey");
flashMapArray[1] = new FlashMap("key1", "_flashKey");
flashMapArray[2] = new FlashMap("key2", "_flashKey");
flashMapArray[3] = new FlashMap("key3", "_flashKey");
flashMapArray[4] = new FlashMap("key4", "_flashKey");
Map<String, FlashMap> allFlashMaps = new HashMap<String, FlashMap>();
for (FlashMap flashMap : flashMapArray) {
allFlashMaps.put(flashMap.getKey(), flashMap);
}
flashMapArray[1].startExpirationPeriod(0);
flashMapArray[3].startExpirationPeriod(0);
Thread.currentThread().sleep(5);
MockHttpServletRequest request = new MockHttpServletRequest();
request.getSession().setAttribute(DefaultFlashMapManager.FLASH_MAPS_SESSION_ATTRIBUTE, allFlashMaps);
request.setParameter("_flashKey", "key0");
this.flashMapManager.requestStarted(request);
assertEquals(2, allFlashMaps.size());
assertNotNull(allFlashMaps.get("key2"));
assertNotNull(allFlashMaps.get("key4"));
}
@SuppressWarnings({ "unchecked", "static-access" })
@Test
public void saveFlashMap() throws InterruptedException {
FlashMap flashMap = new FlashMap("key", "_flashKey");
flashMap.put("name", "value");
request.setAttribute(DefaultFlashMapManager.CURRENT_FLASH_MAP_ATTRIBUTE, flashMap);
this.flashMapManager.setFlashMapTimeout(0);
this.flashMapManager.requestCompleted(this.request);
Thread.currentThread().sleep(1);
String sessionKey = DefaultFlashMapManager.FLASH_MAPS_SESSION_ATTRIBUTE;
Map<String, FlashMap> allFlashMaps = (Map<String, FlashMap>) this.request.getSession().getAttribute(sessionKey);
assertSame(flashMap, allFlashMaps.get("key"));
assertTrue(flashMap.isExpired());
}
@Test
public void saveEmptyFlashMap() throws InterruptedException {
FlashMap flashMap = new FlashMap("key", "_flashKey");
request.setAttribute(DefaultFlashMapManager.CURRENT_FLASH_MAP_ATTRIBUTE, flashMap);
this.flashMapManager.setFlashMapTimeout(0);
this.flashMapManager.requestCompleted(this.request);
assertNull(this.request.getSession().getAttribute(DefaultFlashMapManager.FLASH_MAPS_SESSION_ATTRIBUTE));
}
}

View File

@ -32,6 +32,8 @@ import org.springframework.beans.TestBean;
import org.springframework.http.HttpStatus;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.web.servlet.FlashMap;
import org.springframework.web.servlet.FlashMapManager;
import org.springframework.web.servlet.View;
import org.springframework.web.util.WebUtils;
@ -90,6 +92,35 @@ public class RedirectViewTests {
assertEquals(201, response.getStatus());
assertEquals("http://url.somewhere.com", response.getHeader("Location"));
}
@Test
public void flashMap() throws Exception {
RedirectView rv = new RedirectView();
rv.setUrl("http://url.somewhere.com");
rv.setHttp10Compatible(false);
MockHttpServletRequest request = new MockHttpServletRequest();
MockHttpServletResponse response = new MockHttpServletResponse();
FlashMap flashMap = new FlashMap("key", "_flashKey");
flashMap.put("name", "value");
request.setAttribute(FlashMapManager.CURRENT_FLASH_MAP_ATTRIBUTE, flashMap);
rv.render(new HashMap<String, Object>(), request, response);
assertEquals(303, response.getStatus());
assertEquals("http://url.somewhere.com?_flashKey=key", response.getHeader("Location"));
}
@Test
public void emptyFlashMap() throws Exception {
RedirectView rv = new RedirectView();
rv.setUrl("http://url.somewhere.com");
rv.setHttp10Compatible(false);
MockHttpServletRequest request = new MockHttpServletRequest();
MockHttpServletResponse response = new MockHttpServletResponse();
FlashMap flashMap = new FlashMap("key", "_flashKey");
request.setAttribute(FlashMapManager.CURRENT_FLASH_MAP_ATTRIBUTE, flashMap);
rv.render(new HashMap<String, Object>(), request, response);
assertEquals(303, response.getStatus());
assertEquals("http://url.somewhere.com", response.getHeader("Location"));
}
@Test
public void emptyMap() throws Exception {

View File

@ -1,56 +0,0 @@
/*
* Copyright 2002-2010 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.bind.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Annotation that indicates what attributes should be stored in the session or in
* some conversational storage in order to survive a client-side redirect.
*
* TODO ...
*
* @author Rossen Stoyanchev
* @since 3.1
*
* @see org.springframework.web.bind.support.FlashStatus
*/
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface FlashAttributes {
/**
* The names of flash attributes in the model to be stored.
*
* TODO ...
*/
String[] value() default {};
/**
* TODO ...
*
*/
Class[] types() default {};
}

View File

@ -1,51 +0,0 @@
/*
* 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.bind.support;
import org.springframework.web.bind.annotation.FlashAttributes;
/**
* Simple interface to pass into controller methods to allow them to activate
* a mode in which model attributes identified as "flash attributes" are
* temporarily stored in the session to make them available to the next
* request. The most common scenario is a client-side redirect.
*
* <p>In active mode, model attributes that match the attribute names or
* types declared via @{@link FlashAttributes} are saved in the session.
* On the next request, any flash attributes found in the session are
* automatically added to the model of the target controller method and
* are also cleared from the session.
*
* TODO ...
*
* @author Rossen Stoyanchev
* @since 3.1
*/
public interface FlashStatus {
/**
* TODO ...
*/
void setActive();
/**
* TODO ...
*/
boolean isActive();
}

View File

@ -1,37 +0,0 @@
/*
* 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.bind.support;
/**
* TODO ...
*
* @author Rossen Stoyanchev
* @since 3.1
*/
public class SimpleFlashStatus implements FlashStatus {
private boolean active = false;
public void setActive() {
this.active = true;
}
public boolean isActive() {
return this.active;
}
}

View File

@ -1,108 +0,0 @@
/*
* 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.method.annotation;
import java.util.Arrays;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.web.bind.annotation.FlashAttributes;
import org.springframework.web.context.request.WebRequest;
/**
* Manages flash attributes declared via @{@link FlashAttributes}.
*
* TODO ...
*
* @author Rossen Stoyanchev
* @since 3.1
*/
public class FlashAttributesHandler {
public static final String FLASH_ATTRIBUTES_SESSION_KEY = FlashAttributesHandler.class.getName() + ".attributes";
private final Set<String> attributeNames = new HashSet<String>();
private final Set<Class<?>> attributeTypes = new HashSet<Class<?>>();
/**
* TODO ...
*/
public FlashAttributesHandler(Class<?> handlerType) {
FlashAttributes annotation = AnnotationUtils.findAnnotation(handlerType, FlashAttributes.class);
if (annotation != null) {
this.attributeNames.addAll(Arrays.asList(annotation.value()));
this.attributeTypes.addAll(Arrays.<Class<?>>asList(annotation.types()));
}
}
/**
* Whether the controller represented by this handler has declared flash
* attribute names or types via @{@link FlashAttributes}.
*/
public boolean hasFlashAttributes() {
return ((this.attributeNames.size() > 0) || (this.attributeTypes.size() > 0));
}
/**
* TODO ...
*/
public boolean isFlashAttribute(String attributeName, Class<?> attributeType) {
return (this.attributeNames.contains(attributeName) || this.attributeTypes.contains(attributeType));
}
/**
* TODO ...
*/
public void storeAttributes(WebRequest request, Map<String, ?> attributes) {
Map<String, Object> filtered = filterAttributes(attributes);
if (!filtered.isEmpty()) {
request.setAttribute(FLASH_ATTRIBUTES_SESSION_KEY, filtered, WebRequest.SCOPE_SESSION);
}
}
private Map<String, Object> filterAttributes(Map<String, ?> attributes) {
Map<String, Object> result = new LinkedHashMap<String, Object>();
for (String name : attributes.keySet()) {
Object value = attributes.get(name);
Class<?> type = (value != null) ? value.getClass() : null;
if (isFlashAttribute(name, type)) {
result.put(name, value);
}
}
return result;
}
/**
* TODO ...
*/
@SuppressWarnings("unchecked")
public Map<String, Object> retrieveAttributes(WebRequest request) {
return (Map<String, Object>) request.getAttribute(FLASH_ATTRIBUTES_SESSION_KEY, WebRequest.SCOPE_SESSION);
}
/**
* TODO ...
*/
public void cleanupAttributes(WebRequest request) {
request.removeAttribute(FLASH_ATTRIBUTES_SESSION_KEY, WebRequest.SCOPE_SESSION);
}
}

View File

@ -35,7 +35,6 @@ import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.SessionAttributes;
import org.springframework.web.bind.support.FlashStatus;
import org.springframework.web.bind.support.SessionStatus;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
@ -66,23 +65,18 @@ public final class ModelFactory {
private final SessionAttributesHandler sessionAttributesHandler;
private final FlashAttributesHandler flashAttributesHandler;
/**
* Create a ModelFactory instance with the provided {@link ModelAttribute} methods.
* @param attributeMethods {@link ModelAttribute} methods to initialize model instances with
* @param binderFactory used to add {@link BindingResult} attributes to the model
* @param sessionAttributesHandler used to access handler-specific session attributes
* @param flashAttributesHandler used to access flash attributes
*/
public ModelFactory(List<InvocableHandlerMethod> attributeMethods,
WebDataBinderFactory binderFactory,
SessionAttributesHandler sessionAttributesHandler,
FlashAttributesHandler flashAttributesHandler) {
SessionAttributesHandler sessionAttributesHandler) {
this.attributeMethods = (attributeMethods != null) ? attributeMethods : new ArrayList<InvocableHandlerMethod>();
this.binderFactory = binderFactory;
this.sessionAttributesHandler = sessionAttributesHandler;
this.flashAttributesHandler = flashAttributesHandler;
}
/**
@ -93,21 +87,17 @@ public final class ModelFactory {
* <li>Check the session for any controller-specific attributes not yet "remembered".
* </ol>
* @param request the current request
* @param mavContainer contains the model to initialize
* @param mavContainer contains the model to initialize
* @param handlerMethod the @{@link RequestMapping} method for which the model is initialized
* @throws Exception may arise from the invocation of @{@link ModelAttribute} methods
*/
public void initModel(NativeWebRequest request, ModelAndViewContainer mavContainer, HandlerMethod handlerMethod)
public void initModel(NativeWebRequest request, ModelAndViewContainer mavContainer, HandlerMethod handlerMethod)
throws Exception {
Map<String, ?> sessionAttrs = this.sessionAttributesHandler.retrieveAttributes(request);
mavContainer.addAllAttributes(sessionAttrs);
mavContainer.mergeAttributes(sessionAttrs);
Map<String, ?> flashAttrs = this.flashAttributesHandler.retrieveAttributes(request);
mavContainer.addAllAttributes(flashAttrs);
this.flashAttributesHandler.cleanupAttributes(request);
invokeAttributeMethods(request, mavContainer);
invokeModelAttributeMethods(request, mavContainer);
checkHandlerSessionAttributes(request, mavContainer, handlerMethod);
}
@ -116,7 +106,7 @@ public final class ModelFactory {
* Invoke model attribute methods to populate the model.
* If two methods return the same attribute, the attribute from the first method is added.
*/
private void invokeAttributeMethods(NativeWebRequest request, ModelAndViewContainer mavContainer)
private void invokeModelAttributeMethods(NativeWebRequest request, ModelAndViewContainer mavContainer)
throws Exception {
for (InvocableHandlerMethod attrMethod : this.attributeMethods) {
@ -128,18 +118,23 @@ public final class ModelFactory {
Object returnValue = attrMethod.invokeForRequest(request, mavContainer);
if (!attrMethod.isVoid()){
String valueName = getNameForReturnValue(returnValue, attrMethod.getReturnType());
mavContainer.mergeAttribute(valueName, returnValue);
String returnValueName = getNameForReturnValue(returnValue, attrMethod.getReturnType());
if (!mavContainer.containsAttribute(returnValueName)) {
mavContainer.addAttribute(returnValueName, returnValue);
}
}
}
}
/**
* Checks if any @{@link ModelAttribute} handler method arguments declared as
* session attributes via @{@link SessionAttributes} but are not already in the
* model. If found add them to the model, raise an exception otherwise.
* Checks for @{@link ModelAttribute} arguments in the signature of the
* {@link RequestMapping} method that are declared as session attributes
* via @{@link SessionAttributes} but are not already in the model.
* Those attributes may have been outside of this controller.
* Try to locate the attributes in the session or raise an exception.
*
* @throws HttpSessionRequiredException raised if a handler session attribute could is missing
* @throws HttpSessionRequiredException raised if an attribute declared
* as session attribute is missing.
*/
private void checkHandlerSessionAttributes(NativeWebRequest request,
ModelAndViewContainer mavContainer,
@ -203,11 +198,11 @@ public final class ModelFactory {
* promotes model attributes to the session, and adds {@link BindingResult} attributes where missing.
* @param request the current request
* @param mavContainer the {@link ModelAndViewContainer} for the current request
* @param sessionStatus whether session processing is complete
* @param sessionStatus the session status for the current request
* @throws Exception if the process of creating {@link BindingResult} attributes causes an error
*/
public void updateModel(NativeWebRequest request, ModelAndViewContainer mavContainer,
SessionStatus sessionStatus, FlashStatus flashStatus) throws Exception {
public void updateModel(NativeWebRequest request, ModelAndViewContainer mavContainer, SessionStatus sessionStatus)
throws Exception {
if (sessionStatus.isComplete()){
this.sessionAttributesHandler.cleanupAttributes(request);
@ -215,10 +210,6 @@ public final class ModelFactory {
else {
this.sessionAttributesHandler.storeAttributes(request, mavContainer.getModel());
}
if (flashStatus.isActive()) {
this.flashAttributesHandler.storeAttributes(request, mavContainer.getModel());
}
if (mavContainer.isResolveView()) {
updateBindingResult(request, mavContainer.getModel());

View File

@ -95,7 +95,7 @@ public class ModelAttributeMethodProcessor implements HandlerMethodArgumentResol
WebDataBinderFactory binderFactory) throws Exception {
String name = ModelFactory.getNameForParameter(parameter);
Object target = (mavContainer.containsAttribute(name)) ?
mavContainer.getAttribute(name) : createAttribute(name, parameter, binderFactory, request);
mavContainer.getModel().get(name) : createAttribute(name, parameter, binderFactory, request);
WebDataBinder binder = binderFactory.createBinder(request, target, name);

View File

@ -56,9 +56,7 @@ public class ModelMethodProcessor implements HandlerMethodArgumentResolver, Hand
public boolean supportsReturnType(MethodParameter returnType) {
Class<?> paramType = returnType.getParameterType();
boolean hasModelAttr = returnType.getMethodAnnotation(ModelAttribute.class) != null;
return (Model.class.isAssignableFrom(paramType)
|| (Map.class.isAssignableFrom(paramType) && !hasModelAttr));
return (Model.class.isAssignableFrom(paramType) || (Map.class.isAssignableFrom(paramType) && !hasModelAttr));
}
@SuppressWarnings({ "unchecked", "rawtypes" })

View File

@ -18,142 +18,141 @@ package org.springframework.web.method.support;
import java.util.Map;
import org.springframework.ui.Model;
import org.springframework.ui.ModelMap;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import org.springframework.validation.support.BindingAwareModelMap;
/**
* Provides access to the model and a place to record model and view related decisions made by
* {@link HandlerMethodArgumentResolver}s or a {@link HandlerMethodReturnValueHandler}.
*
* <p>In addition to storing model attributes and a view, the {@link ModelAndViewContainer} also provides
* a {@link #setResolveView(boolean)} flag, which can be used to request or bypass a view resolution phase.
* This is most commonly used from {@link HandlerMethodReturnValueHandler}s but in some cases may also be
* used from {@link HandlerMethodArgumentResolver}s such as when a handler method accepts an argument
* providing access to the response. When that is the case, if the handler method returns {@code null},
* view resolution is skipped.
* 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.
*
* @author Rossen Stoyanchev
* @since 3.1
*/
public class ModelAndViewContainer {
private String viewName;
private Object view;
private final ModelMap model;
private boolean resolveView = true;
private final ModelMap model = new BindingAwareModelMap();
/**
* Create a {@link ModelAndViewContainer} instance with a {@link BindingAwareModelMap}.
* Create a new instance.
*/
public ModelAndViewContainer() {
this.model = new BindingAwareModelMap();
}
/**
* Create a {@link ModelAndViewContainer} instance with the given {@link ModelMap} instance.
* @param model the model to use
*/
public ModelAndViewContainer(ModelMap model) {
Assert.notNull(model);
this.model = model;
}
/**
* @return the model for the current request
*/
public ModelMap getModel() {
return model;
}
/**
* @return the view name to use for view resolution, or {@code null}
*/
public String getViewName() {
return this.viewName;
}
/**
* @param viewName the name of the view to use for view resolution
* Set a view name to be resolved by the DispatcherServlet via a ViewResolver.
* Will override any pre-existing view name or View.
*/
public void setViewName(String viewName) {
this.viewName = viewName;
this.view = viewName;
}
/**
* @return the view instance to use for view resolution
* Return the view name to be resolved by the DispatcherServlet via a
* ViewResolver, or {@code null} if a View object is set.
*/
public Object getView() {
return this.view;
public String getViewName() {
return (this.view instanceof String ? (String) this.view : null);
}
/**
* @param view the view instance to use for view resolution
* Set a View object to be used by the DispatcherServlet.
* Will override any pre-existing view name or View.
*/
public void setView(Object view) {
this.view = view;
}
/**
* @return whether the view resolution is requested ({@code true}), or should be bypassed ({@code false})
* Return the View object, or {@code null} if we using a view name
* to be resolved by the DispatcherServlet via a ViewResolver.
*/
public boolean isResolveView() {
return resolveView;
public Object getView() {
return this.view;
}
/**
* @param resolveView whether the view resolution is requested ({@code true}), or should be bypassed ({@code false})
* Whether the view is a view reference specified via a name to be
* resolved by the DispatcherServlet via a ViewResolver.
*/
public boolean isViewReference() {
return (this.view instanceof String);
}
/**
* 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.
*/
public void setResolveView(boolean resolveView) {
this.resolveView = resolveView;
}
/**
* Whether view resolution is required or not.
*/
public boolean isResolveView() {
return this.resolveView;
}
/**
* Whether model contains an attribute of the given name.
* @param name the name of the model attribute
* @return {@code true} if the model contains an attribute by that name and the name is not an empty string
* Return the underlying {@code ModelMap} instance, never {@code null}.
*/
public ModelMap getModel() {
return this.model;
}
/**
* Add the supplied attribute to the underlying model.
* @see ModelMap#addAttribute(String, Object)
*/
public ModelAndViewContainer addAttribute(String name, Object value) {
this.model.addAttribute(name, value);
return this;
}
/**
* Add the supplied attribute to the underlying model.
* @see Model#addAttribute(Object)
*/
public ModelAndViewContainer addAttribute(Object value) {
this.model.addAttribute(value);
return this;
}
/**
* Copy all attributes to the underlying model.
* @see ModelMap#addAllAttributes(Map)
*/
public ModelAndViewContainer addAllAttributes(Map<String, ?> attributes) {
this.model.addAllAttributes(attributes);
return this;
}
/**
* Copy attributes in the supplied <code>Map</code> with existing objects of
* the same name taking precedence (i.e. not getting replaced).
* @see ModelMap#mergeAttributes(Map)
*/
public ModelAndViewContainer mergeAttributes(Map<String, ?> attributes) {
this.model.mergeAttributes(attributes);
return this;
}
/**
* Whether the underlying model contains the given attribute name.
* @see ModelMap#containsAttribute(String)
*/
public boolean containsAttribute(String name) {
return (StringUtils.hasText(name) && model.containsAttribute(name));
}
/**
* @param name the attribute to get from the model
* @return the attribute or {@code null}
*/
public Object getAttribute(String name) {
return model.get(name);
}
/**
* Add the supplied attribute under the given name.
* @param name the name of the model attribute (never null)
* @param value the model attribute value (can be null)
*/
public void addAttribute(String name, Object value) {
model.addAttribute(name, value);
}
/**
* Copy all attributes in the supplied Map into the model
*/
public void addAllAttributes(Map<String, ?> attributes) {
model.addAllAttributes(attributes);
return this.model.containsAttribute(name);
}
/**
* Add the given attribute if the model does not already contain such an attribute.
* @param name the name of the attribute to check and add
* @param value the value of the attribute
*/
public void mergeAttribute(String name, Object value) {
if (!containsAttribute(name)) {
model.addAttribute(name, value);
}
}
}
}

View File

@ -41,7 +41,7 @@ import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.SessionAttributes;
import org.springframework.web.bind.support.DefaultSessionAttributeStore;
import org.springframework.web.bind.support.SessionAttributeStore;
import org.springframework.web.bind.support.SimpleFlashStatus;
import org.springframework.web.bind.support.SessionStatus;
import org.springframework.web.bind.support.SimpleSessionStatus;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
@ -66,12 +66,8 @@ public class ModelFactoryTests {
private SessionAttributesHandler sessionAttrsHandler;
private FlashAttributesHandler flashAttrsHandler;
private SessionAttributeStore sessionAttributeStore;
private ModelAndViewContainer mavContainer;
private NativeWebRequest webRequest;
@Before
@ -82,42 +78,44 @@ public class ModelFactoryTests {
handleSessionAttrMethod = new InvocableHandlerMethod(handler, method);
sessionAttributeStore = new DefaultSessionAttributeStore();
sessionAttrsHandler = new SessionAttributesHandler(handlerType, sessionAttributeStore);
flashAttrsHandler = new FlashAttributesHandler(handlerType);
mavContainer = new ModelAndViewContainer();
webRequest = new ServletWebRequest(new MockHttpServletRequest());
}
@Test
public void modelAttributeMethod() throws Exception {
ModelFactory modelFactory = createModelFactory("modelAttr", Model.class);
ModelAndViewContainer mavContainer = new ModelAndViewContainer();
modelFactory.initModel(webRequest, mavContainer, handleMethod);
assertEquals(Boolean.TRUE, mavContainer.getAttribute("modelAttr"));
assertEquals(Boolean.TRUE, mavContainer.getModel().get("modelAttr"));
}
@Test
public void modelAttributeMethodWithSpecifiedName() throws Exception {
ModelFactory modelFactory = createModelFactory("modelAttrWithName");
ModelAndViewContainer mavContainer = new ModelAndViewContainer();
modelFactory.initModel(webRequest, mavContainer, handleMethod);
assertEquals(Boolean.TRUE, mavContainer.getAttribute("name"));
assertEquals(Boolean.TRUE, mavContainer.getModel().get("name"));
}
@Test
public void modelAttributeMethodWithNameByConvention() throws Exception {
ModelFactory modelFactory = createModelFactory("modelAttrConvention");
ModelAndViewContainer mavContainer = new ModelAndViewContainer();
modelFactory.initModel(webRequest, mavContainer, handleMethod);
assertEquals(Boolean.TRUE, mavContainer.getAttribute("boolean"));
assertEquals(Boolean.TRUE, mavContainer.getModel().get("boolean"));
}
@Test
public void modelAttributeMethodWithNullReturnValue() throws Exception {
ModelFactory modelFactory = createModelFactory("nullModelAttr");
ModelAndViewContainer mavContainer = new ModelAndViewContainer();
modelFactory.initModel(webRequest, mavContainer, handleMethod);
assertTrue(mavContainer.containsAttribute("name"));
assertNull(mavContainer.getAttribute("name"));
assertNull(mavContainer.getModel().get("name"));
}
@Test
@ -128,30 +126,33 @@ public class ModelFactoryTests {
assertTrue(sessionAttrsHandler.isHandlerSessionAttribute("sessionAttr", null));
ModelFactory modelFactory = createModelFactory("modelAttr", Model.class);
ModelAndViewContainer mavContainer = new ModelAndViewContainer();
modelFactory.initModel(webRequest, mavContainer, handleMethod);
assertEquals("sessionAttrValue", mavContainer.getAttribute("sessionAttr"));
assertEquals("sessionAttrValue", mavContainer.getModel().get("sessionAttr"));
}
@Test
public void requiredSessionAttribute() throws Exception {
ModelFactory modelFactory = new ModelFactory(null, null, sessionAttrsHandler, flashAttrsHandler);
ModelFactory modelFactory = new ModelFactory(null, null, sessionAttrsHandler);
try {
modelFactory.initModel(webRequest, mavContainer, handleSessionAttrMethod);
modelFactory.initModel(webRequest, new ModelAndViewContainer(), handleSessionAttrMethod);
fail("Expected HttpSessionRequiredException");
} catch (HttpSessionRequiredException e) { }
sessionAttributeStore.storeAttribute(webRequest, "sessionAttr", "sessionAttrValue");
ModelAndViewContainer mavContainer = new ModelAndViewContainer();
modelFactory.initModel(webRequest, mavContainer, handleSessionAttrMethod);
assertEquals("sessionAttrValue", mavContainer.getAttribute("sessionAttr"));
assertEquals("sessionAttrValue", mavContainer.getModel().get("sessionAttr"));
}
@Test
public void updateModelBindingResultKeys() throws Exception {
String attrName = "attr1";
Object attrValue = new Object();
ModelAndViewContainer mavContainer = new ModelAndViewContainer();
mavContainer.addAttribute(attrName, attrValue);
WebDataBinder dataBinder = new WebDataBinder(attrValue, attrName);
@ -159,8 +160,8 @@ public class ModelFactoryTests {
expect(binderFactory.createBinder(webRequest, attrValue, attrName)).andReturn(dataBinder);
replay(binderFactory);
ModelFactory modelFactory = new ModelFactory(null, binderFactory, sessionAttrsHandler, flashAttrsHandler);
modelFactory.updateModel(webRequest, mavContainer, new SimpleSessionStatus(), new SimpleFlashStatus());
ModelFactory modelFactory = new ModelFactory(null, binderFactory, sessionAttrsHandler);
modelFactory.updateModel(webRequest, mavContainer, new SimpleSessionStatus());
assertEquals(attrValue, mavContainer.getModel().remove(attrName));
assertSame(dataBinder.getBindingResult(), mavContainer.getModel().remove(bindingResultKey(attrName)));
@ -174,6 +175,7 @@ public class ModelFactoryTests {
String attrName = "sessionAttr";
String attrValue = "sessionAttrValue";
ModelAndViewContainer mavContainer = new ModelAndViewContainer();
mavContainer.addAttribute(attrName, attrValue);
sessionAttributeStore.storeAttribute(webRequest, attrName, attrValue);
@ -185,15 +187,13 @@ public class ModelFactoryTests {
expect(binderFactory.createBinder(webRequest, attrValue, attrName)).andReturn(dataBinder);
replay(binderFactory);
SimpleSessionStatus status = new SimpleSessionStatus();
status.setComplete();
SessionStatus sessionStatus = new SimpleSessionStatus();
sessionStatus.setComplete();
// TODO: test with active FlashStatus
ModelFactory modelFactory = new ModelFactory(null, binderFactory, sessionAttrsHandler);
modelFactory.updateModel(webRequest, mavContainer, sessionStatus);
ModelFactory modelFactory = new ModelFactory(null, binderFactory, sessionAttrsHandler, flashAttrsHandler);
modelFactory.updateModel(webRequest, mavContainer, status, new SimpleFlashStatus());
assertEquals(attrValue, mavContainer.getAttribute(attrName));
assertEquals(attrValue, mavContainer.getModel().get(attrName));
assertNull(sessionAttributeStore.retrieveAttribute(webRequest, attrName));
verify(binderFactory);
@ -214,7 +214,7 @@ public class ModelFactoryTests {
handlerMethod.setDataBinderFactory(null);
handlerMethod.setParameterNameDiscoverer(new LocalVariableTableParameterNameDiscoverer());
return new ModelFactory(Arrays.asList(handlerMethod), null, sessionAttrsHandler, flashAttrsHandler);
return new ModelFactory(Arrays.asList(handlerMethod), null, sessionAttrsHandler);
}
@SessionAttributes("sessionAttr") @SuppressWarnings("unused")