Skip to content

Commit 02ab7a1

Browse files
committed
HV-2004 Add constraint initialization shared data
and use it to cache patterns in the predefined factory Signed-off-by: marko-bekhta <[email protected]>
1 parent 15bfd48 commit 02ab7a1

20 files changed

+514
-23
lines changed

documentation/src/main/asciidoc/ch06.asciidoc

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,63 @@ It is not included in the constraint violations, unless a specific `ConstraintVa
278278
payload to emitted constraint violations by using the <<section-dynamic-payload,constraint violation dynamic payload mechanism>>.
279279
====
280280

281+
[[constraint-validator-shared-data]]
282+
===== Constraint validator initialization shared data
283+
284+
While the <<constraint-validator-payload,constraint payload>> is intended to be used within the `isValid(..)` method,
285+
initialization shared data opens up a way for constraint validators to access a shared instance within the `initialize(..)`
286+
call. This can be used to cache and reuse elements required to construct a constraint validator. For example,
287+
internally, this mechanism is used by the pattern constraint validator to reuse the `java.util.regex.Pattern` instances
288+
when multiple constraints rely on the same pattern string and flags.
289+
290+
This shared data is accessible through the `HibernateConstraintValidator` extension,
291+
more specifically through the `HibernateConstraintValidatorInitializationContext#getSharedData(..)` methods.
292+
The first overloaded variant is `getSharedData(Class<C>)`, which is intended to be used when the shared data is
293+
provided during the Hibernate Validator factory configuration.
294+
295+
[[example-constraint-validator-shared-data-definition-validatorfactory]]
296+
.Defining a constraint validator initialization shared data during the `ValidatorFactory` initialization
297+
====
298+
[source, JAVA, indent=0]
299+
----
300+
include::{sourcedir}/org/hibernate/validator/referenceguide/chapter06/constraintvalidatorshareddata/ConstraintValidatorPayloadTest.java[tags=setSharedData]
301+
----
302+
====
303+
304+
Once you have set the constraint validator initialization shared data, it can be accessed in your constraint validators as shown in the example below:
305+
306+
[[example-constraint-validator-shared-data-usage]]
307+
.Accessing the constraint validator initialization shared data in a constraint validator
308+
====
309+
[source, JAVA, indent=0]
310+
----
311+
include::{sourcedir}/org/hibernate/validator/referenceguide/chapter06/constraintvalidatorshareddata/ZipCodeValidator.java[tags=include]
312+
----
313+
<1> Implement the Hibernate Validator `HibernateConstraintValidator` extension to have access to the initialization context.
314+
<2> Retrieve the shared data from the initialization context.
315+
<3> Perform some actions with the shared data instance.
316+
====
317+
318+
The other overloaded method to access the shared data `getSharedData(Class<C>, Supplier<V>)` is intended
319+
for cases where the shared data is created lazily
320+
and is not defined at the time of configuring the validator factory.
321+
322+
[[example-constraint-validator-shared-data-usage-alternative]]
323+
.Accessing the constraint validator lazy initialization shared data in a constraint validator
324+
====
325+
[source, JAVA, indent=0]
326+
----
327+
include::{sourcedir}/org/hibernate/validator/referenceguide/chapter06/constraintvalidatorshareddata/ParsableDateTimeFormatValidator.java[tags=include]
328+
----
329+
<1> Implement the Hibernate Validator `HibernateConstraintValidator` extension to have access to the initialization context.
330+
<2> Retrieve the shared data from the initialization context, providing the supplier that will be executed
331+
if the `DateTimeFormatterCache` is not yet available in the current initialization context.
332+
<3> Perform some actions with the shared data instance.
333+
<4> A simple wrapper around the map to cache the formatters.
334+
Compared to the use of a static field cache, using the shared data has the benefit that it is tied to the initialization context
335+
and will be garbage collected along with it.
336+
====
337+
281338
[[validator-customconstraints-errormessage]]
282339
==== The error message
283340

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/*
2+
* SPDX-License-Identifier: Apache-2.0
3+
* Copyright Red Hat Inc. and Hibernate Authors
4+
*/
5+
package org.hibernate.validator.referenceguide.chapter06.constraintvalidatorshareddata;
6+
7+
import jakarta.validation.Validation;
8+
import jakarta.validation.Validator;
9+
import jakarta.validation.ValidatorFactory;
10+
11+
import org.hibernate.validator.HibernateValidator;
12+
13+
import org.junit.Test;
14+
15+
@SuppressWarnings("unused")
16+
public class ConstraintValidatorPayloadTest {
17+
18+
@Test
19+
public void setSharedData() {
20+
//tag::setSharedData[]
21+
ValidatorFactory validatorFactory = Validation.byProvider( HibernateValidator.class )
22+
.configure()
23+
.addConstraintValidatorInitializationSharedData( ZipCodeCatalog.class, readZipCodeCatalog() )
24+
.buildValidatorFactory();
25+
26+
Validator validator = validatorFactory.getValidator();
27+
//end::setSharedData[]
28+
}
29+
30+
private ZipCodeCatalog readZipCodeCatalog() {
31+
return new ZipCodeCatalog();
32+
}
33+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/*
2+
* SPDX-License-Identifier: Apache-2.0
3+
* Copyright Red Hat Inc. and Hibernate Authors
4+
*/
5+
package org.hibernate.validator.referenceguide.chapter06.constraintvalidatorshareddata;
6+
7+
import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
8+
import static java.lang.annotation.ElementType.FIELD;
9+
import static java.lang.annotation.ElementType.METHOD;
10+
import static java.lang.annotation.ElementType.TYPE_USE;
11+
import static java.lang.annotation.RetentionPolicy.RUNTIME;
12+
13+
import java.lang.annotation.Documented;
14+
import java.lang.annotation.Retention;
15+
import java.lang.annotation.Target;
16+
17+
import jakarta.validation.Constraint;
18+
import jakarta.validation.Payload;
19+
20+
@Target({ METHOD, FIELD, ANNOTATION_TYPE, TYPE_USE })
21+
@Retention(RUNTIME)
22+
@Constraint(validatedBy = ParsableDateTimeFormatValidator.class)
23+
@Documented
24+
public @interface ParsableDateTimeFormat {
25+
26+
String dateFormat() default "dd/MM/yyyy";
27+
28+
String message() default "{org.hibernate.validator.referenceguide.chapter06.constraintvalidatorshareddata.FutureString.message}";
29+
30+
Class<?>[] groups() default { };
31+
32+
Class<? extends Payload>[] payload() default { };
33+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/*
2+
* SPDX-License-Identifier: Apache-2.0
3+
* Copyright Red Hat Inc. and Hibernate Authors
4+
*/
5+
//spotless:off
6+
//tag::include[]
7+
package org.hibernate.validator.referenceguide.chapter06.constraintvalidatorshareddata;
8+
//end::include[]
9+
10+
import java.time.format.DateTimeFormatter;
11+
import java.time.format.DateTimeParseException;
12+
import java.util.Map;
13+
import java.util.concurrent.ConcurrentHashMap;
14+
15+
import org.hibernate.validator.constraintvalidation.HibernateConstraintValidator;
16+
import org.hibernate.validator.constraintvalidation.HibernateConstraintValidatorInitializationContext;
17+
18+
import jakarta.validation.ConstraintValidatorContext;
19+
import jakarta.validation.metadata.ConstraintDescriptor;
20+
21+
//spotless:on
22+
//tag::include[]
23+
public class ParsableDateTimeFormatValidator implements HibernateConstraintValidator<ParsableDateTimeFormat, String> { // <1>
24+
25+
private DateTimeFormatter formatter;
26+
27+
@Override
28+
public void initialize(ConstraintDescriptor<ParsableDateTimeFormat> constraintDescriptor,
29+
HibernateConstraintValidatorInitializationContext initializationContext) {
30+
formatter = initializationContext.getSharedData( DateTimeFormatterCache.class, DateTimeFormatterCache::new ) // <2>
31+
.get( constraintDescriptor.getAnnotation().dateFormat() ); // <3>
32+
}
33+
34+
@Override
35+
public boolean isValid(String dateTime, ConstraintValidatorContext constraintContext) {
36+
if ( dateTime == null ) {
37+
return true;
38+
}
39+
40+
try {
41+
formatter.parse( dateTime );
42+
}
43+
catch (DateTimeParseException e) {
44+
return false;
45+
}
46+
return true;
47+
}
48+
49+
private static class DateTimeFormatterCache { // <4>
50+
private final Map<String, DateTimeFormatter> cache = new ConcurrentHashMap<>();
51+
52+
DateTimeFormatter get(String format) {
53+
return cache.computeIfAbsent( format, DateTimeFormatter::ofPattern );
54+
}
55+
}
56+
}
57+
//end::include[]
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/*
2+
* SPDX-License-Identifier: Apache-2.0
3+
* Copyright Red Hat Inc. and Hibernate Authors
4+
*/
5+
package org.hibernate.validator.referenceguide.chapter06.constraintvalidatorshareddata;
6+
7+
import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
8+
import static java.lang.annotation.ElementType.FIELD;
9+
import static java.lang.annotation.ElementType.METHOD;
10+
import static java.lang.annotation.ElementType.TYPE_USE;
11+
import static java.lang.annotation.RetentionPolicy.RUNTIME;
12+
13+
import java.lang.annotation.Documented;
14+
import java.lang.annotation.Retention;
15+
import java.lang.annotation.Target;
16+
17+
import jakarta.validation.Constraint;
18+
import jakarta.validation.Payload;
19+
20+
@Target({ METHOD, FIELD, ANNOTATION_TYPE, TYPE_USE })
21+
@Retention(RUNTIME)
22+
@Constraint(validatedBy = ZipCodeValidator.class)
23+
@Documented
24+
public @interface ZipCode {
25+
26+
String countryCode();
27+
28+
String message() default "{org.hibernate.validator.referenceguide.chapter06.constraintvalidatorshareddata.ZipCode.message}";
29+
30+
Class<?>[] groups() default { };
31+
32+
Class<? extends Payload>[] payload() default { };
33+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
/*
2+
* SPDX-License-Identifier: Apache-2.0
3+
* Copyright Red Hat Inc. and Hibernate Authors
4+
*/
5+
package org.hibernate.validator.referenceguide.chapter06.constraintvalidatorshareddata;
6+
7+
public class ZipCodeCatalog {
8+
public ZipCodeCountryCatalog country(String s) {
9+
return new ZipCodeCountryCatalog();
10+
}
11+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
/*
2+
* SPDX-License-Identifier: Apache-2.0
3+
* Copyright Red Hat Inc. and Hibernate Authors
4+
*/
5+
package org.hibernate.validator.referenceguide.chapter06.constraintvalidatorshareddata;
6+
7+
public class ZipCodeCountryCatalog {
8+
public boolean contains(String zip) {
9+
return false;
10+
}
11+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/*
2+
* SPDX-License-Identifier: Apache-2.0
3+
* Copyright Red Hat Inc. and Hibernate Authors
4+
*/
5+
//spotless:off
6+
//tag::include[]
7+
package org.hibernate.validator.referenceguide.chapter06.constraintvalidatorshareddata;
8+
9+
import org.hibernate.validator.constraintvalidation.HibernateConstraintValidator;
10+
import org.hibernate.validator.constraintvalidation.HibernateConstraintValidatorInitializationContext;
11+
//end::include[]
12+
13+
import jakarta.validation.ConstraintValidatorContext;
14+
import jakarta.validation.metadata.ConstraintDescriptor;
15+
16+
//spotless:on
17+
//tag::include[]
18+
public class ZipCodeValidator implements HibernateConstraintValidator<ZipCode, String> { // <1>
19+
20+
private ZipCodeCountryCatalog countryCatalog;
21+
22+
@Override
23+
public void initialize(ConstraintDescriptor<ZipCode> constraintDescriptor,
24+
HibernateConstraintValidatorInitializationContext initializationContext) {
25+
countryCatalog = initializationContext.getSharedData( ZipCodeCatalog.class ) // <2>
26+
.country( constraintDescriptor.getAnnotation().countryCode() ); // <3>
27+
}
28+
29+
@Override
30+
public boolean isValid(String zip, ConstraintValidatorContext constraintContext) {
31+
if ( zip == null ) {
32+
return true;
33+
}
34+
35+
return countryCatalog.contains( zip );
36+
}
37+
}
38+
//end::include[]

engine/src/main/java/org/hibernate/validator/BaseHibernateValidatorConfiguration.java

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -364,6 +364,30 @@ public interface BaseHibernateValidatorConfiguration<S extends BaseHibernateVali
364364
@Incubating
365365
S constraintValidatorPayload(Object constraintValidatorPayload);
366366

367+
/**
368+
* Allows adding a shared data object which will be available during the constraint validators initialization to all constraints.
369+
* If the method is called multiple times passing different instances of the same class,
370+
* only the instance passed last will be available for that type.
371+
*
372+
* @param constraintValidatorInitializationSharedService the data to retrieve from the constraint validator initializers
373+
* @return {@code this} following the chaining method pattern
374+
* @since 9.1.0
375+
*/
376+
@Incubating
377+
S addConstraintValidatorInitializationSharedData(Object constraintValidatorInitializationSharedService);
378+
379+
/**
380+
* Allows adding a shared data object which will be available during the constraint validators initialization to all constraints.
381+
* If the method is called multiple times passing the same {@code dataClass},
382+
* only the instance passed last will be available for that type.
383+
*
384+
* @param constraintValidatorInitializationSharedData the data to retrieve from the constraint validator initializers
385+
* @return {@code this} following the chaining method pattern
386+
* @since 9.1.0
387+
*/
388+
@Incubating
389+
<T, V extends T> S addConstraintValidatorInitializationSharedData(Class<T> dataClass, V constraintValidatorInitializationSharedData);
390+
367391
/**
368392
* Allows to set a getter property selection strategy defining the rules determining if a method is a getter
369393
* or not.

engine/src/main/java/org/hibernate/validator/constraintvalidation/HibernateConstraintValidatorInitializationContext.java

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
import java.time.Clock;
88
import java.time.Duration;
9+
import java.util.function.Supplier;
910

1011
import jakarta.validation.ClockProvider;
1112

@@ -56,4 +57,35 @@ public interface HibernateConstraintValidatorInitializationContext {
5657
*/
5758
@Incubating
5859
Duration getTemporalValidationTolerance();
60+
61+
/**
62+
* Returns an instance of the specified data type or {@code null} if the current context does not
63+
* contain such data.
64+
* The requested data type must match the one with which it was originally added with
65+
* {@link org.hibernate.validator.HibernateValidatorConfiguration#addConstraintValidatorInitializationSharedData(Object)}.
66+
*
67+
* @param type the type of data to retrieve
68+
* @return an instance of the specified type or {@code null} if the current constraint initialization context does not
69+
* contain an instance of such type
70+
*
71+
* @since 9.1.0
72+
* @see org.hibernate.validator.HibernateValidatorConfiguration#addConstraintValidatorInitializationSharedData(Object)
73+
*/
74+
@Incubating
75+
<C> C getSharedData(Class<C> type);
76+
77+
/**
78+
* Returns an instance of the specified data type or attempts to create it with a supplier, if the current context does not
79+
* contain such data.
80+
*
81+
* @param type the type of data to retrieve
82+
* @param createIfNotPresent the supplier to create an instance of shared data, if it is not already present in this context.
83+
* @return an instance of the specified type or {@code null} if the current constraint initialization context does not
84+
* contain an instance of such type
85+
*
86+
* @since 9.1.0
87+
* @see org.hibernate.validator.HibernateValidatorConfiguration#addConstraintValidatorInitializationSharedData(Object)
88+
*/
89+
@Incubating
90+
<C, V extends C> C getSharedData(Class<C> type, Supplier<V> createIfNotPresent);
5991
}

0 commit comments

Comments
 (0)