All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.springframework.web.method.annotation.ModelFactory Maven / Gradle / Ivy

/*
 * Copyright 2002-2019 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.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 {@link 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 {@code @ModelAttribute} methods. * *

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 final List modelMethods = new ArrayList<>(); private final WebDataBinderFactory dataBinderFactory; private final SessionAttributesHandler sessionAttributesHandler; /** * 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 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 {@code @SessionAttributes}. *
  2. Invoke {@code @ModelAttribute} methods *
  3. Find {@code @ModelAttribute} method arguments also listed as * {@code @SessionAttributes} and ensure they're present in the model raising * an exception if necessary. *
* @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 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. */ 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()){ 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 {@code @ModelAttribute} arguments also listed as {@code @SessionAttributes}. */ private List findSessionAttributeArguments(HandlerMethod handlerMethod) { List 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 {@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 {@link BindingResult} attributes to the model for attributes that require it. */ private void updateBindingResult(NativeWebRequest request, ModelMap model) throws Exception { List 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 {@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 {@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 {@code ModelAttribute} annotation value *
  2. the declared return type if it is more specific than {@code Object} *
  3. the actual return value type *
* @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 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(); } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy