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 a187ebafb220cc..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,7 +1,12 @@ 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.AnnotationInstance; import org.jboss.jandex.AnnotationTarget; @@ -9,6 +14,7 @@ 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; @@ -17,6 +23,7 @@ import io.quarkus.deployment.Capability; import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.builditem.CombinedIndexBuildItem; import io.quarkus.deployment.metrics.MetricsCapabilityBuildItem; import io.quarkus.resteasy.common.spi.ResteasyDotNames; import io.quarkus.resteasy.runtime.QuarkusRestPathTemplate; @@ -49,6 +56,7 @@ AdditionalBeanBuildItem registerBeanClasses(Capabilities capabilities, @BuildStep void findRestPaths( + CombinedIndexBuildItem index, Capabilities capabilities, Optional metricsCapability, BuildProducer transformers, Optional restApplicationPathBuildItem) { @@ -80,7 +88,7 @@ public void transform(TransformationContext ctx) { MethodInfo methodInfo = target.asMethod(); ClassInfo classInfo = methodInfo.declaringClass(); - if (!isRestEndpointMethod(methodInfo)) { + if (!isRestEndpointMethod(index, methodInfo)) { return; } // Don't create annotations for rest clients @@ -93,13 +101,23 @@ public void transform(TransformationContext ctx) { if (annotation != null) { stringBuilder = new StringBuilder(slashify(annotation.value().asString())); } else { - stringBuilder = new StringBuilder(); + // 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()); } // Look for @Path annotation on the class annotation = classInfo.classAnnotation(REST_PATH); if (annotation != null) { stringBuilder.insert(0, slashify(annotation.value().asString())); + } else { + // 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) { @@ -134,7 +152,56 @@ String slashify(String path) { return '/' + path; } - static boolean isRestEndpointMethod(MethodInfo methodInfo) { + /** + * 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) { if (!methodInfo.hasAnnotation(REST_PATH)) { // Check for @Path on class and not method @@ -143,7 +210,8 @@ static boolean isRestEndpointMethod(MethodInfo methodInfo) { return true; } } - return false; + // Search for interface + return searchPathAnnotationOnInterfaces(index, methodInfo).isPresent(); } return true; } diff --git a/extensions/resteasy-classic/resteasy/deployment/src/main/java/io/quarkus/resteasy/deployment/ResteasyBuiltinsProcessor.java b/extensions/resteasy-classic/resteasy/deployment/src/main/java/io/quarkus/resteasy/deployment/ResteasyBuiltinsProcessor.java index 56c8fb54947bec..ef20126cd72ade 100644 --- a/extensions/resteasy-classic/resteasy/deployment/src/main/java/io/quarkus/resteasy/deployment/ResteasyBuiltinsProcessor.java +++ b/extensions/resteasy-classic/resteasy/deployment/src/main/java/io/quarkus/resteasy/deployment/ResteasyBuiltinsProcessor.java @@ -63,7 +63,7 @@ void setUpDenyAllJaxRs(CombinedIndexBuildItem index, ClassInfo classInfo = index.getIndex().getClassByName(DotName.createSimple(className)); if (!hasSecurityAnnotation(classInfo)) { for (MethodInfo methodInfo : classInfo.methods()) { - if (isRestEndpointMethod(methodInfo) && !hasSecurityAnnotation(methodInfo)) { + if (isRestEndpointMethod(index, methodInfo) && !hasSecurityAnnotation(methodInfo)) { methods.add(methodInfo); } } diff --git a/integration-tests/micrometer-prometheus/src/main/java/io/quarkus/it/micrometer/prometheus/MessageResource.java b/integration-tests/micrometer-prometheus/src/main/java/io/quarkus/it/micrometer/prometheus/MessageResource.java index 93f87cefe52326..f9da5e3cbeb9a8 100644 --- a/integration-tests/micrometer-prometheus/src/main/java/io/quarkus/it/micrometer/prometheus/MessageResource.java +++ b/integration-tests/micrometer-prometheus/src/main/java/io/quarkus/it/micrometer/prometheus/MessageResource.java @@ -7,7 +7,7 @@ import io.micrometer.core.instrument.MeterRegistry; @Path("/message") -public class MessageResource { +public class MessageResource implements MessageResourceApi { private final MeterRegistry registry; @@ -32,15 +32,13 @@ public String item(@PathParam("id") String id) { return "return message with id " + id; } - @GET - @Path("match/{id}/{sub}") - public String match(@PathParam("id") String id, @PathParam("sub") String sub) { + @Override + public String match(String id, String sub) { return "return message with id " + id + ", and sub " + sub; } - @GET - @Path("match/{text}") - public String optional(@PathParam("text") String text) { + @Override + public String optional(String text) { return "return message with text " + text; } } diff --git a/integration-tests/micrometer-prometheus/src/main/java/io/quarkus/it/micrometer/prometheus/MessageResourceApi.java b/integration-tests/micrometer-prometheus/src/main/java/io/quarkus/it/micrometer/prometheus/MessageResourceApi.java new file mode 100644 index 00000000000000..dc1da68242486c --- /dev/null +++ b/integration-tests/micrometer-prometheus/src/main/java/io/quarkus/it/micrometer/prometheus/MessageResourceApi.java @@ -0,0 +1,14 @@ +package io.quarkus.it.micrometer.prometheus; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; + +// Splitting JAX-RS annotations to interfaces tests the behaviour of +// io.quarkus.resteasy.deployment.RestPathAnnotationProcessor +public interface MessageResourceApi extends MessageResourceNestedApi { + + @GET + @Path("match/{id}/{sub}") + String match(@PathParam("id") String id, @PathParam("sub") String sub); +} diff --git a/integration-tests/micrometer-prometheus/src/main/java/io/quarkus/it/micrometer/prometheus/MessageResourceNestedApi.java b/integration-tests/micrometer-prometheus/src/main/java/io/quarkus/it/micrometer/prometheus/MessageResourceNestedApi.java new file mode 100644 index 00000000000000..66de9a8452abfa --- /dev/null +++ b/integration-tests/micrometer-prometheus/src/main/java/io/quarkus/it/micrometer/prometheus/MessageResourceNestedApi.java @@ -0,0 +1,13 @@ +package io.quarkus.it.micrometer.prometheus; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; + +// Testing deep lookup of Path-annotation +public interface MessageResourceNestedApi { + + @GET + @Path("match/{text}") + String optional(@PathParam("text") String text); +} \ No newline at end of file