/*
 * Copyright 2002-2020 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
 *
 *      https://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.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import org.springframework.beans.BeanUtils;
import org.springframework.core.Conventions;
import org.springframework.core.GenericTypeResolver;
import org.springframework.core.MethodParameter;
import org.springframework.lang.Nullable;
import org.springframework.ui.Model;
import org.springframework.ui.ModelMap;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
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.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.method.support.InvocableHandlerMethod;
import org.springframework.web.method.support.ModelAndViewContainer;

Assist with initialization of the Model before controller method invocation and with updates to it after the invocation.

On initialization the model is populated with attributes temporarily stored in the session and through the invocation of @ModelAttribute methods.

On update model attributes are synchronized with the session and also BindingResult attributes are added if missing.

Author:Rossen Stoyanchev
Since:3.1
/** * Assist with initialization of the {@link Model} before controller method * invocation and with updates to it after the invocation. * * <p>On initialization the model is populated with attributes temporarily stored * in the session and through the invocation of {@code @ModelAttribute} methods. * * <p>On update model attributes are synchronized with the session and also * {@link BindingResult} attributes are added if missing. * * @author Rossen Stoyanchev * @since 3.1 */
public final class ModelFactory { private static final Log logger = LogFactory.getLog(ModelFactory.class); private final List<ModelMethod> modelMethods = new ArrayList<>(); private final WebDataBinderFactory dataBinderFactory; private final SessionAttributesHandler sessionAttributesHandler;
Create a new instance with the given @ModelAttribute methods.
Params:
  • handlerMethods – the @ModelAttribute methods to invoke
  • binderFactory – for preparation of BindingResult attributes
  • attributeHandler – for access to session attributes
/** * Create a new instance with the given {@code @ModelAttribute} methods. * @param handlerMethods the {@code @ModelAttribute} methods to invoke * @param binderFactory for preparation of {@link BindingResult} attributes * @param attributeHandler for access to session attributes */
public ModelFactory(@Nullable List<InvocableHandlerMethod> handlerMethods, WebDataBinderFactory binderFactory, SessionAttributesHandler attributeHandler) { if (handlerMethods != null) { for (InvocableHandlerMethod handlerMethod : handlerMethods) { this.modelMethods.add(new ModelMethod(handlerMethod)); } } this.dataBinderFactory = binderFactory; this.sessionAttributesHandler = attributeHandler; }
Populate the model in the following order:
  1. Retrieve "known" session attributes listed as @SessionAttributes.
  2. Invoke @ModelAttribute methods
  3. Find @ModelAttribute method arguments also listed as @SessionAttributes and ensure they're present in the model raising an exception if necessary.
Params:
  • request – the current request
  • container – a container with the model to be initialized
  • handlerMethod – the method for which the model is initialized
Throws:
  • Exception – may arise from @ModelAttribute methods
/** * Populate the model in the following order: * <ol> * <li>Retrieve "known" session attributes listed as {@code @SessionAttributes}. * <li>Invoke {@code @ModelAttribute} methods * <li>Find {@code @ModelAttribute} method arguments also listed as * {@code @SessionAttributes} and ensure they're present in the model raising * an exception if necessary. * </ol> * @param request the current request * @param container a container with 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 container, HandlerMethod handlerMethod) throws Exception { Map<String, ?> sessionAttributes = this.sessionAttributesHandler.retrieveAttributes(request); container.mergeAttributes(sessionAttributes); invokeModelAttributeMethods(request, container); for (String name : findSessionAttributeArguments(handlerMethod)) { if (!container.containsAttribute(name)) { Object value = this.sessionAttributesHandler.retrieveAttribute(request, name); if (value == null) { throw new HttpSessionRequiredException("Expected session attribute '" + name + "'", name); } container.addAttribute(name, value); } } }
Invoke model attribute methods to populate the model. Attributes are added only if not already present in the model.
/** * 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 container) throws Exception { while (!this.modelMethods.isEmpty()) { InvocableHandlerMethod modelMethod = getNextModelMethod(container).getHandlerMethod(); ModelAttribute ann = modelMethod.getMethodAnnotation(ModelAttribute.class); Assert.state(ann != null, "No ModelAttribute annotation"); if (container.containsAttribute(ann.name())) { if (!ann.binding()) { container.setBindingDisabled(ann.name()); } continue; } Object returnValue = modelMethod.invokeForRequest(request, container); if (modelMethod.isVoid()) { if (StringUtils.hasText(ann.value())) { if (logger.isDebugEnabled()) { logger.debug("Name in @ModelAttribute is ignored because method returns void: " + modelMethod.getShortLogMessage()); } } continue; } String returnValueName = getNameForReturnValue(returnValue, modelMethod.getReturnType()); if (!ann.binding()) { container.setBindingDisabled(returnValueName); } if (!container.containsAttribute(returnValueName)) { container.addAttribute(returnValueName, returnValue); } } } private ModelMethod getNextModelMethod(ModelAndViewContainer container) { for (ModelMethod modelMethod : this.modelMethods) { if (modelMethod.checkDependencies(container)) { this.modelMethods.remove(modelMethod); return modelMethod; } } ModelMethod modelMethod = this.modelMethods.get(0); this.modelMethods.remove(modelMethod); return modelMethod; }
Find @ModelAttribute arguments also listed as @SessionAttributes.
/** * Find {@code @ModelAttribute} arguments also listed as {@code @SessionAttributes}. */
private List<String> findSessionAttributeArguments(HandlerMethod handlerMethod) { List<String> result = new ArrayList<>(); for (MethodParameter parameter : handlerMethod.getMethodParameters()) { if (parameter.hasParameterAnnotation(ModelAttribute.class)) { String name = getNameForParameter(parameter); Class<?> paramType = parameter.getParameterType(); if (this.sessionAttributesHandler.isHandlerSessionAttribute(name, paramType)) { result.add(name); } } } return result; }
Promote model attributes listed as @SessionAttributes to the session. Add BindingResult attributes where necessary.
Params:
  • request – the current request
  • container – contains the model to update
Throws:
  • Exception – if creating BindingResult attributes fails
/** * Promote model attributes listed as {@code @SessionAttributes} to the session. * Add {@link BindingResult} attributes where necessary. * @param request the current request * @param container contains the model to update * @throws Exception if creating BindingResult attributes fails */
public void updateModel(NativeWebRequest request, ModelAndViewContainer container) throws Exception { ModelMap defaultModel = container.getDefaultModel(); if (container.getSessionStatus().isComplete()){ this.sessionAttributesHandler.cleanupAttributes(request); } else { this.sessionAttributesHandler.storeAttributes(request, defaultModel); } if (!container.isRequestHandled() && container.getModel() == defaultModel) { updateBindingResult(request, defaultModel); } }
Add BindingResult attributes to the model for attributes that require it.
/** * Add {@link BindingResult} attributes to the model for attributes that require it. */
private void updateBindingResult(NativeWebRequest request, ModelMap model) throws Exception { List<String> keyNames = new ArrayList<>(model.keySet()); for (String name : keyNames) { Object value = model.get(name); if (value != null && isBindingCandidate(name, value)) { String bindingResultKey = BindingResult.MODEL_KEY_PREFIX + name; if (!model.containsAttribute(bindingResultKey)) { WebDataBinder dataBinder = this.dataBinderFactory.createBinder(request, value, name); model.put(bindingResultKey, dataBinder.getBindingResult()); } } } }
Whether the given attribute requires a BindingResult in 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)) { return false; } if (this.sessionAttributesHandler.isHandlerSessionAttribute(attributeName, value.getClass())) { return true; } return (!value.getClass().isArray() && !(value instanceof Collection) && !(value instanceof Map) && !BeanUtils.isSimpleValueType(value.getClass())); }
Derive the model attribute name for the given method parameter based on a @ModelAttribute parameter annotation (if present) or falling back on parameter type based conventions.
Params:
  • parameter – a descriptor for the method parameter
See Also:
Returns:the derived name
/** * Derive the model attribute name for the given method parameter based on * a {@code @ModelAttribute} parameter annotation (if present) or falling * back on parameter type based conventions. * @param parameter a descriptor for the method parameter * @return the derived name * @see Conventions#getVariableNameForParameter(MethodParameter) */
public static String getNameForParameter(MethodParameter parameter) { ModelAttribute ann = parameter.getParameterAnnotation(ModelAttribute.class); String name = (ann != null ? ann.value() : null); return (StringUtils.hasText(name) ? name : Conventions.getVariableNameForParameter(parameter)); }
Derive the model attribute name for the given return value. Results will be based on:
  1. the method ModelAttribute annotation value
  2. the declared return type if it is more specific than Object
  3. the actual return value type
Params:
  • returnValue – the value returned from a method invocation
  • returnType – a descriptor for the return type of the method
Returns:the derived name (never null or empty String)
/** * Derive the model attribute name for the given return value. Results will be * based on: * <ol> * <li>the method {@code ModelAttribute} annotation value * <li>the declared return type if it is more specific than {@code Object} * <li>the actual return value type * </ol> * @param returnValue the value returned from a method invocation * @param returnType a descriptor for the return type of the method * @return the derived name (never {@code null} or empty String) */
public static String getNameForReturnValue(@Nullable Object returnValue, MethodParameter returnType) { ModelAttribute ann = returnType.getMethodAnnotation(ModelAttribute.class); if (ann != null && StringUtils.hasText(ann.value())) { return ann.value(); } else { Method method = returnType.getMethod(); Assert.state(method != null, "No handler method"); Class<?> containingClass = returnType.getContainingClass(); Class<?> resolvedType = GenericTypeResolver.resolveReturnType(method, containingClass); return Conventions.getVariableNameForReturnType(method, resolvedType, returnValue); } } private static class ModelMethod { private final InvocableHandlerMethod handlerMethod; private final Set<String> dependencies = new HashSet<>(); public ModelMethod(InvocableHandlerMethod handlerMethod) { this.handlerMethod = handlerMethod; for (MethodParameter parameter : handlerMethod.getMethodParameters()) { if (parameter.hasParameterAnnotation(ModelAttribute.class)) { this.dependencies.add(getNameForParameter(parameter)); } } } public InvocableHandlerMethod getHandlerMethod() { return this.handlerMethod; } public boolean checkDependencies(ModelAndViewContainer mavContainer) { for (String name : this.dependencies) { if (!mavContainer.containsAttribute(name)) { return false; } } return true; } @Override public String toString() { return this.handlerMethod.getMethod().toGenericString(); } } }