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}