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.beans.ConstructorProperties;
020import java.lang.annotation.Annotation;
021import java.lang.reflect.Constructor;
022import java.lang.reflect.Field;
023import java.util.ArrayList;
024import java.util.Arrays;
025import java.util.HashSet;
026import java.util.List;
027import java.util.Map;
028import java.util.Optional;
029import java.util.Set;
030
031import org.apache.commons.logging.Log;
032import org.apache.commons.logging.LogFactory;
033
034import org.springframework.beans.BeanUtils;
035import org.springframework.beans.TypeMismatchException;
036import org.springframework.core.DefaultParameterNameDiscoverer;
037import org.springframework.core.MethodParameter;
038import org.springframework.core.ParameterNameDiscoverer;
039import org.springframework.core.annotation.AnnotationUtils;
040import org.springframework.lang.Nullable;
041import org.springframework.util.Assert;
042import org.springframework.validation.BindException;
043import org.springframework.validation.BindingResult;
044import org.springframework.validation.Errors;
045import org.springframework.validation.SmartValidator;
046import org.springframework.validation.Validator;
047import org.springframework.validation.annotation.Validated;
048import org.springframework.web.bind.WebDataBinder;
049import org.springframework.web.bind.annotation.ModelAttribute;
050import org.springframework.web.bind.support.WebDataBinderFactory;
051import org.springframework.web.bind.support.WebRequestDataBinder;
052import org.springframework.web.context.request.NativeWebRequest;
053import org.springframework.web.method.support.HandlerMethodArgumentResolver;
054import org.springframework.web.method.support.HandlerMethodReturnValueHandler;
055import org.springframework.web.method.support.ModelAndViewContainer;
056
057/**
058 * Resolve {@code @ModelAttribute} annotated method arguments and handle
059 * return values from {@code @ModelAttribute} annotated methods.
060 *
061 * <p>Model attributes are obtained from the model or created with a default
062 * constructor (and then added to the model). Once created the attribute is
063 * populated via data binding to Servlet request parameters. Validation may be
064 * applied if the argument is annotated with {@code @javax.validation.Valid}.
065 * or Spring's own {@code @org.springframework.validation.annotation.Validated}.
066 *
067 * <p>When this handler is created with {@code annotationNotRequired=true}
068 * any non-simple type argument and return value is regarded as a model
069 * attribute with or without the presence of an {@code @ModelAttribute}.
070 *
071 * @author Rossen Stoyanchev
072 * @author Juergen Hoeller
073 * @author Sebastien Deleuze
074 * @since 3.1
075 */
076public class ModelAttributeMethodProcessor implements HandlerMethodArgumentResolver, HandlerMethodReturnValueHandler {
077
078        private static final ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer();
079
080        protected final Log logger = LogFactory.getLog(getClass());
081
082        private final boolean annotationNotRequired;
083
084
085        /**
086         * Class constructor.
087         * @param annotationNotRequired if "true", non-simple method arguments and
088         * return values are considered model attributes with or without a
089         * {@code @ModelAttribute} annotation
090         */
091        public ModelAttributeMethodProcessor(boolean annotationNotRequired) {
092                this.annotationNotRequired = annotationNotRequired;
093        }
094
095
096        /**
097         * Returns {@code true} if the parameter is annotated with
098         * {@link ModelAttribute} or, if in default resolution mode, for any
099         * method parameter that is not a simple type.
100         */
101        @Override
102        public boolean supportsParameter(MethodParameter parameter) {
103                return (parameter.hasParameterAnnotation(ModelAttribute.class) ||
104                                (this.annotationNotRequired && !BeanUtils.isSimpleProperty(parameter.getParameterType())));
105        }
106
107        /**
108         * Resolve the argument from the model or if not found instantiate it with
109         * its default if it is available. The model attribute is then populated
110         * with request values via data binding and optionally validated
111         * if {@code @java.validation.Valid} is present on the argument.
112         * @throws BindException if data binding and validation result in an error
113         * and the next method parameter is not of type {@link Errors}
114         * @throws Exception if WebDataBinder initialization fails
115         */
116        @Override
117        @Nullable
118        public final Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
119                        NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
120
121                Assert.state(mavContainer != null, "ModelAttributeMethodProcessor requires ModelAndViewContainer");
122                Assert.state(binderFactory != null, "ModelAttributeMethodProcessor requires WebDataBinderFactory");
123
124                String name = ModelFactory.getNameForParameter(parameter);
125                ModelAttribute ann = parameter.getParameterAnnotation(ModelAttribute.class);
126                if (ann != null) {
127                        mavContainer.setBinding(name, ann.binding());
128                }
129
130                Object attribute = null;
131                BindingResult bindingResult = null;
132
133                if (mavContainer.containsAttribute(name)) {
134                        attribute = mavContainer.getModel().get(name);
135                }
136                else {
137                        // Create attribute instance
138                        try {
139                                attribute = createAttribute(name, parameter, binderFactory, webRequest);
140                        }
141                        catch (BindException ex) {
142                                if (isBindExceptionRequired(parameter)) {
143                                        // No BindingResult parameter -> fail with BindException
144                                        throw ex;
145                                }
146                                // Otherwise, expose null/empty value and associated BindingResult
147                                if (parameter.getParameterType() == Optional.class) {
148                                        attribute = Optional.empty();
149                                }
150                                bindingResult = ex.getBindingResult();
151                        }
152                }
153
154                if (bindingResult == null) {
155                        // Bean property binding and validation;
156                        // skipped in case of binding failure on construction.
157                        WebDataBinder binder = binderFactory.createBinder(webRequest, attribute, name);
158                        if (binder.getTarget() != null) {
159                                if (!mavContainer.isBindingDisabled(name)) {
160                                        bindRequestParameters(binder, webRequest);
161                                }
162                                validateIfApplicable(binder, parameter);
163                                if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) {
164                                        throw new BindException(binder.getBindingResult());
165                                }
166                        }
167                        // Value type adaptation, also covering java.util.Optional
168                        if (!parameter.getParameterType().isInstance(attribute)) {
169                                attribute = binder.convertIfNecessary(binder.getTarget(), parameter.getParameterType(), parameter);
170                        }
171                        bindingResult = binder.getBindingResult();
172                }
173
174                // Add resolved attribute and BindingResult at the end of the model
175                Map<String, Object> bindingResultModel = bindingResult.getModel();
176                mavContainer.removeAttributes(bindingResultModel);
177                mavContainer.addAllAttributes(bindingResultModel);
178
179                return attribute;
180        }
181
182        /**
183         * Extension point to create the model attribute if not found in the model,
184         * with subsequent parameter binding through bean properties (unless suppressed).
185         * <p>The default implementation typically uses the unique public no-arg constructor
186         * if available but also handles a "primary constructor" approach for data classes:
187         * It understands the JavaBeans {@link ConstructorProperties} annotation as well as
188         * runtime-retained parameter names in the bytecode, associating request parameters
189         * with constructor arguments by name. If no such constructor is found, the default
190         * constructor will be used (even if not public), assuming subsequent bean property
191         * bindings through setter methods.
192         * @param attributeName the name of the attribute (never {@code null})
193         * @param parameter the method parameter declaration
194         * @param binderFactory for creating WebDataBinder instance
195         * @param webRequest the current request
196         * @return the created model attribute (never {@code null})
197         * @throws BindException in case of constructor argument binding failure
198         * @throws Exception in case of constructor invocation failure
199         * @see #constructAttribute(Constructor, String, MethodParameter, WebDataBinderFactory, NativeWebRequest)
200         * @see BeanUtils#findPrimaryConstructor(Class)
201         */
202        protected Object createAttribute(String attributeName, MethodParameter parameter,
203                        WebDataBinderFactory binderFactory, NativeWebRequest webRequest) throws Exception {
204
205                MethodParameter nestedParameter = parameter.nestedIfOptional();
206                Class<?> clazz = nestedParameter.getNestedParameterType();
207
208                Constructor<?> ctor = BeanUtils.findPrimaryConstructor(clazz);
209                if (ctor == null) {
210                        Constructor<?>[] ctors = clazz.getConstructors();
211                        if (ctors.length == 1) {
212                                ctor = ctors[0];
213                        }
214                        else {
215                                try {
216                                        ctor = clazz.getDeclaredConstructor();
217                                }
218                                catch (NoSuchMethodException ex) {
219                                        throw new IllegalStateException("No primary or default constructor found for " + clazz, ex);
220                                }
221                        }
222                }
223
224                Object attribute = constructAttribute(ctor, attributeName, parameter, binderFactory, webRequest);
225                if (parameter != nestedParameter) {
226                        attribute = Optional.of(attribute);
227                }
228                return attribute;
229        }
230
231        /**
232         * Construct a new attribute instance with the given constructor.
233         * <p>Called from
234         * {@link #createAttribute(String, MethodParameter, WebDataBinderFactory, NativeWebRequest)}
235         * after constructor resolution.
236         * @param ctor the constructor to use
237         * @param attributeName the name of the attribute (never {@code null})
238         * @param binderFactory for creating WebDataBinder instance
239         * @param webRequest the current request
240         * @return the created model attribute (never {@code null})
241         * @throws BindException in case of constructor argument binding failure
242         * @throws Exception in case of constructor invocation failure
243         * @since 5.1
244         */
245        @SuppressWarnings("deprecation")
246        protected Object constructAttribute(Constructor<?> ctor, String attributeName, MethodParameter parameter,
247                        WebDataBinderFactory binderFactory, NativeWebRequest webRequest) throws Exception {
248
249                Object constructed = constructAttribute(ctor, attributeName, binderFactory, webRequest);
250                if (constructed != null) {
251                        return constructed;
252                }
253
254                if (ctor.getParameterCount() == 0) {
255                        // A single default constructor -> clearly a standard JavaBeans arrangement.
256                        return BeanUtils.instantiateClass(ctor);
257                }
258
259                // A single data class constructor -> resolve constructor arguments from request parameters.
260                ConstructorProperties cp = ctor.getAnnotation(ConstructorProperties.class);
261                String[] paramNames = (cp != null ? cp.value() : parameterNameDiscoverer.getParameterNames(ctor));
262                Assert.state(paramNames != null, () -> "Cannot resolve parameter names for constructor " + ctor);
263                Class<?>[] paramTypes = ctor.getParameterTypes();
264                Assert.state(paramNames.length == paramTypes.length,
265                                () -> "Invalid number of parameter names: " + paramNames.length + " for constructor " + ctor);
266
267                Object[] args = new Object[paramTypes.length];
268                WebDataBinder binder = binderFactory.createBinder(webRequest, null, attributeName);
269                String fieldDefaultPrefix = binder.getFieldDefaultPrefix();
270                String fieldMarkerPrefix = binder.getFieldMarkerPrefix();
271                boolean bindingFailure = false;
272                Set<String> failedParams = new HashSet<>(4);
273
274                for (int i = 0; i < paramNames.length; i++) {
275                        String paramName = paramNames[i];
276                        Class<?> paramType = paramTypes[i];
277                        Object value = webRequest.getParameterValues(paramName);
278                        if (value == null) {
279                                if (fieldDefaultPrefix != null) {
280                                        value = webRequest.getParameter(fieldDefaultPrefix + paramName);
281                                }
282                                if (value == null && fieldMarkerPrefix != null) {
283                                        if (webRequest.getParameter(fieldMarkerPrefix + paramName) != null) {
284                                                value = binder.getEmptyValue(paramType);
285                                        }
286                                }
287                        }
288                        try {
289                                MethodParameter methodParam = new FieldAwareConstructorParameter(ctor, i, paramName);
290                                if (value == null && methodParam.isOptional()) {
291                                        args[i] = (methodParam.getParameterType() == Optional.class ? Optional.empty() : null);
292                                }
293                                else {
294                                        args[i] = binder.convertIfNecessary(value, paramType, methodParam);
295                                }
296                        }
297                        catch (TypeMismatchException ex) {
298                                ex.initPropertyName(paramName);
299                                args[i] = value;
300                                failedParams.add(paramName);
301                                binder.getBindingResult().recordFieldValue(paramName, paramType, value);
302                                binder.getBindingErrorProcessor().processPropertyAccessException(ex, binder.getBindingResult());
303                                bindingFailure = true;
304                        }
305                }
306
307                if (bindingFailure) {
308                        BindingResult result = binder.getBindingResult();
309                        for (int i = 0; i < paramNames.length; i++) {
310                                String paramName = paramNames[i];
311                                if (!failedParams.contains(paramName)) {
312                                        Object value = args[i];
313                                        result.recordFieldValue(paramName, paramTypes[i], value);
314                                        validateValueIfApplicable(binder, parameter, ctor.getDeclaringClass(), paramName, value);
315                                }
316                        }
317                        throw new BindException(result);
318                }
319
320                return BeanUtils.instantiateClass(ctor, args);
321        }
322
323        /**
324         * Construct a new attribute instance with the given constructor.
325         * @since 5.0
326         * @deprecated as of 5.1, in favor of
327         * {@link #constructAttribute(Constructor, String, MethodParameter, WebDataBinderFactory, NativeWebRequest)}
328         */
329        @Deprecated
330        @Nullable
331        protected Object constructAttribute(Constructor<?> ctor, String attributeName,
332                        WebDataBinderFactory binderFactory, NativeWebRequest webRequest) throws Exception {
333
334                return null;
335        }
336
337        /**
338         * Extension point to bind the request to the target object.
339         * @param binder the data binder instance to use for the binding
340         * @param request the current request
341         */
342        protected void bindRequestParameters(WebDataBinder binder, NativeWebRequest request) {
343                ((WebRequestDataBinder) binder).bind(request);
344        }
345
346        /**
347         * Validate the model attribute if applicable.
348         * <p>The default implementation checks for {@code @javax.validation.Valid},
349         * Spring's {@link org.springframework.validation.annotation.Validated},
350         * and custom annotations whose name starts with "Valid".
351         * @param binder the DataBinder to be used
352         * @param parameter the method parameter declaration
353         * @see WebDataBinder#validate(Object...)
354         * @see SmartValidator#validate(Object, Errors, Object...)
355         */
356        protected void validateIfApplicable(WebDataBinder binder, MethodParameter parameter) {
357                for (Annotation ann : parameter.getParameterAnnotations()) {
358                        Object[] validationHints = determineValidationHints(ann);
359                        if (validationHints != null) {
360                                binder.validate(validationHints);
361                                break;
362                        }
363                }
364        }
365
366        /**
367         * Validate the specified candidate value if applicable.
368         * <p>The default implementation checks for {@code @javax.validation.Valid},
369         * Spring's {@link org.springframework.validation.annotation.Validated},
370         * and custom annotations whose name starts with "Valid".
371         * @param binder the DataBinder to be used
372         * @param parameter the method parameter declaration
373         * @param targetType the target type
374         * @param fieldName the name of the field
375         * @param value the candidate value
376         * @since 5.1
377         * @see #validateIfApplicable(WebDataBinder, MethodParameter)
378         * @see SmartValidator#validateValue(Class, String, Object, Errors, Object...)
379         */
380        protected void validateValueIfApplicable(WebDataBinder binder, MethodParameter parameter,
381                        Class<?> targetType, String fieldName, @Nullable Object value) {
382
383                for (Annotation ann : parameter.getParameterAnnotations()) {
384                        Object[] validationHints = determineValidationHints(ann);
385                        if (validationHints != null) {
386                                for (Validator validator : binder.getValidators()) {
387                                        if (validator instanceof SmartValidator) {
388                                                try {
389                                                        ((SmartValidator) validator).validateValue(targetType, fieldName, value,
390                                                                        binder.getBindingResult(), validationHints);
391                                                }
392                                                catch (IllegalArgumentException ex) {
393                                                        // No corresponding field on the target class...
394                                                }
395                                        }
396                                }
397                                break;
398                        }
399                }
400        }
401
402        /**
403         * Determine any validation triggered by the given annotation.
404         * @param ann the annotation (potentially a validation annotation)
405         * @return the validation hints to apply (possibly an empty array),
406         * or {@code null} if this annotation does not trigger any validation
407         * @since 5.1
408         */
409        @Nullable
410        private Object[] determineValidationHints(Annotation ann) {
411                Validated validatedAnn = AnnotationUtils.getAnnotation(ann, Validated.class);
412                if (validatedAnn != null || ann.annotationType().getSimpleName().startsWith("Valid")) {
413                        Object hints = (validatedAnn != null ? validatedAnn.value() : AnnotationUtils.getValue(ann));
414                        if (hints == null) {
415                                return new Object[0];
416                        }
417                        return (hints instanceof Object[] ? (Object[]) hints : new Object[] {hints});
418                }
419                return null;
420        }
421
422        /**
423         * Whether to raise a fatal bind exception on validation errors.
424         * <p>The default implementation delegates to {@link #isBindExceptionRequired(MethodParameter)}.
425         * @param binder the data binder used to perform data binding
426         * @param parameter the method parameter declaration
427         * @return {@code true} if the next method parameter is not of type {@link Errors}
428         * @see #isBindExceptionRequired(MethodParameter)
429         */
430        protected boolean isBindExceptionRequired(WebDataBinder binder, MethodParameter parameter) {
431                return isBindExceptionRequired(parameter);
432        }
433
434        /**
435         * Whether to raise a fatal bind exception on validation errors.
436         * @param parameter the method parameter declaration
437         * @return {@code true} if the next method parameter is not of type {@link Errors}
438         * @since 5.0
439         */
440        protected boolean isBindExceptionRequired(MethodParameter parameter) {
441                int i = parameter.getParameterIndex();
442                Class<?>[] paramTypes = parameter.getExecutable().getParameterTypes();
443                boolean hasBindingResult = (paramTypes.length > (i + 1) && Errors.class.isAssignableFrom(paramTypes[i + 1]));
444                return !hasBindingResult;
445        }
446
447        /**
448         * Return {@code true} if there is a method-level {@code @ModelAttribute}
449         * or, in default resolution mode, for any return value type that is not
450         * a simple type.
451         */
452        @Override
453        public boolean supportsReturnType(MethodParameter returnType) {
454                return (returnType.hasMethodAnnotation(ModelAttribute.class) ||
455                                (this.annotationNotRequired && !BeanUtils.isSimpleProperty(returnType.getParameterType())));
456        }
457
458        /**
459         * Add non-null return values to the {@link ModelAndViewContainer}.
460         */
461        @Override
462        public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,
463                        ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {
464
465                if (returnValue != null) {
466                        String name = ModelFactory.getNameForReturnValue(returnValue, returnType);
467                        mavContainer.addAttribute(name, returnValue);
468                }
469        }
470
471
472        /**
473         * {@link MethodParameter} subclass which detects field annotations as well.
474         * @since 5.1
475         */
476        private static class FieldAwareConstructorParameter extends MethodParameter {
477
478                private final String parameterName;
479
480                @Nullable
481                private volatile Annotation[] combinedAnnotations;
482
483                public FieldAwareConstructorParameter(Constructor<?> constructor, int parameterIndex, String parameterName) {
484                        super(constructor, parameterIndex);
485                        this.parameterName = parameterName;
486                }
487
488                @Override
489                public Annotation[] getParameterAnnotations() {
490                        Annotation[] anns = this.combinedAnnotations;
491                        if (anns == null) {
492                                anns = super.getParameterAnnotations();
493                                try {
494                                        Field field = getDeclaringClass().getDeclaredField(this.parameterName);
495                                        Annotation[] fieldAnns = field.getAnnotations();
496                                        if (fieldAnns.length > 0) {
497                                                List<Annotation> merged = new ArrayList<>(anns.length + fieldAnns.length);
498                                                merged.addAll(Arrays.asList(anns));
499                                                for (Annotation fieldAnn : fieldAnns) {
500                                                        boolean existingType = false;
501                                                        for (Annotation ann : anns) {
502                                                                if (ann.annotationType() == fieldAnn.annotationType()) {
503                                                                        existingType = true;
504                                                                        break;
505                                                                }
506                                                        }
507                                                        if (!existingType) {
508                                                                merged.add(fieldAnn);
509                                                        }
510                                                }
511                                                anns = merged.toArray(new Annotation[0]);
512                                        }
513                                }
514                                catch (NoSuchFieldException | SecurityException ex) {
515                                        // ignore
516                                }
517                                this.combinedAnnotations = anns;
518                        }
519                        return anns;
520                }
521
522                @Override
523                public String getParameterName() {
524                        return this.parameterName;
525                }
526        }
527
528}