Skip to content

Commit

Permalink
Fix type argument annotation inheritance (#311)
Browse files Browse the repository at this point in the history
See #228
  • Loading branch information
altro3 authored Feb 19, 2024
1 parent 54137b4 commit 89efd63
Show file tree
Hide file tree
Showing 7 changed files with 256 additions and 20 deletions.
19 changes: 10 additions & 9 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
@@ -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

Expand Down
4 changes: 3 additions & 1 deletion tests/jakarta-validation-tck/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ plugins {
}

dependencies {
runtimeOnly(mnLogging.logback.classic)
implementation mn.micronaut.inject.java
implementation mn.micronaut.inject
implementation mn.micronaut.aop
Expand All @@ -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"
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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;
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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;
Expand Down Expand Up @@ -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<String> 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<String, ClassElement> typeArguments = element.getGenericType().getTypeArguments();
Map<String, ClassElement> parentTypeArguments = parentElement.getGenericType().getTypeArguments();
if (typeArguments.size() != parentTypeArguments.size()) {
return;
}
for (var entry : typeArguments.entrySet()) {
inheritAnnotationsForParameter(entry.getValue(), parentTypeArguments.get(entry.getKey()));
}
}
}
Original file line number Diff line number Diff line change
@@ -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<String>> 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<List<String>>)

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<List<String>> 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<String, List<@NotNull List<@Pattern(regexp = "[a-zA-Z]+") @Size(min = 3) @NotNull String>>> list);
}
''')
when:
def method = definition.getRequiredMethod("map", Map<String, List<String>>)

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<String, @Size(min=2) String> value) {
}
}
abstract class AbstractTest {
void map(Map<String, @Size(min=2) String> value) {
}
}
''')
when:
def method = definition.getRequiredMethod("map", Map<String, List<String>>)

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
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -1489,7 +1489,7 @@ private <T> 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
Expand All @@ -1505,7 +1505,7 @@ public static <T> 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -283,9 +283,9 @@ final class InternalConstraintValidators {

final SizeValidator<CharSequence> sizeCharSequenceValidator = CharSequence::length;

final SizeValidator<Collection> sizeCollectionValidator = Collection::size;
final SizeValidator<Collection<?>> sizeCollectionValidator = Collection::size;

final SizeValidator<Map> sizeMapValidator = Map::size;
final SizeValidator<Map<?, ?>> sizeMapValidator = Map::size;

final ConstraintValidator<Past, TemporalAccessor> pastTemporalAccessorConstraintValidator =
(value, annotationMetadata, context) -> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ final class InternalValueExtractors {
*/
private static final String LIST_ELEMENT_NODE_NAME = "<list element>";
/**
* 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 = "<map key>";
/**
Expand Down

0 comments on commit 89efd63

Please sign in to comment.