diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/annotation/support/ServletModelAttributeMethodProcessor.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/annotation/support/ServletModelAttributeMethodProcessor.java index 66e3aa3adf4..c22d9c6b6cf 100644 --- a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/annotation/support/ServletModelAttributeMethodProcessor.java +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/annotation/support/ServletModelAttributeMethodProcessor.java @@ -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); diff --git a/org.springframework.web/src/main/java/org/springframework/web/method/annotation/ModelFactory.java b/org.springframework.web/src/main/java/org/springframework/web/method/annotation/ModelFactory.java index d1985e684cc..3d5abfa580a 100644 --- a/org.springframework.web/src/main/java/org/springframework/web/method/annotation/ModelFactory.java +++ b/org.springframework.web/src/main/java/org/springframework/web/method/annotation/ModelFactory.java @@ -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. - * - *

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. - * - *

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 attributeMethods, WebDataBinderFactory binderFactory, @@ -82,29 +76,40 @@ public final class ModelFactory { /** * Populate the model in the following order: *

    - *
  1. Retrieve "remembered" (i.e. previously stored) controller-specific session attributes - *
  2. Invoke @{@link ModelAttribute} methods - *
  3. Check the session for any controller-specific attributes not yet "remembered". + *
  4. Retrieve "known" session attributes -- i.e. attributes listed via + * {@link SessionAttributes @SessionAttributes} and previously stored in + * the in the model at least once + *
  5. Invoke {@link ModelAttribute @ModelAttribute} methods + *
  6. Find method arguments eligible as session attributes and retrieve + * them if they're not already present in the model *
* @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 sessionAttrs = this.sessionAttributesHandler.retrieveAttributes(request); - mavContainer.mergeAttributes(sessionAttrs); - + Map 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 findSessionAttributeArguments(HandlerMethod handlerMethod) { + List result = new ArrayList(); + 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: *
    - *
  1. The method {@link ModelAttribute} annotation value - *
  2. The name of the return type - *
  3. The name of the return value type if the method return type is {@code Object} + *
  4. The method {@code ModelAttribute} annotation value + *
  5. The declared return type if it is other than {@code Object} + *
  6. The actual return value type *
* @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: *
    - *
  1. The parameter {@link ModelAttribute} annotation value - *
  2. The name of the parameter type + *
  3. The parameter {@code @ModelAttribute} annotation value + *
  4. The parameter type *
- * @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)) { diff --git a/org.springframework.web/src/main/java/org/springframework/web/method/annotation/SessionAttributesHandler.java b/org.springframework.web/src/main/java/org/springframework/web/method/annotation/SessionAttributesHandler.java index 19fc71d3cfe..fd7b4e42cf6 100644 --- a/org.springframework.web/src/main/java/org/springframework/web/method/annotation/SessionAttributesHandler.java +++ b/org.springframework.web/src/main/java/org/springframework/web/method/annotation/SessionAttributesHandler.java @@ -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}. * - *

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. + *

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. * - *

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. + *

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) { diff --git a/org.springframework.web/src/main/java/org/springframework/web/method/annotation/support/ModelAttributeMethodProcessor.java b/org.springframework.web/src/main/java/org/springframework/web/method/annotation/support/ModelAttributeMethodProcessor.java index dd1efa95662..da90c809ad4 100644 --- a/org.springframework.web/src/main/java/org/springframework/web/method/annotation/support/ModelAttributeMethodProcessor.java +++ b/org.springframework.web/src/main/java/org/springframework/web/method/annotation/support/ModelAttributeMethodProcessor.java @@ -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) {