001/*
002 * Copyright 2002-2017 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.annotation.Annotation;
020import java.util.Map;
021
022import org.apache.commons.logging.Log;
023import org.apache.commons.logging.LogFactory;
024
025import org.springframework.beans.BeanUtils;
026import org.springframework.core.MethodParameter;
027import org.springframework.core.annotation.AnnotationUtils;
028import org.springframework.validation.BindException;
029import org.springframework.validation.Errors;
030import org.springframework.validation.annotation.Validated;
031import org.springframework.web.bind.WebDataBinder;
032import org.springframework.web.bind.annotation.ModelAttribute;
033import org.springframework.web.bind.support.WebDataBinderFactory;
034import org.springframework.web.bind.support.WebRequestDataBinder;
035import org.springframework.web.context.request.NativeWebRequest;
036import org.springframework.web.method.support.HandlerMethodArgumentResolver;
037import org.springframework.web.method.support.HandlerMethodReturnValueHandler;
038import org.springframework.web.method.support.ModelAndViewContainer;
039
040/**
041 * Resolve {@code @ModelAttribute} annotated method arguments and handle
042 * return values from {@code @ModelAttribute} annotated methods.
043 *
044 * <p>Model attributes are obtained from the model or created with a default
045 * constructor (and then added to the model). Once created the attribute is
046 * populated via data binding to Servlet request parameters. Validation may be
047 * applied if the argument is annotated with {@code @javax.validation.Valid}.
048 * or Spring's own {@code @org.springframework.validation.annotation.Validated}.
049 *
050 * <p>When this handler is created with {@code annotationNotRequired=true}
051 * any non-simple type argument and return value is regarded as a model
052 * attribute with or without the presence of an {@code @ModelAttribute}.
053 *
054 * @author Rossen Stoyanchev
055 * @since 3.1
056 */
057public class ModelAttributeMethodProcessor implements HandlerMethodArgumentResolver, HandlerMethodReturnValueHandler {
058
059        protected final Log logger = LogFactory.getLog(getClass());
060
061        private final boolean annotationNotRequired;
062
063
064        /**
065         * Class constructor.
066         * @param annotationNotRequired if "true", non-simple method arguments and
067         * return values are considered model attributes with or without a
068         * {@code @ModelAttribute} annotation
069         */
070        public ModelAttributeMethodProcessor(boolean annotationNotRequired) {
071                this.annotationNotRequired = annotationNotRequired;
072        }
073
074
075        /**
076         * Returns {@code true} if the parameter is annotated with
077         * {@link ModelAttribute} or, if in default resolution mode, for any
078         * method parameter that is not a simple type.
079         */
080        @Override
081        public boolean supportsParameter(MethodParameter parameter) {
082                return (parameter.hasParameterAnnotation(ModelAttribute.class) ||
083                                (this.annotationNotRequired && !BeanUtils.isSimpleProperty(parameter.getParameterType())));
084        }
085
086        /**
087         * Resolve the argument from the model or if not found instantiate it with
088         * its default if it is available. The model attribute is then populated
089         * with request values via data binding and optionally validated
090         * if {@code @java.validation.Valid} is present on the argument.
091         * @throws BindException if data binding and validation result in an error
092         * and the next method parameter is not of type {@link Errors}
093         * @throws Exception if WebDataBinder initialization fails
094         */
095        @Override
096        public final Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
097                        NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
098
099                String name = ModelFactory.getNameForParameter(parameter);
100                ModelAttribute ann = parameter.getParameterAnnotation(ModelAttribute.class);
101                if (ann != null) {
102                        mavContainer.setBinding(name, ann.binding());
103                }
104
105                Object attribute = (mavContainer.containsAttribute(name) ? mavContainer.getModel().get(name) :
106                                createAttribute(name, parameter, binderFactory, webRequest));
107
108                WebDataBinder binder = binderFactory.createBinder(webRequest, attribute, name);
109                if (binder.getTarget() != null) {
110                        if (!mavContainer.isBindingDisabled(name)) {
111                                bindRequestParameters(binder, webRequest);
112                        }
113                        validateIfApplicable(binder, parameter);
114                        if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) {
115                                throw new BindException(binder.getBindingResult());
116                        }
117                }
118
119                // Add resolved attribute and BindingResult at the end of the model
120                Map<String, Object> bindingResultModel = binder.getBindingResult().getModel();
121                mavContainer.removeAttributes(bindingResultModel);
122                mavContainer.addAllAttributes(bindingResultModel);
123
124                return binder.convertIfNecessary(binder.getTarget(), parameter.getParameterType(), parameter);
125        }
126
127        /**
128         * Extension point to create the model attribute if not found in the model.
129         * The default implementation uses the default constructor.
130         * @param attributeName the name of the attribute (never {@code null})
131         * @param parameter the method parameter
132         * @param binderFactory for creating WebDataBinder instance
133         * @param webRequest the current request
134         * @return the created model attribute (never {@code null})
135         */
136        protected Object createAttribute(String attributeName, MethodParameter parameter,
137                        WebDataBinderFactory binderFactory, NativeWebRequest webRequest) throws Exception {
138
139                return BeanUtils.instantiateClass(parameter.getParameterType());
140        }
141
142        /**
143         * Extension point to bind the request to the target object.
144         * @param binder the data binder instance to use for the binding
145         * @param request the current request
146         */
147        protected void bindRequestParameters(WebDataBinder binder, NativeWebRequest request) {
148                ((WebRequestDataBinder) binder).bind(request);
149        }
150
151        /**
152         * Validate the model attribute if applicable.
153         * <p>The default implementation checks for {@code @javax.validation.Valid},
154         * Spring's {@link org.springframework.validation.annotation.Validated},
155         * and custom annotations whose name starts with "Valid".
156         * @param binder the DataBinder to be used
157         * @param parameter the method parameter declaration
158         */
159        protected void validateIfApplicable(WebDataBinder binder, MethodParameter parameter) {
160                Annotation[] annotations = parameter.getParameterAnnotations();
161                for (Annotation ann : annotations) {
162                        Validated validatedAnn = AnnotationUtils.getAnnotation(ann, Validated.class);
163                        if (validatedAnn != null || ann.annotationType().getSimpleName().startsWith("Valid")) {
164                                Object hints = (validatedAnn != null ? validatedAnn.value() : AnnotationUtils.getValue(ann));
165                                Object[] validationHints = (hints instanceof Object[] ? (Object[]) hints : new Object[] {hints});
166                                binder.validate(validationHints);
167                                break;
168                        }
169                }
170        }
171
172        /**
173         * Whether to raise a fatal bind exception on validation errors.
174         * @param binder the data binder used to perform data binding
175         * @param parameter the method parameter declaration
176         * @return {@code true} if the next method parameter is not of type {@link Errors}
177         */
178        protected boolean isBindExceptionRequired(WebDataBinder binder, MethodParameter parameter) {
179                int i = parameter.getParameterIndex();
180                Class<?>[] paramTypes = parameter.getMethod().getParameterTypes();
181                boolean hasBindingResult = (paramTypes.length > (i + 1) && Errors.class.isAssignableFrom(paramTypes[i + 1]));
182                return !hasBindingResult;
183        }
184
185        /**
186         * Return {@code true} if there is a method-level {@code @ModelAttribute}
187         * or, in default resolution mode, for any return value type that is not
188         * a simple type.
189         */
190        @Override
191        public boolean supportsReturnType(MethodParameter returnType) {
192                return (returnType.hasMethodAnnotation(ModelAttribute.class) ||
193                                (this.annotationNotRequired && !BeanUtils.isSimpleProperty(returnType.getParameterType())));
194        }
195
196        /**
197         * Add non-null return values to the {@link ModelAndViewContainer}.
198         */
199        @Override
200        public void handleReturnValue(Object returnValue, MethodParameter returnType,
201                        ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {
202
203                if (returnValue != null) {
204                        String name = ModelFactory.getNameForReturnValue(returnValue, returnType);
205                        mavContainer.addAttribute(name, returnValue);
206                }
207        }
208
209}