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}