diff --git a/extensions/resteasy-classic/resteasy/deployment/src/main/java/io/quarkus/resteasy/deployment/RestPathAnnotationProcessor.java b/extensions/resteasy-classic/resteasy/deployment/src/main/java/io/quarkus/resteasy/deployment/RestPathAnnotationProcessor.java index b00cde66596586..23d8427d7cef7e 100644 --- a/extensions/resteasy-classic/resteasy/deployment/src/main/java/io/quarkus/resteasy/deployment/RestPathAnnotationProcessor.java +++ b/extensions/resteasy-classic/resteasy/deployment/src/main/java/io/quarkus/resteasy/deployment/RestPathAnnotationProcessor.java @@ -1,9 +1,20 @@ package io.quarkus.resteasy.deployment; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Objects; import java.util.Optional; import java.util.regex.Pattern; +import java.util.stream.Collectors; -import org.jboss.jandex.*; +import org.jboss.jandex.AnnotationInstance; +import org.jboss.jandex.AnnotationTarget; +import org.jboss.jandex.AnnotationValue; +import org.jboss.jandex.ClassInfo; +import org.jboss.jandex.DotName; +import org.jboss.jandex.MethodInfo; +import org.jboss.jandex.Type; import io.quarkus.arc.deployment.AdditionalBeanBuildItem; import io.quarkus.arc.deployment.AnnotationsTransformerBuildItem; @@ -90,8 +101,8 @@ public void transform(TransformationContext ctx) { if (annotation != null) { stringBuilder = new StringBuilder(slashify(annotation.value().asString())); } else { - // Look for @Path on interface-method with same name - stringBuilder = searchPathAnnotationOnInterfaces(index, classInfo, methodInfo) + // Fallback: look for @Path on interface-method with same name + stringBuilder = searchPathAnnotationOnInterfaces(index, methodInfo) .map(annotationInstance -> new StringBuilder(slashify(annotationInstance.value().asString()))) .orElse(new StringBuilder()); } @@ -101,11 +112,12 @@ public void transform(TransformationContext ctx) { if (annotation != null) { stringBuilder.insert(0, slashify(annotation.value().asString())); } else { - // Look for @Path on interfaces - classInfo.interfaceTypes().stream() - .filter(type -> type.hasAnnotation(REST_PATH)).findFirst() - .ifPresent( - type -> stringBuilder.insert(0, slashify(type.annotation(REST_PATH).value().asString()))); + // Fallback: look for @Path on interfaces + getAllClassInterfaces(index, List.of(classInfo), new ArrayList<>()).stream() + .filter(interfaceClassInfo -> interfaceClassInfo.hasAnnotation(REST_PATH)) + .findFirst() + .map(interfaceClassInfo -> interfaceClassInfo.annotation(REST_PATH).value()) + .ifPresent(annotationValue -> stringBuilder.insert(0, slashify(annotationValue.asString()))); } if (restPathPrefix != null) { @@ -140,25 +152,53 @@ String slashify(String path) { return '/' + path; } - static Optional searchPathAnnotationOnInterfaces(CombinedIndexBuildItem index, ClassInfo classInfo, - MethodInfo methodInfo) { - @SuppressWarnings("unchecked") - Optional resolvedMethodInfo = (Optional) classInfo.interfaceNames() - .stream() - // find same method on interface - .map(dotName -> { - ClassInfo interfaceClassInfo = index.getIndex().getClassByName(dotName); - if (interfaceClassInfo == null) { - return Optional.empty(); - } else { - return Optional.ofNullable(interfaceClassInfo.method(methodInfo.name(), - methodInfo.parameterTypes().toArray(new Type[] {}))); - } - }) - .flatMap(Optional::stream) - .findFirst(); - // TODO split necessary due to type-inference with Optional.empty - return resolvedMethodInfo.flatMap(interfaceMethodInfo -> Optional.ofNullable(interfaceMethodInfo.annotation(REST_PATH))); + /** + * Searches for the same method as passed in methodInfo parameter in all implemented interfaces and yields an + * Optional containing the JAX-RS Path annotation. + * + * @param index Jandex-Index for additional lookup + * @param methodInfo the method to find + * @return Optional with the annotation if found. Never null. + */ + static Optional searchPathAnnotationOnInterfaces(CombinedIndexBuildItem index, MethodInfo methodInfo) { + + Collection allClassInterfaces = getAllClassInterfaces(index, List.of(methodInfo.declaringClass()), + new ArrayList<>()); + + return allClassInterfaces.stream() + .map(interfaceClassInfo -> interfaceClassInfo.method( + methodInfo.name(), + methodInfo.parameterTypes().toArray(new Type[] {}))) + .filter(Objects::nonNull) + .findFirst() + .map(resolvedMethodInfo -> resolvedMethodInfo.annotation(REST_PATH)); + } + + /** + * Recursively get all interfaces given as classInfo collection. + * + * @param index Jandex-Index for additional lookup + * @param classInfos the class(es) to search. Ends the recursion when empty. + * @param resultAcc accumulator for tail-recursion + * @return Collection of all interfaces und their parents. Never null. + */ + private static Collection getAllClassInterfaces( + CombinedIndexBuildItem index, + Collection classInfos, + List resultAcc) { + Objects.requireNonNull(index); + Objects.requireNonNull(classInfos); + Objects.requireNonNull(resultAcc); + if (classInfos.isEmpty()) { + return resultAcc; + } + List interfaces = classInfos.stream() + .flatMap(classInfo -> classInfo.interfaceNames().stream()) + .map(dotName -> index.getIndex().getClassByName(dotName)) + .filter(Objects::nonNull) + .collect(Collectors.toUnmodifiableList()); + resultAcc.addAll(interfaces); + return getAllClassInterfaces(index, interfaces, resultAcc); } static boolean isRestEndpointMethod(CombinedIndexBuildItem index, MethodInfo methodInfo) { @@ -171,7 +211,7 @@ static boolean isRestEndpointMethod(CombinedIndexBuildItem index, MethodInfo met } } // Search for interface - return searchPathAnnotationOnInterfaces(index, methodInfo.declaringClass(), methodInfo).isPresent(); + return searchPathAnnotationOnInterfaces(index, methodInfo).isPresent(); } return true; }