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