Create ParameterErrors for type level constraint
Closes gh-34105
This commit is contained in:
		
							parent
							
								
									d5bebd5ced
								
							
						
					
					
						commit
						59ed4686c5
					
				|  | @ -367,7 +367,7 @@ public class MethodValidationAdapter implements MethodValidator { | |||
| 					container = null; | ||||
| 				} | ||||
| 
 | ||||
| 				if (node.getKind().equals(ElementKind.PROPERTY)) { | ||||
| 				if (node.getKind().equals(ElementKind.PROPERTY) || node.getKind().equals(ElementKind.BEAN)) { | ||||
| 					nestedViolations | ||||
| 							.computeIfAbsent(parameterNode, k -> | ||||
| 									new ParamErrorsBuilder(parameter, value, container, index, key)) | ||||
|  |  | |||
|  | @ -16,19 +16,32 @@ | |||
| 
 | ||||
| package org.springframework.web.servlet.mvc.method.annotation; | ||||
| 
 | ||||
| import java.lang.annotation.ElementType; | ||||
| import java.lang.annotation.Retention; | ||||
| import java.lang.annotation.RetentionPolicy; | ||||
| import java.lang.annotation.Target; | ||||
| import java.lang.reflect.Method; | ||||
| import java.util.Arrays; | ||||
| import java.util.HashMap; | ||||
| import java.util.LinkedHashMap; | ||||
| import java.util.List; | ||||
| import java.util.Locale; | ||||
| import java.util.Map; | ||||
| import java.util.Set; | ||||
| import java.util.function.Consumer; | ||||
| 
 | ||||
| import com.fasterxml.jackson.annotation.JsonProperty; | ||||
| import jakarta.validation.Constraint; | ||||
| import jakarta.validation.ConstraintValidator; | ||||
| import jakarta.validation.ConstraintValidatorContext; | ||||
| import jakarta.validation.ConstraintValidatorFactory; | ||||
| import jakarta.validation.ConstraintViolation; | ||||
| import jakarta.validation.Payload; | ||||
| import jakarta.validation.Valid; | ||||
| import jakarta.validation.constraints.Size; | ||||
| import jakarta.validation.executable.ExecutableValidator; | ||||
| import jakarta.validation.metadata.BeanDescriptor; | ||||
| import org.hibernate.validator.internal.engine.constraintvalidation.ConstraintValidatorFactoryImpl; | ||||
| import org.junit.jupiter.api.AfterEach; | ||||
| import org.junit.jupiter.api.BeforeEach; | ||||
| import org.junit.jupiter.api.Test; | ||||
|  | @ -39,11 +52,12 @@ import org.springframework.http.MediaType; | |||
| import org.springframework.http.converter.StringHttpMessageConverter; | ||||
| import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; | ||||
| import org.springframework.validation.Errors; | ||||
| import org.springframework.validation.FieldError; | ||||
| import org.springframework.validation.ObjectError; | ||||
| import org.springframework.validation.Validator; | ||||
| import org.springframework.validation.annotation.Validated; | ||||
| import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean; | ||||
| import org.springframework.validation.beanvalidation.SpringValidatorAdapter; | ||||
| import org.springframework.validation.method.ParameterErrors; | ||||
| import org.springframework.validation.method.ParameterValidationResult; | ||||
| import org.springframework.web.bind.MethodArgumentNotValidException; | ||||
| import org.springframework.web.bind.WebDataBinder; | ||||
|  | @ -91,13 +105,17 @@ class MethodValidationTests { | |||
| 
 | ||||
| 	private InvocationCountingValidator jakartaValidator; | ||||
| 
 | ||||
| 	private final TestConstraintValidator testConstraintValidator = new TestConstraintValidator(); | ||||
| 
 | ||||
| 
 | ||||
| 	@BeforeEach | ||||
| 	void setup() throws Exception { | ||||
| 		LocaleContextHolder.setDefaultLocale(Locale.UK); | ||||
| 
 | ||||
| 		LocalValidatorFactoryBean validatorBean = new LocalValidatorFactoryBean(); | ||||
| 		validatorBean.setConstraintValidatorFactory(new TestConstraintValidatorFactory(this.testConstraintValidator)); | ||||
| 		validatorBean.afterPropertiesSet(); | ||||
| 
 | ||||
| 		this.jakartaValidator = new InvocationCountingValidator(validatorBean); | ||||
| 
 | ||||
| 		this.handlerAdapter = initHandlerAdapter(this.jakartaValidator); | ||||
|  | @ -296,6 +314,30 @@ class MethodValidationTests { | |||
| 			arguments []; default message [length must be 10 or under]"""); | ||||
| 	} | ||||
| 
 | ||||
| 	@Test // gh-34105 | ||||
| 	void typeConstraint() { | ||||
| 		this.testConstraintValidator.setReject(true); | ||||
| 
 | ||||
| 		HandlerMethod hm = handlerMethod(new ValidController(), c -> c.handle(mockPerson, "")); | ||||
| 		this.request.addHeader("header", "12345"); | ||||
| 		this.request.setContentType("application/json"); | ||||
| 		this.request.setContent("{\"name\":\"Faustino\"}".getBytes(UTF_8)); | ||||
| 
 | ||||
| 		HandlerMethodValidationException ex = catchThrowableOfType(HandlerMethodValidationException.class, | ||||
| 				() -> this.handlerAdapter.handle(this.request, this.response, hm)); | ||||
| 
 | ||||
| 		List<ParameterValidationResult> results = ex.getParameterValidationResults(); | ||||
| 		assertThat(results).hasSize(1); | ||||
| 		ParameterValidationResult result = results.get(0); | ||||
| 		assertThat(result).isInstanceOf(ParameterErrors.class); | ||||
| 
 | ||||
| 		assertBeanResult((Errors) result, "person", List.of(""" | ||||
| 			Error in object 'person': codes [TestConstraint.person,TestConstraint]; \ | ||||
| 			arguments [org.springframework.context.support.DefaultMessageSourceResolvable: \ | ||||
| 			codes [person]; arguments []; default message []]; default message [Fail message]\ | ||||
| 			""" | ||||
| 		)); | ||||
| 	} | ||||
| 
 | ||||
| 	@SuppressWarnings("unchecked") | ||||
| 	private static <T> HandlerMethod handlerMethod(T controller, Consumer<T> mockCallConsumer) { | ||||
|  | @ -306,8 +348,8 @@ class MethodValidationTests { | |||
| 	@SuppressWarnings("SameParameterValue") | ||||
| 	private static void assertBeanResult(Errors errors, String objectName, List<String> fieldErrors) { | ||||
| 		assertThat(errors.getObjectName()).isEqualTo(objectName); | ||||
| 		assertThat(errors.getFieldErrors()) | ||||
| 				.extracting(FieldError::toString) | ||||
| 		assertThat(errors.getAllErrors()) | ||||
| 				.extracting(ObjectError::toString) | ||||
| 				.containsExactlyInAnyOrderElementsOf(fieldErrors); | ||||
| 	} | ||||
| 
 | ||||
|  | @ -323,6 +365,7 @@ class MethodValidationTests { | |||
| 	} | ||||
| 
 | ||||
| 
 | ||||
| 	@TestConstraint | ||||
| 	@SuppressWarnings("unused") | ||||
| 	private record Person(@Size(min = 1, max = 10) @JsonProperty("name") String name) { | ||||
| 
 | ||||
|  | @ -356,6 +399,9 @@ class MethodValidationTests { | |||
| 
 | ||||
| 		void handle(@Valid @RequestBody List<Person> persons) { | ||||
| 		} | ||||
| 
 | ||||
| 		void handle(@Valid @RequestBody Person person, @RequestHeader @Size(min=4) String header) { | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 
 | ||||
|  | @ -477,4 +523,57 @@ class MethodValidationTests { | |||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| 	@Constraint(validatedBy = TestConstraintValidator.class) | ||||
| 	@Target({ElementType.TYPE}) | ||||
| 	@Retention(RetentionPolicy.RUNTIME) | ||||
| 	public @interface TestConstraint { | ||||
| 
 | ||||
| 		String message() default "Fail message"; | ||||
| 
 | ||||
| 		Class<?>[] groups() default {}; | ||||
| 
 | ||||
| 		Class<? extends Payload>[] payload() default {}; | ||||
| 	} | ||||
| 
 | ||||
| 
 | ||||
| 	private static class TestConstraintValidator implements ConstraintValidator<TestConstraint, Person> { | ||||
| 
 | ||||
| 		private boolean reject; | ||||
| 
 | ||||
| 		public void setReject(boolean reject) { | ||||
| 			this.reject = reject; | ||||
| 		} | ||||
| 
 | ||||
| 		@Override | ||||
| 		public boolean isValid(Person person, ConstraintValidatorContext context) { | ||||
| 			return !this.reject; | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 
 | ||||
| 	private static class TestConstraintValidatorFactory implements ConstraintValidatorFactory { | ||||
| 
 | ||||
| 		private final Map<Class<?>, ConstraintValidator<?, ?>> validators; | ||||
| 
 | ||||
| 		private final ConstraintValidatorFactory delegate = new ConstraintValidatorFactoryImpl(); | ||||
| 
 | ||||
| 		private TestConstraintValidatorFactory(ConstraintValidator<?, ?>... validators) { | ||||
| 			this.validators = new LinkedHashMap<>(validators.length); | ||||
| 			Arrays.stream(validators).forEach(validator -> this.validators.put(validator.getClass(), validator)); | ||||
| 		} | ||||
| 
 | ||||
| 		@SuppressWarnings("unchecked") | ||||
| 		@Override | ||||
| 		public <T extends ConstraintValidator<?, ?>> T getInstance(Class<T> aClass) { | ||||
| 			ConstraintValidator<?, ?> validator = this.validators.get(aClass); | ||||
| 			return (validator != null ? (T) validator : this.delegate.getInstance(aClass)); | ||||
| 		} | ||||
| 
 | ||||
| 		@Override | ||||
| 		public void releaseInstance(ConstraintValidator<?, ?> constraintValidator) { | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| } | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue