diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 16278e29..7acd880f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,19 +1,20 @@ [versions] -micronaut = "4.3.1" -micronaut-platform = "4.1.5" -micronaut-docs = "2.0.0" -micronaut-test = "4.2.0" -micronaut-reactor = "3.2.1" -micronaut-rxjava2 = "2.2.1" -micronaut-kotlin = "4.2.0" -micronaut-logging = "1.1.2" +managed-validation = "3.0.2" groovy = "4.0.18" spotbugs = "4.8.3" arquillian-container = "1.8.0.Final" javax-annotation-api = "1.3.2" jakarta-validation-tck = "3.0.1" -managed-validation = "3.0.2" + +micronaut = "4.3.7" +micronaut-platform = "4.3.2" +micronaut-docs = "2.0.0" +micronaut-test = "4.2.0" +micronaut-reactor = "3.2.1" +micronaut-rxjava2 = "2.2.1" +micronaut-kotlin = "4.2.0" +micronaut-logging = "1.2.3" # Gradle plugins diff --git a/tests/jakarta-validation-tck/build.gradle b/tests/jakarta-validation-tck/build.gradle index 1e51f8bd..c54b085a 100644 --- a/tests/jakarta-validation-tck/build.gradle +++ b/tests/jakarta-validation-tck/build.gradle @@ -4,7 +4,6 @@ plugins { } dependencies { - runtimeOnly(mnLogging.logback.classic) implementation mn.micronaut.inject.java implementation mn.micronaut.inject implementation mn.micronaut.aop @@ -15,6 +14,9 @@ dependencies { implementation libs.arquillian.container implementation libs.jakarta.validation.tck.tests + + runtimeOnly(mnLogging.logback.classic) + testImplementation(libs.jakarta.validation.tck.tests) { artifact { classifier = "sources" diff --git a/validation-processor/src/main/java/io/micronaut/validation/visitor/ValidationVisitor.java b/validation-processor/src/main/java/io/micronaut/validation/visitor/ValidationVisitor.java index 85e77cb8..c2144f2c 100644 --- a/validation-processor/src/main/java/io/micronaut/validation/visitor/ValidationVisitor.java +++ b/validation-processor/src/main/java/io/micronaut/validation/visitor/ValidationVisitor.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2020 original authors + * Copyright 2017-2024 original authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,6 +15,10 @@ */ package io.micronaut.validation.visitor; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.stream.Stream; import io.micronaut.core.annotation.AnnotationMetadata; import io.micronaut.core.annotation.Internal; @@ -33,9 +37,6 @@ import io.micronaut.inject.visitor.TypeElementVisitor; import io.micronaut.inject.visitor.VisitorContext; -import java.util.HashSet; -import java.util.Set; - /** * The visitor creates annotations utilized by the Validator. * It adds @RequiresValidation annotation to fields if they require validation, and to methods @@ -78,7 +79,6 @@ public void visitClass(ClassElement element, VisitorContext context) { classElement.annotate(Introspected.class); } classElement.getMethods().forEach(m -> visitMethod(m, context)); -// classElement.getFields().forEach(f -> visitField(f, context)); } @Override @@ -105,6 +105,9 @@ public void visitMethod(MethodElement element, VisitorContext context) { if (!visited.add(element)) { return; } + + element.getOverriddenMethods().forEach(m -> inheritAnnotationsForMethod(element, m)); + boolean isPrivate = element.isPrivate(); boolean isAbstract = element.getOwningType().isInterface() || element.getOwningType().isAbstract(); boolean requireOnConstraint = isAbstract || !isPrivate; @@ -182,4 +185,45 @@ private boolean visitTypedElementValidationAndMarkForValidationIfNeeded(TypedEle } return requires; } + + /** + * Method that makes sure that all the annotations are inherited from parent. + * In particular, type arguments annotations are not inherited by default. + */ + private void inheritAnnotationsForMethod(MethodElement method, MethodElement parent) { + ParameterElement[] methodParameters = method.getParameters(); + ParameterElement[] parentParameters = parent.getParameters(); + + for (int i = 0; i < methodParameters.length; ++i) { + inheritAnnotationsForParameter(methodParameters[i], parentParameters[i]); + } + inheritAnnotationsForParameter(method.getReturnType(), parent.getReturnType()); + } + + /** + * Method that makes sure that all the annotations are inherited from parent. + * In particular, type arguments annotations are not inherited by default. + */ + private void inheritAnnotationsForParameter(TypedElement element, TypedElement parentElement) { + if (!element.getType().equals(parentElement.getType())) { + return; + } + Stream parentAnnotations = Stream.concat( + parentElement.getAnnotationNamesByStereotype(ANN_CONSTRAINT).stream(), + parentElement.getAnnotationNamesByStereotype(ANN_VALID).stream() + ); + parentAnnotations + .filter(name -> !element.hasAnnotation(name)) + .flatMap(name -> parentElement.getAnnotationValuesByName(name).stream()) + .forEach(element::annotate); + + Map typeArguments = element.getGenericType().getTypeArguments(); + Map parentTypeArguments = parentElement.getGenericType().getTypeArguments(); + if (typeArguments.size() != parentTypeArguments.size()) { + return; + } + for (var entry : typeArguments.entrySet()) { + inheritAnnotationsForParameter(entry.getValue(), parentTypeArguments.get(entry.getKey())); + } + } } diff --git a/validation-processor/src/test/groovy/io/micronaut/validation/visitor/ValidatedTypeArgumentInheritanceSpec.groovy b/validation-processor/src/test/groovy/io/micronaut/validation/visitor/ValidatedTypeArgumentInheritanceSpec.groovy new file mode 100644 index 00000000..d3328083 --- /dev/null +++ b/validation-processor/src/test/groovy/io/micronaut/validation/visitor/ValidatedTypeArgumentInheritanceSpec.groovy @@ -0,0 +1,189 @@ +package io.micronaut.validation.visitor + +import io.micronaut.annotation.processing.test.AbstractTypeElementSpec +import io.micronaut.validation.annotation.ValidatedElement +import jakarta.validation.constraints.NotBlank +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size + +class ValidatedTypeArgumentInheritanceSpec extends AbstractTypeElementSpec { + + final static String VALIDATED_ANN = "io.micronaut.validation.Validated" + + void "test constraints inherit for generic parameters"() { + given: + def definition = buildBeanDefinition('test.Test',''' +package test; + +import java.util.List; + +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Pattern; +import jakarta.validation.constraints.Size; + +@jakarta.inject.Singleton +class Test implements TestBase { + @Override + public void setList(List> list) { + } +} + +interface TestBase { + @io.micronaut.context.annotation.Executable + void setList(List<@NotNull List<@Pattern(regexp = "[a-zA-Z]+") @Size(min = 3) @NotNull String>> list); +} +''') + when: + def method = definition.getRequiredMethod("setList", List>) + + then: + method.hasStereotype(VALIDATED_ANN) + method.arguments.size() == 1 + method.arguments[0].annotationMetadata.hasAnnotation(ValidatedElement) + method.arguments[0].typeParameters.size() == 1 + + def firstTypeParamAnnMetadataAnnMetadata = method.arguments[0].typeParameters[0].annotationMetadata + + firstTypeParamAnnMetadataAnnMetadata.hasAnnotation(NotNull) + firstTypeParamAnnMetadataAnnMetadata.hasAnnotation(ValidatedElement) + method.arguments[0].typeParameters[0].typeParameters.size() == 1 + + def secTypeParamAnnMetadata = method.arguments[0].typeParameters[0].typeParameters[0].annotationMetadata + + secTypeParamAnnMetadata.hasAnnotation(NotNull) + secTypeParamAnnMetadata.hasAnnotation(Size) + secTypeParamAnnMetadata.getAnnotation(Size).intValue("min").orElse(-1) == 3 + secTypeParamAnnMetadata.hasAnnotation(Pattern) + secTypeParamAnnMetadata.getAnnotation(Pattern).stringValue("regexp").orElse(null) == "[a-zA-Z]+" + } + + void "test constraints inherit for generic parameters of return type"() { + given: + def definition = buildBeanDefinition('test.Test',''' +package test; + +import java.util.List; + +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Pattern; +import jakarta.validation.constraints.Size; + +@jakarta.inject.Singleton +class Test implements TestBase { + @Override + public List> getList() { + return null; + } +} + +interface TestBase { + @io.micronaut.context.annotation.Executable + List<@NotNull List<@Pattern(regexp = "[a-zA-Z]+") @Size(min = 3) @NotNull String>> getList(); +} +''') + when: + def method = definition.getRequiredMethod("getList") + + then: + method.hasStereotype(VALIDATED_ANN) + method.returnType.annotationMetadata.hasAnnotation("io.micronaut.validation.annotation.ValidatedElement") + method.returnType.typeParameters.size() == 1 + def firstTypeParamAnnMetadataAnnMetadata = method.returnType.typeParameters[0].annotationMetadata + + firstTypeParamAnnMetadataAnnMetadata.hasAnnotation(NotNull) + firstTypeParamAnnMetadataAnnMetadata.hasAnnotation(ValidatedElement) + method.returnType.typeParameters[0].typeParameters.size() == 1 + + def secTypeParamAnnMetadata = method.returnType.typeParameters[0].typeParameters[0].annotationMetadata + + secTypeParamAnnMetadata.hasAnnotation(NotNull) + secTypeParamAnnMetadata.hasAnnotation(Size) + secTypeParamAnnMetadata.getAnnotation(Size).intValue("min").orElse(-1) == 3 + secTypeParamAnnMetadata.hasAnnotation(Pattern) + secTypeParamAnnMetadata.getAnnotation(Pattern).stringValue("regexp").orElse(null) == "[a-zA-Z]+" + } + + void "test constraints inherit for deep generic parameters"() { + given: + def definition = buildBeanDefinition('test.Test',''' +package test; + +import java.util.List; +import java.util.Map; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Pattern; +import jakarta.validation.constraints.Size; + +@jakarta.inject.Singleton +class Test implements TestBase { + @Override + public void map(Map<@NotBlank String, List<@NotNull List<@NotBlank String>>> list) { + } +} + +interface TestBase { + @io.micronaut.context.annotation.Executable + void map(Map>> list); +} +''') + when: + def method = definition.getRequiredMethod("map", Map>) + + then: + method.hasStereotype(VALIDATED_ANN) + method.arguments.size() == 1 + method.arguments[0].annotationMetadata.hasAnnotation("io.micronaut.validation.annotation.ValidatedElement") + method.arguments[0].typeParameters.size() == 2 + method.arguments[0].typeParameters[0].annotationMetadata.hasAnnotation(NotBlank) + method.arguments[0].typeParameters[0].annotationMetadata.hasAnnotation(ValidatedElement) + method.arguments[0].typeParameters[1].annotationMetadata.hasAnnotation(ValidatedElement) + method.arguments[0].typeParameters[1].typeParameters.length == 1 + method.arguments[0].typeParameters[1].typeParameters[0].annotationMetadata.hasAnnotation(ValidatedElement) + method.arguments[0].typeParameters[1].typeParameters[0].annotationMetadata.hasAnnotation(NotNull) + + method.arguments[0].typeParameters[1].typeParameters[0].typeParameters.length == 1 + method.arguments[0].typeParameters[1].typeParameters[0].typeParameters[0].annotationMetadata.hasAnnotation(ValidatedElement) + method.arguments[0].typeParameters[1].typeParameters[0].typeParameters[0].annotationMetadata.hasAnnotation(Pattern) + method.arguments[0].typeParameters[1].typeParameters[0].typeParameters[0].annotationMetadata.hasAnnotation(NotNull) + method.arguments[0].typeParameters[1].typeParameters[0].typeParameters[0].annotationMetadata.hasAnnotation(Size) + method.arguments[0].typeParameters[1].typeParameters[0].typeParameters[0].annotationMetadata.hasAnnotation(NotBlank) + } + + void "test constraints inherit for generic parameters from abstract class"() { + given: + def definition = buildBeanDefinition('test.Test',''' +package test; + +import java.util.*; +import jakarta.validation.constraints.Size; + +@jakarta.inject.Singleton +class Test extends AbstractTest { + @Override + public void map(Map value) { + } +} + +abstract class AbstractTest { + void map(Map value) { + + } +} +''') + when: + def method = definition.getRequiredMethod("map", Map>) + + then: + method.hasStereotype(VALIDATED_ANN) + method.arguments.size() == 1 + method.arguments[0].annotationMetadata.hasAnnotation("io.micronaut.validation.annotation.ValidatedElement") + method.arguments[0].typeParameters.size() == 2 + var anns = method.arguments[0].typeParameters[1].annotationMetadata.getAnnotationValuesByType(Size) + anns.size() == 1 + anns.get(0).intValue("min").get() == 2 + } + +} diff --git a/validation/src/main/java/io/micronaut/validation/validator/DefaultValidator.java b/validation/src/main/java/io/micronaut/validation/validator/DefaultValidator.java index 248d7015..b8594a6f 100644 --- a/validation/src/main/java/io/micronaut/validation/validator/DefaultValidator.java +++ b/validation/src/main/java/io/micronaut/validation/validator/DefaultValidator.java @@ -1489,7 +1489,7 @@ private void failOnError(@NonNull BeanResolutionContext resolutionContext, } /** - * Throws a {@link IllegalArgumentException} if the {@value} is null. + * Throws a {@link IllegalArgumentException} if the value is null. * @param name check name * @param value value being checked * @return the value @@ -1505,7 +1505,7 @@ public static T requireNonNull(String name, T value) { } /** - * Throws a {@link IllegalArgumentException} if the {@value} null or an empty string. + * Throws a {@link IllegalArgumentException} if the value null or an empty string. * @param name check name * @param value value being checked * @return the value diff --git a/validation/src/main/java/io/micronaut/validation/validator/constraints/InternalConstraintValidators.java b/validation/src/main/java/io/micronaut/validation/validator/constraints/InternalConstraintValidators.java index 2a5fc359..9f5d4275 100644 --- a/validation/src/main/java/io/micronaut/validation/validator/constraints/InternalConstraintValidators.java +++ b/validation/src/main/java/io/micronaut/validation/validator/constraints/InternalConstraintValidators.java @@ -283,9 +283,9 @@ final class InternalConstraintValidators { final SizeValidator sizeCharSequenceValidator = CharSequence::length; - final SizeValidator sizeCollectionValidator = Collection::size; + final SizeValidator> sizeCollectionValidator = Collection::size; - final SizeValidator sizeMapValidator = Map::size; + final SizeValidator> sizeMapValidator = Map::size; final ConstraintValidator pastTemporalAccessorConstraintValidator = (value, annotationMetadata, context) -> { diff --git a/validation/src/main/java/io/micronaut/validation/validator/extractors/InternalValueExtractors.java b/validation/src/main/java/io/micronaut/validation/validator/extractors/InternalValueExtractors.java index e0674439..1b50a230 100644 --- a/validation/src/main/java/io/micronaut/validation/validator/extractors/InternalValueExtractors.java +++ b/validation/src/main/java/io/micronaut/validation/validator/extractors/InternalValueExtractors.java @@ -47,7 +47,7 @@ final class InternalValueExtractors { */ private static final String LIST_ELEMENT_NODE_NAME = ""; /** - * Node name for an key element of a map. + * Node name for a key element of a map. */ private static final String MAP_KEY_NODE_NAME = ""; /**