View Javadoc
1   /*
2    * Copyright 2002-2014 the original author or authors.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *      http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  
17  package org.springframework.web.method.annotation;
18  
19  import java.lang.reflect.Method;
20  import java.util.ArrayList;
21  import java.util.Collection;
22  import java.util.HashSet;
23  import java.util.List;
24  import java.util.Map;
25  import java.util.Set;
26  
27  import org.apache.commons.logging.Log;
28  import org.apache.commons.logging.LogFactory;
29  
30  import org.springframework.beans.BeanUtils;
31  import org.springframework.core.Conventions;
32  import org.springframework.core.GenericTypeResolver;
33  import org.springframework.core.MethodParameter;
34  import org.springframework.ui.Model;
35  import org.springframework.ui.ModelMap;
36  import org.springframework.util.StringUtils;
37  import org.springframework.validation.BindingResult;
38  import org.springframework.web.HttpSessionRequiredException;
39  import org.springframework.web.bind.WebDataBinder;
40  import org.springframework.web.bind.annotation.ModelAttribute;
41  import org.springframework.web.bind.support.WebDataBinderFactory;
42  import org.springframework.web.context.request.NativeWebRequest;
43  import org.springframework.web.method.HandlerMethod;
44  import org.springframework.web.method.support.InvocableHandlerMethod;
45  import org.springframework.web.method.support.ModelAndViewContainer;
46  
47  /**
48   * Provides methods to initialize the {@link Model} before controller method
49   * invocation and to update it afterwards.
50   *
51   * <p>On initialization, the model is populated with attributes from the session
52   * and by invoking methods annotated with {@code @ModelAttribute}.
53   *
54   * <p>On update, model attributes are synchronized with the session and also
55   * {@link BindingResult} attributes are added where missing.
56   *
57   * @author Rossen Stoyanchev
58   * @since 3.1
59   */
60  public final class ModelFactory {
61  
62  	private static final Log logger = LogFactory.getLog(ModelFactory.class);
63  
64  	private final List<ModelMethod> modelMethods = new ArrayList<ModelMethod>();
65  
66  	private final WebDataBinderFactory dataBinderFactory;
67  
68  	private final SessionAttributesHandler sessionAttributesHandler;
69  
70  
71  	/**
72  	 * Create a new instance with the given {@code @ModelAttribute} methods.
73  	 * @param invocableMethods the {@code @ModelAttribute} methods to invoke
74  	 * @param dataBinderFactory for preparation of {@link BindingResult} attributes
75  	 * @param sessionAttributesHandler for access to session attributes
76  	 */
77  	public ModelFactory(List<InvocableHandlerMethod> invocableMethods, WebDataBinderFactory dataBinderFactory,
78  			SessionAttributesHandler sessionAttributesHandler) {
79  
80  		if (invocableMethods != null) {
81  			for (InvocableHandlerMethod method : invocableMethods) {
82  				this.modelMethods.add(new ModelMethod(method));
83  			}
84  		}
85  		this.dataBinderFactory = dataBinderFactory;
86  		this.sessionAttributesHandler = sessionAttributesHandler;
87  	}
88  
89  	/**
90  	 * Populate the model in the following order:
91  	 * <ol>
92  	 * 	<li>Retrieve "known" session attributes listed as {@code @SessionAttributes}.
93  	 * 	<li>Invoke {@code @ModelAttribute} methods
94  	 * 	<li>Find {@code @ModelAttribute} method arguments also listed as
95  	 * 	{@code @SessionAttributes} and ensure they're present in the model raising
96  	 * 	an exception if necessary.
97  	 * </ol>
98  	 * @param request the current request
99  	 * @param mavContainer a container with the model to be initialized
100 	 * @param handlerMethod the method for which the model is initialized
101 	 * @throws Exception may arise from {@code @ModelAttribute} methods
102 	 */
103 	public void initModel(NativeWebRequest request, ModelAndViewContainer mavContainer, HandlerMethod handlerMethod)
104 			throws Exception {
105 
106 		Map<String, ?> sessionAttributes = this.sessionAttributesHandler.retrieveAttributes(request);
107 		mavContainer.mergeAttributes(sessionAttributes);
108 
109 		invokeModelAttributeMethods(request, mavContainer);
110 
111 		for (String name : findSessionAttributeArguments(handlerMethod)) {
112 			if (!mavContainer.containsAttribute(name)) {
113 				Object value = this.sessionAttributesHandler.retrieveAttribute(request, name);
114 				if (value == null) {
115 					throw new HttpSessionRequiredException("Expected session attribute '" + name + "'");
116 				}
117 				mavContainer.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 mavContainer)
127 			throws Exception {
128 
129 		while (!this.modelMethods.isEmpty()) {
130 			InvocableHandlerMethod attrMethod = getNextModelMethod(mavContainer).getHandlerMethod();
131 			String modelName = attrMethod.getMethodAnnotation(ModelAttribute.class).value();
132 			if (mavContainer.containsAttribute(modelName)) {
133 				continue;
134 			}
135 
136 			Object returnValue = attrMethod.invokeForRequest(request, mavContainer);
137 
138 			if (!attrMethod.isVoid()){
139 				String returnValueName = getNameForReturnValue(returnValue, attrMethod.getReturnType());
140 				if (!mavContainer.containsAttribute(returnValueName)) {
141 					mavContainer.addAttribute(returnValueName, returnValue);
142 				}
143 			}
144 		}
145 	}
146 
147 	private ModelMethod getNextModelMethod(ModelAndViewContainer mavContainer) {
148 		for (ModelMethod modelMethod : this.modelMethods) {
149 			if (modelMethod.checkDependencies(mavContainer)) {
150 				if (logger.isTraceEnabled()) {
151 					logger.trace("Selected @ModelAttribute method " + modelMethod);
152 				}
153 				this.modelMethods.remove(modelMethod);
154 				return modelMethod;
155 			}
156 		}
157 		ModelMethod modelMethod = this.modelMethods.get(0);
158 		if (logger.isTraceEnabled()) {
159 			logger.trace("Selected @ModelAttribute method (not present: " +
160 					modelMethod.getUnresolvedDependencies(mavContainer)+ ") " + modelMethod);
161 		}
162 		this.modelMethods.remove(modelMethod);
163 		return modelMethod;
164 	}
165 
166 	/**
167 	 * Find {@code @ModelAttribute} arguments also listed as {@code @SessionAttributes}.
168 	 */
169 	private List<String> findSessionAttributeArguments(HandlerMethod handlerMethod) {
170 		List<String> result = new ArrayList<String>();
171 		for (MethodParameter parameter : handlerMethod.getMethodParameters()) {
172 			if (parameter.hasParameterAnnotation(ModelAttribute.class)) {
173 				String name = getNameForParameter(parameter);
174 				if (this.sessionAttributesHandler.isHandlerSessionAttribute(name, parameter.getParameterType())) {
175 					result.add(name);
176 				}
177 			}
178 		}
179 		return result;
180 	}
181 
182 	/**
183 	 * Derives the model attribute name for a method parameter based on:
184 	 * <ol>
185 	 * 	<li>The parameter {@code @ModelAttribute} annotation value
186 	 * 	<li>The parameter type
187 	 * </ol>
188 	 * @return the derived name; never {@code null} or an empty string
189 	 */
190 	public static String getNameForParameter(MethodParameter parameter) {
191 		ModelAttribute annot = parameter.getParameterAnnotation(ModelAttribute.class);
192 		String attrName = (annot != null) ? annot.value() : null;
193 		return StringUtils.hasText(attrName) ? attrName :  Conventions.getVariableNameForParameter(parameter);
194 	}
195 
196 	/**
197 	 * Derive the model attribute name for the given return value using one of:
198 	 * <ol>
199 	 * 	<li>The method {@code ModelAttribute} annotation value
200 	 * 	<li>The declared return type if it is more specific than {@code Object}
201 	 * 	<li>The actual return value type
202 	 * </ol>
203 	 * @param returnValue the value returned from a method invocation
204 	 * @param returnType the return type of the method
205 	 * @return the model name, never {@code null} nor empty
206 	 */
207 	public static String getNameForReturnValue(Object returnValue, MethodParameter returnType) {
208 		ModelAttribute annotation = returnType.getMethodAnnotation(ModelAttribute.class);
209 		if (annotation != null && StringUtils.hasText(annotation.value())) {
210 			return annotation.value();
211 		}
212 		else {
213 			Method method = returnType.getMethod();
214 			Class<?> resolvedType = GenericTypeResolver.resolveReturnType(method, returnType.getContainingClass());
215 			return Conventions.getVariableNameForReturnType(method, resolvedType, returnValue);
216 		}
217 	}
218 
219 	/**
220 	 * Promote model attributes listed as {@code @SessionAttributes} to the session.
221 	 * Add {@link BindingResult} attributes where necessary.
222 	 * @param request the current request
223 	 * @param mavContainer contains the model to update
224 	 * @throws Exception if creating BindingResult attributes fails
225 	 */
226 	public void updateModel(NativeWebRequest request, ModelAndViewContainer mavContainer) throws Exception {
227 		ModelMap defaultModel = mavContainer.getDefaultModel();
228 		if (mavContainer.getSessionStatus().isComplete()){
229 			this.sessionAttributesHandler.cleanupAttributes(request);
230 		}
231 		else {
232 			this.sessionAttributesHandler.storeAttributes(request, defaultModel);
233 		}
234 		if (!mavContainer.isRequestHandled() && mavContainer.getModel() == defaultModel) {
235 			updateBindingResult(request, defaultModel);
236 		}
237 	}
238 
239 	/**
240 	 * Add {@link BindingResult} attributes to the model for attributes that require it.
241 	 */
242 	private void updateBindingResult(NativeWebRequest request, ModelMap model) throws Exception {
243 		List<String> keyNames = new ArrayList<String>(model.keySet());
244 		for (String name : keyNames) {
245 			Object value = model.get(name);
246 
247 			if (isBindingCandidate(name, value)) {
248 				String bindingResultKey = BindingResult.MODEL_KEY_PREFIX + name;
249 
250 				if (!model.containsAttribute(bindingResultKey)) {
251 					WebDataBinder dataBinder = dataBinderFactory.createBinder(request, value, name);
252 					model.put(bindingResultKey, dataBinder.getBindingResult());
253 				}
254 			}
255 		}
256 	}
257 
258 	/**
259 	 * Whether the given attribute requires a {@link BindingResult} in the model.
260 	 */
261 	private boolean isBindingCandidate(String attributeName, Object value) {
262 		if (attributeName.startsWith(BindingResult.MODEL_KEY_PREFIX)) {
263 			return false;
264 		}
265 
266 		Class<?> attrType = (value != null) ? value.getClass() : null;
267 		if (this.sessionAttributesHandler.isHandlerSessionAttribute(attributeName, attrType)) {
268 			return true;
269 		}
270 
271 		return (value != null && !value.getClass().isArray() && !(value instanceof Collection) &&
272 				!(value instanceof Map) && !BeanUtils.isSimpleValueType(value.getClass()));
273 	}
274 
275 
276 	private static class ModelMethod {
277 
278 		private final InvocableHandlerMethod handlerMethod;
279 
280 		private final Set<String> dependencies = new HashSet<String>();
281 
282 
283 		private ModelMethod(InvocableHandlerMethod handlerMethod) {
284 			this.handlerMethod = handlerMethod;
285 			for (MethodParameter parameter : handlerMethod.getMethodParameters()) {
286 				if (parameter.hasParameterAnnotation(ModelAttribute.class)) {
287 					this.dependencies.add(getNameForParameter(parameter));
288 				}
289 			}
290 		}
291 
292 		public InvocableHandlerMethod getHandlerMethod() {
293 			return this.handlerMethod;
294 		}
295 
296 		public boolean checkDependencies(ModelAndViewContainer mavContainer) {
297 			for (String name : this.dependencies) {
298 				if (!mavContainer.containsAttribute(name)) {
299 					return false;
300 				}
301 			}
302 			return true;
303 		}
304 
305 		public List<String> getUnresolvedDependencies(ModelAndViewContainer mavContainer) {
306 			List<String> result = new ArrayList<String>(this.dependencies.size());
307 			for (String name : this.dependencies) {
308 				if (!mavContainer.containsAttribute(name)) {
309 					result.add(name);
310 				}
311 			}
312 			return result;
313 		}
314 
315 		@Override
316 		public String toString() {
317 			return this.handlerMethod.getMethod().toGenericString();
318 		}
319 	}
320 
321 }