001/* 002 * Copyright 2002-2019 the original author or authors. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * https://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 017package org.springframework.web.method.annotation; 018 019import java.lang.reflect.Method; 020import java.util.ArrayList; 021import java.util.Collection; 022import java.util.HashSet; 023import java.util.List; 024import java.util.Map; 025import java.util.Set; 026 027import org.springframework.beans.BeanUtils; 028import org.springframework.core.Conventions; 029import org.springframework.core.GenericTypeResolver; 030import org.springframework.core.MethodParameter; 031import org.springframework.lang.Nullable; 032import org.springframework.ui.Model; 033import org.springframework.ui.ModelMap; 034import org.springframework.util.Assert; 035import org.springframework.util.StringUtils; 036import org.springframework.validation.BindingResult; 037import org.springframework.web.HttpSessionRequiredException; 038import org.springframework.web.bind.WebDataBinder; 039import org.springframework.web.bind.annotation.ModelAttribute; 040import org.springframework.web.bind.support.WebDataBinderFactory; 041import org.springframework.web.context.request.NativeWebRequest; 042import org.springframework.web.method.HandlerMethod; 043import org.springframework.web.method.support.InvocableHandlerMethod; 044import org.springframework.web.method.support.ModelAndViewContainer; 045 046/** 047 * Assist with initialization of the {@link Model} before controller method 048 * invocation and with updates to it after the invocation. 049 * 050 * <p>On initialization the model is populated with attributes temporarily stored 051 * in the session and through the invocation of {@code @ModelAttribute} methods. 052 * 053 * <p>On update model attributes are synchronized with the session and also 054 * {@link BindingResult} attributes are added if missing. 055 * 056 * @author Rossen Stoyanchev 057 * @since 3.1 058 */ 059public final class ModelFactory { 060 061 private final List<ModelMethod> modelMethods = new ArrayList<>(); 062 063 private final WebDataBinderFactory dataBinderFactory; 064 065 private final SessionAttributesHandler sessionAttributesHandler; 066 067 068 /** 069 * Create a new instance with the given {@code @ModelAttribute} methods. 070 * @param handlerMethods the {@code @ModelAttribute} methods to invoke 071 * @param binderFactory for preparation of {@link BindingResult} attributes 072 * @param attributeHandler for access to session attributes 073 */ 074 public ModelFactory(@Nullable List<InvocableHandlerMethod> handlerMethods, 075 WebDataBinderFactory binderFactory, SessionAttributesHandler attributeHandler) { 076 077 if (handlerMethods != null) { 078 for (InvocableHandlerMethod handlerMethod : handlerMethods) { 079 this.modelMethods.add(new ModelMethod(handlerMethod)); 080 } 081 } 082 this.dataBinderFactory = binderFactory; 083 this.sessionAttributesHandler = attributeHandler; 084 } 085 086 087 /** 088 * Populate the model in the following order: 089 * <ol> 090 * <li>Retrieve "known" session attributes listed as {@code @SessionAttributes}. 091 * <li>Invoke {@code @ModelAttribute} methods 092 * <li>Find {@code @ModelAttribute} method arguments also listed as 093 * {@code @SessionAttributes} and ensure they're present in the model raising 094 * an exception if necessary. 095 * </ol> 096 * @param request the current request 097 * @param container a container with the model to be initialized 098 * @param handlerMethod the method for which the model is initialized 099 * @throws Exception may arise from {@code @ModelAttribute} methods 100 */ 101 public void initModel(NativeWebRequest request, ModelAndViewContainer container, HandlerMethod handlerMethod) 102 throws Exception { 103 104 Map<String, ?> sessionAttributes = this.sessionAttributesHandler.retrieveAttributes(request); 105 container.mergeAttributes(sessionAttributes); 106 invokeModelAttributeMethods(request, container); 107 108 for (String name : findSessionAttributeArguments(handlerMethod)) { 109 if (!container.containsAttribute(name)) { 110 Object value = this.sessionAttributesHandler.retrieveAttribute(request, name); 111 if (value == null) { 112 throw new HttpSessionRequiredException("Expected session attribute '" + name + "'", name); 113 } 114 container.addAttribute(name, value); 115 } 116 } 117 } 118 119 /** 120 * Invoke model attribute methods to populate the model. 121 * Attributes are added only if not already present in the model. 122 */ 123 private void invokeModelAttributeMethods(NativeWebRequest request, ModelAndViewContainer container) 124 throws Exception { 125 126 while (!this.modelMethods.isEmpty()) { 127 InvocableHandlerMethod modelMethod = getNextModelMethod(container).getHandlerMethod(); 128 ModelAttribute ann = modelMethod.getMethodAnnotation(ModelAttribute.class); 129 Assert.state(ann != null, "No ModelAttribute annotation"); 130 if (container.containsAttribute(ann.name())) { 131 if (!ann.binding()) { 132 container.setBindingDisabled(ann.name()); 133 } 134 continue; 135 } 136 137 Object returnValue = modelMethod.invokeForRequest(request, container); 138 if (!modelMethod.isVoid()){ 139 String returnValueName = getNameForReturnValue(returnValue, modelMethod.getReturnType()); 140 if (!ann.binding()) { 141 container.setBindingDisabled(returnValueName); 142 } 143 if (!container.containsAttribute(returnValueName)) { 144 container.addAttribute(returnValueName, returnValue); 145 } 146 } 147 } 148 } 149 150 private ModelMethod getNextModelMethod(ModelAndViewContainer container) { 151 for (ModelMethod modelMethod : this.modelMethods) { 152 if (modelMethod.checkDependencies(container)) { 153 this.modelMethods.remove(modelMethod); 154 return modelMethod; 155 } 156 } 157 ModelMethod modelMethod = this.modelMethods.get(0); 158 this.modelMethods.remove(modelMethod); 159 return modelMethod; 160 } 161 162 /** 163 * Find {@code @ModelAttribute} arguments also listed as {@code @SessionAttributes}. 164 */ 165 private List<String> findSessionAttributeArguments(HandlerMethod handlerMethod) { 166 List<String> result = new ArrayList<>(); 167 for (MethodParameter parameter : handlerMethod.getMethodParameters()) { 168 if (parameter.hasParameterAnnotation(ModelAttribute.class)) { 169 String name = getNameForParameter(parameter); 170 Class<?> paramType = parameter.getParameterType(); 171 if (this.sessionAttributesHandler.isHandlerSessionAttribute(name, paramType)) { 172 result.add(name); 173 } 174 } 175 } 176 return result; 177 } 178 179 /** 180 * Promote model attributes listed as {@code @SessionAttributes} to the session. 181 * Add {@link BindingResult} attributes where necessary. 182 * @param request the current request 183 * @param container contains the model to update 184 * @throws Exception if creating BindingResult attributes fails 185 */ 186 public void updateModel(NativeWebRequest request, ModelAndViewContainer container) throws Exception { 187 ModelMap defaultModel = container.getDefaultModel(); 188 if (container.getSessionStatus().isComplete()){ 189 this.sessionAttributesHandler.cleanupAttributes(request); 190 } 191 else { 192 this.sessionAttributesHandler.storeAttributes(request, defaultModel); 193 } 194 if (!container.isRequestHandled() && container.getModel() == defaultModel) { 195 updateBindingResult(request, defaultModel); 196 } 197 } 198 199 /** 200 * Add {@link BindingResult} attributes to the model for attributes that require it. 201 */ 202 private void updateBindingResult(NativeWebRequest request, ModelMap model) throws Exception { 203 List<String> keyNames = new ArrayList<>(model.keySet()); 204 for (String name : keyNames) { 205 Object value = model.get(name); 206 if (value != null && isBindingCandidate(name, value)) { 207 String bindingResultKey = BindingResult.MODEL_KEY_PREFIX + name; 208 if (!model.containsAttribute(bindingResultKey)) { 209 WebDataBinder dataBinder = this.dataBinderFactory.createBinder(request, value, name); 210 model.put(bindingResultKey, dataBinder.getBindingResult()); 211 } 212 } 213 } 214 } 215 216 /** 217 * Whether the given attribute requires a {@link BindingResult} in the model. 218 */ 219 private boolean isBindingCandidate(String attributeName, Object value) { 220 if (attributeName.startsWith(BindingResult.MODEL_KEY_PREFIX)) { 221 return false; 222 } 223 224 if (this.sessionAttributesHandler.isHandlerSessionAttribute(attributeName, value.getClass())) { 225 return true; 226 } 227 228 return (!value.getClass().isArray() && !(value instanceof Collection) && 229 !(value instanceof Map) && !BeanUtils.isSimpleValueType(value.getClass())); 230 } 231 232 233 /** 234 * Derive the model attribute name for the given method parameter based on 235 * a {@code @ModelAttribute} parameter annotation (if present) or falling 236 * back on parameter type based conventions. 237 * @param parameter a descriptor for the method parameter 238 * @return the derived name 239 * @see Conventions#getVariableNameForParameter(MethodParameter) 240 */ 241 public static String getNameForParameter(MethodParameter parameter) { 242 ModelAttribute ann = parameter.getParameterAnnotation(ModelAttribute.class); 243 String name = (ann != null ? ann.value() : null); 244 return (StringUtils.hasText(name) ? name : Conventions.getVariableNameForParameter(parameter)); 245 } 246 247 /** 248 * Derive the model attribute name for the given return value. Results will be 249 * based on: 250 * <ol> 251 * <li>the method {@code ModelAttribute} annotation value 252 * <li>the declared return type if it is more specific than {@code Object} 253 * <li>the actual return value type 254 * </ol> 255 * @param returnValue the value returned from a method invocation 256 * @param returnType a descriptor for the return type of the method 257 * @return the derived name (never {@code null} or empty String) 258 */ 259 public static String getNameForReturnValue(@Nullable Object returnValue, MethodParameter returnType) { 260 ModelAttribute ann = returnType.getMethodAnnotation(ModelAttribute.class); 261 if (ann != null && StringUtils.hasText(ann.value())) { 262 return ann.value(); 263 } 264 else { 265 Method method = returnType.getMethod(); 266 Assert.state(method != null, "No handler method"); 267 Class<?> containingClass = returnType.getContainingClass(); 268 Class<?> resolvedType = GenericTypeResolver.resolveReturnType(method, containingClass); 269 return Conventions.getVariableNameForReturnType(method, resolvedType, returnValue); 270 } 271 } 272 273 274 private static class ModelMethod { 275 276 private final InvocableHandlerMethod handlerMethod; 277 278 private final Set<String> dependencies = new HashSet<>(); 279 280 public ModelMethod(InvocableHandlerMethod handlerMethod) { 281 this.handlerMethod = handlerMethod; 282 for (MethodParameter parameter : handlerMethod.getMethodParameters()) { 283 if (parameter.hasParameterAnnotation(ModelAttribute.class)) { 284 this.dependencies.add(getNameForParameter(parameter)); 285 } 286 } 287 } 288 289 public InvocableHandlerMethod getHandlerMethod() { 290 return this.handlerMethod; 291 } 292 293 public boolean checkDependencies(ModelAndViewContainer mavContainer) { 294 for (String name : this.dependencies) { 295 if (!mavContainer.containsAttribute(name)) { 296 return false; 297 } 298 } 299 return true; 300 } 301 302 @Override 303 public String toString() { 304 return this.handlerMethod.getMethod().toGenericString(); 305 } 306 } 307 308}