16
16
17
17
package org .springframework .web .servlet .mvc .method .annotation ;
18
18
19
+ import java .lang .annotation .ElementType ;
20
+ import java .lang .annotation .Retention ;
21
+ import java .lang .annotation .RetentionPolicy ;
22
+ import java .lang .annotation .Target ;
19
23
import java .lang .reflect .Method ;
24
+ import java .util .Arrays ;
20
25
import java .util .HashMap ;
26
+ import java .util .LinkedHashMap ;
21
27
import java .util .List ;
22
28
import java .util .Locale ;
29
+ import java .util .Map ;
23
30
import java .util .Set ;
24
31
import java .util .function .Consumer ;
25
32
26
33
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 ;
27
38
import jakarta .validation .ConstraintViolation ;
39
+ import jakarta .validation .Payload ;
28
40
import jakarta .validation .Valid ;
29
41
import jakarta .validation .constraints .Size ;
30
42
import jakarta .validation .executable .ExecutableValidator ;
31
43
import jakarta .validation .metadata .BeanDescriptor ;
44
+ import org .hibernate .validator .internal .engine .constraintvalidation .ConstraintValidatorFactoryImpl ;
32
45
import org .junit .jupiter .api .AfterEach ;
33
46
import org .junit .jupiter .api .BeforeEach ;
34
47
import org .junit .jupiter .api .Test ;
39
52
import org .springframework .http .converter .StringHttpMessageConverter ;
40
53
import org .springframework .http .converter .json .MappingJackson2HttpMessageConverter ;
41
54
import org .springframework .validation .Errors ;
42
- import org .springframework .validation .FieldError ;
55
+ import org .springframework .validation .ObjectError ;
43
56
import org .springframework .validation .Validator ;
44
57
import org .springframework .validation .annotation .Validated ;
45
58
import org .springframework .validation .beanvalidation .LocalValidatorFactoryBean ;
46
59
import org .springframework .validation .beanvalidation .SpringValidatorAdapter ;
60
+ import org .springframework .validation .method .ParameterErrors ;
47
61
import org .springframework .validation .method .ParameterValidationResult ;
48
62
import org .springframework .web .bind .MethodArgumentNotValidException ;
49
63
import org .springframework .web .bind .WebDataBinder ;
@@ -91,13 +105,17 @@ class MethodValidationTests {
91
105
92
106
private InvocationCountingValidator jakartaValidator ;
93
107
108
+ private final TestConstraintValidator testConstraintValidator = new TestConstraintValidator ();
109
+
94
110
95
111
@ BeforeEach
96
112
void setup () throws Exception {
97
113
LocaleContextHolder .setDefaultLocale (Locale .UK );
98
114
99
115
LocalValidatorFactoryBean validatorBean = new LocalValidatorFactoryBean ();
116
+ validatorBean .setConstraintValidatorFactory (new TestConstraintValidatorFactory (this .testConstraintValidator ));
100
117
validatorBean .afterPropertiesSet ();
118
+
101
119
this .jakartaValidator = new InvocationCountingValidator (validatorBean );
102
120
103
121
this .handlerAdapter = initHandlerAdapter (this .jakartaValidator );
@@ -296,6 +314,30 @@ void springValidator() throws Exception {
296
314
arguments []; default message [length must be 10 or under]""" );
297
315
}
298
316
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
+ }
299
341
300
342
@ SuppressWarnings ("unchecked" )
301
343
private static <T > HandlerMethod handlerMethod (T controller , Consumer <T > mockCallConsumer ) {
@@ -306,8 +348,8 @@ private static <T> HandlerMethod handlerMethod(T controller, Consumer<T> mockCal
306
348
@ SuppressWarnings ("SameParameterValue" )
307
349
private static void assertBeanResult (Errors errors , String objectName , List <String > fieldErrors ) {
308
350
assertThat (errors .getObjectName ()).isEqualTo (objectName );
309
- assertThat (errors .getFieldErrors ())
310
- .extracting (FieldError ::toString )
351
+ assertThat (errors .getAllErrors ())
352
+ .extracting (ObjectError ::toString )
311
353
.containsExactlyInAnyOrderElementsOf (fieldErrors );
312
354
}
313
355
@@ -323,6 +365,7 @@ private static void assertValueResult(
323
365
}
324
366
325
367
368
+ @ TestConstraint
326
369
@ SuppressWarnings ("unused" )
327
370
private record Person (@ Size (min = 1 , max = 10 ) @ JsonProperty ("name" ) String name ) {
328
371
@@ -356,6 +399,9 @@ String handleValidated(@Validated Person person, Errors errors,
356
399
357
400
void handle (@ Valid @ RequestBody List <Person > persons ) {
358
401
}
402
+
403
+ void handle (@ Valid @ RequestBody Person person , @ RequestHeader @ Size (min =4 ) String header ) {
404
+ }
359
405
}
360
406
361
407
@@ -477,4 +523,57 @@ private void assertCountAndIncrement() {
477
523
}
478
524
}
479
525
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
+
480
579
}
0 commit comments