From ad78f04877075dc1ed1c6cb7653b66c3f9fab472 Mon Sep 17 00:00:00 2001 From: Roberto Cortez Date: Wed, 12 Apr 2023 20:00:43 +0100 Subject: [PATCH] Validator for Config only --- .../runtime/configuration/ConfigUtils.java | 2 - .../HibernateValidatorProcessor.java | 228 ++++++++++++++---- ...ConfigMappingInjectionInValidatorTest.java | 75 ++++++ .../test/config/ConfigMappingInvalidTest.java | 44 +++- ...ibernateBeanValidationConfigValidator.java | 45 +++- .../runtime/HibernateValidatorRecorder.java | 13 + .../io.smallrye.config.ConfigValidator | 1 - .../ConfigMappingStartupValidatorTest.java | 21 +- 8 files changed, 373 insertions(+), 56 deletions(-) create mode 100644 extensions/hibernate-validator/deployment/src/test/java/io/quarkus/hibernate/validator/test/config/ConfigMappingInjectionInValidatorTest.java delete mode 100644 extensions/hibernate-validator/runtime/src/main/resources/META-INF/services/io.smallrye.config.ConfigValidator diff --git a/core/runtime/src/main/java/io/quarkus/runtime/configuration/ConfigUtils.java b/core/runtime/src/main/java/io/quarkus/runtime/configuration/ConfigUtils.java index e9fe18f97ed53..7169fceea750e 100644 --- a/core/runtime/src/main/java/io/quarkus/runtime/configuration/ConfigUtils.java +++ b/core/runtime/src/main/java/io/quarkus/runtime/configuration/ConfigUtils.java @@ -94,8 +94,6 @@ public static SmallRyeConfigBuilder configBuilder(final boolean runTime, final b builder.withSources(new RuntimeOverrideConfigSource(Thread.currentThread().getContextClassLoader())); } if (runTime || bootstrap) { - // Validator only for runtime. We cannot use the current validator for build time (chicken / egg problem) - builder.addDiscoveredValidator(); builder.withDefaultValue(UUID_KEY, UUID.randomUUID().toString()); } if (addDiscovered) { diff --git a/extensions/hibernate-validator/deployment/src/main/java/io/quarkus/hibernate/validator/deployment/HibernateValidatorProcessor.java b/extensions/hibernate-validator/deployment/src/main/java/io/quarkus/hibernate/validator/deployment/HibernateValidatorProcessor.java index fa8efcb7360ae..b8eea6efa36c6 100644 --- a/extensions/hibernate-validator/deployment/src/main/java/io/quarkus/hibernate/validator/deployment/HibernateValidatorProcessor.java +++ b/extensions/hibernate-validator/deployment/src/main/java/io/quarkus/hibernate/validator/deployment/HibernateValidatorProcessor.java @@ -63,6 +63,7 @@ import io.quarkus.deployment.Capabilities; import io.quarkus.deployment.Capability; import io.quarkus.deployment.Feature; +import io.quarkus.deployment.GeneratedClassGizmoAdaptor; import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.deployment.annotations.ExecutionTime; @@ -71,9 +72,12 @@ import io.quarkus.deployment.builditem.CombinedIndexBuildItem; import io.quarkus.deployment.builditem.ConfigClassBuildItem; import io.quarkus.deployment.builditem.FeatureBuildItem; +import io.quarkus.deployment.builditem.GeneratedClassBuildItem; import io.quarkus.deployment.builditem.HotDeploymentWatchedFileBuildItem; import io.quarkus.deployment.builditem.NativeImageFeatureBuildItem; +import io.quarkus.deployment.builditem.RunTimeConfigBuilderBuildItem; import io.quarkus.deployment.builditem.ShutdownContextBuildItem; +import io.quarkus.deployment.builditem.StaticInitConfigBuilderBuildItem; import io.quarkus.deployment.builditem.nativeimage.NativeImageResourceBundleBuildItem; import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem; import io.quarkus.deployment.builditem.nativeimage.ReflectiveFieldBuildItem; @@ -81,9 +85,15 @@ import io.quarkus.deployment.logging.LogCleanupFilterBuildItem; import io.quarkus.deployment.pkg.steps.NativeOrNativeSourcesBuild; import io.quarkus.deployment.recording.RecorderContext; +import io.quarkus.gizmo.ClassCreator; +import io.quarkus.gizmo.FieldDescriptor; import io.quarkus.gizmo.Gizmo; +import io.quarkus.gizmo.MethodCreator; +import io.quarkus.gizmo.MethodDescriptor; +import io.quarkus.gizmo.ResultHandle; import io.quarkus.hibernate.validator.ValidatorFactoryCustomizer; import io.quarkus.hibernate.validator.runtime.DisableLoggingFeature; +import io.quarkus.hibernate.validator.runtime.HibernateBeanValidationConfigValidator; import io.quarkus.hibernate.validator.runtime.HibernateValidatorBuildTimeConfig; import io.quarkus.hibernate.validator.runtime.HibernateValidatorRecorder; import io.quarkus.hibernate.validator.runtime.ValidationSupport; @@ -98,6 +108,12 @@ import io.quarkus.resteasy.common.spi.ResteasyDotNames; import io.quarkus.resteasy.reactive.spi.ExceptionMapperBuildItem; import io.quarkus.runtime.LocalesBuildTimeConfig; +import io.quarkus.runtime.configuration.ConfigBuilder; +import io.smallrye.config.ConfigMappingLoader; +import io.smallrye.config.ConfigMappingMetadata; +import io.smallrye.config.ConfigValidator; +import io.smallrye.config.SmallRyeConfigBuilder; +import io.smallrye.config.validator.BeanValidationConfigValidator; class HibernateValidatorProcessor { @@ -146,6 +162,167 @@ NativeImageFeatureBuildItem nativeImageFeature() { return new NativeImageFeatureBuildItem(DisableLoggingFeature.class); } + @BuildStep + void beanValidationAnnotations( + BeanArchiveIndexBuildItem beanArchiveIndexBuildItem, + CombinedIndexBuildItem combinedIndexBuildItem, + BuildProducer beanValidationAnnotations) { + + IndexView indexView = CompositeIndex.create(beanArchiveIndexBuildItem.getIndex(), combinedIndexBuildItem.getIndex()); + + Set constraints = new HashSet<>(); + Set builtinConstraints = ConstraintHelper.getBuiltinConstraints(); + + // Collect the constraint annotations provided by Hibernate Validator and Bean Validation + contributeBuiltinConstraints(builtinConstraints, constraints); + + // Add the constraint annotations present in the application itself + for (AnnotationInstance constraint : indexView.getAnnotations(DotName.createSimple(Constraint.class.getName()))) { + constraints.add(constraint.target().asClass().name()); + + if (constraint.target().asClass().annotationsMap().containsKey(REPEATABLE)) { + for (AnnotationInstance repeatableConstraint : constraint.target().asClass().annotationsMap() + .get(REPEATABLE)) { + constraints.add(repeatableConstraint.value().asClass().name()); + } + } + } + + Set allConsideredAnnotations = new HashSet<>(); + allConsideredAnnotations.addAll(constraints); + + // Also consider elements that are marked with @Valid + allConsideredAnnotations.add(VALID); + + // Also consider elements that are marked with @ValidateOnExecution + allConsideredAnnotations.add(VALIDATE_ON_EXECUTION); + + beanValidationAnnotations.produce(new BeanValidationAnnotationsBuildItem( + VALID, + constraints, + allConsideredAnnotations)); + } + + @BuildStep + void configValidator( + CombinedIndexBuildItem combinedIndex, + List configClasses, + BeanValidationAnnotationsBuildItem beanValidationAnnotations, + BuildProducer generatedClass, + BuildProducer reflectiveClass, + BuildProducer staticInitConfigBuilder, + BuildProducer runTimeConfigBuilder) { + + Set configMappings = new HashSet<>(); + Set configClassesToValidate = new HashSet<>(); + for (ConfigClassBuildItem configClass : configClasses) { + for (String generatedConfigClass : configClass.getGeneratedClasses()) { + DotName simple = DotName.createSimple(generatedConfigClass); + configClassesToValidate.add(simple); + } + + for (ConfigMappingMetadata mappingsMetadata : ConfigMappingLoader + .getConfigMappingsMetadata(configClass.getConfigClass())) { + configMappings.add(DotName.createSimple(mappingsMetadata.getInterfaceType())); + } + } + + Set configMappingsConstraints = new HashSet<>(); + for (DotName consideredAnnotation : beanValidationAnnotations.getAllAnnotations()) { + Collection annotationInstances = combinedIndex.getIndex().getAnnotations(consideredAnnotation); + + if (annotationInstances.isEmpty()) { + continue; + } + + for (AnnotationInstance annotation : annotationInstances) { + String builtinConstraintCandidate = BUILT_IN_CONSTRAINT_REPEATABLE_CONTAINER_PATTERN + .matcher(consideredAnnotation.toString()).replaceAll(""); + + if (annotation.target().kind() == AnnotationTarget.Kind.METHOD) { + MethodInfo methodInfo = annotation.target().asMethod(); + ClassInfo declaringClass = methodInfo.declaringClass(); + if (configMappings.contains(declaringClass.name())) { + configMappingsConstraints.add(builtinConstraintCandidate); + } + } else if (annotation.target().kind() == AnnotationTarget.Kind.CLASS) { + ClassInfo classInfo = annotation.target().asClass(); + if (configMappings.contains(classInfo.name())) { + configMappingsConstraints.add(builtinConstraintCandidate); + } + } + } + } + + if (configMappingsConstraints.isEmpty()) { + return; + } + + String builderClassName = HibernateBeanValidationConfigValidator.class.getName() + "Builder"; + try (ClassCreator classCreator = ClassCreator.builder() + .classOutput(new GeneratedClassGizmoAdaptor(generatedClass, true)) + .className(builderClassName) + .interfaces(ConfigBuilder.class) + .setFinal(true) + .build()) { + + // Static Init Validator + MethodCreator clinit = classCreator + .getMethodCreator(MethodDescriptor.ofMethod(builderClassName, "", void.class)); + clinit.setModifiers(Opcodes.ACC_STATIC); + + ResultHandle constraints = clinit.newInstance(MethodDescriptor.ofConstructor(HashSet.class)); + for (String configMappingsConstraint : configMappingsConstraints) { + clinit.invokeVirtualMethod(MethodDescriptor.ofMethod(HashSet.class, "add", boolean.class, Object.class), + constraints, clinit.load(configMappingsConstraint)); + } + + ResultHandle classes = clinit.newInstance(MethodDescriptor.ofConstructor(HashSet.class)); + for (DotName configClassToValidate : configClassesToValidate) { + clinit.invokeVirtualMethod(MethodDescriptor.ofMethod(HashSet.class, "add", boolean.class, Object.class), + classes, clinit.loadClass(configClassToValidate.toString())); + } + + ResultHandle configValidator = clinit.newInstance( + MethodDescriptor.ofConstructor(HibernateBeanValidationConfigValidator.class, Set.class, Set.class), + constraints, classes); + + FieldDescriptor configValidatorField = FieldDescriptor.of(builderClassName, "configValidator", + BeanValidationConfigValidator.class); + classCreator.getFieldCreator(configValidatorField) + .setModifiers(Opcodes.ACC_STATIC | Opcodes.ACC_FINAL | Opcodes.ACC_PRIVATE); + clinit.writeStaticField(configValidatorField, configValidator); + + clinit.returnNull(); + clinit.close(); + + MethodCreator configBuilderMethod = classCreator.getMethodCreator( + MethodDescriptor.ofMethod( + ConfigBuilder.class, "configBuilder", + SmallRyeConfigBuilder.class, SmallRyeConfigBuilder.class)); + ResultHandle configBuilder = configBuilderMethod.getMethodParam(0); + + // Add Validator to the builder + configBuilderMethod.invokeVirtualMethod( + MethodDescriptor.ofMethod(SmallRyeConfigBuilder.class, "withValidator", SmallRyeConfigBuilder.class, + ConfigValidator.class), + configBuilder, configBuilderMethod.readStaticField(configValidatorField)); + + configBuilderMethod.returnValue(configBuilder); + } + + reflectiveClass.produce(ReflectiveClassBuildItem.builder(builderClassName).build()); + staticInitConfigBuilder.produce(new StaticInitConfigBuilderBuildItem(builderClassName)); + runTimeConfigBuilder.produce(new RunTimeConfigBuilderBuildItem(builderClassName)); + } + + @BuildStep + @Record(ExecutionTime.STATIC_INIT) + void shutdownConfigValidator(HibernateValidatorRecorder hibernateValidatorRecorder, + ShutdownContextBuildItem shutdownContext) { + hibernateValidatorRecorder.shutdownConfigValidator(shutdownContext); + } + @BuildStep @Record(ExecutionTime.STATIC_INIT) void registerAdditionalBeans(HibernateValidatorRecorder hibernateValidatorRecorder, @@ -207,7 +384,9 @@ public boolean test(BeanInfo beanInfo) { @BuildStep @Record(STATIC_INIT) - public void build(HibernateValidatorRecorder recorder, RecorderContext recorderContext, + public void build( + HibernateValidatorRecorder recorder, RecorderContext recorderContext, + BeanValidationAnnotationsBuildItem beanValidationAnnotations, BuildProducer reflectiveFields, BuildProducer reflectiveMethods, BuildProducer annotationsTransformers, @@ -215,7 +394,6 @@ public void build(HibernateValidatorRecorder recorder, RecorderContext recorderC CombinedIndexBuildItem combinedIndexBuildItem, BuildProducer feature, BuildProducer beanContainerListener, - BuildProducer beanValidationAnnotations, BuildProducer unremovableBeans, ShutdownContextBuildItem shutdownContext, List configClasses, @@ -229,44 +407,11 @@ public void build(HibernateValidatorRecorder recorder, RecorderContext recorderC // we use both indexes to support both generated beans and jars that contain no CDI beans but only Validation annotations IndexView indexView = CompositeIndex.create(beanArchiveIndexBuildItem.getIndex(), combinedIndexBuildItem.getIndex()); - Set constraints = new HashSet<>(); - - Set builtinConstraints = ConstraintHelper.getBuiltinConstraints(); - - // Collect the constraint annotations provided by Hibernate Validator and Bean Validation - contributeBuiltinConstraints(builtinConstraints, constraints); - - // Add the constraint annotations present in the application itself - for (AnnotationInstance constraint : indexView.getAnnotations(DotName.createSimple(Constraint.class.getName()))) { - constraints.add(constraint.target().asClass().name()); - - if (constraint.target().asClass().annotationsMap().containsKey(REPEATABLE)) { - for (AnnotationInstance repeatableConstraint : constraint.target().asClass().annotationsMap() - .get(REPEATABLE)) { - constraints.add(repeatableConstraint.value().asClass().name()); - } - } - } - - Set allConsideredAnnotations = new HashSet<>(); - allConsideredAnnotations.addAll(constraints); - - // Also consider elements that are marked with @Valid - allConsideredAnnotations.add(VALID); - - // Also consider elements that are marked with @ValidateOnExecution - allConsideredAnnotations.add(VALIDATE_ON_EXECUTION); - - beanValidationAnnotations.produce(new BeanValidationAnnotationsBuildItem( - VALID, - constraints, - allConsideredAnnotations)); - Set classNamesToBeValidated = new HashSet<>(); Map> methodsWithInheritedValidation = new HashMap<>(); Set detectedBuiltinConstraints = new HashSet<>(); - for (DotName consideredAnnotation : allConsideredAnnotations) { + for (DotName consideredAnnotation : beanValidationAnnotations.getAllAnnotations()) { Collection annotationInstances = indexView.getAnnotations(consideredAnnotation); if (annotationInstances.isEmpty()) { @@ -276,7 +421,8 @@ public void build(HibernateValidatorRecorder recorder, RecorderContext recorderC // we trim the repeatable container suffix if needed String builtinConstraintCandidate = BUILT_IN_CONSTRAINT_REPEATABLE_CONTAINER_PATTERN .matcher(consideredAnnotation.toString()).replaceAll(""); - if (builtinConstraints.contains(builtinConstraintCandidate)) { + if (beanValidationAnnotations.getConstraintAnnotations() + .contains(DotName.createSimple(builtinConstraintCandidate))) { detectedBuiltinConstraints.add(builtinConstraintCandidate); } @@ -333,12 +479,6 @@ public void build(HibernateValidatorRecorder recorder, RecorderContext recorderC } } - for (ConfigClassBuildItem configClass : configClasses) { - for (String generatedClass : configClass.getGeneratedClasses()) { - classNamesToBeValidated.add(DotName.createSimple(generatedClass)); - } - } - // JAX-RS methods are handled differently by the transformer so those need to be gathered here. // Note: The focus only on methods is basically an incomplete solution, since there could also be // class-level JAX-RS annotations but currently the transformer only looks at methods. @@ -351,7 +491,7 @@ public void build(HibernateValidatorRecorder recorder, RecorderContext recorderC annotationsTransformers .produce(new AnnotationsTransformerBuildItem( - new MethodValidatedAnnotationsTransformer(allConsideredAnnotations, + new MethodValidatedAnnotationsTransformer(beanValidationAnnotations.getAllAnnotations(), jaxRsMethods, methodsWithInheritedValidation))); diff --git a/extensions/hibernate-validator/deployment/src/test/java/io/quarkus/hibernate/validator/test/config/ConfigMappingInjectionInValidatorTest.java b/extensions/hibernate-validator/deployment/src/test/java/io/quarkus/hibernate/validator/test/config/ConfigMappingInjectionInValidatorTest.java new file mode 100644 index 0000000000000..ea6f634b02f4a --- /dev/null +++ b/extensions/hibernate-validator/deployment/src/test/java/io/quarkus/hibernate/validator/test/config/ConfigMappingInjectionInValidatorTest.java @@ -0,0 +1,75 @@ +package io.quarkus.hibernate.validator.test.config; + +import static java.lang.annotation.ElementType.ANNOTATION_TYPE; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import jakarta.inject.Inject; +import jakarta.validation.Constraint; +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; +import jakarta.validation.Payload; +import jakarta.validation.Validator; + +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.runtime.annotations.StaticInitSafe; +import io.quarkus.test.QuarkusUnitTest; +import io.smallrye.config.ConfigMapping; +import io.smallrye.config.WithDefault; + +class ConfigMappingInjectionInValidatorTest { + @RegisterExtension + private static final QuarkusUnitTest TEST = new QuarkusUnitTest() + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class)); + + @Inject + Validator validator; + + @Test + void valid() { + assertTrue(validator.validate(new Entity()).isEmpty()); + } + + @StaticInitSafe + @ConfigMapping(prefix = "valid.config") + public interface ValidConfig { + @WithDefault("true") + boolean isValid(); + } + + @Target({ TYPE, ANNOTATION_TYPE }) + @Retention(RUNTIME) + @Constraint(validatedBy = { ValidEntityValidator.class }) + @Documented + public @interface ValidEntity { + String message() default ""; + + Class[] groups() default {}; + + Class[] payload() default {}; + } + + public static class ValidEntityValidator implements ConstraintValidator { + @Inject + ValidConfig validConfig; + + @Override + public boolean isValid(Entity value, ConstraintValidatorContext context) { + return validConfig.isValid(); + } + } + + @ValidEntity + public static class Entity { + + } +} diff --git a/extensions/hibernate-validator/deployment/src/test/java/io/quarkus/hibernate/validator/test/config/ConfigMappingInvalidTest.java b/extensions/hibernate-validator/deployment/src/test/java/io/quarkus/hibernate/validator/test/config/ConfigMappingInvalidTest.java index f2c3680b8cf72..1a7839f09ef12 100644 --- a/extensions/hibernate-validator/deployment/src/test/java/io/quarkus/hibernate/validator/test/config/ConfigMappingInvalidTest.java +++ b/extensions/hibernate-validator/deployment/src/test/java/io/quarkus/hibernate/validator/test/config/ConfigMappingInvalidTest.java @@ -4,11 +4,13 @@ import jakarta.inject.Inject; import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.Size; -import org.eclipse.microprofile.config.Config; import org.jboss.shrinkwrap.api.ShrinkWrap; import org.jboss.shrinkwrap.api.asset.StringAsset; import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; @@ -22,22 +24,54 @@ public class ConfigMappingInvalidTest { @RegisterExtension static final QuarkusUnitTest UNIT_TEST = new QuarkusUnitTest().setArchiveProducer( () -> ShrinkWrap.create(JavaArchive.class) - .addAsResource(new StringAsset("validator.server.host=localhost\n"), "application.properties")); + .addAsResource(new StringAsset("validator.server.host=localhost\n" + + "validator.hierarchy.number=1\n" + + "validator.repeatable.name=a"), "application.properties")); @Inject - Config config; + SmallRyeConfig config; @Test void invalid() { - assertThrows(ConfigValidationException.class, - () -> config.unwrap(SmallRyeConfig.class).getConfigMapping(Server.class), + assertThrows(ConfigValidationException.class, () -> config.getConfigMapping(Server.class), "validator.server.host must be less than or equal to 3"); } + @Test + @Disabled("Requires https://github.com/smallrye/smallrye-config/pull/923") + void invalidHierarchy() { + assertThrows(ConfigValidationException.class, () -> config.getConfigMapping(Child.class), + "validator.hierarchy.number must be greater than or equal to 10"); + } + + @Test + void repeatable() { + assertThrows(ConfigValidationException.class, () -> config.getConfigMapping(Repeatable.class)); + } + @Unremovable @ConfigMapping(prefix = "validator.server") public interface Server { @Max(3) String host(); } + + public interface Parent { + @Min(10) + Integer number(); + } + + @Unremovable + @ConfigMapping(prefix = "validator.hierarchy") + public interface Child extends Parent { + + } + + @Unremovable + @ConfigMapping(prefix = "validator.repeatable") + public interface Repeatable { + @Size(max = 10) + @Size(min = 2) + String name(); + } } diff --git a/extensions/hibernate-validator/runtime/src/main/java/io/quarkus/hibernate/validator/runtime/HibernateBeanValidationConfigValidator.java b/extensions/hibernate-validator/runtime/src/main/java/io/quarkus/hibernate/validator/runtime/HibernateBeanValidationConfigValidator.java index 774e7a5e3c7e9..ff08220c4865c 100644 --- a/extensions/hibernate-validator/runtime/src/main/java/io/quarkus/hibernate/validator/runtime/HibernateBeanValidationConfigValidator.java +++ b/extensions/hibernate-validator/runtime/src/main/java/io/quarkus/hibernate/validator/runtime/HibernateBeanValidationConfigValidator.java @@ -1,12 +1,55 @@ package io.quarkus.hibernate.validator.runtime; +import java.util.Set; + +import jakarta.validation.Validation; import jakarta.validation.Validator; +import jakarta.validation.ValidatorFactory; + +import org.hibernate.validator.PredefinedScopeHibernateValidator; +import org.hibernate.validator.PredefinedScopeHibernateValidatorConfiguration; +import org.hibernate.validator.internal.engine.constraintvalidation.ConstraintValidatorFactoryImpl; import io.smallrye.config.validator.BeanValidationConfigValidator; public class HibernateBeanValidationConfigValidator implements BeanValidationConfigValidator { + + public HibernateBeanValidationConfigValidator(Set constraints, Set> classesToBeValidated) { + PredefinedScopeHibernateValidatorConfiguration configuration = Validation + .byProvider(PredefinedScopeHibernateValidator.class) + .configure(); + + // TODO - There is no way to retrieve locales from configuration here (even manually). We need to add a way to configure the validator from SmallRye Config. + configuration + .ignoreXmlConfiguration() + .builtinConstraints(constraints) + .initializeBeanMetaData(classesToBeValidated) + .constraintValidatorFactory(new ConstraintValidatorFactoryImpl()); + + ConfigValidatorHolder.initialize(configuration.buildValidatorFactory()); + } + @Override public Validator getValidator() { - return ValidatorHolder.getValidator(); + return ConfigValidatorHolder.getValidator(); + } + + // Store in a holder, so we can easily reference it and shutdown the validator + public static class ConfigValidatorHolder { + private static ValidatorFactory validatorFactory; + private static Validator validator; + + static void initialize(ValidatorFactory validatorFactory) { + ConfigValidatorHolder.validatorFactory = validatorFactory; + ConfigValidatorHolder.validator = validatorFactory.getValidator(); + } + + static ValidatorFactory getValidatorFactory() { + return validatorFactory; + } + + static Validator getValidator() { + return validator; + } } } diff --git a/extensions/hibernate-validator/runtime/src/main/java/io/quarkus/hibernate/validator/runtime/HibernateValidatorRecorder.java b/extensions/hibernate-validator/runtime/src/main/java/io/quarkus/hibernate/validator/runtime/HibernateValidatorRecorder.java index fd24d663fda7e..00564dc90488d 100644 --- a/extensions/hibernate-validator/runtime/src/main/java/io/quarkus/hibernate/validator/runtime/HibernateValidatorRecorder.java +++ b/extensions/hibernate-validator/runtime/src/main/java/io/quarkus/hibernate/validator/runtime/HibernateValidatorRecorder.java @@ -37,6 +37,19 @@ @Recorder public class HibernateValidatorRecorder { + public void shutdownConfigValidator(ShutdownContext shutdownContext) { + shutdownContext.addShutdownTask(new Runnable() { + @Override + public void run() { + ValidatorFactory validatorFactory = HibernateBeanValidationConfigValidator.ConfigValidatorHolder + .getValidatorFactory(); + if (validatorFactory != null) { + validatorFactory.close(); + } + } + }); + } + public BeanContainerListener initializeValidatorFactory(Set> classesToBeValidated, Set detectedBuiltinConstraints, Set> valueExtractorClasses, boolean hasXmlConfiguration, boolean jpaInClasspath, diff --git a/extensions/hibernate-validator/runtime/src/main/resources/META-INF/services/io.smallrye.config.ConfigValidator b/extensions/hibernate-validator/runtime/src/main/resources/META-INF/services/io.smallrye.config.ConfigValidator deleted file mode 100644 index 5c35a7ee00e44..0000000000000 --- a/extensions/hibernate-validator/runtime/src/main/resources/META-INF/services/io.smallrye.config.ConfigValidator +++ /dev/null @@ -1 +0,0 @@ -io.quarkus.hibernate.validator.runtime.HibernateBeanValidationConfigValidator diff --git a/integration-tests/hibernate-validator-resteasy-reactive/src/test/java/io/quarkus/it/hibernate/validator/ConfigMappingStartupValidatorTest.java b/integration-tests/hibernate-validator-resteasy-reactive/src/test/java/io/quarkus/it/hibernate/validator/ConfigMappingStartupValidatorTest.java index 94e82cd2f1e8c..3abcdfc7058e2 100644 --- a/integration-tests/hibernate-validator-resteasy-reactive/src/test/java/io/quarkus/it/hibernate/validator/ConfigMappingStartupValidatorTest.java +++ b/integration-tests/hibernate-validator-resteasy-reactive/src/test/java/io/quarkus/it/hibernate/validator/ConfigMappingStartupValidatorTest.java @@ -4,7 +4,9 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import java.util.HashMap; +import java.util.HashSet; import java.util.Map; +import java.util.Set; import jakarta.inject.Inject; import jakarta.validation.constraints.Pattern; @@ -22,6 +24,8 @@ import io.quarkus.test.junit.QuarkusTestProfile; import io.quarkus.test.junit.TestProfile; import io.smallrye.config.ConfigMapping; +import io.smallrye.config.ConfigMappingLoader; +import io.smallrye.config.ConfigMappingMetadata; import io.smallrye.config.ConfigValidationException; import io.smallrye.config.ConfigValidationException.Problem; import io.smallrye.config.ConfigValidator; @@ -34,8 +38,8 @@ public class ConfigMappingStartupValidatorTest { @ConfigMapping(prefix = "config") - public static interface ConfigWithValidation { - public static final String CONFIG_WITH_VALIDATION_VALUE_MUST_BE_D_3 = "ConfigWithValidation.value() must be \"-\\d{3}\""; + public interface ConfigWithValidation { + String CONFIG_WITH_VALIDATION_VALUE_MUST_BE_D_3 = "ConfigWithValidation.value() must be \"-\\d{3}\""; @WithDefault("invalid-value") @Pattern(regexp = "-\\d{3}", message = CONFIG_WITH_VALIDATION_VALUE_MUST_BE_D_3) @@ -57,10 +61,21 @@ public boolean disableGlobalTestResources() { @BeforeAll public static void doBefore() { + Set constraints = Set.of(Pattern.class.getName()); + Set> classesToBeValidated = new HashSet<>(); + for (ConfigMappingMetadata configMappingMetadata : ConfigMappingLoader + .getConfigMappingsMetadata(ConfigWithValidation.class)) { + try { + classesToBeValidated.add(Class.forName(configMappingMetadata.getClassName())); + } catch (ClassNotFoundException e) { + // Ignore + } + } + SmallRyeConfigBuilder builder = new SmallRyeConfigBuilder(); builder.withMapping(ConfigWithValidation.class).setAddDefaultSources(true) .withValidator(new ConfigValidator() { - final ConfigValidator base = new HibernateBeanValidationConfigValidator(); + final ConfigValidator base = new HibernateBeanValidationConfigValidator(constraints, classesToBeValidated); @Override public void validateMapping(Class mappingClass, String prefix, Object mappingObject)