parent
							
								
									cb04c3b335
								
							
						
					
					
						commit
						bd054a4918
					
				|  | @ -24,6 +24,7 @@ import java.util.Collections; | ||||||
| import java.util.HashMap; | import java.util.HashMap; | ||||||
| import java.util.List; | import java.util.List; | ||||||
| import java.util.Map; | import java.util.Map; | ||||||
|  | import java.util.function.Predicate; | ||||||
| 
 | 
 | ||||||
| import org.apache.commons.logging.Log; | import org.apache.commons.logging.Log; | ||||||
| import org.apache.commons.logging.LogFactory; | import org.apache.commons.logging.LogFactory; | ||||||
|  | @ -164,6 +165,9 @@ public class DataBinder implements PropertyEditorRegistry, TypeConverter { | ||||||
| 
 | 
 | ||||||
| 	private final List<Validator> validators = new ArrayList<>(); | 	private final List<Validator> validators = new ArrayList<>(); | ||||||
| 
 | 
 | ||||||
|  | 	@Nullable | ||||||
|  | 	private Predicate<Validator> excludedValidators; | ||||||
|  | 
 | ||||||
| 
 | 
 | ||||||
| 	/** | 	/** | ||||||
| 	 * Create a new DataBinder instance, with default object name. | 	 * Create a new DataBinder instance, with default object name. | ||||||
|  | @ -580,6 +584,14 @@ public class DataBinder implements PropertyEditorRegistry, TypeConverter { | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | 	/** | ||||||
|  | 	 * Configure a predicate to exclude validators. | ||||||
|  | 	 * @since 6.1 | ||||||
|  | 	 */ | ||||||
|  | 	public void setExcludedValidators(Predicate<Validator> predicate) { | ||||||
|  | 		this.excludedValidators = predicate; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
| 	/** | 	/** | ||||||
| 	 * Add Validators to apply after each binding step. | 	 * Add Validators to apply after each binding step. | ||||||
| 	 * @see #setValidator(Validator) | 	 * @see #setValidator(Validator) | ||||||
|  | @ -616,6 +628,18 @@ public class DataBinder implements PropertyEditorRegistry, TypeConverter { | ||||||
| 		return Collections.unmodifiableList(this.validators); | 		return Collections.unmodifiableList(this.validators); | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | 	/** | ||||||
|  | 	 * Return the Validators to apply after data binding. This includes the | ||||||
|  | 	 * configured {@link #getValidators() validators} filtered by the | ||||||
|  | 	 * {@link #setExcludedValidators(Predicate) exclude predicate}. | ||||||
|  | 	 * @since 6.1 | ||||||
|  | 	 */ | ||||||
|  | 	public List<Validator> getValidatorsToApply() { | ||||||
|  | 		return (this.excludedValidators != null ? | ||||||
|  | 				this.validators.stream().filter(validator -> !this.excludedValidators.test(validator)).toList() : | ||||||
|  | 				Collections.unmodifiableList(this.validators)); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
| 
 | 
 | ||||||
| 	//--------------------------------------------------------------------- | 	//--------------------------------------------------------------------- | ||||||
| 	// Implementation of PropertyEditorRegistry/TypeConverter interface | 	// Implementation of PropertyEditorRegistry/TypeConverter interface | ||||||
|  | @ -906,7 +930,7 @@ public class DataBinder implements PropertyEditorRegistry, TypeConverter { | ||||||
| 		Assert.state(target != null, "No target to validate"); | 		Assert.state(target != null, "No target to validate"); | ||||||
| 		BindingResult bindingResult = getBindingResult(); | 		BindingResult bindingResult = getBindingResult(); | ||||||
| 		// Call each validator with the same binding result | 		// Call each validator with the same binding result | ||||||
| 		for (Validator validator : getValidators()) { | 		for (Validator validator : getValidatorsToApply()) { | ||||||
| 			validator.validate(target, bindingResult); | 			validator.validate(target, bindingResult); | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  | @ -924,7 +948,7 @@ public class DataBinder implements PropertyEditorRegistry, TypeConverter { | ||||||
| 		Assert.state(target != null, "No target to validate"); | 		Assert.state(target != null, "No target to validate"); | ||||||
| 		BindingResult bindingResult = getBindingResult(); | 		BindingResult bindingResult = getBindingResult(); | ||||||
| 		// Call each validator with the same binding result | 		// Call each validator with the same binding result | ||||||
| 		for (Validator validator : getValidators()) { | 		for (Validator validator : getValidatorsToApply()) { | ||||||
| 			if (!ObjectUtils.isEmpty(validationHints) && validator instanceof SmartValidator smartValidator) { | 			if (!ObjectUtils.isEmpty(validationHints) && validator instanceof SmartValidator smartValidator) { | ||||||
| 				smartValidator.validate(target, bindingResult, validationHints); | 				smartValidator.validate(target, bindingResult, validationHints); | ||||||
| 			} | 			} | ||||||
|  |  | ||||||
|  | @ -1,5 +1,5 @@ | ||||||
| /* | /* | ||||||
|  * Copyright 2002-2021 the original author or authors. |  * Copyright 2002-2023 the original author or authors. | ||||||
|  * |  * | ||||||
|  * Licensed under the Apache License, Version 2.0 (the "License"); |  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||||
|  * you may not use this file except in compliance with the License. |  * you may not use this file except in compliance with the License. | ||||||
|  | @ -16,7 +16,11 @@ | ||||||
| 
 | 
 | ||||||
| package org.springframework.web.bind.support; | package org.springframework.web.bind.support; | ||||||
| 
 | 
 | ||||||
|  | import java.lang.annotation.Annotation; | ||||||
|  | 
 | ||||||
|  | import org.springframework.core.MethodParameter; | ||||||
| import org.springframework.lang.Nullable; | import org.springframework.lang.Nullable; | ||||||
|  | import org.springframework.validation.DataBinder; | ||||||
| import org.springframework.web.bind.WebDataBinder; | import org.springframework.web.bind.WebDataBinder; | ||||||
| import org.springframework.web.context.request.NativeWebRequest; | import org.springframework.web.context.request.NativeWebRequest; | ||||||
| 
 | 
 | ||||||
|  | @ -32,6 +36,8 @@ public class DefaultDataBinderFactory implements WebDataBinderFactory { | ||||||
| 	@Nullable | 	@Nullable | ||||||
| 	private final WebBindingInitializer initializer; | 	private final WebBindingInitializer initializer; | ||||||
| 
 | 
 | ||||||
|  | 	private boolean methodValidationApplicable; | ||||||
|  | 
 | ||||||
| 
 | 
 | ||||||
| 	/** | 	/** | ||||||
| 	 * Create a new {@code DefaultDataBinderFactory} instance. | 	 * Create a new {@code DefaultDataBinderFactory} instance. | ||||||
|  | @ -43,6 +49,17 @@ public class DefaultDataBinderFactory implements WebDataBinderFactory { | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | 	/** | ||||||
|  | 	 * Configure flag to signal whether validation will be applied to handler | ||||||
|  | 	 * method arguments, which is the case if Bean Validation is enabled in | ||||||
|  | 	 * Spring MVC, and method parameters have {@code @Constraint} annotations. | ||||||
|  | 	 * @since 6.1 | ||||||
|  | 	 */ | ||||||
|  | 	public void setMethodValidationApplicable(boolean methodValidationApplicable) { | ||||||
|  | 		this.methodValidationApplicable = methodValidationApplicable; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
| 	/** | 	/** | ||||||
| 	 * Create a new {@link WebDataBinder} for the given target object and | 	 * Create a new {@link WebDataBinder} for the given target object and | ||||||
| 	 * initialize it through a {@link WebBindingInitializer}. | 	 * initialize it through a {@link WebBindingInitializer}. | ||||||
|  | @ -87,4 +104,36 @@ public class DefaultDataBinderFactory implements WebDataBinderFactory { | ||||||
| 
 | 
 | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | 	/** | ||||||
|  | 	 * {@inheritDoc}. | ||||||
|  |  	 * <p>By default, if the parameter has {@code @Valid}, Bean Validation is | ||||||
|  | 	 * excluded, deferring to method validation. | ||||||
|  | 	 */ | ||||||
|  | 	@Override | ||||||
|  | 	public WebDataBinder createBinder( | ||||||
|  | 			NativeWebRequest webRequest, @Nullable Object target, String objectName, | ||||||
|  | 			MethodParameter parameter) throws Exception { | ||||||
|  | 
 | ||||||
|  | 		WebDataBinder dataBinder = createBinder(webRequest, target, objectName); | ||||||
|  | 		if (this.methodValidationApplicable) { | ||||||
|  | 			MethodValidationInitializer.updateBinder(dataBinder, parameter); | ||||||
|  | 		} | ||||||
|  | 		return dataBinder; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 	/** | ||||||
|  | 	 * Excludes Bean Validation if the method parameter has {@code @Valid}. | ||||||
|  | 	 */ | ||||||
|  | 	private static class MethodValidationInitializer { | ||||||
|  | 
 | ||||||
|  | 		public static void updateBinder(DataBinder binder, MethodParameter parameter) { | ||||||
|  | 			for (Annotation annotation : parameter.getParameterAnnotations()) { | ||||||
|  | 				if (annotation.annotationType().getName().equals("jakarta.validation.Valid")) { | ||||||
|  | 					binder.setExcludedValidators(validator -> validator instanceof jakarta.validation.Validator); | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -1,5 +1,5 @@ | ||||||
| /* | /* | ||||||
|  * Copyright 2002-2017 the original author or authors. |  * Copyright 2002-2023 the original author or authors. | ||||||
|  * |  * | ||||||
|  * Licensed under the Apache License, Version 2.0 (the "License"); |  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||||
|  * you may not use this file except in compliance with the License. |  * you may not use this file except in compliance with the License. | ||||||
|  | @ -16,6 +16,7 @@ | ||||||
| 
 | 
 | ||||||
| package org.springframework.web.bind.support; | package org.springframework.web.bind.support; | ||||||
| 
 | 
 | ||||||
|  | import org.springframework.core.MethodParameter; | ||||||
| import org.springframework.lang.Nullable; | import org.springframework.lang.Nullable; | ||||||
| import org.springframework.web.bind.WebDataBinder; | import org.springframework.web.bind.WebDataBinder; | ||||||
| import org.springframework.web.context.request.NativeWebRequest; | import org.springframework.web.context.request.NativeWebRequest; | ||||||
|  | @ -24,6 +25,7 @@ import org.springframework.web.context.request.NativeWebRequest; | ||||||
|  * A factory for creating a {@link WebDataBinder} instance for a named target object. |  * A factory for creating a {@link WebDataBinder} instance for a named target object. | ||||||
|  * |  * | ||||||
|  * @author Arjen Poutsma |  * @author Arjen Poutsma | ||||||
|  |  * @author Rossen Stoyanchev | ||||||
|  * @since 3.1 |  * @since 3.1 | ||||||
|  */ |  */ | ||||||
| public interface WebDataBinderFactory { | public interface WebDataBinderFactory { | ||||||
|  | @ -40,4 +42,18 @@ public interface WebDataBinderFactory { | ||||||
| 	WebDataBinder createBinder(NativeWebRequest webRequest, @Nullable Object target, String objectName) | 	WebDataBinder createBinder(NativeWebRequest webRequest, @Nullable Object target, String objectName) | ||||||
| 			throws Exception; | 			throws Exception; | ||||||
| 
 | 
 | ||||||
|  | 	/** | ||||||
|  | 	 * Variant of {@link #createBinder(NativeWebRequest, Object, String)} with a | ||||||
|  | 	 * {@link MethodParameter} for which the {@code DataBinder} is created. This | ||||||
|  | 	 * may provide more insight to initialize the {@link WebDataBinder}. | ||||||
|  | 	 * @since 6.1 | ||||||
|  | 	 */ | ||||||
|  | 	default WebDataBinder createBinder( | ||||||
|  | 			NativeWebRequest webRequest, @Nullable Object target, String objectName, | ||||||
|  | 			MethodParameter parameter) throws Exception { | ||||||
|  | 
 | ||||||
|  | 		return createBinder(webRequest, target, objectName); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -1,5 +1,5 @@ | ||||||
| /* | /* | ||||||
|  * Copyright 2002-2022 the original author or authors. |  * Copyright 2002-2023 the original author or authors. | ||||||
|  * |  * | ||||||
|  * Licensed under the Apache License, Version 2.0 (the "License"); |  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||||
|  * you may not use this file except in compliance with the License. |  * you may not use this file except in compliance with the License. | ||||||
|  | @ -22,6 +22,7 @@ import java.util.ArrayList; | ||||||
| import java.util.Arrays; | import java.util.Arrays; | ||||||
| import java.util.List; | import java.util.List; | ||||||
| import java.util.StringJoiner; | import java.util.StringJoiner; | ||||||
|  | import java.util.function.Predicate; | ||||||
| import java.util.stream.Collectors; | import java.util.stream.Collectors; | ||||||
| import java.util.stream.IntStream; | import java.util.stream.IntStream; | ||||||
| 
 | 
 | ||||||
|  | @ -35,6 +36,10 @@ import org.springframework.core.BridgeMethodResolver; | ||||||
| import org.springframework.core.MethodParameter; | import org.springframework.core.MethodParameter; | ||||||
| import org.springframework.core.ResolvableType; | import org.springframework.core.ResolvableType; | ||||||
| import org.springframework.core.annotation.AnnotatedElementUtils; | import org.springframework.core.annotation.AnnotatedElementUtils; | ||||||
|  | import org.springframework.core.annotation.AnnotationUtils; | ||||||
|  | import org.springframework.core.annotation.MergedAnnotation; | ||||||
|  | import org.springframework.core.annotation.MergedAnnotationPredicates; | ||||||
|  | import org.springframework.core.annotation.MergedAnnotations; | ||||||
| import org.springframework.core.annotation.SynthesizingMethodParameter; | import org.springframework.core.annotation.SynthesizingMethodParameter; | ||||||
| import org.springframework.http.HttpStatusCode; | import org.springframework.http.HttpStatusCode; | ||||||
| import org.springframework.lang.NonNull; | import org.springframework.lang.NonNull; | ||||||
|  | @ -44,6 +49,7 @@ import org.springframework.util.ClassUtils; | ||||||
| import org.springframework.util.ObjectUtils; | import org.springframework.util.ObjectUtils; | ||||||
| import org.springframework.util.ReflectionUtils; | import org.springframework.util.ReflectionUtils; | ||||||
| import org.springframework.util.StringUtils; | import org.springframework.util.StringUtils; | ||||||
|  | import org.springframework.validation.annotation.Validated; | ||||||
| import org.springframework.web.bind.annotation.ResponseStatus; | import org.springframework.web.bind.annotation.ResponseStatus; | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  | @ -84,6 +90,10 @@ public class HandlerMethod { | ||||||
| 
 | 
 | ||||||
| 	private final MethodParameter[] parameters; | 	private final MethodParameter[] parameters; | ||||||
| 
 | 
 | ||||||
|  | 	private final boolean validateArguments; | ||||||
|  | 
 | ||||||
|  | 	private final boolean validateReturnValue; | ||||||
|  | 
 | ||||||
| 	@Nullable | 	@Nullable | ||||||
| 	private HttpStatusCode responseStatus; | 	private HttpStatusCode responseStatus; | ||||||
| 
 | 
 | ||||||
|  | @ -122,6 +132,8 @@ public class HandlerMethod { | ||||||
| 		this.bridgedMethod = BridgeMethodResolver.findBridgedMethod(method); | 		this.bridgedMethod = BridgeMethodResolver.findBridgedMethod(method); | ||||||
| 		ReflectionUtils.makeAccessible(this.bridgedMethod); | 		ReflectionUtils.makeAccessible(this.bridgedMethod); | ||||||
| 		this.parameters = initMethodParameters(); | 		this.parameters = initMethodParameters(); | ||||||
|  | 		this.validateArguments = MethodValidationInitializer.checkArguments(this.beanType, this.parameters); | ||||||
|  | 		this.validateReturnValue = MethodValidationInitializer.checkReturnValue(this.beanType, this.bridgedMethod); | ||||||
| 		evaluateResponseStatus(); | 		evaluateResponseStatus(); | ||||||
| 		this.description = initDescription(this.beanType, this.method); | 		this.description = initDescription(this.beanType, this.method); | ||||||
| 	} | 	} | ||||||
|  | @ -141,6 +153,8 @@ public class HandlerMethod { | ||||||
| 		this.bridgedMethod = BridgeMethodResolver.findBridgedMethod(this.method); | 		this.bridgedMethod = BridgeMethodResolver.findBridgedMethod(this.method); | ||||||
| 		ReflectionUtils.makeAccessible(this.bridgedMethod); | 		ReflectionUtils.makeAccessible(this.bridgedMethod); | ||||||
| 		this.parameters = initMethodParameters(); | 		this.parameters = initMethodParameters(); | ||||||
|  | 		this.validateArguments = MethodValidationInitializer.checkArguments(this.beanType, this.parameters); | ||||||
|  | 		this.validateReturnValue = MethodValidationInitializer.checkReturnValue(this.beanType, this.bridgedMethod); | ||||||
| 		evaluateResponseStatus(); | 		evaluateResponseStatus(); | ||||||
| 		this.description = initDescription(this.beanType, this.method); | 		this.description = initDescription(this.beanType, this.method); | ||||||
| 	} | 	} | ||||||
|  | @ -177,6 +191,8 @@ public class HandlerMethod { | ||||||
| 		this.bridgedMethod = BridgeMethodResolver.findBridgedMethod(method); | 		this.bridgedMethod = BridgeMethodResolver.findBridgedMethod(method); | ||||||
| 		ReflectionUtils.makeAccessible(this.bridgedMethod); | 		ReflectionUtils.makeAccessible(this.bridgedMethod); | ||||||
| 		this.parameters = initMethodParameters(); | 		this.parameters = initMethodParameters(); | ||||||
|  | 		this.validateArguments = MethodValidationInitializer.checkArguments(this.beanType, this.parameters); | ||||||
|  | 		this.validateReturnValue = MethodValidationInitializer.checkReturnValue(this.beanType, this.bridgedMethod); | ||||||
| 		evaluateResponseStatus(); | 		evaluateResponseStatus(); | ||||||
| 		this.description = initDescription(this.beanType, this.method); | 		this.description = initDescription(this.beanType, this.method); | ||||||
| 	} | 	} | ||||||
|  | @ -193,6 +209,8 @@ public class HandlerMethod { | ||||||
| 		this.method = handlerMethod.method; | 		this.method = handlerMethod.method; | ||||||
| 		this.bridgedMethod = handlerMethod.bridgedMethod; | 		this.bridgedMethod = handlerMethod.bridgedMethod; | ||||||
| 		this.parameters = handlerMethod.parameters; | 		this.parameters = handlerMethod.parameters; | ||||||
|  | 		this.validateArguments = handlerMethod.validateArguments; | ||||||
|  | 		this.validateReturnValue = handlerMethod.validateReturnValue; | ||||||
| 		this.responseStatus = handlerMethod.responseStatus; | 		this.responseStatus = handlerMethod.responseStatus; | ||||||
| 		this.responseStatusReason = handlerMethod.responseStatusReason; | 		this.responseStatusReason = handlerMethod.responseStatusReason; | ||||||
| 		this.description = handlerMethod.description; | 		this.description = handlerMethod.description; | ||||||
|  | @ -212,6 +230,8 @@ public class HandlerMethod { | ||||||
| 		this.method = handlerMethod.method; | 		this.method = handlerMethod.method; | ||||||
| 		this.bridgedMethod = handlerMethod.bridgedMethod; | 		this.bridgedMethod = handlerMethod.bridgedMethod; | ||||||
| 		this.parameters = handlerMethod.parameters; | 		this.parameters = handlerMethod.parameters; | ||||||
|  | 		this.validateArguments = handlerMethod.validateArguments; | ||||||
|  | 		this.validateReturnValue = handlerMethod.validateReturnValue; | ||||||
| 		this.responseStatus = handlerMethod.responseStatus; | 		this.responseStatus = handlerMethod.responseStatus; | ||||||
| 		this.responseStatusReason = handlerMethod.responseStatusReason; | 		this.responseStatusReason = handlerMethod.responseStatusReason; | ||||||
| 		this.resolvedFromHandlerMethod = handlerMethod; | 		this.resolvedFromHandlerMethod = handlerMethod; | ||||||
|  | @ -290,6 +310,33 @@ public class HandlerMethod { | ||||||
| 		return this.parameters; | 		return this.parameters; | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | 	/** | ||||||
|  | 	 * Whether the method arguments are a candidate for method validation, which | ||||||
|  | 	 * is the case when there are parameter {@code jakarta.validation.Constraint} | ||||||
|  | 	 * annotations. | ||||||
|  | 	 * <p>The presence of {@code jakarta.validation.Valid} by itself does not | ||||||
|  | 	 * trigger method validation since such parameters are already validated at | ||||||
|  | 	 * the level of argument resolvers. | ||||||
|  | 	 * <p><strong>Note:</strong> if the class is annotated with {@link Validated}, | ||||||
|  | 	 * this method returns false, deferring to method validation via AOP proxy. | ||||||
|  | 	 * @since 6.1 | ||||||
|  | 	 */ | ||||||
|  | 	public boolean shouldValidateArguments() { | ||||||
|  | 		return this.validateArguments; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	/** | ||||||
|  | 	 * Whether the method return value is a candidate for method validation, which | ||||||
|  | 	 * is the case when there are method {@code jakarta.validation.Constraint} | ||||||
|  | 	 * or {@code jakarta.validation.Valid} annotations. | ||||||
|  | 	 * <p><strong>Note:</strong> if the class is annotated with {@link Validated}, | ||||||
|  | 	 * this method returns false, deferring to method validation via AOP proxy. | ||||||
|  | 	 * @since 6.1 | ||||||
|  | 	 */ | ||||||
|  | 	public boolean shouldValidateReturnValue() { | ||||||
|  | 		return this.validateReturnValue; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
| 	/** | 	/** | ||||||
| 	 * Return the specified response status, if any. | 	 * Return the specified response status, if any. | ||||||
| 	 * @since 4.3.8 | 	 * @since 4.3.8 | ||||||
|  | @ -603,4 +650,38 @@ public class HandlerMethod { | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | 
 | ||||||
|  | 	/** | ||||||
|  | 	 * Checks for the presence of {@code @Constraint} and {@code @Valid} | ||||||
|  | 	 * annotations on the method and method parameters. | ||||||
|  | 	 */ | ||||||
|  | 	private static class MethodValidationInitializer { | ||||||
|  | 
 | ||||||
|  | 		private static final Predicate<MergedAnnotation<? extends Annotation>> INPUT_PREDICATE = | ||||||
|  | 				MergedAnnotationPredicates.typeIn("jakarta.validation.Constraint"); | ||||||
|  | 
 | ||||||
|  | 		private static final Predicate<MergedAnnotation<? extends Annotation>> OUTPUT_PREDICATE = | ||||||
|  | 				MergedAnnotationPredicates.typeIn("jakarta.validation.Valid", "jakarta.validation.Constraint"); | ||||||
|  | 
 | ||||||
|  | 		public static boolean checkArguments(Class<?> beanType, MethodParameter[] parameters) { | ||||||
|  | 			if (AnnotationUtils.findAnnotation(beanType, Validated.class) == null) { | ||||||
|  | 				for (MethodParameter parameter : parameters) { | ||||||
|  | 					MergedAnnotations merged = MergedAnnotations.from(parameter.getParameterAnnotations()); | ||||||
|  | 					if (merged.stream().anyMatch(INPUT_PREDICATE)) { | ||||||
|  | 						return true; | ||||||
|  | 					} | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 			return false; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		public static boolean checkReturnValue(Class<?> beanType, Method method) { | ||||||
|  | 			if (AnnotationUtils.findAnnotation(beanType, Validated.class) == null) { | ||||||
|  | 				MergedAnnotations merged = MergedAnnotations.from(method, MergedAnnotations.SearchStrategy.TYPE_HIERARCHY); | ||||||
|  | 				return merged.stream().anyMatch(OUTPUT_PREDICATE); | ||||||
|  | 			} | ||||||
|  | 			return false; | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -164,7 +164,7 @@ public class ModelAttributeMethodProcessor implements HandlerMethodArgumentResol | ||||||
| 		if (bindingResult == null) { | 		if (bindingResult == null) { | ||||||
| 			// Bean property binding and validation; | 			// Bean property binding and validation; | ||||||
| 			// skipped in case of binding failure on construction. | 			// skipped in case of binding failure on construction. | ||||||
| 			WebDataBinder binder = binderFactory.createBinder(webRequest, attribute, name); | 			WebDataBinder binder = binderFactory.createBinder(webRequest, attribute, name, parameter); | ||||||
| 			if (binder.getTarget() != null) { | 			if (binder.getTarget() != null) { | ||||||
| 				if (!mavContainer.isBindingDisabled(name)) { | 				if (!mavContainer.isBindingDisabled(name)) { | ||||||
| 					bindRequestParameters(binder, webRequest); | 					bindRequestParameters(binder, webRequest); | ||||||
|  | @ -251,7 +251,7 @@ public class ModelAttributeMethodProcessor implements HandlerMethodArgumentResol | ||||||
| 		String[] paramNames = BeanUtils.getParameterNames(ctor); | 		String[] paramNames = BeanUtils.getParameterNames(ctor); | ||||||
| 		Class<?>[] paramTypes = ctor.getParameterTypes(); | 		Class<?>[] paramTypes = ctor.getParameterTypes(); | ||||||
| 		Object[] args = new Object[paramTypes.length]; | 		Object[] args = new Object[paramTypes.length]; | ||||||
| 		WebDataBinder binder = binderFactory.createBinder(webRequest, null, attributeName); | 		WebDataBinder binder = binderFactory.createBinder(webRequest, null, attributeName, parameter); | ||||||
| 		String fieldDefaultPrefix = binder.getFieldDefaultPrefix(); | 		String fieldDefaultPrefix = binder.getFieldDefaultPrefix(); | ||||||
| 		String fieldMarkerPrefix = binder.getFieldMarkerPrefix(); | 		String fieldMarkerPrefix = binder.getFieldMarkerPrefix(); | ||||||
| 		boolean bindingFailure = false; | 		boolean bindingFailure = false; | ||||||
|  |  | ||||||
|  | @ -0,0 +1,124 @@ | ||||||
|  | /* | ||||||
|  |  * Copyright 2002-2023 the original author or authors. | ||||||
|  |  * | ||||||
|  |  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||||
|  |  * you may not use this file except in compliance with the License. | ||||||
|  |  * You may obtain a copy of the License at | ||||||
|  |  * | ||||||
|  |  *      https://www.apache.org/licenses/LICENSE-2.0 | ||||||
|  |  * | ||||||
|  |  * Unless required by applicable law or agreed to in writing, software | ||||||
|  |  * distributed under the License is distributed on an "AS IS" BASIS, | ||||||
|  |  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||||
|  |  * See the License for the specific language governing permissions and | ||||||
|  |  * limitations under the License. | ||||||
|  |  */ | ||||||
|  | 
 | ||||||
|  | package org.springframework.web.method.support; | ||||||
|  | 
 | ||||||
|  | import java.lang.reflect.Method; | ||||||
|  | 
 | ||||||
|  | import jakarta.validation.Validator; | ||||||
|  | 
 | ||||||
|  | import org.springframework.core.Conventions; | ||||||
|  | import org.springframework.core.MethodParameter; | ||||||
|  | import org.springframework.core.ParameterNameDiscoverer; | ||||||
|  | import org.springframework.lang.Nullable; | ||||||
|  | import org.springframework.validation.BindingResult; | ||||||
|  | import org.springframework.validation.MessageCodesResolver; | ||||||
|  | import org.springframework.validation.beanvalidation.DefaultMethodValidator; | ||||||
|  | import org.springframework.validation.beanvalidation.MethodValidationAdapter; | ||||||
|  | import org.springframework.validation.beanvalidation.MethodValidationResult; | ||||||
|  | import org.springframework.validation.beanvalidation.MethodValidator; | ||||||
|  | import org.springframework.validation.beanvalidation.ParameterErrors; | ||||||
|  | import org.springframework.web.bind.annotation.RequestBody; | ||||||
|  | import org.springframework.web.bind.annotation.RequestPart; | ||||||
|  | import org.springframework.web.bind.support.ConfigurableWebBindingInitializer; | ||||||
|  | import org.springframework.web.bind.support.WebBindingInitializer; | ||||||
|  | import org.springframework.web.method.annotation.ModelFactory; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * {@link org.springframework.validation.beanvalidation.MethodValidator} for | ||||||
|  |  * use with {@code @RequestMapping} methods. Helps to determine object names | ||||||
|  |  * and populates {@link BindingResult} method arguments with errors from | ||||||
|  |  * {@link MethodValidationResult#getBeanResults() beanResults}. | ||||||
|  |  * | ||||||
|  |  * @author Rossen Stoyanchev | ||||||
|  |  * @since 6.1 | ||||||
|  |  */ | ||||||
|  | public class HandlerMethodValidator extends DefaultMethodValidator { | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 	public HandlerMethodValidator(MethodValidationAdapter adapter) { | ||||||
|  | 		super(adapter); | ||||||
|  | 		adapter.setBindingResultNameResolver(this::determineObjectName); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	private String determineObjectName(MethodParameter param, @Nullable Object argument) { | ||||||
|  | 		if (param.hasParameterAnnotation(RequestBody.class) || param.hasParameterAnnotation(RequestPart.class)) { | ||||||
|  | 			return Conventions.getVariableNameForParameter(param); | ||||||
|  | 		} | ||||||
|  | 		else { | ||||||
|  | 			return ((param.getParameterIndex() != -1) ? | ||||||
|  | 					ModelFactory.getNameForParameter(param) : | ||||||
|  | 					ModelFactory.getNameForReturnValue(argument, param)); | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 	@Override | ||||||
|  | 	protected void handleArgumentsResult( | ||||||
|  | 			Object bean, Method method, Object[] arguments, Class<?>[] groups, MethodValidationResult result) { | ||||||
|  | 
 | ||||||
|  | 		if (result.getConstraintViolations().isEmpty()) { | ||||||
|  | 			return; | ||||||
|  | 		} | ||||||
|  | 		if (!result.getBeanResults().isEmpty()) { | ||||||
|  | 			int bindingResultCount = 0; | ||||||
|  | 			for (ParameterErrors errors : result.getBeanResults()) { | ||||||
|  | 				for (Object arg : arguments) { | ||||||
|  | 					if (arg instanceof BindingResult bindingResult) { | ||||||
|  | 						if (bindingResult.getObjectName().equals(errors.getObjectName())) { | ||||||
|  | 							bindingResult.addAllErrors(errors); | ||||||
|  | 							bindingResultCount++; | ||||||
|  | 							break; | ||||||
|  | 						} | ||||||
|  | 					} | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 			if (result.getAllValidationResults().size() == bindingResultCount) { | ||||||
|  | 				return; | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 		result.throwIfViolationsPresent(); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 	/** | ||||||
|  | 	 * Create a {@link MethodValidator} if Bean Validation is enabled in Spring MVC or WebFlux. | ||||||
|  | 	 * @param bindingInitializer for the configured Validator and MessageCodesResolver | ||||||
|  | 	 * @param parameterNameDiscoverer the {@code ParameterNameDiscoverer} to use | ||||||
|  | 	 * for {@link MethodValidationAdapter#setParameterNameDiscoverer} | ||||||
|  | 	 */ | ||||||
|  | 	@Nullable | ||||||
|  | 	public static MethodValidator from( | ||||||
|  | 			@Nullable WebBindingInitializer bindingInitializer, | ||||||
|  | 			@Nullable ParameterNameDiscoverer parameterNameDiscoverer) { | ||||||
|  | 
 | ||||||
|  | 		if (bindingInitializer instanceof ConfigurableWebBindingInitializer configurableInitializer) { | ||||||
|  | 			if (configurableInitializer.getValidator() instanceof Validator validator) { | ||||||
|  | 				MethodValidationAdapter validationAdapter = new MethodValidationAdapter(validator); | ||||||
|  | 				if (parameterNameDiscoverer != null) { | ||||||
|  | 					validationAdapter.setParameterNameDiscoverer(parameterNameDiscoverer); | ||||||
|  | 				} | ||||||
|  | 				MessageCodesResolver codesResolver = configurableInitializer.getMessageCodesResolver(); | ||||||
|  | 				if (codesResolver != null) { | ||||||
|  | 					validationAdapter.setMessageCodesResolver(codesResolver); | ||||||
|  | 				} | ||||||
|  | 				return new HandlerMethodValidator(validationAdapter); | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 		return null; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | @ -1,5 +1,5 @@ | ||||||
| /* | /* | ||||||
|  * Copyright 2002-2022 the original author or authors. |  * Copyright 2002-2023 the original author or authors. | ||||||
|  * |  * | ||||||
|  * Licensed under the Apache License, Version 2.0 (the "License"); |  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||||
|  * you may not use this file except in compliance with the License. |  * you may not use this file except in compliance with the License. | ||||||
|  | @ -30,6 +30,7 @@ import org.springframework.core.MethodParameter; | ||||||
| import org.springframework.core.ParameterNameDiscoverer; | import org.springframework.core.ParameterNameDiscoverer; | ||||||
| import org.springframework.lang.Nullable; | import org.springframework.lang.Nullable; | ||||||
| import org.springframework.util.ObjectUtils; | import org.springframework.util.ObjectUtils; | ||||||
|  | import org.springframework.validation.beanvalidation.MethodValidator; | ||||||
| import org.springframework.web.bind.WebDataBinder; | import org.springframework.web.bind.WebDataBinder; | ||||||
| import org.springframework.web.bind.support.SessionStatus; | import org.springframework.web.bind.support.SessionStatus; | ||||||
| import org.springframework.web.bind.support.WebDataBinderFactory; | import org.springframework.web.bind.support.WebDataBinderFactory; | ||||||
|  | @ -50,6 +51,8 @@ public class InvocableHandlerMethod extends HandlerMethod { | ||||||
| 
 | 
 | ||||||
| 	private static final Object[] EMPTY_ARGS = new Object[0]; | 	private static final Object[] EMPTY_ARGS = new Object[0]; | ||||||
| 
 | 
 | ||||||
|  | 	private static final Class<?>[] EMPTY_GROUPS = new Class<?>[0]; | ||||||
|  | 
 | ||||||
| 
 | 
 | ||||||
| 	private HandlerMethodArgumentResolverComposite resolvers = new HandlerMethodArgumentResolverComposite(); | 	private HandlerMethodArgumentResolverComposite resolvers = new HandlerMethodArgumentResolverComposite(); | ||||||
| 
 | 
 | ||||||
|  | @ -58,6 +61,9 @@ public class InvocableHandlerMethod extends HandlerMethod { | ||||||
| 	@Nullable | 	@Nullable | ||||||
| 	private WebDataBinderFactory dataBinderFactory; | 	private WebDataBinderFactory dataBinderFactory; | ||||||
| 
 | 
 | ||||||
|  | 	@Nullable | ||||||
|  | 	private MethodValidator methodValidator; | ||||||
|  | 
 | ||||||
| 
 | 
 | ||||||
| 	/** | 	/** | ||||||
| 	 * Create an instance from a {@code HandlerMethod}. | 	 * Create an instance from a {@code HandlerMethod}. | ||||||
|  | @ -121,6 +127,16 @@ public class InvocableHandlerMethod extends HandlerMethod { | ||||||
| 		this.dataBinderFactory = dataBinderFactory; | 		this.dataBinderFactory = dataBinderFactory; | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | 	/** | ||||||
|  | 	 * Set the {@link MethodValidator} to perform method validation with if the | ||||||
|  | 	 * controller method {@link #shouldValidateArguments()} or | ||||||
|  | 	 * {@link #shouldValidateReturnValue()}. | ||||||
|  | 	 * @since 6.1 | ||||||
|  | 	 */ | ||||||
|  | 	public void setMethodValidator(@Nullable MethodValidator methodValidator) { | ||||||
|  | 		this.methodValidator = methodValidator; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
| 
 | 
 | ||||||
| 	/** | 	/** | ||||||
| 	 * Invoke the method after resolving its argument values in the context of the given request. | 	 * Invoke the method after resolving its argument values in the context of the given request. | ||||||
|  | @ -149,7 +165,19 @@ public class InvocableHandlerMethod extends HandlerMethod { | ||||||
| 		if (logger.isTraceEnabled()) { | 		if (logger.isTraceEnabled()) { | ||||||
| 			logger.trace("Arguments: " + Arrays.toString(args)); | 			logger.trace("Arguments: " + Arrays.toString(args)); | ||||||
| 		} | 		} | ||||||
| 		return doInvoke(args); | 
 | ||||||
|  | 		Class<?>[] groups = getValidationGroups(); | ||||||
|  | 		if (shouldValidateArguments() && this.methodValidator != null) { | ||||||
|  | 			this.methodValidator.validateArguments(getBean(), getBridgedMethod(), args, groups); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		Object returnValue = doInvoke(args); | ||||||
|  | 
 | ||||||
|  | 		if (shouldValidateReturnValue() && this.methodValidator != null) { | ||||||
|  | 			this.methodValidator.validateReturnValue(getBean(), getBridgedMethod(), returnValue, groups); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		return returnValue; | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	/** | 	/** | ||||||
|  | @ -194,6 +222,11 @@ public class InvocableHandlerMethod extends HandlerMethod { | ||||||
| 		return args; | 		return args; | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | 	private Class<?>[] getValidationGroups() { | ||||||
|  | 		return ((shouldValidateArguments() || shouldValidateReturnValue()) && this.methodValidator != null ? | ||||||
|  | 				this.methodValidator.determineValidationGroups(getBean(), getBridgedMethod()) : EMPTY_GROUPS); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
| 	/** | 	/** | ||||||
| 	 * Invoke the handler method with the given argument values. | 	 * Invoke the handler method with the given argument values. | ||||||
| 	 */ | 	 */ | ||||||
|  |  | ||||||
|  | @ -0,0 +1,191 @@ | ||||||
|  | /* | ||||||
|  |  * Copyright 2002-2023 the original author or authors. | ||||||
|  |  * | ||||||
|  |  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||||
|  |  * you may not use this file except in compliance with the License. | ||||||
|  |  * You may obtain a copy of the License at | ||||||
|  |  * | ||||||
|  |  *      https://www.apache.org/licenses/LICENSE-2.0 | ||||||
|  |  * | ||||||
|  |  * Unless required by applicable law or agreed to in writing, software | ||||||
|  |  * distributed under the License is distributed on an "AS IS" BASIS, | ||||||
|  |  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||||
|  |  * See the License for the specific language governing permissions and | ||||||
|  |  * limitations under the License. | ||||||
|  |  */ | ||||||
|  | 
 | ||||||
|  | package org.springframework.web.method; | ||||||
|  | 
 | ||||||
|  | import java.lang.reflect.Method; | ||||||
|  | import java.util.List; | ||||||
|  | 
 | ||||||
|  | import jakarta.validation.Valid; | ||||||
|  | import jakarta.validation.constraints.Max; | ||||||
|  | import jakarta.validation.constraints.Size; | ||||||
|  | import org.junit.jupiter.api.Test; | ||||||
|  | 
 | ||||||
|  | import org.springframework.util.ClassUtils; | ||||||
|  | import org.springframework.validation.annotation.Validated; | ||||||
|  | 
 | ||||||
|  | import static org.assertj.core.api.Assertions.assertThat; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Unit tests for {@link HandlerMethod}. | ||||||
|  |  * @author Rossen Stoyanchev | ||||||
|  |  */ | ||||||
|  | public class HandlerMethodTests { | ||||||
|  | 
 | ||||||
|  | 	@Test | ||||||
|  | 	void shouldValidateArgsWithConstraintsDirectlyOnClass() { | ||||||
|  | 		Object target = new MyClass(); | ||||||
|  | 		testShouldValidateArguments(target, List.of("addIntValue", "addPersonAndIntValue"), true); | ||||||
|  | 		testShouldValidateArguments(target, List.of("addPerson", "getPerson", "getIntValue", "addPersonNotValidated"), false); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	@Test | ||||||
|  | 	void shouldValidateArgsWithConstraintsOnInterface() { | ||||||
|  | 		Object target = new MyInterfaceImpl(); | ||||||
|  | 		testShouldValidateArguments(target, List.of("addIntValue", "addPersonAndIntValue"), true); | ||||||
|  | 		testShouldValidateArguments(target, List.of("addPerson", "addPersonNotValidated", "getPerson", "getIntValue"), false); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	@Test | ||||||
|  | 	void shouldValidateReturnValueWithConstraintsDirectlyOnClass() { | ||||||
|  | 		Object target = new MyClass(); | ||||||
|  | 		testShouldValidateReturnValue(target, List.of("getPerson", "getIntValue"), true); | ||||||
|  | 		testShouldValidateReturnValue(target, List.of("addPerson", "addIntValue", "addPersonNotValidated"), false); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	@Test | ||||||
|  | 	void shouldValidateReturnValueWithConstraintsOnInterface() { | ||||||
|  | 		Object target = new MyInterfaceImpl(); | ||||||
|  | 		testShouldValidateReturnValue(target, List.of("getPerson", "getIntValue"), true); | ||||||
|  | 		testShouldValidateReturnValue(target, List.of("addPerson", "addIntValue", "addPersonNotValidated"), false); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	@Test | ||||||
|  | 	void classLevelValidatedAnnotation() { | ||||||
|  | 		Object target = new MyValidatedClass(); | ||||||
|  | 		testShouldValidateArguments(target, List.of("addPerson"), false); | ||||||
|  | 		testShouldValidateReturnValue(target, List.of("getPerson"), false); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	private static void testShouldValidateArguments(Object target, List<String> methodNames, boolean expected) { | ||||||
|  | 		for (String methodName : methodNames) { | ||||||
|  | 			assertThat(getHandlerMethod(target, methodName).shouldValidateArguments()).isEqualTo(expected); | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	private static void testShouldValidateReturnValue(Object target, List<String> methodNames, boolean expected) { | ||||||
|  | 		for (String methodName : methodNames) { | ||||||
|  | 			assertThat(getHandlerMethod(target, methodName).shouldValidateReturnValue()).isEqualTo(expected); | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	private static HandlerMethod getHandlerMethod(Object target, String methodName) { | ||||||
|  | 		Method method = ClassUtils.getMethod(target.getClass(), methodName, (Class<?>[]) null); | ||||||
|  | 		return new HandlerMethod(target, method); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 	@SuppressWarnings("unused") | ||||||
|  | 	private record Person(@Size(min = 1, max = 10) String name) { | ||||||
|  | 
 | ||||||
|  | 		@Override | ||||||
|  | 		public String name() { | ||||||
|  | 			return this.name; | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 	@SuppressWarnings("unused") | ||||||
|  | 	private static class MyClass { | ||||||
|  | 
 | ||||||
|  | 		public void addPerson(@Valid Person person) { | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		public void addIntValue(@Max(10) int value) { | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		public void addPersonAndIntValue(@Valid Person person, @Max(10) int value) { | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		public void addPersonNotValidated(Person person) { | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		@Valid | ||||||
|  | 		public Person getPerson() { | ||||||
|  | 			throw new UnsupportedOperationException(); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		@Max(10) | ||||||
|  | 		public int getIntValue() { | ||||||
|  | 			throw new UnsupportedOperationException(); | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 	@SuppressWarnings("unused") | ||||||
|  | 	private interface MyInterface { | ||||||
|  | 
 | ||||||
|  | 		void addPerson(@Valid Person person); | ||||||
|  | 
 | ||||||
|  | 		void addIntValue(@Max(10) int value); | ||||||
|  | 
 | ||||||
|  | 		void addPersonAndIntValue(@Valid Person person, @Max(10) int value); | ||||||
|  | 
 | ||||||
|  | 		void addPersonNotValidated(Person person); | ||||||
|  | 
 | ||||||
|  | 		@Valid | ||||||
|  | 		Person getPerson(); | ||||||
|  | 
 | ||||||
|  | 		@Max(10) | ||||||
|  | 		int getIntValue(); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 	@SuppressWarnings("unused") | ||||||
|  | 	private static class MyInterfaceImpl implements MyInterface { | ||||||
|  | 
 | ||||||
|  | 		@Override | ||||||
|  | 		public void addPerson(Person person) { | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		@Override | ||||||
|  | 		public void addIntValue(int value) { | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		@Override | ||||||
|  | 		public void addPersonAndIntValue(Person person, int value) { | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		@Override | ||||||
|  | 		public void addPersonNotValidated(Person person) { | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		@Override | ||||||
|  | 		public Person getPerson() { | ||||||
|  | 			throw new UnsupportedOperationException(); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		@Override | ||||||
|  | 		public int getIntValue() { | ||||||
|  | 			throw new UnsupportedOperationException(); | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 	@SuppressWarnings("unused") | ||||||
|  | 	@Validated | ||||||
|  | 	private static class MyValidatedClass { | ||||||
|  | 
 | ||||||
|  | 		public void addPerson(@Valid Person person) { | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		@Valid | ||||||
|  | 		public Person getPerson() { | ||||||
|  | 			throw new UnsupportedOperationException(); | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | @ -162,10 +162,10 @@ public class ModelAttributeMethodProcessorTests { | ||||||
| 	public void resolveArgumentViaDefaultConstructor() throws Exception { | 	public void resolveArgumentViaDefaultConstructor() throws Exception { | ||||||
| 		WebDataBinder dataBinder = new WebRequestDataBinder(null); | 		WebDataBinder dataBinder = new WebRequestDataBinder(null); | ||||||
| 		WebDataBinderFactory factory = mock(); | 		WebDataBinderFactory factory = mock(); | ||||||
| 		given(factory.createBinder(any(), notNull(), eq("attrName"))).willReturn(dataBinder); | 		given(factory.createBinder(any(), notNull(), eq("attrName"), any())).willReturn(dataBinder); | ||||||
| 
 | 
 | ||||||
| 		this.processor.resolveArgument(this.paramNamedValidModelAttr, this.container, this.request, factory); | 		this.processor.resolveArgument(this.paramNamedValidModelAttr, this.container, this.request, factory); | ||||||
| 		verify(factory).createBinder(any(), notNull(), eq("attrName")); | 		verify(factory).createBinder(any(), notNull(), eq("attrName"), any()); | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	@Test | 	@Test | ||||||
|  | @ -176,7 +176,7 @@ public class ModelAttributeMethodProcessorTests { | ||||||
| 
 | 
 | ||||||
| 		StubRequestDataBinder dataBinder = new StubRequestDataBinder(target, name); | 		StubRequestDataBinder dataBinder = new StubRequestDataBinder(target, name); | ||||||
| 		WebDataBinderFactory factory = mock(); | 		WebDataBinderFactory factory = mock(); | ||||||
| 		given(factory.createBinder(this.request, target, name)).willReturn(dataBinder); | 		given(factory.createBinder(this.request, target, name, this.paramNamedValidModelAttr)).willReturn(dataBinder); | ||||||
| 
 | 
 | ||||||
| 		this.processor.resolveArgument(this.paramNamedValidModelAttr, this.container, this.request, factory); | 		this.processor.resolveArgument(this.paramNamedValidModelAttr, this.container, this.request, factory); | ||||||
| 
 | 
 | ||||||
|  | @ -195,7 +195,7 @@ public class ModelAttributeMethodProcessorTests { | ||||||
| 
 | 
 | ||||||
| 		StubRequestDataBinder dataBinder = new StubRequestDataBinder(target, name); | 		StubRequestDataBinder dataBinder = new StubRequestDataBinder(target, name); | ||||||
| 		WebDataBinderFactory factory = mock(); | 		WebDataBinderFactory factory = mock(); | ||||||
| 		given(factory.createBinder(this.request, target, name)).willReturn(dataBinder); | 		given(factory.createBinder(this.request, target, name, this.paramNamedValidModelAttr)).willReturn(dataBinder); | ||||||
| 
 | 
 | ||||||
| 		this.processor.resolveArgument(this.paramNamedValidModelAttr, this.container, this.request, factory); | 		this.processor.resolveArgument(this.paramNamedValidModelAttr, this.container, this.request, factory); | ||||||
| 
 | 
 | ||||||
|  | @ -211,7 +211,7 @@ public class ModelAttributeMethodProcessorTests { | ||||||
| 
 | 
 | ||||||
| 		StubRequestDataBinder dataBinder = new StubRequestDataBinder(target, name); | 		StubRequestDataBinder dataBinder = new StubRequestDataBinder(target, name); | ||||||
| 		WebDataBinderFactory factory = mock(); | 		WebDataBinderFactory factory = mock(); | ||||||
| 		given(factory.createBinder(this.request, target, name)).willReturn(dataBinder); | 		given(factory.createBinder(this.request, target, name, this.paramBindingDisabledAttr)).willReturn(dataBinder); | ||||||
| 
 | 
 | ||||||
| 		this.processor.resolveArgument(this.paramBindingDisabledAttr, this.container, this.request, factory); | 		this.processor.resolveArgument(this.paramBindingDisabledAttr, this.container, this.request, factory); | ||||||
| 
 | 
 | ||||||
|  | @ -229,12 +229,12 @@ public class ModelAttributeMethodProcessorTests { | ||||||
| 		dataBinder.getBindingResult().reject("error"); | 		dataBinder.getBindingResult().reject("error"); | ||||||
| 
 | 
 | ||||||
| 		WebDataBinderFactory binderFactory = mock(); | 		WebDataBinderFactory binderFactory = mock(); | ||||||
| 		given(binderFactory.createBinder(this.request, target, name)).willReturn(dataBinder); | 		given(binderFactory.createBinder(this.request, target, name, this.paramNonSimpleType)).willReturn(dataBinder); | ||||||
| 
 | 
 | ||||||
| 		assertThatExceptionOfType(MethodArgumentNotValidException.class).isThrownBy(() -> | 		assertThatExceptionOfType(MethodArgumentNotValidException.class).isThrownBy(() -> | ||||||
| 				this.processor.resolveArgument(this.paramNonSimpleType, this.container, this.request, binderFactory)); | 				this.processor.resolveArgument(this.paramNonSimpleType, this.container, this.request, binderFactory)); | ||||||
| 
 | 
 | ||||||
| 		verify(binderFactory).createBinder(this.request, target, name); | 		verify(binderFactory).createBinder(this.request, target, name, this.paramNonSimpleType); | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	@Test  // SPR-9378 | 	@Test  // SPR-9378 | ||||||
|  | @ -249,7 +249,7 @@ public class ModelAttributeMethodProcessorTests { | ||||||
| 
 | 
 | ||||||
| 		StubRequestDataBinder dataBinder = new StubRequestDataBinder(testBean, name); | 		StubRequestDataBinder dataBinder = new StubRequestDataBinder(testBean, name); | ||||||
| 		WebDataBinderFactory binderFactory = mock(); | 		WebDataBinderFactory binderFactory = mock(); | ||||||
| 		given(binderFactory.createBinder(this.request, testBean, name)).willReturn(dataBinder); | 		given(binderFactory.createBinder(this.request, testBean, name, this.paramModelAttr)).willReturn(dataBinder); | ||||||
| 
 | 
 | ||||||
| 		this.processor.resolveArgument(this.paramModelAttr, this.container, this.request, binderFactory); | 		this.processor.resolveArgument(this.paramModelAttr, this.container, this.request, binderFactory); | ||||||
| 
 | 
 | ||||||
|  | @ -278,7 +278,7 @@ public class ModelAttributeMethodProcessorTests { | ||||||
| 		ServletWebRequest requestWithParam = new ServletWebRequest(mockRequest); | 		ServletWebRequest requestWithParam = new ServletWebRequest(mockRequest); | ||||||
| 
 | 
 | ||||||
| 		WebDataBinderFactory factory = mock(); | 		WebDataBinderFactory factory = mock(); | ||||||
| 		given(factory.createBinder(any(), any(), eq("testBeanWithConstructorArgs"))) | 		given(factory.createBinder(any(), any(), eq("testBeanWithConstructorArgs"), any())) | ||||||
| 				.willAnswer(invocation -> { | 				.willAnswer(invocation -> { | ||||||
| 					WebRequestDataBinder binder = new WebRequestDataBinder(invocation.getArgument(1)); | 					WebRequestDataBinder binder = new WebRequestDataBinder(invocation.getArgument(1)); | ||||||
| 					// Add conversion service which will convert "1,2" to a list | 					// Add conversion service which will convert "1,2" to a list | ||||||
|  | @ -297,10 +297,10 @@ public class ModelAttributeMethodProcessorTests { | ||||||
| 
 | 
 | ||||||
| 		WebDataBinder dataBinder = new WebRequestDataBinder(target); | 		WebDataBinder dataBinder = new WebRequestDataBinder(target); | ||||||
| 		WebDataBinderFactory factory = mock(); | 		WebDataBinderFactory factory = mock(); | ||||||
| 		given(factory.createBinder(this.request, target, expectedAttrName)).willReturn(dataBinder); | 		given(factory.createBinder(this.request, target, expectedAttrName, param)).willReturn(dataBinder); | ||||||
| 
 | 
 | ||||||
| 		this.processor.resolveArgument(param, this.container, this.request, factory); | 		this.processor.resolveArgument(param, this.container, this.request, factory); | ||||||
| 		verify(factory).createBinder(this.request, target, expectedAttrName); | 		verify(factory).createBinder(this.request, target, expectedAttrName, param); | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -60,7 +60,7 @@ class ModelAttributeMethodProcessorKotlinTests { | ||||||
| 		val mockRequest = MockHttpServletRequest().apply { addParameter("a", "b") } | 		val mockRequest = MockHttpServletRequest().apply { addParameter("a", "b") } | ||||||
| 		val requestWithParam = ServletWebRequest(mockRequest) | 		val requestWithParam = ServletWebRequest(mockRequest) | ||||||
| 		val factory = mock<WebDataBinderFactory>() | 		val factory = mock<WebDataBinderFactory>() | ||||||
| 		given(factory.createBinder(any(), any(), eq("param"))) | 		given(factory.createBinder(any(), any(), eq("param"), any())) | ||||||
| 			.willAnswer { WebRequestDataBinder(it.getArgument(1)) } | 			.willAnswer { WebRequestDataBinder(it.getArgument(1)) } | ||||||
| 		assertThat(processor.resolveArgument(this.param, container, requestWithParam, factory)).isEqualTo(Param("b")) | 		assertThat(processor.resolveArgument(this.param, container, requestWithParam, factory)).isEqualTo(Param("b")) | ||||||
| 	} | 	} | ||||||
|  | @ -70,7 +70,7 @@ class ModelAttributeMethodProcessorKotlinTests { | ||||||
| 		val mockRequest = MockHttpServletRequest().apply { addParameter("a", null) } | 		val mockRequest = MockHttpServletRequest().apply { addParameter("a", null) } | ||||||
| 		val requestWithParam = ServletWebRequest(mockRequest) | 		val requestWithParam = ServletWebRequest(mockRequest) | ||||||
| 		val factory = mock<WebDataBinderFactory>() | 		val factory = mock<WebDataBinderFactory>() | ||||||
| 		given(factory.createBinder(any(), any(), eq("param"))) | 		given(factory.createBinder(any(), any(), eq("param"), any())) | ||||||
| 			.willAnswer { WebRequestDataBinder(it.getArgument(1)) } | 			.willAnswer { WebRequestDataBinder(it.getArgument(1)) } | ||||||
| 		assertThatThrownBy { | 		assertThatThrownBy { | ||||||
| 			processor.resolveArgument(this.param, container, requestWithParam, factory) | 			processor.resolveArgument(this.param, container, requestWithParam, factory) | ||||||
|  |  | ||||||
|  | @ -18,6 +18,7 @@ dependencies { | ||||||
| 	optional("jakarta.servlet.jsp:jakarta.servlet.jsp-api") | 	optional("jakarta.servlet.jsp:jakarta.servlet.jsp-api") | ||||||
| 	optional("jakarta.servlet.jsp.jstl:jakarta.servlet.jsp.jstl-api") | 	optional("jakarta.servlet.jsp.jstl:jakarta.servlet.jsp.jstl-api") | ||||||
| 	optional("jakarta.el:jakarta.el-api") | 	optional("jakarta.el:jakarta.el-api") | ||||||
|  | 	optional("jakarta.validation:jakarta.validation-api") | ||||||
| 	optional("jakarta.xml.bind:jakarta.xml.bind-api") | 	optional("jakarta.xml.bind:jakarta.xml.bind-api") | ||||||
| 	optional('io.micrometer:context-propagation') | 	optional('io.micrometer:context-propagation') | ||||||
| 	optional("org.webjars:webjars-locator-core") | 	optional("org.webjars:webjars-locator-core") | ||||||
|  |  | ||||||
|  | @ -1,5 +1,5 @@ | ||||||
| /* | /* | ||||||
|  * Copyright 2002-2022 the original author or authors. |  * Copyright 2002-2023 the original author or authors. | ||||||
|  * |  * | ||||||
|  * Licensed under the Apache License, Version 2.0 (the "License"); |  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||||
|  * you may not use this file except in compliance with the License. |  * you may not use this file except in compliance with the License. | ||||||
|  | @ -48,8 +48,10 @@ import org.springframework.http.converter.StringHttpMessageConverter; | ||||||
| import org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter; | import org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter; | ||||||
| import org.springframework.lang.Nullable; | import org.springframework.lang.Nullable; | ||||||
| import org.springframework.ui.ModelMap; | import org.springframework.ui.ModelMap; | ||||||
|  | import org.springframework.util.ClassUtils; | ||||||
| import org.springframework.util.CollectionUtils; | import org.springframework.util.CollectionUtils; | ||||||
| import org.springframework.util.ReflectionUtils.MethodFilter; | import org.springframework.util.ReflectionUtils.MethodFilter; | ||||||
|  | import org.springframework.validation.beanvalidation.MethodValidator; | ||||||
| import org.springframework.web.accept.ContentNegotiationManager; | import org.springframework.web.accept.ContentNegotiationManager; | ||||||
| import org.springframework.web.bind.annotation.InitBinder; | import org.springframework.web.bind.annotation.InitBinder; | ||||||
| import org.springframework.web.bind.annotation.ModelAttribute; | import org.springframework.web.bind.annotation.ModelAttribute; | ||||||
|  | @ -86,6 +88,7 @@ import org.springframework.web.method.support.HandlerMethodArgumentResolver; | ||||||
| import org.springframework.web.method.support.HandlerMethodArgumentResolverComposite; | import org.springframework.web.method.support.HandlerMethodArgumentResolverComposite; | ||||||
| import org.springframework.web.method.support.HandlerMethodReturnValueHandler; | import org.springframework.web.method.support.HandlerMethodReturnValueHandler; | ||||||
| import org.springframework.web.method.support.HandlerMethodReturnValueHandlerComposite; | import org.springframework.web.method.support.HandlerMethodReturnValueHandlerComposite; | ||||||
|  | import org.springframework.web.method.support.HandlerMethodValidator; | ||||||
| import org.springframework.web.method.support.InvocableHandlerMethod; | import org.springframework.web.method.support.InvocableHandlerMethod; | ||||||
| import org.springframework.web.method.support.ModelAndViewContainer; | import org.springframework.web.method.support.ModelAndViewContainer; | ||||||
| import org.springframework.web.servlet.ModelAndView; | import org.springframework.web.servlet.ModelAndView; | ||||||
|  | @ -128,6 +131,9 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter | ||||||
| 			(!AnnotatedElementUtils.hasAnnotation(method, RequestMapping.class) && | 			(!AnnotatedElementUtils.hasAnnotation(method, RequestMapping.class) && | ||||||
| 					AnnotatedElementUtils.hasAnnotation(method, ModelAttribute.class)); | 					AnnotatedElementUtils.hasAnnotation(method, ModelAttribute.class)); | ||||||
| 
 | 
 | ||||||
|  | 	private final static boolean BEAN_VALIDATION_PRESENT = | ||||||
|  | 			ClassUtils.isPresent("jakarta.validation.Validator", HandlerMethod.class.getClassLoader()); | ||||||
|  | 
 | ||||||
| 
 | 
 | ||||||
| 	@Nullable | 	@Nullable | ||||||
| 	private List<HandlerMethodArgumentResolver> customArgumentResolvers; | 	private List<HandlerMethodArgumentResolver> customArgumentResolvers; | ||||||
|  | @ -156,6 +162,9 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter | ||||||
| 	@Nullable | 	@Nullable | ||||||
| 	private WebBindingInitializer webBindingInitializer; | 	private WebBindingInitializer webBindingInitializer; | ||||||
| 
 | 
 | ||||||
|  | 	@Nullable | ||||||
|  | 	private MethodValidator methodValidator; | ||||||
|  | 
 | ||||||
| 	private AsyncTaskExecutor taskExecutor = new SimpleAsyncTaskExecutor("MvcAsync"); | 	private AsyncTaskExecutor taskExecutor = new SimpleAsyncTaskExecutor("MvcAsync"); | ||||||
| 
 | 
 | ||||||
| 	@Nullable | 	@Nullable | ||||||
|  | @ -559,6 +568,9 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter | ||||||
| 			List<HandlerMethodReturnValueHandler> handlers = getDefaultReturnValueHandlers(); | 			List<HandlerMethodReturnValueHandler> handlers = getDefaultReturnValueHandlers(); | ||||||
| 			this.returnValueHandlers = new HandlerMethodReturnValueHandlerComposite().addHandlers(handlers); | 			this.returnValueHandlers = new HandlerMethodReturnValueHandlerComposite().addHandlers(handlers); | ||||||
| 		} | 		} | ||||||
|  | 		if (BEAN_VALIDATION_PRESENT) { | ||||||
|  | 			this.methodValidator = HandlerMethodValidator.from(this.webBindingInitializer, this.parameterNameDiscoverer); | ||||||
|  | 		} | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	private void initMessageConverters() { | 	private void initMessageConverters() { | ||||||
|  | @ -855,6 +867,7 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter | ||||||
| 		} | 		} | ||||||
| 		invocableMethod.setDataBinderFactory(binderFactory); | 		invocableMethod.setDataBinderFactory(binderFactory); | ||||||
| 		invocableMethod.setParameterNameDiscoverer(this.parameterNameDiscoverer); | 		invocableMethod.setParameterNameDiscoverer(this.parameterNameDiscoverer); | ||||||
|  | 		invocableMethod.setMethodValidator(this.methodValidator); | ||||||
| 
 | 
 | ||||||
| 		ModelAndViewContainer mavContainer = new ModelAndViewContainer(); | 		ModelAndViewContainer mavContainer = new ModelAndViewContainer(); | ||||||
| 		mavContainer.addAllAttributes(RequestContextUtils.getInputFlashMap(request)); | 		mavContainer.addAllAttributes(RequestContextUtils.getInputFlashMap(request)); | ||||||
|  | @ -955,7 +968,9 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter | ||||||
| 			Object bean = handlerMethod.getBean(); | 			Object bean = handlerMethod.getBean(); | ||||||
| 			initBinderMethods.add(createInitBinderMethod(bean, method)); | 			initBinderMethods.add(createInitBinderMethod(bean, method)); | ||||||
| 		} | 		} | ||||||
| 		return createDataBinderFactory(initBinderMethods); | 		DefaultDataBinderFactory factory = createDataBinderFactory(initBinderMethods); | ||||||
|  | 		factory.setMethodValidationApplicable(this.methodValidator != null && handlerMethod.shouldValidateArguments()); | ||||||
|  | 		return factory; | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	private InvocableHandlerMethod createInitBinderMethod(Object bean, Method method) { | 	private InvocableHandlerMethod createInitBinderMethod(Object bean, Method method) { | ||||||
|  |  | ||||||
|  | @ -1,5 +1,5 @@ | ||||||
| /* | /* | ||||||
|  * Copyright 2002-2022 the original author or authors. |  * Copyright 2002-2023 the original author or authors. | ||||||
|  * |  * | ||||||
|  * Licensed under the Apache License, Version 2.0 (the "License"); |  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||||
|  * you may not use this file except in compliance with the License. |  * you may not use this file except in compliance with the License. | ||||||
|  | @ -139,7 +139,7 @@ public class RequestPartMethodArgumentResolver extends AbstractMessageConverterM | ||||||
| 				HttpInputMessage inputMessage = new RequestPartServletServerHttpRequest(servletRequest, name); | 				HttpInputMessage inputMessage = new RequestPartServletServerHttpRequest(servletRequest, name); | ||||||
| 				arg = readWithMessageConverters(inputMessage, parameter, parameter.getNestedGenericParameterType()); | 				arg = readWithMessageConverters(inputMessage, parameter, parameter.getNestedGenericParameterType()); | ||||||
| 				if (binderFactory != null) { | 				if (binderFactory != null) { | ||||||
| 					WebDataBinder binder = binderFactory.createBinder(request, arg, name); | 					WebDataBinder binder = binderFactory.createBinder(request, arg, name, parameter); | ||||||
| 					if (arg != null) { | 					if (arg != null) { | ||||||
| 						validateIfApplicable(binder, parameter); | 						validateIfApplicable(binder, parameter); | ||||||
| 						if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) { | 						if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) { | ||||||
|  |  | ||||||
|  | @ -134,7 +134,7 @@ public class RequestResponseBodyMethodProcessor extends AbstractMessageConverter | ||||||
| 		String name = Conventions.getVariableNameForParameter(parameter); | 		String name = Conventions.getVariableNameForParameter(parameter); | ||||||
| 
 | 
 | ||||||
| 		if (binderFactory != null) { | 		if (binderFactory != null) { | ||||||
| 			WebDataBinder binder = binderFactory.createBinder(webRequest, arg, name); | 			WebDataBinder binder = binderFactory.createBinder(webRequest, arg, name, parameter); | ||||||
| 			if (arg != null) { | 			if (arg != null) { | ||||||
| 				validateIfApplicable(binder, parameter); | 				validateIfApplicable(binder, parameter); | ||||||
| 				if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) { | 				if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) { | ||||||
|  |  | ||||||
|  | @ -0,0 +1,439 @@ | ||||||
|  | /* | ||||||
|  |  * Copyright 2002-2023 the original author or authors. | ||||||
|  |  * | ||||||
|  |  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||||
|  |  * you may not use this file except in compliance with the License. | ||||||
|  |  * You may obtain a copy of the License at | ||||||
|  |  * | ||||||
|  |  *      https://www.apache.org/licenses/LICENSE-2.0 | ||||||
|  |  * | ||||||
|  |  * Unless required by applicable law or agreed to in writing, software | ||||||
|  |  * distributed under the License is distributed on an "AS IS" BASIS, | ||||||
|  |  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||||
|  |  * See the License for the specific language governing permissions and | ||||||
|  |  * limitations under the License. | ||||||
|  |  */ | ||||||
|  | 
 | ||||||
|  | package org.springframework.web.servlet.mvc.method.annotation; | ||||||
|  | 
 | ||||||
|  | import java.lang.reflect.Method; | ||||||
|  | import java.util.Collections; | ||||||
|  | import java.util.HashMap; | ||||||
|  | import java.util.List; | ||||||
|  | import java.util.Set; | ||||||
|  | import java.util.function.Consumer; | ||||||
|  | 
 | ||||||
|  | import jakarta.validation.ConstraintViolation; | ||||||
|  | import jakarta.validation.Valid; | ||||||
|  | import jakarta.validation.constraints.Size; | ||||||
|  | import jakarta.validation.executable.ExecutableValidator; | ||||||
|  | import jakarta.validation.metadata.BeanDescriptor; | ||||||
|  | import org.junit.jupiter.api.BeforeEach; | ||||||
|  | import org.junit.jupiter.api.Test; | ||||||
|  | 
 | ||||||
|  | import org.springframework.context.MessageSourceResolvable; | ||||||
|  | import org.springframework.http.MediaType; | ||||||
|  | import org.springframework.util.Assert; | ||||||
|  | import org.springframework.validation.Errors; | ||||||
|  | import org.springframework.validation.FieldError; | ||||||
|  | import org.springframework.validation.Validator; | ||||||
|  | import org.springframework.validation.annotation.Validated; | ||||||
|  | import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean; | ||||||
|  | import org.springframework.validation.beanvalidation.MethodValidationException; | ||||||
|  | import org.springframework.validation.beanvalidation.ParameterValidationResult; | ||||||
|  | import org.springframework.validation.beanvalidation.SpringValidatorAdapter; | ||||||
|  | import org.springframework.web.bind.MethodArgumentNotValidException; | ||||||
|  | import org.springframework.web.bind.WebDataBinder; | ||||||
|  | import org.springframework.web.bind.annotation.InitBinder; | ||||||
|  | import org.springframework.web.bind.annotation.ModelAttribute; | ||||||
|  | import org.springframework.web.bind.annotation.RequestHeader; | ||||||
|  | import org.springframework.web.bind.annotation.RestController; | ||||||
|  | import org.springframework.web.bind.support.ConfigurableWebBindingInitializer; | ||||||
|  | import org.springframework.web.context.support.GenericWebApplicationContext; | ||||||
|  | import org.springframework.web.method.HandlerMethod; | ||||||
|  | import org.springframework.web.servlet.HandlerMapping; | ||||||
|  | import org.springframework.web.testfixture.method.ResolvableMethod; | ||||||
|  | import org.springframework.web.testfixture.servlet.MockHttpServletRequest; | ||||||
|  | import org.springframework.web.testfixture.servlet.MockHttpServletResponse; | ||||||
|  | 
 | ||||||
|  | import static org.assertj.core.api.Assertions.assertThat; | ||||||
|  | import static org.assertj.core.api.Assertions.catchThrowableOfType; | ||||||
|  | import static org.mockito.Mockito.mock; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Method validation tests for Spring MVC controller methods. | ||||||
|  |  * <p>When adding tests, consider the following others: | ||||||
|  |  * <ul> | ||||||
|  |  * <li>{@code HandlerMethodTests} -- detection if methods need validation | ||||||
|  |  * <li>{@code MethodValidationAdapterTests} -- method validation independent of Spring MVC | ||||||
|  |  * <li>{@code MethodValidationProxyTests} -- method validation with proxy scenarios | ||||||
|  |  * </ul> | ||||||
|  |  * @author Rossen Stoyanchev | ||||||
|  |  */ | ||||||
|  | public class MethodValidationTests { | ||||||
|  | 
 | ||||||
|  | 	private static final Person mockPerson = mock(Person.class); | ||||||
|  | 
 | ||||||
|  | 	private static final Errors mockErrors = mock(Errors.class); | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 	private final MockHttpServletRequest request = new MockHttpServletRequest(); | ||||||
|  | 
 | ||||||
|  | 	private final MockHttpServletResponse response = new MockHttpServletResponse(); | ||||||
|  | 
 | ||||||
|  | 	private RequestMappingHandlerAdapter handlerAdapter; | ||||||
|  | 
 | ||||||
|  | 	private InvocationCountingValidator jakartaValidator; | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 	@BeforeEach | ||||||
|  | 	void setup() throws Exception { | ||||||
|  | 		LocalValidatorFactoryBean validatorBean = new LocalValidatorFactoryBean(); | ||||||
|  | 		validatorBean.afterPropertiesSet(); | ||||||
|  | 		this.jakartaValidator = new InvocationCountingValidator(validatorBean); | ||||||
|  | 
 | ||||||
|  | 		this.handlerAdapter = initHandlerAdapter(this.jakartaValidator); | ||||||
|  | 
 | ||||||
|  | 		this.request.setMethod("POST"); | ||||||
|  | 		this.request.setContentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE); | ||||||
|  | 		this.request.setAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, new HashMap<String, String>(0)); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	private static RequestMappingHandlerAdapter initHandlerAdapter(Validator validator) { | ||||||
|  | 		ConfigurableWebBindingInitializer bindingInitializer = new ConfigurableWebBindingInitializer(); | ||||||
|  | 		bindingInitializer.setValidator(validator); | ||||||
|  | 
 | ||||||
|  | 		GenericWebApplicationContext context = new GenericWebApplicationContext(); | ||||||
|  | 		context.refresh(); | ||||||
|  | 
 | ||||||
|  | 		RequestMappingHandlerAdapter handlerAdapter = new RequestMappingHandlerAdapter(); | ||||||
|  | 		handlerAdapter.setWebBindingInitializer(bindingInitializer); | ||||||
|  | 		handlerAdapter.setApplicationContext(context); | ||||||
|  | 		handlerAdapter.setBeanFactory(context.getBeanFactory()); | ||||||
|  | 		handlerAdapter.afterPropertiesSet(); | ||||||
|  | 		return handlerAdapter; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 	@Test | ||||||
|  | 	void modelAttribute() { | ||||||
|  | 		HandlerMethod hm = handlerMethod(new ValidController(), c -> c.handle(mockPerson)); | ||||||
|  | 		this.request.addParameter("name", "name=Faustino1234"); | ||||||
|  | 
 | ||||||
|  | 		MethodArgumentNotValidException ex = catchThrowableOfType( | ||||||
|  | 				() -> this.handlerAdapter.handle(this.request, this.response, hm), | ||||||
|  | 				MethodArgumentNotValidException.class); | ||||||
|  | 
 | ||||||
|  | 		assertThat(this.jakartaValidator.getValidationCount()).isEqualTo(1); | ||||||
|  | 		assertThat(this.jakartaValidator.getMethodValidationCount()).as("Method validation unexpected").isEqualTo(0); | ||||||
|  | 
 | ||||||
|  | 		assertBeanResult(ex.getBindingResult(), "student", Collections.singletonList( | ||||||
|  | 				""" | ||||||
|  | 			Field error in object 'student' on field 'name': rejected value [name=Faustino1234]; \ | ||||||
|  | 			codes [Size.student.name,Size.name,Size.java.lang.String,Size]; \ | ||||||
|  | 			arguments [org.springframework.context.support.DefaultMessageSourceResolvable: \ | ||||||
|  | 			codes [student.name,name]; arguments []; default message [name],10,1]; \ | ||||||
|  | 			default message [size must be between 1 and 10]""")); | ||||||
|  | 
 | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	@Test | ||||||
|  | 	void modelAttributeWithBindingResult() throws Exception { | ||||||
|  | 		HandlerMethod hm = handlerMethod(new ValidController(), c -> c.handle(mockPerson, mockErrors)); | ||||||
|  | 		this.request.addParameter("name", "name=Faustino1234"); | ||||||
|  | 
 | ||||||
|  | 		this.handlerAdapter.handle(this.request, this.response, hm); | ||||||
|  | 
 | ||||||
|  | 		assertThat(this.jakartaValidator.getValidationCount()).isEqualTo(1); | ||||||
|  | 		assertThat(this.jakartaValidator.getMethodValidationCount()).as("Method validation unexpected").isEqualTo(0); | ||||||
|  | 
 | ||||||
|  | 		assertThat(response.getContentAsString()).isEqualTo( | ||||||
|  | 				""" | ||||||
|  | 			org.springframework.validation.BeanPropertyBindingResult: 1 errors | ||||||
|  | 			Field error in object 'student' on field 'name': rejected value [name=Faustino1234]; \ | ||||||
|  | 			codes [Size.student.name,Size.name,Size.java.lang.String,Size]; \ | ||||||
|  | 			arguments [org.springframework.context.support.DefaultMessageSourceResolvable: \ | ||||||
|  | 			codes [student.name,name]; arguments []; default message [name],10,1]; \ | ||||||
|  | 			default message [size must be between 1 and 10]"""); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	@Test | ||||||
|  | 	void modelAttributeWithBindingResultAndRequestHeader() { | ||||||
|  | 		HandlerMethod hm = handlerMethod(new ValidController(), c -> c.handle(mockPerson, mockErrors, "")); | ||||||
|  | 		this.request.addParameter("name", "name=Faustino1234"); | ||||||
|  | 		this.request.addHeader("myHeader", "123"); | ||||||
|  | 
 | ||||||
|  | 		MethodValidationException ex = catchThrowableOfType( | ||||||
|  | 				() -> this.handlerAdapter.handle(this.request, this.response, hm), | ||||||
|  | 				MethodValidationException.class); | ||||||
|  | 
 | ||||||
|  | 		assertThat(this.jakartaValidator.getValidationCount()).isEqualTo(1); | ||||||
|  | 		assertThat(this.jakartaValidator.getMethodValidationCount()).isEqualTo(1); | ||||||
|  | 
 | ||||||
|  | 		assertThat(ex.getConstraintViolations()).hasSize(2); | ||||||
|  | 		assertThat(ex.getAllValidationResults()).hasSize(2); | ||||||
|  | 
 | ||||||
|  | 		assertBeanResult(ex.getBeanResults().get(0), "student", Collections.singletonList( | ||||||
|  | 				""" | ||||||
|  | 			Field error in object 'student' on field 'name': rejected value [name=Faustino1234]; \ | ||||||
|  | 			codes [Size.student.name,Size.name,Size.java.lang.String,Size]; \ | ||||||
|  | 			arguments [org.springframework.context.support.DefaultMessageSourceResolvable: \ | ||||||
|  | 			codes [student.name,name]; arguments []; default message [name],10,1]; \ | ||||||
|  | 			default message [size must be between 1 and 10]""")); | ||||||
|  | 
 | ||||||
|  | 		assertValueResult(ex.getValueResults().get(0), 2, "123", Collections.singletonList( | ||||||
|  | 				""" | ||||||
|  | 			org.springframework.context.support.DefaultMessageSourceResolvable: \ | ||||||
|  | 			codes [Size.validController#handle.myHeader,Size.myHeader,Size.java.lang.String,Size]; \ | ||||||
|  | 			arguments [org.springframework.context.support.DefaultMessageSourceResolvable: \ | ||||||
|  | 			codes [validController#handle.myHeader,myHeader]; arguments []; default message [myHeader],10,5]; \ | ||||||
|  | 			default message [size must be between 5 and 10]""" | ||||||
|  | 		)); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	@Test | ||||||
|  | 	void validatedWithMethodValidation() throws Exception { | ||||||
|  | 
 | ||||||
|  | 		// 1 for @Validated argument validation + 1 for method validation of @RequestHeader | ||||||
|  | 		this.jakartaValidator.setMaxInvocationsExpected(2); | ||||||
|  | 
 | ||||||
|  | 		HandlerMethod hm = handlerMethod(new ValidController(), c -> c.handleValidated(mockPerson, mockErrors, "")); | ||||||
|  | 		this.request.addParameter("name", "name=Faustino1234"); | ||||||
|  | 		this.request.addHeader("myHeader", "12345"); | ||||||
|  | 
 | ||||||
|  | 		this.handlerAdapter.handle(this.request, this.response, hm); | ||||||
|  | 
 | ||||||
|  | 		assertThat(jakartaValidator.getValidationCount()).isEqualTo(2); | ||||||
|  | 		assertThat(jakartaValidator.getMethodValidationCount()).isEqualTo(1); | ||||||
|  | 
 | ||||||
|  | 		assertThat(response.getContentAsString()).isEqualTo( | ||||||
|  | 				""" | ||||||
|  | 			org.springframework.validation.BeanPropertyBindingResult: 1 errors | ||||||
|  | 			Field error in object 'person' on field 'name': rejected value [name=Faustino1234]; \ | ||||||
|  | 			codes [Size.person.name,Size.name,Size.java.lang.String,Size]; \ | ||||||
|  | 			arguments [org.springframework.context.support.DefaultMessageSourceResolvable: \ | ||||||
|  | 			codes [person.name,name]; arguments []; default message [name],10,1]; \ | ||||||
|  | 			default message [size must be between 1 and 10]"""); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	@Test | ||||||
|  | 	void jakartaAndSpringValidator() throws Exception { | ||||||
|  | 		HandlerMethod hm = handlerMethod(new InitBinderController(), ibc -> ibc.handle(mockPerson, mockErrors, "")); | ||||||
|  | 		this.request.addParameter("name", "name=Faustino1234"); | ||||||
|  | 		this.request.addHeader("myHeader", "12345"); | ||||||
|  | 
 | ||||||
|  | 		this.handlerAdapter.handle(this.request, this.response, hm); | ||||||
|  | 
 | ||||||
|  | 		assertThat(jakartaValidator.getValidationCount()).isEqualTo(1); | ||||||
|  | 		assertThat(jakartaValidator.getMethodValidationCount()).isEqualTo(1); | ||||||
|  | 
 | ||||||
|  | 		assertThat(response.getContentAsString()).isEqualTo( | ||||||
|  | 				""" | ||||||
|  | 			org.springframework.validation.BeanPropertyBindingResult: 2 errors | ||||||
|  | 			Field error in object 'person' on field 'name': rejected value [name=Faustino1234]; \ | ||||||
|  | 			codes [TOO_LONG.person.name,TOO_LONG.name,TOO_LONG.java.lang.String,TOO_LONG]; \ | ||||||
|  | 			arguments []; default message [length must be 10 or under] | ||||||
|  | 			Field error in object 'person' on field 'name': rejected value [name=Faustino1234]; \ | ||||||
|  | 			codes [Size.person.name,Size.name,Size.java.lang.String,Size]; \ | ||||||
|  | 			arguments [org.springframework.context.support.DefaultMessageSourceResolvable: \ | ||||||
|  | 			codes [person.name,name]; arguments []; default message [name],10,1]; \ | ||||||
|  | 			default message [size must be between 1 and 10]"""); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 	@Test | ||||||
|  | 	void springValidator() throws Exception { | ||||||
|  | 		HandlerMethod hm = handlerMethod(new ValidController(), c -> c.handle(mockPerson, mockErrors)); | ||||||
|  | 		this.request.addParameter("name", "name=Faustino1234"); | ||||||
|  | 
 | ||||||
|  | 		RequestMappingHandlerAdapter springValidatorHandlerAdapter = initHandlerAdapter(new PersonValidator()); | ||||||
|  | 		springValidatorHandlerAdapter.handle(this.request, this.response, hm); | ||||||
|  | 
 | ||||||
|  | 		assertThat(response.getContentAsString()).isEqualTo( | ||||||
|  | 				""" | ||||||
|  | 			org.springframework.validation.BeanPropertyBindingResult: 1 errors | ||||||
|  | 			Field error in object 'student' on field 'name': rejected value [name=Faustino1234]; \ | ||||||
|  | 			codes [TOO_LONG.student.name,TOO_LONG.name,TOO_LONG.java.lang.String,TOO_LONG]; \ | ||||||
|  | 			arguments []; default message [length must be 10 or under]"""); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 	@SuppressWarnings("unchecked") | ||||||
|  | 	private static <T> HandlerMethod handlerMethod(T controller, Consumer<T> mockCallConsumer) { | ||||||
|  | 		Assert.isTrue(!(controller instanceof Class<?>), "Expected controller instance"); | ||||||
|  | 		Method method = ResolvableMethod.on((Class<T>) controller.getClass()).mockCall(mockCallConsumer).method(); | ||||||
|  | 		return new HandlerMethod(controller, method); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	@SuppressWarnings("SameParameterValue") | ||||||
|  | 	private static void assertBeanResult(Errors errors, String objectName, List<String> fieldErrors) { | ||||||
|  | 		assertThat(errors.getObjectName()).isEqualTo(objectName); | ||||||
|  | 		assertThat(errors.getFieldErrors()) | ||||||
|  | 				.extracting(FieldError::toString) | ||||||
|  | 				.containsExactlyInAnyOrderElementsOf(fieldErrors); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	@SuppressWarnings("SameParameterValue") | ||||||
|  | 	private static void assertValueResult( | ||||||
|  | 			ParameterValidationResult result, int parameterIndex, Object argument, List<String> errors) { | ||||||
|  | 
 | ||||||
|  | 		assertThat(result.getMethodParameter().getParameterIndex()).isEqualTo(parameterIndex); | ||||||
|  | 		assertThat(result.getArgument()).isEqualTo(argument); | ||||||
|  | 		assertThat(result.getResolvableErrors()) | ||||||
|  | 				.extracting(MessageSourceResolvable::toString) | ||||||
|  | 				.containsExactlyInAnyOrderElementsOf(errors); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 	@SuppressWarnings("unused") | ||||||
|  | 	private record Person(@Size(min = 1, max = 10) String name) { | ||||||
|  | 
 | ||||||
|  | 		@Override | ||||||
|  | 		public String name() { | ||||||
|  | 			return this.name; | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 	@SuppressWarnings({"unused", "SameParameterValue", "UnusedReturnValue"}) | ||||||
|  | 	@RestController | ||||||
|  | 	static class ValidController { | ||||||
|  | 
 | ||||||
|  | 		void handle(@Valid @ModelAttribute("student") Person person) { | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		String handle(@Valid @ModelAttribute("student") Person person, Errors errors) { | ||||||
|  | 			return errors.toString(); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		void handle(@Valid @ModelAttribute("student") Person person, Errors errors, | ||||||
|  | 				@RequestHeader @Size(min = 5, max = 10) String myHeader) { | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		String handleValidated(@Validated Person person, Errors errors, | ||||||
|  | 				@RequestHeader @Size(min = 5, max = 10) String myHeader) { | ||||||
|  | 
 | ||||||
|  | 			return errors.toString(); | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 	@SuppressWarnings({"unused", "UnusedReturnValue", "SameParameterValue"}) | ||||||
|  | 	@RestController | ||||||
|  | 	static class InitBinderController { | ||||||
|  | 
 | ||||||
|  | 		@InitBinder | ||||||
|  | 		void initBinder(WebDataBinder dataBinder) { | ||||||
|  | 			dataBinder.addValidators(new PersonValidator()); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		String handle(@Valid Person person, Errors errors, @RequestHeader @Size(min = 5, max = 10) String myHeader) { | ||||||
|  | 			return errors.toString(); | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 	private static class PersonValidator implements Validator { | ||||||
|  | 
 | ||||||
|  | 		@Override | ||||||
|  | 		public boolean supports(Class<?> clazz) { | ||||||
|  | 			return (clazz == Person.class); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		@Override | ||||||
|  | 		public void validate(Object target, Errors errors) { | ||||||
|  | 			Person person = (Person) target; | ||||||
|  | 			if (person.name().length() > 10) { | ||||||
|  | 				errors.rejectValue("name", "TOO_LONG", "length must be 10 or under"); | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 	/** | ||||||
|  | 	 * Intercept and count number of method validation calls. | ||||||
|  | 	 */ | ||||||
|  | 	private static class InvocationCountingValidator implements jakarta.validation.Validator, Validator { | ||||||
|  | 
 | ||||||
|  | 		private final SpringValidatorAdapter delegate; | ||||||
|  | 
 | ||||||
|  | 		private int maxInvocationsExpected = 1; | ||||||
|  | 
 | ||||||
|  | 		private int validationCount; | ||||||
|  | 
 | ||||||
|  | 		private int methodValidationCount; | ||||||
|  | 
 | ||||||
|  | 		/** | ||||||
|  | 		 * Constructor with maxCount=1. | ||||||
|  | 		 */ | ||||||
|  | 		private InvocationCountingValidator(SpringValidatorAdapter delegate) { | ||||||
|  | 			this.delegate = delegate; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		public void setMaxInvocationsExpected(int maxInvocationsExpected) { | ||||||
|  | 			this.maxInvocationsExpected = maxInvocationsExpected; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		/** | ||||||
|  | 		 * Total number of times Bean Validation was invoked. | ||||||
|  | 		 */ | ||||||
|  | 		public int getValidationCount() { | ||||||
|  | 			return this.validationCount; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		/** | ||||||
|  | 		 * Number of times method level Bean Validation was invoked. | ||||||
|  | 		 */ | ||||||
|  | 		public int getMethodValidationCount() { | ||||||
|  | 			return this.methodValidationCount; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		@Override | ||||||
|  | 		public <T> Set<ConstraintViolation<T>> validate(T object, Class<?>... groups) { | ||||||
|  | 			throw new UnsupportedOperationException(); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		@Override | ||||||
|  | 		public <T> Set<ConstraintViolation<T>> validateProperty(T object, String propertyName, Class<?>... groups) { | ||||||
|  | 			throw new UnsupportedOperationException(); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		@Override | ||||||
|  | 		public <T> Set<ConstraintViolation<T>> validateValue(Class<T> beanType, String propertyName, Object value, Class<?>... groups) { | ||||||
|  | 			throw new UnsupportedOperationException(); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		@Override | ||||||
|  | 		public BeanDescriptor getConstraintsForClass(Class<?> clazz) { | ||||||
|  | 			throw new UnsupportedOperationException(); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		@Override | ||||||
|  | 		public <T> T unwrap(Class<T> type) { | ||||||
|  | 			throw new UnsupportedOperationException(); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		@Override | ||||||
|  | 		public ExecutableValidator forExecutables() { | ||||||
|  | 			this.methodValidationCount++; | ||||||
|  | 			assertCountAndIncrement(); | ||||||
|  | 			return this.delegate.forExecutables(); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		@Override | ||||||
|  | 		public boolean supports(Class<?> clazz) { | ||||||
|  | 			return true; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		@Override | ||||||
|  | 		public void validate(Object target, Errors errors) { | ||||||
|  | 			assertCountAndIncrement(); | ||||||
|  | 			this.delegate.validate(target, errors); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		private void assertCountAndIncrement() { | ||||||
|  | 			assertThat(this.validationCount++).as("Too many calls to Bean Validation").isLessThan(this.maxInvocationsExpected); | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | } | ||||||
		Loading…
	
		Reference in New Issue