From 81f08f00c61be8fc67ce53d137e3931fc8dfcb97 Mon Sep 17 00:00:00 2001 From: nKwiatkowski Date: Wed, 11 Dec 2024 11:42:48 +0100 Subject: [PATCH] feat(core): add partial fix to micronaut hibernate validator and ValueExtractor --- .../kestra/core/models/property/Property.java | 5 + .../extractors/PropertyValueExtractor.java | 15 +++ .../CustomValidatorFactoryProvider.java | 105 ++++++++++++++++++ .../extractors/DynamicPropertyDto.java | 19 ++++ .../PropertyValueExtractorTest.java | 35 ++++++ 5 files changed, 179 insertions(+) create mode 100644 core/src/main/java/io/kestra/core/validations/extractors/PropertyValueExtractor.java create mode 100644 core/src/main/java/io/kestra/core/validations/factory/CustomValidatorFactoryProvider.java create mode 100644 core/src/test/java/io/kestra/core/validations/extractors/DynamicPropertyDto.java create mode 100644 core/src/test/java/io/kestra/core/validations/extractors/PropertyValueExtractorTest.java diff --git a/core/src/main/java/io/kestra/core/models/property/Property.java b/core/src/main/java/io/kestra/core/models/property/Property.java index 48c777723c0..cecf9365471 100644 --- a/core/src/main/java/io/kestra/core/models/property/Property.java +++ b/core/src/main/java/io/kestra/core/models/property/Property.java @@ -44,6 +44,11 @@ public class Property { private String expression; private T value; + //TODO: Temporary to make the ValueExtractor work, but it's not supposed to say here + public T getValue(){ + return this.value; + } + // used only by the deserializer and in tests @VisibleForTesting public Property(String expression) { diff --git a/core/src/main/java/io/kestra/core/validations/extractors/PropertyValueExtractor.java b/core/src/main/java/io/kestra/core/validations/extractors/PropertyValueExtractor.java new file mode 100644 index 00000000000..beecb538d13 --- /dev/null +++ b/core/src/main/java/io/kestra/core/validations/extractors/PropertyValueExtractor.java @@ -0,0 +1,15 @@ +package io.kestra.core.validations.extractors; + +import io.kestra.core.models.property.Property; +import io.micronaut.context.annotation.Context; +import jakarta.validation.valueextraction.ExtractedValue; +import jakarta.validation.valueextraction.ValueExtractor; + +@Context +public class PropertyValueExtractor implements ValueExtractor> { + + @Override + public void extractValues(Property originalValue, ValueReceiver receiver) { + receiver.value( null, originalValue.getValue()); + } +} diff --git a/core/src/main/java/io/kestra/core/validations/factory/CustomValidatorFactoryProvider.java b/core/src/main/java/io/kestra/core/validations/factory/CustomValidatorFactoryProvider.java new file mode 100644 index 00000000000..117ef8bec90 --- /dev/null +++ b/core/src/main/java/io/kestra/core/validations/factory/CustomValidatorFactoryProvider.java @@ -0,0 +1,105 @@ +package io.kestra.core.validations.factory; + +import io.kestra.core.validations.extractors.PropertyValueExtractor; +import io.micronaut.configuration.hibernate.validator.ValidatorFactoryProvider; +import io.micronaut.context.annotation.Factory; +import io.micronaut.context.annotation.Replaces; +import io.micronaut.context.annotation.Requires; +import io.micronaut.context.annotation.Value; +import io.micronaut.context.env.Environment; +import io.micronaut.core.annotation.TypeHint; +import jakarta.inject.Inject; +import jakarta.inject.Singleton; +import jakarta.validation.Configuration; +import jakarta.validation.ConstraintValidatorFactory; +import jakarta.validation.MessageInterpolator; +import jakarta.validation.ParameterNameProvider; +import jakarta.validation.TraversableResolver; +import jakarta.validation.Validation; +import jakarta.validation.ValidatorFactory; +import java.util.Map; +import java.util.Optional; +import java.util.Properties; +import org.hibernate.validator.HibernateValidator; +import org.hibernate.validator.messageinterpolation.ParameterMessageInterpolator; + +/** + * Produce a Validator factory provider that replace {@link ValidatorFactoryProvider} from micronaut + * hibernate validator. This has to be done because of a conflict between micronaut validation and micronaut hibernate validation + * that prevent {@link jakarta.validation.valueextraction.ValueExtractor} to work + *
+ * This provider allows to manually register the ValueExtractors. To do that, they have to be injected + * and set in the {@link CustomValidatorFactoryProvider#configureValueExtractor(Configuration)} method + */ +@Factory +@Requires(classes = HibernateValidator.class) +@TypeHint(HibernateValidator.class) +@Replaces(ValidatorFactoryProvider.class) +public class CustomValidatorFactoryProvider { + + @Inject + protected Optional messageInterpolator = Optional.empty(); + + @Inject + protected Optional traversableResolver = Optional.empty(); + + @Inject + protected Optional constraintValidatorFactory = Optional.empty(); + + @Inject + protected Optional parameterNameProvider = Optional.empty(); + + @Inject + protected PropertyValueExtractor propertyValueExtractor; + + @Value("${hibernate.validator.ignore-xml-configuration:true}") + protected boolean ignoreXmlConfiguration = true; + + /** + * Produces a Validator factory class. + * @param environment optional param for environment + * @return validator factory + */ + @Singleton + @Requires(classes = HibernateValidator.class) + @Replaces(ValidatorFactory.class) + ValidatorFactory validatorFactory(Optional environment) { + Configuration validatorConfiguration = Validation.byDefaultProvider() + .configure(); + + validatorConfiguration.messageInterpolator(messageInterpolator.orElseGet(ParameterMessageInterpolator::new)); + messageInterpolator.ifPresent(validatorConfiguration::messageInterpolator); + traversableResolver.ifPresent(validatorConfiguration::traversableResolver); + constraintValidatorFactory.ifPresent(validatorConfiguration::constraintValidatorFactory); + parameterNameProvider.ifPresent(validatorConfiguration::parameterNameProvider); + + if (ignoreXmlConfiguration) { + validatorConfiguration.ignoreXmlConfiguration(); + } + environment.ifPresent(env -> { + Optional config = env.getProperty("hibernate.validator", Properties.class); + config.ifPresent(properties -> { + for (Map.Entry entry : properties.entrySet()) { + Object value = entry.getValue(); + if (value != null) { + validatorConfiguration.addProperty( + "hibernate.validator." + entry.getKey(), + value.toString() + ); + } + } + }); + }); + + configureValueExtractor(validatorConfiguration); + + return validatorConfiguration.buildValidatorFactory(); + } + + /** + * The custom ValueExtractors has to be set here + */ + protected void configureValueExtractor(Configuration validatorConfiguration ){ + validatorConfiguration.addValueExtractor(propertyValueExtractor); + } +} diff --git a/core/src/test/java/io/kestra/core/validations/extractors/DynamicPropertyDto.java b/core/src/test/java/io/kestra/core/validations/extractors/DynamicPropertyDto.java new file mode 100644 index 00000000000..df568f34e74 --- /dev/null +++ b/core/src/test/java/io/kestra/core/validations/extractors/DynamicPropertyDto.java @@ -0,0 +1,19 @@ +package io.kestra.core.validations.extractors; + +import io.kestra.core.models.property.Property; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotNull; + +public class DynamicPropertyDto { + + @NotNull + private Property<@Min(10) Integer> number; + + @NotNull + private Property string; + + public DynamicPropertyDto(Property<@Min(value = 10, message = "must be greater than or equal to {value}") Integer> number, Property string) { + this.number = number; + this.string = string; + } +} \ No newline at end of file diff --git a/core/src/test/java/io/kestra/core/validations/extractors/PropertyValueExtractorTest.java b/core/src/test/java/io/kestra/core/validations/extractors/PropertyValueExtractorTest.java new file mode 100644 index 00000000000..877ed5f0b6a --- /dev/null +++ b/core/src/test/java/io/kestra/core/validations/extractors/PropertyValueExtractorTest.java @@ -0,0 +1,35 @@ +package io.kestra.core.validations.extractors; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import io.kestra.core.models.property.Property; +import io.micronaut.test.extensions.junit5.annotation.MicronautTest; +import io.micronaut.validation.validator.Validator; +import jakarta.inject.Inject; +import jakarta.validation.ConstraintViolation; +import java.util.Optional; +import java.util.Set; +import org.junit.jupiter.api.Test; + +@MicronautTest +public class PropertyValueExtractorTest { + + @Inject + private Validator validator; + + @Test + public void should_extract_and_validate_integer_value(){ + DynamicPropertyDto dto = new DynamicPropertyDto(Property.of(20), Property.of("Test")); + Set> violations = validator.validate(dto); + assertTrue(violations.isEmpty()); + + dto = new DynamicPropertyDto(Property.of(5), Property.of("Test")); + violations = validator.validate(dto); + assertThat(violations.size(), is(1)); + ConstraintViolation violation = violations.stream().findFirst().get(); + assertThat(violation.getMessage(), is("must be greater than or equal to 10")); + } + +}