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