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}