From 59ed4686c52992e45ac4daabac1c576257c6f190 Mon Sep 17 00:00:00 2001 From: rstoyanchev Date: Thu, 19 Dec 2024 14:45:44 +0000 Subject: [PATCH] Create ParameterErrors for type level constraint Closes gh-34105 --- .../MethodValidationAdapter.java | 2 +- .../annotation/MethodValidationTests.java | 105 +++++++++++++++++- 2 files changed, 103 insertions(+), 4 deletions(-) diff --git a/spring-context/src/main/java/org/springframework/validation/beanvalidation/MethodValidationAdapter.java b/spring-context/src/main/java/org/springframework/validation/beanvalidation/MethodValidationAdapter.java index f65fb56b7f4..4304848c53d 100644 --- a/spring-context/src/main/java/org/springframework/validation/beanvalidation/MethodValidationAdapter.java +++ b/spring-context/src/main/java/org/springframework/validation/beanvalidation/MethodValidationAdapter.java @@ -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)) diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/MethodValidationTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/MethodValidationTests.java index ca7e06a3563..bfee95c745e 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/MethodValidationTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/MethodValidationTests.java @@ -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 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 HandlerMethod handlerMethod(T controller, Consumer mockCallConsumer) { @@ -306,8 +348,8 @@ class MethodValidationTests { @SuppressWarnings("SameParameterValue") private static void assertBeanResult(Errors errors, String objectName, List 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 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[] payload() default {}; + } + + + private static class TestConstraintValidator implements ConstraintValidator { + + 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, 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 getInstance(Class aClass) { + ConstraintValidator validator = this.validators.get(aClass); + return (validator != null ? (T) validator : this.delegate.getInstance(aClass)); + } + + @Override + public void releaseInstance(ConstraintValidator constraintValidator) { + } + } + }