Polish synchronization of model attributes with the session.

This commit is contained in:
Rossen Stoyanchev 2011-11-04 22:14:13 +00:00
parent d3f4c69f00
commit bef75aab07
4 changed files with 94 additions and 105 deletions

View File

@ -50,10 +50,9 @@ import org.springframework.web.servlet.mvc.method.annotation.ServletRequestDataB
public class ServletModelAttributeMethodProcessor extends ModelAttributeMethodProcessor {
/**
* @param annotationNotRequired if {@code true}, any non-simple type
* argument or return value is regarded as a model attribute even without
* the presence of a {@code @ModelAttribute} annotation in which case the
* attribute name is derived from the model attribute's type.
* @param annotationNotRequired if "true", non-simple method arguments and
* return values are considered model attributes with or without a
* {@code @ModelAttribute} annotation.
*/
public ServletModelAttributeMethodProcessor(boolean annotationNotRequired) {
super(annotationNotRequired);

View File

@ -33,9 +33,7 @@ import org.springframework.validation.BindingResult;
import org.springframework.web.HttpSessionRequiredException;
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.SessionStatus;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.HandlerMethod;
@ -43,16 +41,12 @@ import org.springframework.web.method.support.InvocableHandlerMethod;
import org.springframework.web.method.support.ModelAndViewContainer;
/**
* Provides methods to initialize the {@link Model} before a controller method
* invocation and to update it after the controller method has been invoked.
*
* <p>On initialization the model may be populated with session attributes
* stored during a previous request as a result of a {@link SessionAttributes}
* annotation. @{@link ModelAttribute} methods in the same controller may
* also be invoked to populate the model.
*
* <p>On update attributes may be removed from or stored in the session.
* {@link BindingResult} attributes may also be added as necessary.
* Provides methods to initialize the {@link Model} before controller method
* invocation and to update it afterwards. On initialization, the model is
* populated with attributes from the session or by invoking
* {@code @ModelAttribute} methods. On update, model attributes are
* synchronized with the session -- either adding or removing them.
* Also {@link BindingResult} attributes where missing.
*
* @author Rossen Stoyanchev
* @since 3.1
@ -66,10 +60,10 @@ public final class ModelFactory {
private final SessionAttributesHandler sessionAttributesHandler;
/**
* 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
* Create a new instance with the given {@code @ModelAttribute} methods.
* @param attributeMethods for model initialization
* @param binderFactory for adding {@link BindingResult} attributes
* @param sessionAttributesHandler for access to session attributes
*/
public ModelFactory(List<InvocableHandlerMethod> attributeMethods,
WebDataBinderFactory binderFactory,
@ -82,29 +76,40 @@ public final class ModelFactory {
/**
* Populate the model in the following order:
* <ol>
* <li>Retrieve "remembered" (i.e. previously stored) controller-specific session attributes
* <li>Invoke @{@link ModelAttribute} methods
* <li>Check the session for any controller-specific attributes not yet "remembered".
* <li>Retrieve "known" session attributes -- i.e. attributes listed via
* {@link SessionAttributes @SessionAttributes} and previously stored in
* the in the model at least once
* <li>Invoke {@link ModelAttribute @ModelAttribute} methods
* <li>Find method arguments eligible as session attributes and retrieve
* them if they're not already present in the model
* </ol>
* @param request the current request
* @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
* @param mavContainer contains the model to be initialized
* @param handlerMethod the method for which the model is initialized
* @throws Exception may arise from {@code @ModelAttribute} methods
*/
public void initModel(NativeWebRequest request, ModelAndViewContainer mavContainer, HandlerMethod handlerMethod)
throws Exception {
Map<String, ?> sessionAttrs = this.sessionAttributesHandler.retrieveAttributes(request);
mavContainer.mergeAttributes(sessionAttrs);
Map<String, ?> attributesInSession = this.sessionAttributesHandler.retrieveAttributes(request);
mavContainer.mergeAttributes(attributesInSession);
invokeModelAttributeMethods(request, mavContainer);
checkHandlerSessionAttributes(request, mavContainer, handlerMethod);
for (String name : findSessionAttributeArguments(handlerMethod)) {
if (!mavContainer.containsAttribute(name)) {
Object value = this.sessionAttributesHandler.retrieveAttribute(request, name);
if (value == null) {
throw new HttpSessionRequiredException("Expected session attribute '" + name + "'");
}
mavContainer.addAttribute(name, value);
}
}
}
/**
* Invoke model attribute methods to populate the model.
* If two methods return the same attribute, the attribute from the first method is added.
* Invoke model attribute methods to populate the model. Attributes are
* added only if not already present in the model.
*/
private void invokeModelAttributeMethods(NativeWebRequest request, ModelAndViewContainer mavContainer)
throws Exception {
@ -127,41 +132,29 @@ public final class ModelFactory {
}
/**
* 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 an attribute declared
* as session attribute is missing.
* Return all {@code @ModelAttribute} arguments declared as session
* attributes via {@code @SessionAttributes}.
*/
private void checkHandlerSessionAttributes(NativeWebRequest request,
ModelAndViewContainer mavContainer,
HandlerMethod handlerMethod) throws HttpSessionRequiredException {
for (MethodParameter parameter : handlerMethod.getMethodParameters()) {
if (parameter.hasParameterAnnotation(ModelAttribute.class)) {
String attrName = getNameForParameter(parameter);
if (!mavContainer.containsAttribute(attrName)) {
if (sessionAttributesHandler.isHandlerSessionAttribute(attrName, parameter.getParameterType())) {
Object attrValue = sessionAttributesHandler.retrieveAttribute(request, attrName);
if (attrValue == null){
throw new HttpSessionRequiredException(
"Session attribute '" + attrName + "' not found in session: " + handlerMethod);
}
mavContainer.addAttribute(attrName, attrValue);
}
private List<String> findSessionAttributeArguments(HandlerMethod handlerMethod) {
List<String> result = new ArrayList<String>();
for (MethodParameter param : handlerMethod.getMethodParameters()) {
if (param.hasParameterAnnotation(ModelAttribute.class)) {
String name = getNameForParameter(param);
if (this.sessionAttributesHandler.isHandlerSessionAttribute(name, param.getParameterType())) {
result.add(name);
}
}
}
return result;
}
/**
* Derive the model attribute name for the given return value using one of the following:
* Derive the model attribute name for the given return value using
* one of the following:
* <ol>
* <li>The method {@link ModelAttribute} annotation value
* <li>The name of the return type
* <li>The name of the return value type if the method return type is {@code Object}
* <li>The method {@code ModelAttribute} annotation value
* <li>The declared return type if it is other than {@code Object}
* <li>The actual return value type
* </ol>
* @param returnValue the value returned from a method invocation
* @param returnType the return type of the method
@ -180,12 +173,12 @@ public final class ModelFactory {
}
/**
* Derives the model attribute name for the given method parameter using one of the following:
* Derives the model attribute name for a method parameter based on:
* <ol>
* <li>The parameter {@link ModelAttribute} annotation value
* <li>The name of the parameter type
* <li>The parameter {@code @ModelAttribute} annotation value
* <li>The parameter type
* </ol>
* @return the method parameter model name, never {@code null} or an empty string
* @return the derived name; never {@code null} or an empty string
*/
public static String getNameForParameter(MethodParameter parameter) {
ModelAttribute annot = parameter.getParameterAnnotation(ModelAttribute.class);
@ -194,11 +187,11 @@ public final class ModelFactory {
}
/**
* Updates the model by cleaning handler session attributes depending on {@link SessionStatus#isComplete()},
* promotes model attributes to the session, and adds {@link BindingResult} attributes where missing.
* Synchronize model attributes with the session. Add {@link BindingResult}
* attributes where necessary.
* @param request the current request
* @param mavContainer the {@link ModelAndViewContainer} for the current request
* @throws Exception if the process of creating {@link BindingResult} attributes causes an error
* @param mavContainer contains the model to update
* @throws Exception if creating BindingResult attributes fails
*/
public void updateModel(NativeWebRequest request, ModelAndViewContainer mavContainer) throws Exception {
@ -234,7 +227,7 @@ public final class ModelFactory {
}
/**
* Whether the given attribute requires a {@link BindingResult} added to the model.
* Whether the given attribute requires a {@link BindingResult} in the model.
*/
private boolean isBindingCandidate(String attributeName, Object value) {
if (attributeName.startsWith(BindingResult.MODEL_KEY_PREFIX)) {

View File

@ -31,16 +31,15 @@ import org.springframework.web.bind.support.SessionStatus;
import org.springframework.web.context.request.WebRequest;
/**
* Manages handler-specific session attributes declared via @{@link SessionAttributes}.
* Actual storage is performed through an instance of {@link SessionAttributeStore}.
* Manages controller-specific session attributes declared via
* {@link SessionAttributes @SessionAttributes}. Actual storage is
* performed via {@link SessionAttributeStore}.
*
* <p>A typical scenario begins with a controller adding attributes to the
* {@link org.springframework.ui.Model Model}. At the end of the request, model
* attributes are checked to see if any of them match the names and types declared
* via @{@link SessionAttributes}. Matching model attributes are "promoted" to
* the session and remain there until the controller calls
* {@link SessionStatus#setComplete()} to indicate the session attributes are
* no longer needed and can be removed.
* <p>When a controller annotated with {@code @SessionAttributes} adds
* attributes to its model, those attributes are checked against names and
* types specified via {@code @SessionAttributes}. Matching model attributes
* are saved in the HTTP session and remain there until the controller calls
* {@link SessionStatus#setComplete()}.
*
* @author Rossen Stoyanchev
* @since 3.1
@ -56,11 +55,9 @@ public class SessionAttributesHandler {
private final SessionAttributeStore sessionAttributeStore;
/**
* Creates a {@link SessionAttributesHandler} instance for the specified handler type
* Inspects the given handler type for the presence of an @{@link SessionAttributes}
* and stores that information to identify model attribute that need to be stored,
* retrieved, or removed from the session.
* @param handlerType the handler type to inspect for a {@link SessionAttributes} annotation
* Creates a new instance for a controller type. Session attribute names/types
* are extracted from a type-level {@code @SessionAttributes} if found.
* @param handlerType the controller type
* @param sessionAttributeStore used for session access
*/
public SessionAttributesHandler(Class<?> handlerType, SessionAttributeStore sessionAttributeStore) {
@ -75,25 +72,24 @@ public class SessionAttributesHandler {
}
/**
* Whether the controller represented by this handler has declared session
* attribute names or types of interest via @{@link SessionAttributes}.
* Whether the controller represented by this instance has declared session
* attribute names or types of interest via {@link SessionAttributes}.
*/
public boolean hasSessionAttributes() {
return ((this.attributeNames.size() > 0) || (this.attributeTypes.size() > 0));
}
/**
* Whether the controller represented by this instance has declared a specific
* attribute as a session attribute via @{@link SessionAttributes}.
* Whether the attribute name and/or type match those specified in the
* controller's {@code @SessionAttributes} annotation.
*
* <p>Attributes successfully resolved through this method are "remembered" and
* used by calls to {@link #retrieveAttributes(WebRequest)} and
* {@link #cleanupAttributes(WebRequest)}. In other words unless attributes
* have been resolved and stored before, retrieval and cleanup have no impact.
* <p>Attributes successfully resolved through this method are "remembered"
* and used in {@link #retrieveAttributes(WebRequest)} and
* {@link #cleanupAttributes(WebRequest)}. In other words, retrieval and
* cleanup only affect attributes previously resolved through here.
*
* @param attributeName the attribute name to check, must not be null
* @param attributeType the type for the attribute, not required but should be provided when
* available as session attributes of interest can be matched by type
* @param attributeName the attribute name to check; must not be null
* @param attributeType the type for the attribute; or {@code null}
*/
public boolean isHandlerSessionAttribute(String attributeName, Class<?> attributeType) {
Assert.notNull(attributeName, "Attribute name must not be null");
@ -108,7 +104,7 @@ public class SessionAttributesHandler {
/**
* Stores a subset of the given attributes in the session. Attributes not
* declared as session attributes via @{@link SessionAttributes} are ignored.
* declared as session attributes via {@code @SessionAttributes} are ignored.
* @param request the current request
* @param attributes candidate attributes for session storage
*/
@ -124,8 +120,9 @@ public class SessionAttributesHandler {
}
/**
* Retrieves "remembered" (i.e. previously stored) session attributes
* for the controller represented by this handler.
* Retrieve "known" attributes from the session -- i.e. attributes listed
* in {@code @SessionAttributes} and previously stored in the in the model
* at least once.
* @param request the current request
* @return a map with handler session attributes; possibly empty.
*/
@ -141,8 +138,9 @@ public class SessionAttributesHandler {
}
/**
* Cleans "remembered" (i.e. previously stored) session attributes
* for the controller represented by this handler.
* Cleans "known" attributes from the session - i.e. attributes listed
* in {@code @SessionAttributes} and previously stored in the in the model
* at least once.
* @param request the current request
*/
public void cleanupAttributes(WebRequest request) {

View File

@ -54,14 +54,13 @@ import org.springframework.web.method.support.ModelAndViewContainer;
public class ModelAttributeMethodProcessor implements HandlerMethodArgumentResolver, HandlerMethodReturnValueHandler {
protected Log logger = LogFactory.getLog(this.getClass());
private final boolean annotationNotRequired;
/**
* @param annotationNotRequired if {@code true}, any non-simple type
* argument or return value is regarded as a model attribute even without
* the presence of a {@code @ModelAttribute} annotation in which case the
* attribute name is derived from the model attribute's type.
* @param annotationNotRequired if "true", non-simple method arguments and
* return values are considered model attributes with or without a
* {@code @ModelAttribute} annotation.
*/
public ModelAttributeMethodProcessor(boolean annotationNotRequired) {
this.annotationNotRequired = annotationNotRequired;
@ -99,7 +98,7 @@ public class ModelAttributeMethodProcessor implements HandlerMethodArgumentResol
String name = ModelFactory.getNameForParameter(parameter);
Object target = (mavContainer.containsAttribute(name)) ?
mavContainer.getModel().get(name) : createAttribute(name, parameter, binderFactory, request);
WebDataBinder binder = binderFactory.createBinder(request, target, name);
if (binder.getTarget() != null) {