Skip to content
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
8 changes: 8 additions & 0 deletions all/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -474,6 +474,14 @@
<groupId>io.helidon.openapi</groupId>
<artifactId>helidon-openapi</artifactId>
</dependency>
<dependency>
<groupId>io.helidon.openapi</groupId>
<artifactId>helidon-openapi-31</artifactId>
</dependency>
<dependency>
<groupId>io.helidon.openapi</groupId>
<artifactId>helidon-openapi-32</artifactId>
</dependency>
<dependency>
<groupId>io.helidon.logging</groupId>
<artifactId>helidon-logging-common</artifactId>
Expand Down
10 changes: 10 additions & 0 deletions bom/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -647,6 +647,16 @@
<artifactId>helidon-openapi</artifactId>
<version>${helidon.version}</version>
</dependency>
<dependency>
<groupId>io.helidon.openapi</groupId>
<artifactId>helidon-openapi-31</artifactId>
<version>${helidon.version}</version>
</dependency>
<dependency>
<groupId>io.helidon.openapi</groupId>
<artifactId>helidon-openapi-32</artifactId>
<version>${helidon.version}</version>
</dependency>
<!-- CORS support -->
<!-- Testing -->
<dependency>
Expand Down
135 changes: 132 additions & 3 deletions codegen/codegen/src/main/java/io/helidon/codegen/TypeHierarchy.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.BiConsumer;
import java.util.stream.Collectors;

import io.helidon.common.Api;
Expand Down Expand Up @@ -177,6 +178,83 @@ public static List<Annotation> hierarchyAnnotations(CodegenContext ctx, TypeInfo
return List.copyOf(annotations.values());
}

/**
* Find all distinct effective occurrences of any of the annotation types on matching methods in a method's type
* hierarchy, grouped by the declaring method. Meta-annotations are included. Unlike
* {@link #hierarchyAnnotations(CodegenContext, TypeInfo, TypedElementInfo)}, this method does not apply
* annotation-type precedence between declarations.
* A candidate-bearing declaration excludes candidates on declarations it overrides. A declaration without matching
* candidates does not exclude candidates inherited from its ancestors. Candidates on unrelated hierarchy branches
* are retained. The provided method itself is excluded. The outer list contains distinct declaration multisets; each
* inner list preserves candidate order and multiplicity.
*
* @param ctx codegen context
* @param type type info owning the method
* @param method method element
* @param annotationTypes annotation types to find
* @return distinct annotation candidates grouped by declaring method
*/
@Api.Internal
public static List<List<Annotation>> hierarchyAnnotationCandidates(CodegenContext ctx,
TypeInfo type,
TypedElementInfo method,
Set<TypeName> annotationTypes) {
Objects.requireNonNull(ctx, "ctx is null");
Objects.requireNonNull(type, "type is null");
Objects.requireNonNull(method, "method is null");
Set<TypeName> candidateTypes = Set.copyOf(Objects.requireNonNull(annotationTypes, "annotationTypes is null"));
if (method.kind() != ElementKind.METHOD) {
throw new CodegenException("Only method elements have hierarchy annotation candidates: " + method.kind());
}

List<HierarchyMethod> prototypes = new ArrayList<>();
BiConsumer<TypeInfo, TypedElementInfo> collector = (declaringType, inheritedMethod) ->
prototypes.add(new HierarchyMethod(declaringType, inheritedMethod));
Set<TypeName> processedTypes = new HashSet<>();
String packageName = type.typeName().packageName();
type.superTypeInfo().ifPresent(it -> collectInheritedMethods(
processedTypes,
collector,
it,
method,
packageName));
type.interfaceTypeInfo().forEach(it -> collectInheritedMethods(
processedTypes,
collector,
it,
method,
packageName));

List<HierarchyAnnotationCandidate> annotationCandidates = new ArrayList<>();
for (HierarchyMethod prototype : prototypes) {
List<Annotation> candidates = new ArrayList<>();
prototype.method().annotations()
.forEach(it -> collectAnnotationCandidates(ctx,
candidateTypes,
candidates,
it,
new HashSet<>()));
if (!candidates.isEmpty()) {
annotationCandidates.add(new HierarchyAnnotationCandidate(prototype.declaringType(),
List.copyOf(candidates)));
}
}

List<List<Annotation>> result = new ArrayList<>();
Set<Map<Annotation, Long>> distinctCandidates = new HashSet<>();
for (HierarchyAnnotationCandidate candidate : annotationCandidates) {
if (isOverriddenCandidate(candidate, annotationCandidates)) {
continue;
}
Map<Annotation, Long> candidateCounts = candidate.annotations().stream()
.collect(Collectors.groupingBy(it -> it, Collectors.counting()));
if (distinctCandidates.add(candidateCounts)) {
result.add(candidate.annotations());
}
}
return List.copyOf(result);
}

/**
* Annotations of a parameter, taken from the full inheritance hierarchy (super type(s), interface(s).
*
Expand Down Expand Up @@ -556,6 +634,30 @@ private static void processMetaAnnotations(CodegenContext ctx,
newAnnotations.forEach(it -> annotations.putIfAbsent(it.typeName(), it));
}

private static void collectAnnotationCandidates(CodegenContext ctx,
Set<TypeName> annotationTypes,
List<Annotation> result,
Annotation annotation,
Set<TypeName> path) {
if (!path.add(annotation.typeName())) {
return;
}
if (annotationTypes.contains(annotation.typeName())) {
result.add(annotation);
}
List<Annotation> metaAnnotations = annotation.metaAnnotations();
if (metaAnnotations.isEmpty()) {
metaAnnotations = ctx.typeInfo(annotation.typeName())
.map(TypeInfo::annotations)
.orElseGet(List::of);
}
metaAnnotations.forEach(it -> collectAnnotationCandidates(ctx,
annotationTypes,
result,
it,
new HashSet<>(path)));
}

private static void collectMetaAnnotations(CodegenContext ctx,
Set<TypeName> processedTypes,
List<Annotation> metaAnnotations,
Expand All @@ -582,6 +684,18 @@ private static void collectInheritedMethods(Set<TypeName> processed,
TypeInfo type,
TypedElementInfo method,
String currentPackage) {
collectInheritedMethods(processed,
(_, inheritedMethod) -> collected.add(inheritedMethod),
type,
method,
currentPackage);
}

private static void collectInheritedMethods(Set<TypeName> processed,
BiConsumer<TypeInfo, TypedElementInfo> collector,
TypeInfo type,
TypedElementInfo method,
String currentPackage) {
if (!processed.add(type.typeName())) {
// already handled this type
return;
Expand Down Expand Up @@ -617,20 +731,29 @@ private static void collectInheritedMethods(Set<TypeName> processed,
.collect(Collectors.toUnmodifiableList()),
currentPackage,
substitutions)
.ifPresent(collected::add);
.ifPresent(it -> collector.accept(type, it));

type.superTypeInfo()
.map(it -> substituteTypeParameters(it, substitutions))
.ifPresent(it -> collectInheritedMethods(processed, collected, it, method, currentPackage));
.ifPresent(it -> collectInheritedMethods(processed, collector, it, method, currentPackage));
for (TypeInfo typeInfo : type.interfaceTypeInfo()) {
collectInheritedMethods(processed,
collected,
collector,
substituteTypeParameters(typeInfo, substitutions),
method,
currentPackage);
}
}

private static boolean isOverriddenCandidate(HierarchyAnnotationCandidate candidate,
List<HierarchyAnnotationCandidate> candidates) {
TypeName candidateType = candidate.declaringType().typeName().genericTypeName();
return candidates.stream()
.map(HierarchyAnnotationCandidate::declaringType)
.filter(it -> !it.typeName().genericTypeName().equals(candidateType))
.anyMatch(it -> it.findInHierarchy(candidateType).isPresent());
}

/**
* Check if the provided type declares a method that is overridden.
*
Expand Down Expand Up @@ -807,4 +930,10 @@ private static TypeName substituteTypeParameters(TypeName typeName, Map<String,
return builder.build();
}

private record HierarchyMethod(TypeInfo declaringType, TypedElementInfo method) {
}

private record HierarchyAnnotationCandidate(TypeInfo declaringType, List<Annotation> annotations) {
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.not;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;

class TypeHierarchyTest {
private static final TypeName NOT_BLANK = TypeName.create("io.helidon.validation.Validation.String.NotBlank");
Expand Down Expand Up @@ -130,6 +131,108 @@ void methodSignaturesRetainMethodTypeVariableBounds() {
not(TypeHierarchy.methodSignature(charSequenceMethod)));
}

@Test
void hierarchyAnnotationCandidatesRejectNullArguments() {
TypeName annotationType = TypeName.create("io.helidon.codegen.test.Candidate");
TypedElementInfo method = TypedElementInfo.builder()
.kind(ElementKind.METHOD)
.elementName("candidate")
.typeName(TypeNames.PRIMITIVE_VOID)
.build();
TypeInfo type = TypeInfo.builder()
.typeName(TypeName.create("io.helidon.codegen.test.CandidateContract"))
.kind(ElementKind.INTERFACE)
.addElementInfo(method)
.build();

NullPointerException nullContext = assertThrows(
NullPointerException.class,
() -> TypeHierarchy.hierarchyAnnotationCandidates(null, type, method, Set.of(annotationType)));
NullPointerException nullType = assertThrows(
NullPointerException.class,
() -> TypeHierarchy.hierarchyAnnotationCandidates(CTX, null, method, Set.of(annotationType)));
NullPointerException nullMethod = assertThrows(
NullPointerException.class,
() -> TypeHierarchy.hierarchyAnnotationCandidates(CTX, type, null, Set.of(annotationType)));
NullPointerException nullAnnotationTypes = assertThrows(
NullPointerException.class,
() -> TypeHierarchy.hierarchyAnnotationCandidates(CTX, type, method, null));

assertThat(nullContext.getMessage(), is("ctx is null"));
assertThat(nullType.getMessage(), is("type is null"));
assertThat(nullMethod.getMessage(), is("method is null"));
assertThat(nullAnnotationTypes.getMessage(), is("annotationTypes is null"));
}

@Test
void hierarchyAnnotationCandidatesUseNearestAnnotatedDeclaration() {
TypeName annotationType = TypeName.create("io.helidon.codegen.test.Candidate");
Annotation baseCandidate = Annotation.create(annotationType, "base");
Annotation narrowedCandidate = Annotation.create(annotationType, "narrowed");
TypedElementInfo baseMethod = candidateMethod(baseCandidate);
TypeInfo base = candidateType("BaseApi", baseMethod);
TypedElementInfo narrowedMethod = candidateMethod(narrowedCandidate);
TypeInfo narrowed = TypeInfo.builder(candidateType("NarrowedApi", narrowedMethod))
.addInterfaceTypeInfo(base)
.build();
TypedElementInfo endpointMethod = candidateMethod();
TypeInfo endpoint = TypeInfo.builder()
.typeName(TypeName.create("io.helidon.codegen.test.Endpoint"))
.kind(ElementKind.CLASS)
.addInterfaceTypeInfo(narrowed)
.addElementInfo(endpointMethod)
.build();

List<List<Annotation>> candidates = TypeHierarchy.hierarchyAnnotationCandidates(
CTX, endpoint, endpointMethod, Set.of(annotationType));

assertThat(candidates, is(List.of(List.of(narrowedCandidate))));
}

@Test
void hierarchyAnnotationCandidatesFallBackToAnnotatedAncestor() {
TypeName annotationType = TypeName.create("io.helidon.codegen.test.Candidate");
Annotation baseCandidate = Annotation.create(annotationType, "base");
TypeInfo base = candidateType("BaseApi", candidateMethod(baseCandidate));
TypeInfo narrowed = TypeInfo.builder(candidateType("NarrowedApi", candidateMethod()))
.addInterfaceTypeInfo(base)
.build();
TypedElementInfo endpointMethod = candidateMethod();
TypeInfo endpoint = TypeInfo.builder()
.typeName(TypeName.create("io.helidon.codegen.test.Endpoint"))
.kind(ElementKind.CLASS)
.addInterfaceTypeInfo(narrowed)
.addElementInfo(endpointMethod)
.build();

List<List<Annotation>> candidates = TypeHierarchy.hierarchyAnnotationCandidates(
CTX, endpoint, endpointMethod, Set.of(annotationType));

assertThat(candidates, is(List.of(List.of(baseCandidate))));
}

@Test
void hierarchyAnnotationCandidatesKeepUnrelatedDeclarations() {
TypeName annotationType = TypeName.create("io.helidon.codegen.test.Candidate");
Annotation firstCandidate = Annotation.create(annotationType, "first");
Annotation secondCandidate = Annotation.create(annotationType, "second");
TypeInfo first = candidateType("FirstApi", candidateMethod(firstCandidate));
TypeInfo second = candidateType("SecondApi", candidateMethod(secondCandidate));
TypedElementInfo endpointMethod = candidateMethod();
TypeInfo endpoint = TypeInfo.builder()
.typeName(TypeName.create("io.helidon.codegen.test.Endpoint"))
.kind(ElementKind.CLASS)
.addInterfaceTypeInfo(first)
.addInterfaceTypeInfo(second)
.addElementInfo(endpointMethod)
.build();

List<List<Annotation>> candidates = TypeHierarchy.hierarchyAnnotationCandidates(
CTX, endpoint, endpointMethod, Set.of(annotationType));

assertThat(candidates, is(List.of(List.of(firstCandidate), List.of(secondCandidate))));
}

@Test
void nestedAnnotationsIncludeDeepGenericTypeArgumentAnnotations() {
TypeName mapType = TypeName.builder(TypeNames.MAP)
Expand Down Expand Up @@ -158,6 +261,24 @@ void nestedAnnotationsIncludeDeepGenericTypeArgumentAnnotations() {
assertThat(TypeHierarchy.nestedAnnotations(CTX, typeInfo), hasItem(NOT_BLANK));
}

private static TypeInfo candidateType(String className, TypedElementInfo method) {
return TypeInfo.builder()
.typeName(TypeName.create("io.helidon.codegen.test." + className))
.kind(ElementKind.INTERFACE)
.addElementInfo(method)
.build();
}

private static TypedElementInfo candidateMethod(Annotation... annotations) {
return TypedElementInfo.builder()
.kind(ElementKind.METHOD)
.accessModifier(AccessModifier.PUBLIC)
.elementName("candidate")
.typeName(TypeNames.PRIMITIVE_VOID)
.annotations(List.of(annotations))
.build();
}

@Test
void typeNameAnnotationsIncludeWildcardBoundsAndArrayComponents() {
Annotation notBlank = Annotation.create(NOT_BLANK);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,16 @@ public boolean codegen(ParameterCodegenContext ctx) {
}

var contentBuilder = ctx.contentBuilder();
var parameterType = ctx.parameterType();
contentBuilder.addContent(ctx.serverRequestParamName())
.addContent(".content().as(");
addTypeArgument(ctx, contentBuilder, ctx.parameterType());
.addContent(".content().");
if (parameterType.isOptional()) {
contentBuilder.addContent("asOptional(");
Comment thread
tomas-langer marked this conversation as resolved.
addTypeArgument(ctx, contentBuilder, parameterType.typeArguments().getFirst());
} else {
contentBuilder.addContent("as(");
addTypeArgument(ctx, contentBuilder, parameterType);
}
contentBuilder.addContent(");");

return true;
Expand Down
Loading
Loading