diff --git a/documentation/src/main/asciidoc/ch06.asciidoc b/documentation/src/main/asciidoc/ch06.asciidoc index 3b44e36e9e..1187338caa 100644 --- a/documentation/src/main/asciidoc/ch06.asciidoc +++ b/documentation/src/main/asciidoc/ch06.asciidoc @@ -278,6 +278,63 @@ It is not included in the constraint violations, unless a specific `ConstraintVa payload to emitted constraint violations by using the <>. ==== +[[constraint-validator-shared-data]] +===== Constraint validator initialization shared data + +While the <> is intended to be used within the `isValid(..)` method, +initialization shared data opens up a way for constraint validators to access a shared instance within the `initialize(..)` +call. This can be used to cache and reuse elements required to construct a constraint validator. For example, +internally, this mechanism is used by the pattern constraint validator to reuse the `java.util.regex.Pattern` instances +when multiple constraints rely on the same pattern string and flags. + +This shared data is accessible through the `HibernateConstraintValidator` extension, +more specifically through the `HibernateConstraintValidatorInitializationContext#getSharedData(..)` methods. +The first overloaded variant is `getSharedData(Class)`, which is intended to be used when the shared data is +provided during the Hibernate Validator factory configuration. + +[[example-constraint-validator-shared-data-definition-validatorfactory]] +.Defining a constraint validator initialization shared data during the `ValidatorFactory` initialization +==== +[source, JAVA, indent=0] +---- +include::{sourcedir}/org/hibernate/validator/referenceguide/chapter06/constraintvalidatorshareddata/ConstraintValidatorPayloadTest.java[tags=setSharedData] +---- +==== + +Once you have set the constraint validator initialization shared data, it can be accessed in your constraint validators as shown in the example below: + +[[example-constraint-validator-shared-data-usage]] +.Accessing the constraint validator initialization shared data in a constraint validator +==== +[source, JAVA, indent=0] +---- +include::{sourcedir}/org/hibernate/validator/referenceguide/chapter06/constraintvalidatorshareddata/ZipCodeValidator.java[tags=include] +---- +<1> Implement the Hibernate Validator `HibernateConstraintValidator` extension to have access to the initialization context. +<2> Retrieve the shared data from the initialization context. +<3> Perform some actions with the shared data instance. +==== + +The other overloaded method to access the shared data `getSharedData(Class, Supplier)` is intended +for cases where the shared data is created lazily +and is not defined at the time of configuring the validator factory. + +[[example-constraint-validator-shared-data-usage-alternative]] +.Accessing the constraint validator lazy initialization shared data in a constraint validator +==== +[source, JAVA, indent=0] +---- +include::{sourcedir}/org/hibernate/validator/referenceguide/chapter06/constraintvalidatorshareddata/ParsableDateTimeFormatValidator.java[tags=include] +---- +<1> Implement the Hibernate Validator `HibernateConstraintValidator` extension to have access to the initialization context. +<2> Retrieve the shared data from the initialization context, providing the supplier that will be executed +if the `DateTimeFormatterCache` is not yet available in the current initialization context. +<3> Perform some actions with the shared data instance. +<4> A simple wrapper around the map to cache the formatters. +Compared to the use of a static field cache, using the shared data has the benefit that it is tied to the initialization context +and will be garbage collected along with it. +==== + [[validator-customconstraints-errormessage]] ==== The error message diff --git a/documentation/src/test/java/org/hibernate/validator/referenceguide/chapter06/constraintvalidatorshareddata/ConstraintValidatorPayloadTest.java b/documentation/src/test/java/org/hibernate/validator/referenceguide/chapter06/constraintvalidatorshareddata/ConstraintValidatorPayloadTest.java new file mode 100644 index 0000000000..427fd80987 --- /dev/null +++ b/documentation/src/test/java/org/hibernate/validator/referenceguide/chapter06/constraintvalidatorshareddata/ConstraintValidatorPayloadTest.java @@ -0,0 +1,33 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.validator.referenceguide.chapter06.constraintvalidatorshareddata; + +import jakarta.validation.Validation; +import jakarta.validation.Validator; +import jakarta.validation.ValidatorFactory; + +import org.hibernate.validator.HibernateValidator; + +import org.junit.Test; + +@SuppressWarnings("unused") +public class ConstraintValidatorPayloadTest { + + @Test + public void setSharedData() { + //tag::setSharedData[] + ValidatorFactory validatorFactory = Validation.byProvider( HibernateValidator.class ) + .configure() + .addConstraintValidatorInitializationSharedData( ZipCodeCatalog.class, readZipCodeCatalog() ) + .buildValidatorFactory(); + + Validator validator = validatorFactory.getValidator(); + //end::setSharedData[] + } + + private ZipCodeCatalog readZipCodeCatalog() { + return new ZipCodeCatalog(); + } +} diff --git a/documentation/src/test/java/org/hibernate/validator/referenceguide/chapter06/constraintvalidatorshareddata/ParsableDateTimeFormat.java b/documentation/src/test/java/org/hibernate/validator/referenceguide/chapter06/constraintvalidatorshareddata/ParsableDateTimeFormat.java new file mode 100644 index 0000000000..8a875f645b --- /dev/null +++ b/documentation/src/test/java/org/hibernate/validator/referenceguide/chapter06/constraintvalidatorshareddata/ParsableDateTimeFormat.java @@ -0,0 +1,33 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.validator.referenceguide.chapter06.constraintvalidatorshareddata; + +import static java.lang.annotation.ElementType.ANNOTATION_TYPE; +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.TYPE_USE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import jakarta.validation.Constraint; +import jakarta.validation.Payload; + +@Target({ METHOD, FIELD, ANNOTATION_TYPE, TYPE_USE }) +@Retention(RUNTIME) +@Constraint(validatedBy = ParsableDateTimeFormatValidator.class) +@Documented +public @interface ParsableDateTimeFormat { + + String dateFormat() default "dd/MM/yyyy"; + + String message() default "{org.hibernate.validator.referenceguide.chapter06.constraintvalidatorshareddata.FutureString.message}"; + + Class[] groups() default { }; + + Class[] payload() default { }; +} diff --git a/documentation/src/test/java/org/hibernate/validator/referenceguide/chapter06/constraintvalidatorshareddata/ParsableDateTimeFormatValidator.java b/documentation/src/test/java/org/hibernate/validator/referenceguide/chapter06/constraintvalidatorshareddata/ParsableDateTimeFormatValidator.java new file mode 100644 index 0000000000..0de18ca2c4 --- /dev/null +++ b/documentation/src/test/java/org/hibernate/validator/referenceguide/chapter06/constraintvalidatorshareddata/ParsableDateTimeFormatValidator.java @@ -0,0 +1,57 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +//spotless:off +//tag::include[] +package org.hibernate.validator.referenceguide.chapter06.constraintvalidatorshareddata; +//end::include[] + +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import org.hibernate.validator.constraintvalidation.HibernateConstraintValidator; +import org.hibernate.validator.constraintvalidation.HibernateConstraintValidatorInitializationContext; + +import jakarta.validation.ConstraintValidatorContext; +import jakarta.validation.metadata.ConstraintDescriptor; + +//spotless:on +//tag::include[] +public class ParsableDateTimeFormatValidator implements HibernateConstraintValidator { // <1> + + private DateTimeFormatter formatter; + + @Override + public void initialize(ConstraintDescriptor constraintDescriptor, + HibernateConstraintValidatorInitializationContext initializationContext) { + formatter = initializationContext.getSharedData( DateTimeFormatterCache.class, DateTimeFormatterCache::new ) // <2> + .get( constraintDescriptor.getAnnotation().dateFormat() ); // <3> + } + + @Override + public boolean isValid(String dateTime, ConstraintValidatorContext constraintContext) { + if ( dateTime == null ) { + return true; + } + + try { + formatter.parse( dateTime ); + } + catch (DateTimeParseException e) { + return false; + } + return true; + } + + private static class DateTimeFormatterCache { // <4> + private final Map cache = new ConcurrentHashMap<>(); + + DateTimeFormatter get(String format) { + return cache.computeIfAbsent( format, DateTimeFormatter::ofPattern ); + } + } +} +//end::include[] diff --git a/documentation/src/test/java/org/hibernate/validator/referenceguide/chapter06/constraintvalidatorshareddata/ZipCode.java b/documentation/src/test/java/org/hibernate/validator/referenceguide/chapter06/constraintvalidatorshareddata/ZipCode.java new file mode 100644 index 0000000000..502886f4e5 --- /dev/null +++ b/documentation/src/test/java/org/hibernate/validator/referenceguide/chapter06/constraintvalidatorshareddata/ZipCode.java @@ -0,0 +1,33 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.validator.referenceguide.chapter06.constraintvalidatorshareddata; + +import static java.lang.annotation.ElementType.ANNOTATION_TYPE; +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.TYPE_USE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import jakarta.validation.Constraint; +import jakarta.validation.Payload; + +@Target({ METHOD, FIELD, ANNOTATION_TYPE, TYPE_USE }) +@Retention(RUNTIME) +@Constraint(validatedBy = ZipCodeValidator.class) +@Documented +public @interface ZipCode { + + String countryCode(); + + String message() default "{org.hibernate.validator.referenceguide.chapter06.constraintvalidatorshareddata.ZipCode.message}"; + + Class[] groups() default { }; + + Class[] payload() default { }; +} diff --git a/documentation/src/test/java/org/hibernate/validator/referenceguide/chapter06/constraintvalidatorshareddata/ZipCodeCatalog.java b/documentation/src/test/java/org/hibernate/validator/referenceguide/chapter06/constraintvalidatorshareddata/ZipCodeCatalog.java new file mode 100644 index 0000000000..09f354f15c --- /dev/null +++ b/documentation/src/test/java/org/hibernate/validator/referenceguide/chapter06/constraintvalidatorshareddata/ZipCodeCatalog.java @@ -0,0 +1,11 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.validator.referenceguide.chapter06.constraintvalidatorshareddata; + +public class ZipCodeCatalog { + public ZipCodeCountryCatalog country(String s) { + return new ZipCodeCountryCatalog(); + } +} diff --git a/documentation/src/test/java/org/hibernate/validator/referenceguide/chapter06/constraintvalidatorshareddata/ZipCodeCountryCatalog.java b/documentation/src/test/java/org/hibernate/validator/referenceguide/chapter06/constraintvalidatorshareddata/ZipCodeCountryCatalog.java new file mode 100644 index 0000000000..cedc7cfc51 --- /dev/null +++ b/documentation/src/test/java/org/hibernate/validator/referenceguide/chapter06/constraintvalidatorshareddata/ZipCodeCountryCatalog.java @@ -0,0 +1,11 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.validator.referenceguide.chapter06.constraintvalidatorshareddata; + +public class ZipCodeCountryCatalog { + public boolean contains(String zip) { + return false; + } +} diff --git a/documentation/src/test/java/org/hibernate/validator/referenceguide/chapter06/constraintvalidatorshareddata/ZipCodeValidator.java b/documentation/src/test/java/org/hibernate/validator/referenceguide/chapter06/constraintvalidatorshareddata/ZipCodeValidator.java new file mode 100644 index 0000000000..bf7de35de3 --- /dev/null +++ b/documentation/src/test/java/org/hibernate/validator/referenceguide/chapter06/constraintvalidatorshareddata/ZipCodeValidator.java @@ -0,0 +1,38 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +//spotless:off +//tag::include[] +package org.hibernate.validator.referenceguide.chapter06.constraintvalidatorshareddata; + +import org.hibernate.validator.constraintvalidation.HibernateConstraintValidator; +import org.hibernate.validator.constraintvalidation.HibernateConstraintValidatorInitializationContext; +//end::include[] + +import jakarta.validation.ConstraintValidatorContext; +import jakarta.validation.metadata.ConstraintDescriptor; + +//spotless:on +//tag::include[] +public class ZipCodeValidator implements HibernateConstraintValidator { // <1> + + private ZipCodeCountryCatalog countryCatalog; + + @Override + public void initialize(ConstraintDescriptor constraintDescriptor, + HibernateConstraintValidatorInitializationContext initializationContext) { + countryCatalog = initializationContext.getSharedData( ZipCodeCatalog.class ) // <2> + .country( constraintDescriptor.getAnnotation().countryCode() ); // <3> + } + + @Override + public boolean isValid(String zip, ConstraintValidatorContext constraintContext) { + if ( zip == null ) { + return true; + } + + return countryCatalog.contains( zip ); + } +} +//end::include[] diff --git a/engine/src/main/java/org/hibernate/validator/BaseHibernateValidatorConfiguration.java b/engine/src/main/java/org/hibernate/validator/BaseHibernateValidatorConfiguration.java index 5db0cc56e5..13b47cd73b 100644 --- a/engine/src/main/java/org/hibernate/validator/BaseHibernateValidatorConfiguration.java +++ b/engine/src/main/java/org/hibernate/validator/BaseHibernateValidatorConfiguration.java @@ -364,6 +364,30 @@ public interface BaseHibernateValidatorConfiguration S addConstraintValidatorInitializationSharedData(Class dataClass, V constraintValidatorInitializationSharedData); + /** * Allows to set a getter property selection strategy defining the rules determining if a method is a getter * or not. diff --git a/engine/src/main/java/org/hibernate/validator/constraintvalidation/HibernateConstraintValidatorInitializationContext.java b/engine/src/main/java/org/hibernate/validator/constraintvalidation/HibernateConstraintValidatorInitializationContext.java index 24f5cd4d38..87943420b1 100644 --- a/engine/src/main/java/org/hibernate/validator/constraintvalidation/HibernateConstraintValidatorInitializationContext.java +++ b/engine/src/main/java/org/hibernate/validator/constraintvalidation/HibernateConstraintValidatorInitializationContext.java @@ -6,6 +6,7 @@ import java.time.Clock; import java.time.Duration; +import java.util.function.Supplier; import jakarta.validation.ClockProvider; @@ -56,4 +57,35 @@ public interface HibernateConstraintValidatorInitializationContext { */ @Incubating Duration getTemporalValidationTolerance(); + + /** + * Returns an instance of the specified data type or {@code null} if the current context does not + * contain such data. + * The requested data type must match the one with which it was originally added with + * {@link org.hibernate.validator.HibernateValidatorConfiguration#addConstraintValidatorInitializationSharedData(Object)}. + * + * @param type the type of data to retrieve + * @return an instance of the specified type or {@code null} if the current constraint initialization context does not + * contain an instance of such type + * + * @since 9.1.0 + * @see org.hibernate.validator.HibernateValidatorConfiguration#addConstraintValidatorInitializationSharedData(Object) + */ + @Incubating + C getSharedData(Class type); + + /** + * Returns an instance of the specified data type or attempts to create it with a supplier, if the current context does not + * contain such data. + * + * @param type the type of data to retrieve + * @param createIfNotPresent the supplier to create an instance of shared data, if it is not already present in this context. + * @return an instance of the specified type or {@code null} if the current constraint initialization context does not + * contain an instance of such type + * + * @since 9.1.0 + * @see org.hibernate.validator.HibernateValidatorConfiguration#addConstraintValidatorInitializationSharedData(Object) + */ + @Incubating + C getSharedData(Class type, Supplier createIfNotPresent); } diff --git a/engine/src/main/java/org/hibernate/validator/internal/constraintvalidators/bv/PatternValidator.java b/engine/src/main/java/org/hibernate/validator/internal/constraintvalidators/bv/PatternValidator.java index 6475693b76..7a76981a6a 100644 --- a/engine/src/main/java/org/hibernate/validator/internal/constraintvalidators/bv/PatternValidator.java +++ b/engine/src/main/java/org/hibernate/validator/internal/constraintvalidators/bv/PatternValidator.java @@ -5,14 +5,18 @@ package org.hibernate.validator.internal.constraintvalidators.bv; import java.lang.invoke.MethodHandles; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; import java.util.regex.Matcher; import java.util.regex.PatternSyntaxException; -import jakarta.validation.ConstraintValidator; import jakarta.validation.ConstraintValidatorContext; import jakarta.validation.constraints.Pattern; +import jakarta.validation.metadata.ConstraintDescriptor; +import org.hibernate.validator.constraintvalidation.HibernateConstraintValidator; import org.hibernate.validator.constraintvalidation.HibernateConstraintValidatorContext; +import org.hibernate.validator.constraintvalidation.HibernateConstraintValidatorInitializationContext; import org.hibernate.validator.internal.engine.messageinterpolation.util.InterpolationHelper; import org.hibernate.validator.internal.util.logging.Log; import org.hibernate.validator.internal.util.logging.LoggerFactory; @@ -20,7 +24,7 @@ /** * @author Hardy Ferentschik */ -public class PatternValidator implements ConstraintValidator { +public class PatternValidator implements HibernateConstraintValidator { private static final Log LOG = LoggerFactory.make( MethodHandles.lookup() ); @@ -28,7 +32,8 @@ public class PatternValidator implements ConstraintValidator constraintDescriptor, HibernateConstraintValidatorInitializationContext initializationContext) { + Pattern parameters = constraintDescriptor.getAnnotation(); Pattern.Flag[] flags = parameters.flags(); int intFlag = 0; for ( Pattern.Flag flag : flags ) { @@ -36,7 +41,8 @@ public void initialize(Pattern parameters) { } try { - pattern = java.util.regex.Pattern.compile( parameters.regexp(), intFlag ); + pattern = initializationContext.getSharedData( PatternConstraintInitializer.class, PatternConstraintInitializer::getInstance ) + .of( parameters.regexp(), intFlag ); } catch (PatternSyntaxException e) { throw LOG.getInvalidRegularExpressionException( e ); @@ -58,4 +64,24 @@ public boolean isValid(CharSequence value, ConstraintValidatorContext constraint Matcher m = pattern.matcher( value ); return m.matches(); } + + private static final class PatternConstraintInitializer { + private final Map cache; + + public static PatternConstraintInitializer getInstance() { + //TODO: do we cache the instance and share it? + return new PatternConstraintInitializer(); + } + + private PatternConstraintInitializer() { + this.cache = new ConcurrentHashMap<>(); + } + + public java.util.regex.Pattern of(String pattern, int flags) { + return cache.computeIfAbsent( new PatternKey( pattern, flags ), key -> java.util.regex.Pattern.compile( pattern, flags ) ); + } + + private record PatternKey(String pattern, int flags) { + } + } } diff --git a/engine/src/main/java/org/hibernate/validator/internal/engine/AbstractConfigurationImpl.java b/engine/src/main/java/org/hibernate/validator/internal/engine/AbstractConfigurationImpl.java index 768ae1dafc..d89be47bef 100644 --- a/engine/src/main/java/org/hibernate/validator/internal/engine/AbstractConfigurationImpl.java +++ b/engine/src/main/java/org/hibernate/validator/internal/engine/AbstractConfigurationImpl.java @@ -37,6 +37,7 @@ import org.hibernate.validator.cfg.ConstraintMapping; import org.hibernate.validator.constraintvalidation.spi.DefaultConstraintValidatorFactory; import org.hibernate.validator.internal.cfg.context.DefaultConstraintMapping; +import org.hibernate.validator.internal.engine.constraintvalidation.HibernateConstraintValidatorInitializationSharedDataManager; import org.hibernate.validator.internal.engine.resolver.TraversableResolvers; import org.hibernate.validator.internal.engine.valueextraction.ValueExtractorDescriptor; import org.hibernate.validator.internal.engine.valueextraction.ValueExtractorManager; @@ -114,11 +115,12 @@ public abstract class AbstractConfigurationImpl valueExtractorDescriptors = new HashMap<>(); // HV-specific options + private final HibernateConstraintValidatorInitializationSharedDataManager sharedDataManager; private final Set programmaticMappings = newHashSet(); + private final MethodValidationConfiguration.Builder methodValidationConfigurationBuilder = new MethodValidationConfiguration.Builder(); private boolean failFast; private boolean failFastOnPropertyViolation; private ClassLoader externalClassLoader; - private final MethodValidationConfiguration.Builder methodValidationConfigurationBuilder = new MethodValidationConfiguration.Builder(); private boolean traversableResolverResultCacheEnabled = true; private ScriptEvaluatorFactory scriptEvaluatorFactory; private Duration temporalValidationTolerance; @@ -159,6 +161,7 @@ private AbstractConfigurationImpl() { this.defaultParameterNameProvider = new DefaultParameterNameProvider(); this.defaultClockProvider = DefaultClockProvider.INSTANCE; this.defaultPropertyNodeNameProvider = new DefaultPropertyNodeNameProvider(); + this.sharedDataManager = new HibernateConstraintValidatorInitializationSharedDataManager(); } @Override @@ -352,6 +355,23 @@ public T constraintValidatorPayload(Object constraintValidatorPayload) { return thisAsT(); } + @Override + public T addConstraintValidatorInitializationSharedData(Object constraintValidatorInitializationSharedData) { + Contracts.assertNotNull( constraintValidatorInitializationSharedData, MESSAGES.parameterMustNotBeNull( "constraintValidatorInitializationSharedData" ) ); + + this.sharedDataManager.register( constraintValidatorInitializationSharedData ); + return thisAsT(); + } + + @Override + public T addConstraintValidatorInitializationSharedData(Class dataClass, S constraintValidatorInitializationSharedData) { + Contracts.assertNotNull( constraintValidatorInitializationSharedData, MESSAGES.parameterMustNotBeNull( "dataClass" ) ); + Contracts.assertNotNull( constraintValidatorInitializationSharedData, MESSAGES.parameterMustNotBeNull( "constraintValidatorInitializationSharedData" ) ); + this.sharedDataManager.register( dataClass, constraintValidatorInitializationSharedData ); + + return thisAsT(); + } + @Override public T getterPropertySelectionStrategy(GetterPropertySelectionStrategy getterPropertySelectionStrategy) { Contracts.assertNotNull( getterPropertySelectionStrategy, MESSAGES.parameterMustNotBeNull( "getterPropertySelectionStrategy" ) ); @@ -548,6 +568,10 @@ public Object getConstraintValidatorPayload() { return constraintValidatorPayload; } + public HibernateConstraintValidatorInitializationSharedDataManager getSharedDataManager() { + return sharedDataManager; + } + public GetterPropertySelectionStrategy getGetterPropertySelectionStrategy() { return getterPropertySelectionStrategy; } diff --git a/engine/src/main/java/org/hibernate/validator/internal/engine/PredefinedScopeValidatorFactoryImpl.java b/engine/src/main/java/org/hibernate/validator/internal/engine/PredefinedScopeValidatorFactoryImpl.java index eb5a55bd31..4399471084 100644 --- a/engine/src/main/java/org/hibernate/validator/internal/engine/PredefinedScopeValidatorFactoryImpl.java +++ b/engine/src/main/java/org/hibernate/validator/internal/engine/PredefinedScopeValidatorFactoryImpl.java @@ -20,6 +20,7 @@ import static org.hibernate.validator.internal.engine.ValidatorFactoryConfigurationHelper.determineShowValidatedValuesInTraceLogs; import static org.hibernate.validator.internal.engine.ValidatorFactoryConfigurationHelper.determineTemporalValidationTolerance; import static org.hibernate.validator.internal.engine.ValidatorFactoryConfigurationHelper.determineTraversableResolverResultCacheEnabled; +import static org.hibernate.validator.internal.engine.ValidatorFactoryConfigurationHelper.initializeConstraintValidatorInitializationShareDataManager; import static org.hibernate.validator.internal.engine.ValidatorFactoryConfigurationHelper.logValidatorFactoryScopedConfiguration; import static org.hibernate.validator.internal.engine.ValidatorFactoryConfigurationHelper.registerCustomConstraintValidators; import static org.hibernate.validator.internal.util.CollectionHelper.newArrayList; @@ -125,8 +126,8 @@ public PredefinedScopeValidatorFactoryImpl(ConfigurationState configurationState Duration temporalValidationTolerance = determineTemporalValidationTolerance( configurationState, properties ); HibernateConstraintValidatorInitializationContextImpl constraintValidatorInitializationContext = new HibernateConstraintValidatorInitializationContextImpl( - scriptEvaluatorFactory, configurationState.getClockProvider(), temporalValidationTolerance ); - + scriptEvaluatorFactory, configurationState.getClockProvider(), temporalValidationTolerance, + initializeConstraintValidatorInitializationShareDataManager( hibernateSpecificConfig ) ); this.validatorFactoryScopedContext = new ValidatorFactoryScopedContext( configurationState.getMessageInterpolator(), diff --git a/engine/src/main/java/org/hibernate/validator/internal/engine/ValidatorFactoryConfigurationHelper.java b/engine/src/main/java/org/hibernate/validator/internal/engine/ValidatorFactoryConfigurationHelper.java index e835053b55..2770010489 100644 --- a/engine/src/main/java/org/hibernate/validator/internal/engine/ValidatorFactoryConfigurationHelper.java +++ b/engine/src/main/java/org/hibernate/validator/internal/engine/ValidatorFactoryConfigurationHelper.java @@ -21,6 +21,7 @@ import org.hibernate.validator.cfg.ConstraintMapping; import org.hibernate.validator.internal.cfg.context.DefaultConstraintMapping; import org.hibernate.validator.internal.engine.constraintdefinition.ConstraintDefinitionContribution; +import org.hibernate.validator.internal.engine.constraintvalidation.HibernateConstraintValidatorInitializationSharedDataManager; import org.hibernate.validator.internal.engine.messageinterpolation.DefaultLocaleResolver; import org.hibernate.validator.internal.engine.scripting.DefaultScriptEvaluatorFactory; import org.hibernate.validator.internal.metadata.DefaultBeanMetaDataClassNormalizer; @@ -252,8 +253,7 @@ static Duration determineTemporalValidationTolerance(ConfigurationState configur } static Object determineConstraintValidatorPayload(ConfigurationState configurationState) { - if ( configurationState instanceof AbstractConfigurationImpl ) { - AbstractConfigurationImpl hibernateSpecificConfig = (AbstractConfigurationImpl) configurationState; + if ( configurationState instanceof AbstractConfigurationImpl hibernateSpecificConfig ) { if ( hibernateSpecificConfig.getConstraintValidatorPayload() != null ) { LOG.logConstraintValidatorPayload( hibernateSpecificConfig.getConstraintValidatorPayload() ); return hibernateSpecificConfig.getConstraintValidatorPayload(); @@ -263,6 +263,20 @@ static Object determineConstraintValidatorPayload(ConfigurationState configurati return null; } + static HibernateConstraintValidatorInitializationSharedDataManager initializeConstraintValidatorInitializationShareDataManager(ConfigurationState configurationState) { + HibernateConstraintValidatorInitializationSharedDataManager configured = null; + if ( configurationState instanceof AbstractConfigurationImpl hibernateSpecificConfig ) { + if ( hibernateSpecificConfig.getSharedDataManager() != null ) { + configured = hibernateSpecificConfig.getSharedDataManager(); + } + } + if ( configured == null ) { + configured = new HibernateConstraintValidatorInitializationSharedDataManager(); + } + + return configured.copy(); + } + static ExpressionLanguageFeatureLevel determineConstraintExpressionLanguageFeatureLevel(AbstractConfigurationImpl hibernateSpecificConfig, Map properties) { if ( hibernateSpecificConfig != null && hibernateSpecificConfig.getConstraintExpressionLanguageFeatureLevel() != null ) { diff --git a/engine/src/main/java/org/hibernate/validator/internal/engine/ValidatorFactoryImpl.java b/engine/src/main/java/org/hibernate/validator/internal/engine/ValidatorFactoryImpl.java index aa0f7a0187..2faa0042af 100644 --- a/engine/src/main/java/org/hibernate/validator/internal/engine/ValidatorFactoryImpl.java +++ b/engine/src/main/java/org/hibernate/validator/internal/engine/ValidatorFactoryImpl.java @@ -20,6 +20,7 @@ import static org.hibernate.validator.internal.engine.ValidatorFactoryConfigurationHelper.determineShowValidatedValuesInTraceLogs; import static org.hibernate.validator.internal.engine.ValidatorFactoryConfigurationHelper.determineTemporalValidationTolerance; import static org.hibernate.validator.internal.engine.ValidatorFactoryConfigurationHelper.determineTraversableResolverResultCacheEnabled; +import static org.hibernate.validator.internal.engine.ValidatorFactoryConfigurationHelper.initializeConstraintValidatorInitializationShareDataManager; import static org.hibernate.validator.internal.engine.ValidatorFactoryConfigurationHelper.logValidatorFactoryScopedConfiguration; import static org.hibernate.validator.internal.engine.ValidatorFactoryConfigurationHelper.registerCustomConstraintValidators; import static org.hibernate.validator.internal.util.CollectionHelper.newArrayList; @@ -168,6 +169,7 @@ public ValidatorFactoryImpl(ConfigurationState configurationState) { determineTraversableResolverResultCacheEnabled( hibernateSpecificConfig, properties ), determineShowValidatedValuesInTraceLogs( hibernateSpecificConfig, properties ), determineConstraintValidatorPayload( hibernateSpecificConfig ), + initializeConstraintValidatorInitializationShareDataManager( hibernateSpecificConfig ), determineConstraintExpressionLanguageFeatureLevel( hibernateSpecificConfig, properties ), determineCustomViolationExpressionLanguageFeatureLevel( hibernateSpecificConfig, properties ) ); diff --git a/engine/src/main/java/org/hibernate/validator/internal/engine/ValidatorFactoryScopedContext.java b/engine/src/main/java/org/hibernate/validator/internal/engine/ValidatorFactoryScopedContext.java index ae56d924da..b63670181a 100644 --- a/engine/src/main/java/org/hibernate/validator/internal/engine/ValidatorFactoryScopedContext.java +++ b/engine/src/main/java/org/hibernate/validator/internal/engine/ValidatorFactoryScopedContext.java @@ -13,6 +13,7 @@ import org.hibernate.validator.constraintvalidation.HibernateConstraintValidatorInitializationContext; import org.hibernate.validator.internal.engine.constraintvalidation.HibernateConstraintValidatorInitializationContextImpl; +import org.hibernate.validator.internal.engine.constraintvalidation.HibernateConstraintValidatorInitializationSharedDataManager; import org.hibernate.validator.internal.util.Contracts; import org.hibernate.validator.internal.util.ExecutableParameterNameProvider; import org.hibernate.validator.messageinterpolation.ExpressionLanguageFeatureLevel; @@ -103,13 +104,15 @@ public class ValidatorFactoryScopedContext { boolean traversableResolverResultCacheEnabled, boolean showValidatedValuesInTraceLogs, Object constraintValidatorPayload, + HibernateConstraintValidatorInitializationSharedDataManager constraintValidatorInitializationSharedServiceManager, ExpressionLanguageFeatureLevel constraintExpressionLanguageFeatureLevel, ExpressionLanguageFeatureLevel customViolationExpressionLanguageFeatureLevel) { this( messageInterpolator, traversableResolver, parameterNameProvider, clockProvider, temporalValidationTolerance, scriptEvaluatorFactory, failFast, failFastOnPropertyViolation, traversableResolverResultCacheEnabled, showValidatedValuesInTraceLogs, constraintValidatorPayload, constraintExpressionLanguageFeatureLevel, customViolationExpressionLanguageFeatureLevel, new HibernateConstraintValidatorInitializationContextImpl( scriptEvaluatorFactory, clockProvider, - temporalValidationTolerance ) ); + temporalValidationTolerance, constraintValidatorInitializationSharedServiceManager + ) ); } ValidatorFactoryScopedContext(MessageInterpolator messageInterpolator, @@ -214,7 +217,7 @@ static class Builder { private ExpressionLanguageFeatureLevel constraintExpressionLanguageFeatureLevel; private ExpressionLanguageFeatureLevel customViolationExpressionLanguageFeatureLevel; private boolean showValidatedValuesInTraceLogs; - private HibernateConstraintValidatorInitializationContextImpl constraintValidatorInitializationContext; + private final HibernateConstraintValidatorInitializationContextImpl constraintValidatorInitializationContext; Builder(ValidatorFactoryScopedContext defaultContext) { Contracts.assertNotNull( defaultContext, "Default context cannot be null." ); @@ -348,7 +351,8 @@ public ValidatorFactoryScopedContext build() { constraintValidatorInitializationContext, scriptEvaluatorFactory, clockProvider, - temporalValidationTolerance + temporalValidationTolerance, + constraintValidatorInitializationContext.getConstraintValidatorInitializationSharedServiceManager() ) ); } diff --git a/engine/src/main/java/org/hibernate/validator/internal/engine/constraintvalidation/HibernateConstraintValidatorInitializationContextImpl.java b/engine/src/main/java/org/hibernate/validator/internal/engine/constraintvalidation/HibernateConstraintValidatorInitializationContextImpl.java index a3df91dc06..5955e5f70f 100644 --- a/engine/src/main/java/org/hibernate/validator/internal/engine/constraintvalidation/HibernateConstraintValidatorInitializationContextImpl.java +++ b/engine/src/main/java/org/hibernate/validator/internal/engine/constraintvalidation/HibernateConstraintValidatorInitializationContextImpl.java @@ -5,6 +5,7 @@ package org.hibernate.validator.internal.engine.constraintvalidation; import java.time.Duration; +import java.util.function.Supplier; import jakarta.validation.ClockProvider; @@ -23,25 +24,30 @@ public class HibernateConstraintValidatorInitializationContextImpl implements Hi private final Duration temporalValidationTolerance; + private final HibernateConstraintValidatorInitializationSharedDataManager constraintValidatorInitializationSharedServiceManager; + private final int hashCode; public HibernateConstraintValidatorInitializationContextImpl(ScriptEvaluatorFactory scriptEvaluatorFactory, ClockProvider clockProvider, - Duration temporalValidationTolerance) { + Duration temporalValidationTolerance, HibernateConstraintValidatorInitializationSharedDataManager constraintValidatorInitializationSharedServiceManager + ) { this.scriptEvaluatorFactory = scriptEvaluatorFactory; this.clockProvider = clockProvider; this.temporalValidationTolerance = temporalValidationTolerance; + this.constraintValidatorInitializationSharedServiceManager = constraintValidatorInitializationSharedServiceManager; this.hashCode = createHashCode(); } public static HibernateConstraintValidatorInitializationContextImpl of(HibernateConstraintValidatorInitializationContextImpl defaultContext, - ScriptEvaluatorFactory scriptEvaluatorFactory, ClockProvider clockProvider, Duration temporalValidationTolerance) { + ScriptEvaluatorFactory scriptEvaluatorFactory, ClockProvider clockProvider, Duration temporalValidationTolerance, + HibernateConstraintValidatorInitializationSharedDataManager constraintValidatorInitializationSharedServiceManager) { if ( scriptEvaluatorFactory == defaultContext.scriptEvaluatorFactory && clockProvider == defaultContext.clockProvider && temporalValidationTolerance.equals( defaultContext.temporalValidationTolerance ) ) { return defaultContext; } - return new HibernateConstraintValidatorInitializationContextImpl( scriptEvaluatorFactory, clockProvider, temporalValidationTolerance ); + return new HibernateConstraintValidatorInitializationContextImpl( scriptEvaluatorFactory, clockProvider, temporalValidationTolerance, constraintValidatorInitializationSharedServiceManager ); } @Override @@ -59,6 +65,20 @@ public Duration getTemporalValidationTolerance() { return temporalValidationTolerance; } + @Override + public C getSharedData(Class type) { + return constraintValidatorInitializationSharedServiceManager.retrieve( type ); + } + + @Override + public C getSharedData(Class type, Supplier createIfNotPresent) { + return constraintValidatorInitializationSharedServiceManager.retrieve( type, createIfNotPresent ); + } + + public HibernateConstraintValidatorInitializationSharedDataManager getConstraintValidatorInitializationSharedServiceManager() { + return constraintValidatorInitializationSharedServiceManager; + } + @Override public boolean equals(Object o) { if ( this == o ) { @@ -73,6 +93,9 @@ public boolean equals(Object o) { if ( scriptEvaluatorFactory != hibernateConstraintValidatorInitializationContextImpl.scriptEvaluatorFactory ) { return false; } + if ( constraintValidatorInitializationSharedServiceManager != hibernateConstraintValidatorInitializationContextImpl.constraintValidatorInitializationSharedServiceManager ) { + return false; + } if ( clockProvider != hibernateConstraintValidatorInitializationContextImpl.clockProvider ) { return false; } @@ -91,6 +114,7 @@ private int createHashCode() { int result = System.identityHashCode( scriptEvaluatorFactory ); result = 31 * result + System.identityHashCode( clockProvider ); result = 31 * result + temporalValidationTolerance.hashCode(); + result = 31 * result + System.identityHashCode( constraintValidatorInitializationSharedServiceManager ); return result; } } diff --git a/engine/src/main/java/org/hibernate/validator/internal/engine/constraintvalidation/HibernateConstraintValidatorInitializationSharedDataManager.java b/engine/src/main/java/org/hibernate/validator/internal/engine/constraintvalidation/HibernateConstraintValidatorInitializationSharedDataManager.java new file mode 100644 index 0000000000..a4d979f800 --- /dev/null +++ b/engine/src/main/java/org/hibernate/validator/internal/engine/constraintvalidation/HibernateConstraintValidatorInitializationSharedDataManager.java @@ -0,0 +1,55 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.validator.internal.engine.constraintvalidation; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Supplier; + +import org.hibernate.validator.internal.util.Contracts; + +public class HibernateConstraintValidatorInitializationSharedDataManager { + private final Map, Object> dataMap; + + public HibernateConstraintValidatorInitializationSharedDataManager() { + this( new HashMap<>() ); + } + + private HibernateConstraintValidatorInitializationSharedDataManager(Map, Object> dataMap) { + this.dataMap = dataMap; + } + + public void register(Object data) { + Contracts.assertNotNull( data, "Data must not be null" ); + + dataMap.put( data.getClass(), data ); + } + + public void register(Class dataClass, S data) { + Contracts.assertNotNull( dataClass, "Data class must not be null" ); + Contracts.assertNotNull( data, "Data must not be null" ); + + dataMap.put( dataClass, data ); + } + + @SuppressWarnings("unchecked") // because of the way we populate that map + public T retrieve(Class dataClass) { + Contracts.assertNotNull( dataClass, "Data class must not be null" ); + return (T) dataMap.get( dataClass ); + } + + @SuppressWarnings("unchecked") // because of the way we populate that map + public C retrieve(Class dataClass, Supplier createIfNotPresent) { + Contracts.assertNotNull( dataClass, "Data class must not be null" ); + Contracts.assertNotNull( createIfNotPresent, "CreateIfNotPresent must not be null" ); + + return (C) dataMap.computeIfAbsent( dataClass, d -> createIfNotPresent.get() ); + } + + public HibernateConstraintValidatorInitializationSharedDataManager copy() { + return new HibernateConstraintValidatorInitializationSharedDataManager( new ConcurrentHashMap<>( this.dataMap ) ); + } +} diff --git a/engine/src/test/java/org/hibernate/validator/test/internal/constraintvalidators/bv/PatternValidatorTest.java b/engine/src/test/java/org/hibernate/validator/test/internal/constraintvalidators/bv/PatternValidatorTest.java index ab287fbc2f..588bc91eed 100644 --- a/engine/src/test/java/org/hibernate/validator/test/internal/constraintvalidators/bv/PatternValidatorTest.java +++ b/engine/src/test/java/org/hibernate/validator/test/internal/constraintvalidators/bv/PatternValidatorTest.java @@ -4,6 +4,7 @@ */ package org.hibernate.validator.test.internal.constraintvalidators.bv; +import static org.hibernate.validator.testutils.ConstraintValidatorInitializationHelper.initialize; import static org.testng.Assert.assertFalse; import static org.testng.Assert.assertTrue; @@ -26,10 +27,10 @@ public void testIsValid() { ConstraintAnnotationDescriptor.Builder descriptorBuilder = new ConstraintAnnotationDescriptor.Builder<>( Pattern.class ); descriptorBuilder.setAttribute( "regexp", "foobar" ); descriptorBuilder.setMessage( "pattern does not match" ); - Pattern p = descriptorBuilder.build().getAnnotation(); + ConstraintAnnotationDescriptor descriptor = descriptorBuilder.build(); PatternValidator constraint = new PatternValidator(); - constraint.initialize( p ); + initialize( constraint, descriptor ); assertTrue( constraint.isValid( null, null ) ); assertFalse( constraint.isValid( "", null ) ); @@ -42,10 +43,10 @@ public void testIsValid() { public void testIsValidForCharSequence() { ConstraintAnnotationDescriptor.Builder descriptorBuilder = new ConstraintAnnotationDescriptor.Builder<>( Pattern.class ); descriptorBuilder.setAttribute( "regexp", "char sequence" ); - Pattern p = descriptorBuilder.build().getAnnotation(); + ConstraintAnnotationDescriptor descriptor = descriptorBuilder.build(); PatternValidator constraint = new PatternValidator(); - constraint.initialize( p ); + initialize( constraint, descriptor ); assertTrue( constraint.isValid( new MyCustomStringImpl( "char sequence" ), null ) ); } @@ -55,10 +56,10 @@ public void testIsValidForEmptyStringRegexp() { ConstraintAnnotationDescriptor.Builder descriptorBuilder = new ConstraintAnnotationDescriptor.Builder<>( Pattern.class ); descriptorBuilder.setAttribute( "regexp", "|^.*foo$" ); descriptorBuilder.setMessage( "pattern does not match" ); - Pattern p = descriptorBuilder.build().getAnnotation(); + ConstraintAnnotationDescriptor descriptor = descriptorBuilder.build(); PatternValidator constraint = new PatternValidator(); - constraint.initialize( p ); + initialize( constraint, descriptor ); assertTrue( constraint.isValid( null, null ) ); assertTrue( constraint.isValid( "", null ) ); @@ -72,9 +73,9 @@ public void testInvalidRegularExpression() { ConstraintAnnotationDescriptor.Builder descriptorBuilder = new ConstraintAnnotationDescriptor.Builder<>( Pattern.class ); descriptorBuilder.setAttribute( "regexp", "(unbalanced parentheses" ); descriptorBuilder.setMessage( "pattern does not match" ); - Pattern p = descriptorBuilder.build().getAnnotation(); + ConstraintAnnotationDescriptor descriptor = descriptorBuilder.build(); PatternValidator constraint = new PatternValidator(); - constraint.initialize( p ); + initialize( constraint, descriptor ); } } diff --git a/engine/src/test/java/org/hibernate/validator/testutils/ConstraintValidatorInitializationHelper.java b/engine/src/test/java/org/hibernate/validator/testutils/ConstraintValidatorInitializationHelper.java index c99741b3ba..89b9da56bd 100644 --- a/engine/src/test/java/org/hibernate/validator/testutils/ConstraintValidatorInitializationHelper.java +++ b/engine/src/test/java/org/hibernate/validator/testutils/ConstraintValidatorInitializationHelper.java @@ -7,6 +7,7 @@ import java.lang.annotation.Annotation; import java.time.Duration; import java.util.Collections; +import java.util.function.Supplier; import jakarta.validation.ClockProvider; import jakarta.validation.metadata.ConstraintDescriptor; @@ -96,6 +97,16 @@ public ClockProvider getClockProvider() { public Duration getTemporalValidationTolerance() { return duration; } + + @Override + public C getSharedData(Class type) { + return null; + } + + @Override + public C getSharedData(Class type, Supplier createIfNotPresent) { + return createIfNotPresent.get(); + } }; } }