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;
|
container = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (node.getKind().equals(ElementKind.PROPERTY)) {
|
if (node.getKind().equals(ElementKind.PROPERTY) || node.getKind().equals(ElementKind.BEAN)) {
|
||||||
nestedViolations
|
nestedViolations
|
||||||
.computeIfAbsent(parameterNode, k ->
|
.computeIfAbsent(parameterNode, k ->
|
||||||
new ParamErrorsBuilder(parameter, value, container, index, key))
|
new ParamErrorsBuilder(parameter, value, container, index, key))
|
||||||
|
|
|
||||||
|
|
@ -16,19 +16,32 @@
|
||||||
|
|
||||||
package org.springframework.web.servlet.mvc.method.annotation;
|
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.lang.reflect.Method;
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
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.ConstraintViolation;
|
||||||
|
import jakarta.validation.Payload;
|
||||||
import jakarta.validation.Valid;
|
import jakarta.validation.Valid;
|
||||||
import jakarta.validation.constraints.Size;
|
import jakarta.validation.constraints.Size;
|
||||||
import jakarta.validation.executable.ExecutableValidator;
|
import jakarta.validation.executable.ExecutableValidator;
|
||||||
import jakarta.validation.metadata.BeanDescriptor;
|
import jakarta.validation.metadata.BeanDescriptor;
|
||||||
|
import org.hibernate.validator.internal.engine.constraintvalidation.ConstraintValidatorFactoryImpl;
|
||||||
import org.junit.jupiter.api.AfterEach;
|
import org.junit.jupiter.api.AfterEach;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Test;
|
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.StringHttpMessageConverter;
|
||||||
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
|
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
|
||||||
import org.springframework.validation.Errors;
|
import org.springframework.validation.Errors;
|
||||||
import org.springframework.validation.FieldError;
|
import org.springframework.validation.ObjectError;
|
||||||
import org.springframework.validation.Validator;
|
import org.springframework.validation.Validator;
|
||||||
import org.springframework.validation.annotation.Validated;
|
import org.springframework.validation.annotation.Validated;
|
||||||
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
|
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
|
||||||
import org.springframework.validation.beanvalidation.SpringValidatorAdapter;
|
import org.springframework.validation.beanvalidation.SpringValidatorAdapter;
|
||||||
|
import org.springframework.validation.method.ParameterErrors;
|
||||||
import org.springframework.validation.method.ParameterValidationResult;
|
import org.springframework.validation.method.ParameterValidationResult;
|
||||||
import org.springframework.web.bind.MethodArgumentNotValidException;
|
import org.springframework.web.bind.MethodArgumentNotValidException;
|
||||||
import org.springframework.web.bind.WebDataBinder;
|
import org.springframework.web.bind.WebDataBinder;
|
||||||
|
|
@ -91,13 +105,17 @@ class MethodValidationTests {
|
||||||
|
|
||||||
private InvocationCountingValidator jakartaValidator;
|
private InvocationCountingValidator jakartaValidator;
|
||||||
|
|
||||||
|
private final TestConstraintValidator testConstraintValidator = new TestConstraintValidator();
|
||||||
|
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
void setup() throws Exception {
|
void setup() throws Exception {
|
||||||
LocaleContextHolder.setDefaultLocale(Locale.UK);
|
LocaleContextHolder.setDefaultLocale(Locale.UK);
|
||||||
|
|
||||||
LocalValidatorFactoryBean validatorBean = new LocalValidatorFactoryBean();
|
LocalValidatorFactoryBean validatorBean = new LocalValidatorFactoryBean();
|
||||||
|
validatorBean.setConstraintValidatorFactory(new TestConstraintValidatorFactory(this.testConstraintValidator));
|
||||||
validatorBean.afterPropertiesSet();
|
validatorBean.afterPropertiesSet();
|
||||||
|
|
||||||
this.jakartaValidator = new InvocationCountingValidator(validatorBean);
|
this.jakartaValidator = new InvocationCountingValidator(validatorBean);
|
||||||
|
|
||||||
this.handlerAdapter = initHandlerAdapter(this.jakartaValidator);
|
this.handlerAdapter = initHandlerAdapter(this.jakartaValidator);
|
||||||
|
|
@ -296,6 +314,30 @@ class MethodValidationTests {
|
||||||
arguments []; default message [length must be 10 or under]""");
|
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")
|
@SuppressWarnings("unchecked")
|
||||||
private static <T> HandlerMethod handlerMethod(T controller, Consumer<T> mockCallConsumer) {
|
private static <T> HandlerMethod handlerMethod(T controller, Consumer<T> mockCallConsumer) {
|
||||||
|
|
@ -306,8 +348,8 @@ class MethodValidationTests {
|
||||||
@SuppressWarnings("SameParameterValue")
|
@SuppressWarnings("SameParameterValue")
|
||||||
private static void assertBeanResult(Errors errors, String objectName, List<String> fieldErrors) {
|
private static void assertBeanResult(Errors errors, String objectName, List<String> fieldErrors) {
|
||||||
assertThat(errors.getObjectName()).isEqualTo(objectName);
|
assertThat(errors.getObjectName()).isEqualTo(objectName);
|
||||||
assertThat(errors.getFieldErrors())
|
assertThat(errors.getAllErrors())
|
||||||
.extracting(FieldError::toString)
|
.extracting(ObjectError::toString)
|
||||||
.containsExactlyInAnyOrderElementsOf(fieldErrors);
|
.containsExactlyInAnyOrderElementsOf(fieldErrors);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -323,6 +365,7 @@ class MethodValidationTests {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@TestConstraint
|
||||||
@SuppressWarnings("unused")
|
@SuppressWarnings("unused")
|
||||||
private record Person(@Size(min = 1, max = 10) @JsonProperty("name") String name) {
|
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 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