Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix type argument annotation inheritance #311

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading