diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 3926eb67..57bbdf76 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -7,13 +7,13 @@ arquillian-container = "1.9.1.Final" javax-annotation-api = "1.3.2" jakarta-validation-tck = "3.0.1" -micronaut = "4.6.6" -micronaut-platform = "4.4.3" +micronaut = "4.7.0" +micronaut-platform = "4.6.3" micronaut-docs = "2.0.0" micronaut-test = "4.5.0" -micronaut-reactor = "3.4.1" -micronaut-rxjava2 = "2.4.0" -micronaut-kotlin = "4.3.0" +micronaut-reactor = "3.5.0" +micronaut-rxjava2 = "2.5.0" +micronaut-kotlin = "4.4.0" micronaut-logging = "1.3.0" # Gradle plugins diff --git a/tests/jakarta-validation-tck/tck-tests.xml b/tests/jakarta-validation-tck/tck-tests.xml index d19397af..42266ad8 100644 --- a/tests/jakarta-validation-tck/tck-tests.xml +++ b/tests/jakarta-validation-tck/tck-tests.xml @@ -368,6 +368,13 @@ + + + + + + + diff --git a/validation/src/main/java/io/micronaut/validation/validator/extractors/DefaultValueExtractors.java b/validation/src/main/java/io/micronaut/validation/validator/extractors/DefaultValueExtractors.java index 942d04bf..ff7977d8 100644 --- a/validation/src/main/java/io/micronaut/validation/validator/extractors/DefaultValueExtractors.java +++ b/validation/src/main/java/io/micronaut/validation/validator/extractors/DefaultValueExtractors.java @@ -23,6 +23,7 @@ import io.micronaut.core.annotation.Nullable; import io.micronaut.core.type.Argument; import io.micronaut.core.util.CollectionUtils; +import io.micronaut.inject.BeanDefinition; import jakarta.inject.Inject; import jakarta.inject.Singleton; import jakarta.validation.valueextraction.ValueExtractor; @@ -78,10 +79,23 @@ protected DefaultValueExtractors(@Nullable BeanContext beanContext) { final Collection> valueExtractors = beanContext.getBeanRegistrations(ValueExtractor.class); if (CollectionUtils.isNotEmpty(valueExtractors)) { for (BeanRegistration reg : valueExtractors) { - addValueExtractor(localValueExtractors, new ValueExtractorDefinition( - reg.getBeanDefinition().asArgument(), - reg.getBean() - )); + BeanDefinition beanDefinition = reg.getBeanDefinition(); + Argument argument = beanDefinition.asArgument(); + if (argument.getType().equals(ValueExtractor.class)) { + addValueExtractor(localValueExtractors, new ValueExtractorDefinition( + argument, + reg.getBean() + )); + } else { + List> typeArguments = beanDefinition.getTypeArguments(ValueExtractor.class); + if (typeArguments.isEmpty()) { + throw new IllegalStateException("No value-extractors found for bean definition: " + beanDefinition); + } + addValueExtractor(localValueExtractors, new ValueExtractorDefinition( + Argument.of(ValueExtractor.class, beanDefinition.getAnnotationMetadata(), typeArguments.toArray(new Argument[0])), + reg.getBean() + )); + } } } } diff --git a/validation/src/main/java/io/micronaut/validation/validator/extractors/ValueExtractorDefinition.java b/validation/src/main/java/io/micronaut/validation/validator/extractors/ValueExtractorDefinition.java index 296f4622..0d78956a 100644 --- a/validation/src/main/java/io/micronaut/validation/validator/extractors/ValueExtractorDefinition.java +++ b/validation/src/main/java/io/micronaut/validation/validator/extractors/ValueExtractorDefinition.java @@ -82,6 +82,10 @@ private static Integer findExtractedTypeArgumentIndex(@NotNull Argument argum if (typeArgumentIndex != null) { return typeArgumentIndex; } + if (typeParameters.length == 1) { + // On missing @ExtractedValue select first type parameter by default + return 0; + } throw new ValueExtractorDefinitionException("ValueExtractor definition is missing @ExtractedValue on an argument: " + argument); } @@ -96,6 +100,10 @@ private static AnnotationValue findExtractedValue(@NotNull Argument argume } AnnotationValue annotationValue = argument.getAnnotationMetadata().getAnnotation(ExtractedValue.class); if (annotationValue == null) { + if (typeParameters.length == 1) { + // On missing @ExtractedValue select first type parameter by default + return AnnotationValue.builder(ExtractedValue.class).build(); + } throw new ValueExtractorDefinitionException("ValueExtractor definition '" + valueExtractor + "' is missing @ExtractedValue!"); } if (annotationValue.classValue("type").isEmpty()) { diff --git a/validation/src/test/groovy/io/micronaut/validation/validator/constraints/ConstraintUnwrapSpec.groovy b/validation/src/test/groovy/io/micronaut/validation/validator/constraints/ConstraintUnwrapSpec.groovy index 6f2674d1..1a6ab096 100644 --- a/validation/src/test/groovy/io/micronaut/validation/validator/constraints/ConstraintUnwrapSpec.groovy +++ b/validation/src/test/groovy/io/micronaut/validation/validator/constraints/ConstraintUnwrapSpec.groovy @@ -1,18 +1,12 @@ package io.micronaut.validation.validator.constraints -import io.micronaut.annotation.processing.TypeElementVisitorProcessor + import io.micronaut.annotation.processing.test.AbstractTypeElementSpec import io.micronaut.context.ApplicationContext -import io.micronaut.inject.beans.visitor.IntrospectedTypeElementVisitor -import io.micronaut.inject.visitor.TypeElementVisitor import io.micronaut.validation.validator.Validator -import io.micronaut.validation.visitor.IntrospectedValidationIndexesVisitor -import io.micronaut.validation.visitor.ValidationVisitor import spock.lang.AutoCleanup import spock.lang.Shared -import javax.annotation.processing.SupportedAnnotationTypes - class ConstraintUnwrapSpec extends AbstractTypeElementSpec { @Shared @@ -259,11 +253,4 @@ class Test { constraintViolations.iterator().next().message == "must not be null" } - @SupportedAnnotationTypes("*") - static class MyTypeElementVisitorProcessor extends TypeElementVisitorProcessor { - @Override - protected Collection findTypeElementVisitors() { - return [new ValidationVisitor(), new IntrospectedValidationIndexesVisitor(), new IntrospectedTypeElementVisitor()] - } - } } diff --git a/validation/src/test/groovy/io/micronaut/validation/validator/constraints/unwrapped/MyOptional.java b/validation/src/test/groovy/io/micronaut/validation/validator/constraints/unwrapped/MyOptional.java new file mode 100644 index 00000000..a9c18713 --- /dev/null +++ b/validation/src/test/groovy/io/micronaut/validation/validator/constraints/unwrapped/MyOptional.java @@ -0,0 +1,4 @@ +package io.micronaut.validation.validator.constraints.unwrapped; + +public record MyOptional(V value) { +} diff --git a/validation/src/test/groovy/io/micronaut/validation/validator/constraints/unwrapped/MyOptionalExtractor.java b/validation/src/test/groovy/io/micronaut/validation/validator/constraints/unwrapped/MyOptionalExtractor.java new file mode 100644 index 00000000..2b12b929 --- /dev/null +++ b/validation/src/test/groovy/io/micronaut/validation/validator/constraints/unwrapped/MyOptionalExtractor.java @@ -0,0 +1,16 @@ +package io.micronaut.validation.validator.constraints.unwrapped; + +import jakarta.inject.Singleton; +import jakarta.validation.valueextraction.UnwrapByDefault; +import jakarta.validation.valueextraction.ValueExtractor; + +@UnwrapByDefault +@Singleton +public class MyOptionalExtractor implements ValueExtractor> { + + @Override + public void extractValues(MyOptional originalValue, ValueReceiver receiver) { + receiver.value("value", originalValue.value()); + } + +} diff --git a/validation/src/test/groovy/io/micronaut/validation/validator/constraints/unwrapped/ValueExtractorsSpec.groovy b/validation/src/test/groovy/io/micronaut/validation/validator/constraints/unwrapped/ValueExtractorsSpec.groovy new file mode 100644 index 00000000..455e3ba5 --- /dev/null +++ b/validation/src/test/groovy/io/micronaut/validation/validator/constraints/unwrapped/ValueExtractorsSpec.groovy @@ -0,0 +1,51 @@ +package io.micronaut.validation.validator.constraints.unwrapped + + +import io.micronaut.annotation.processing.test.AbstractTypeElementSpec +import io.micronaut.context.ApplicationContext +import io.micronaut.validation.validator.Validator +import spock.lang.AutoCleanup +import spock.lang.Shared + +class ValueExtractorsSpec extends AbstractTypeElementSpec { + + @Shared + @AutoCleanup + ApplicationContext context = ApplicationContext.run() + + @Shared + Validator validator = context.getBean(Validator) + + void "test @NotNull should be applied on the optional value"() { + given: + def introspection = buildBeanIntrospection('test.Test', """ +package test; + +import jakarta.validation.Payload; +import jakarta.validation.constraints.NotNull; +import io.micronaut.validation.validator.constraints.unwrapped.MyOptional; + +@io.micronaut.core.annotation.Introspected +class Test { + @NotNull + private MyOptional field; + + public MyOptional getField() { + return field; + } + + public void setField(MyOptional f) { + this.field = f; + } +} +""") + def instance = introspection.instantiate() + def prop = introspection.getProperty("field").get() + prop.set(instance, new MyOptional(null)) + def constraintViolations = validator.validate(introspection, instance) + + expect: + constraintViolations.size() == 1 + constraintViolations.iterator().next().message == "must not be null" + } +}