Skip to content

Commit 59ed468

Browse files
committedDec 30, 2024
Create ParameterErrors for type level constraint
Closes gh-34105
1 parent d5bebd5 commit 59ed468

File tree

2 files changed

+103
-4
lines changed

2 files changed

+103
-4
lines changed
 

‎spring-context/src/main/java/org/springframework/validation/beanvalidation/MethodValidationAdapter.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -367,7 +367,7 @@ else if (arg instanceof Optional<?> optional) {
367367
container = null;
368368
}
369369

370-
if (node.getKind().equals(ElementKind.PROPERTY)) {
370+
if (node.getKind().equals(ElementKind.PROPERTY) || node.getKind().equals(ElementKind.BEAN)) {
371371
nestedViolations
372372
.computeIfAbsent(parameterNode, k ->
373373
new ParamErrorsBuilder(parameter, value, container, index, key))

‎spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/MethodValidationTests.java

+102-3
Original file line numberDiff line numberDiff line change
@@ -16,19 +16,32 @@
1616

1717
package org.springframework.web.servlet.mvc.method.annotation;
1818

19+
import java.lang.annotation.ElementType;
20+
import java.lang.annotation.Retention;
21+
import java.lang.annotation.RetentionPolicy;
22+
import java.lang.annotation.Target;
1923
import java.lang.reflect.Method;
24+
import java.util.Arrays;
2025
import java.util.HashMap;
26+
import java.util.LinkedHashMap;
2127
import java.util.List;
2228
import java.util.Locale;
29+
import java.util.Map;
2330
import java.util.Set;
2431
import java.util.function.Consumer;
2532

2633
import com.fasterxml.jackson.annotation.JsonProperty;
34+
import jakarta.validation.Constraint;
35+
import jakarta.validation.ConstraintValidator;
36+
import jakarta.validation.ConstraintValidatorContext;
37+
import jakarta.validation.ConstraintValidatorFactory;
2738
import jakarta.validation.ConstraintViolation;
39+
import jakarta.validation.Payload;
2840
import jakarta.validation.Valid;
2941
import jakarta.validation.constraints.Size;
3042
import jakarta.validation.executable.ExecutableValidator;
3143
import jakarta.validation.metadata.BeanDescriptor;
44+
import org.hibernate.validator.internal.engine.constraintvalidation.ConstraintValidatorFactoryImpl;
3245
import org.junit.jupiter.api.AfterEach;
3346
import org.junit.jupiter.api.BeforeEach;
3447
import org.junit.jupiter.api.Test;
@@ -39,11 +52,12 @@
3952
import org.springframework.http.converter.StringHttpMessageConverter;
4053
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
4154
import org.springframework.validation.Errors;
42-
import org.springframework.validation.FieldError;
55+
import org.springframework.validation.ObjectError;
4356
import org.springframework.validation.Validator;
4457
import org.springframework.validation.annotation.Validated;
4558
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
4659
import org.springframework.validation.beanvalidation.SpringValidatorAdapter;
60+
import org.springframework.validation.method.ParameterErrors;
4761
import org.springframework.validation.method.ParameterValidationResult;
4862
import org.springframework.web.bind.MethodArgumentNotValidException;
4963
import org.springframework.web.bind.WebDataBinder;
@@ -91,13 +105,17 @@ class MethodValidationTests {
91105

92106
private InvocationCountingValidator jakartaValidator;
93107

108+
private final TestConstraintValidator testConstraintValidator = new TestConstraintValidator();
109+
94110

95111
@BeforeEach
96112
void setup() throws Exception {
97113
LocaleContextHolder.setDefaultLocale(Locale.UK);
98114

99115
LocalValidatorFactoryBean validatorBean = new LocalValidatorFactoryBean();
116+
validatorBean.setConstraintValidatorFactory(new TestConstraintValidatorFactory(this.testConstraintValidator));
100117
validatorBean.afterPropertiesSet();
118+
101119
this.jakartaValidator = new InvocationCountingValidator(validatorBean);
102120

103121
this.handlerAdapter = initHandlerAdapter(this.jakartaValidator);
@@ -296,6 +314,30 @@ void springValidator() throws Exception {
296314
arguments []; default message [length must be 10 or under]""");
297315
}
298316

317+
@Test // gh-34105
318+
void typeConstraint() {
319+
this.testConstraintValidator.setReject(true);
320+
321+
HandlerMethod hm = handlerMethod(new ValidController(), c -> c.handle(mockPerson, ""));
322+
this.request.addHeader("header", "12345");
323+
this.request.setContentType("application/json");
324+
this.request.setContent("{\"name\":\"Faustino\"}".getBytes(UTF_8));
325+
326+
HandlerMethodValidationException ex = catchThrowableOfType(HandlerMethodValidationException.class,
327+
() -> this.handlerAdapter.handle(this.request, this.response, hm));
328+
329+
List<ParameterValidationResult> results = ex.getParameterValidationResults();
330+
assertThat(results).hasSize(1);
331+
ParameterValidationResult result = results.get(0);
332+
assertThat(result).isInstanceOf(ParameterErrors.class);
333+
334+
assertBeanResult((Errors) result, "person", List.of("""
335+
Error in object 'person': codes [TestConstraint.person,TestConstraint]; \
336+
arguments [org.springframework.context.support.DefaultMessageSourceResolvable: \
337+
codes [person]; arguments []; default message []]; default message [Fail message]\
338+
"""
339+
));
340+
}
299341

300342
@SuppressWarnings("unchecked")
301343
private static <T> HandlerMethod handlerMethod(T controller, Consumer<T> mockCallConsumer) {
@@ -306,8 +348,8 @@ private static <T> HandlerMethod handlerMethod(T controller, Consumer<T> mockCal
306348
@SuppressWarnings("SameParameterValue")
307349
private static void assertBeanResult(Errors errors, String objectName, List<String> fieldErrors) {
308350
assertThat(errors.getObjectName()).isEqualTo(objectName);
309-
assertThat(errors.getFieldErrors())
310-
.extracting(FieldError::toString)
351+
assertThat(errors.getAllErrors())
352+
.extracting(ObjectError::toString)
311353
.containsExactlyInAnyOrderElementsOf(fieldErrors);
312354
}
313355

@@ -323,6 +365,7 @@ private static void assertValueResult(
323365
}
324366

325367

368+
@TestConstraint
326369
@SuppressWarnings("unused")
327370
private record Person(@Size(min = 1, max = 10) @JsonProperty("name") String name) {
328371

@@ -356,6 +399,9 @@ String handleValidated(@Validated Person person, Errors errors,
356399

357400
void handle(@Valid @RequestBody List<Person> persons) {
358401
}
402+
403+
void handle(@Valid @RequestBody Person person, @RequestHeader @Size(min=4) String header) {
404+
}
359405
}
360406

361407

@@ -477,4 +523,57 @@ private void assertCountAndIncrement() {
477523
}
478524
}
479525

526+
527+
528+
@Constraint(validatedBy = TestConstraintValidator.class)
529+
@Target({ElementType.TYPE})
530+
@Retention(RetentionPolicy.RUNTIME)
531+
public @interface TestConstraint {
532+
533+
String message() default "Fail message";
534+
535+
Class<?>[] groups() default {};
536+
537+
Class<? extends Payload>[] payload() default {};
538+
}
539+
540+
541+
private static class TestConstraintValidator implements ConstraintValidator<TestConstraint, Person> {
542+
543+
private boolean reject;
544+
545+
public void setReject(boolean reject) {
546+
this.reject = reject;
547+
}
548+
549+
@Override
550+
public boolean isValid(Person person, ConstraintValidatorContext context) {
551+
return !this.reject;
552+
}
553+
}
554+
555+
556+
private static class TestConstraintValidatorFactory implements ConstraintValidatorFactory {
557+
558+
private final Map<Class<?>, ConstraintValidator<?, ?>> validators;
559+
560+
private final ConstraintValidatorFactory delegate = new ConstraintValidatorFactoryImpl();
561+
562+
private TestConstraintValidatorFactory(ConstraintValidator<?, ?>... validators) {
563+
this.validators = new LinkedHashMap<>(validators.length);
564+
Arrays.stream(validators).forEach(validator -> this.validators.put(validator.getClass(), validator));
565+
}
566+
567+
@SuppressWarnings("unchecked")
568+
@Override
569+
public <T extends ConstraintValidator<?, ?>> T getInstance(Class<T> aClass) {
570+
ConstraintValidator<?, ?> validator = this.validators.get(aClass);
571+
return (validator != null ? (T) validator : this.delegate.getInstance(aClass));
572+
}
573+
574+
@Override
575+
public void releaseInstance(ConstraintValidator<?, ?> constraintValidator) {
576+
}
577+
}
578+
480579
}

0 commit comments

Comments
 (0)
Please sign in to comment.