Create ParameterErrors for type level constraint

Closes gh-34105
This commit is contained in:
rstoyanchev 2024-12-19 14:45:44 +00:00
parent d5bebd5ced
commit 59ed4686c5
2 changed files with 103 additions and 4 deletions

View File

@ -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))

View File

@ -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) {
}
}
}