From 6df0385dbf9a787cd5c14883bb04456869ab3894 Mon Sep 17 00:00:00 2001 From: Martin Kouba Date: Wed, 11 Sep 2019 23:06:39 +0200 Subject: [PATCH] Scan JAX-RS sub-resource locators - resolves #3919 --- .../ResteasyServerCommonProcessor.java | 123 ++++++++++++++---- .../resteasy/test/subresource/MyService.java | 12 ++ .../test/subresource/PingResource.java | 16 +++ .../test/subresource/PingsResource.java | 18 +++ .../subresource/SubresourceLocatorTest.java | 24 ++++ 5 files changed, 168 insertions(+), 25 deletions(-) create mode 100644 extensions/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/subresource/MyService.java create mode 100644 extensions/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/subresource/PingResource.java create mode 100644 extensions/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/subresource/PingsResource.java create mode 100644 extensions/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/subresource/SubresourceLocatorTest.java diff --git a/extensions/resteasy-server-common/deployment/src/main/java/io/quarkus/resteasy/server/common/deployment/ResteasyServerCommonProcessor.java b/extensions/resteasy-server-common/deployment/src/main/java/io/quarkus/resteasy/server/common/deployment/ResteasyServerCommonProcessor.java index 1fbc6919d4d4f..6db40040a2bef 100755 --- a/extensions/resteasy-server-common/deployment/src/main/java/io/quarkus/resteasy/server/common/deployment/ResteasyServerCommonProcessor.java +++ b/extensions/resteasy-server-common/deployment/src/main/java/io/quarkus/resteasy/server/common/deployment/ResteasyServerCommonProcessor.java @@ -9,11 +9,13 @@ import java.util.Collection; import java.util.HashMap; import java.util.HashSet; +import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import java.util.function.BiFunction; import java.util.function.Function; +import java.util.stream.Collectors; import org.jboss.jandex.AnnotationInstance; import org.jboss.jandex.AnnotationTarget; @@ -151,6 +153,7 @@ public void build( BuildProducer transformers, BuildProducer resteasyServerConfig, BuildProducer unremovableBeans, + BuildProducer annotationsTransformer, List additionalJaxRsResourceDefiningAnnotations, List additionalJaxRsResourceMethodAnnotations, List additionalJaxRsResourceMethodParamAnnotations, @@ -195,7 +198,7 @@ public void build( appClass = null; } - Set scannedResources = new HashSet<>(); + Map scannedResources = new HashMap<>(); Set pathInterfaces = new HashSet<>(); Set withoutDefaultCtor = new HashSet<>(); for (AnnotationInstance annotation : allPaths) { @@ -204,7 +207,8 @@ public void build( if (!Modifier.isInterface(clazz.flags())) { String className = clazz.name().toString(); if (!additionalPaths.contains(annotation)) { // scanned resources only contains real JAX-RS resources - scannedResources.add(className); + scannedResources.putIfAbsent(clazz.name(), clazz); + } reflectiveClass.produce(new ReflectiveClassBuildItem(true, true, className)); @@ -223,8 +227,19 @@ public void build( for (final ClassInfo implementor : implementors) { String className = implementor.name().toString(); reflectiveClass.produce(new ReflectiveClassBuildItem(true, true, className)); - scannedResources.add(className); + scannedResources.putIfAbsent(implementor.name(), implementor); + } + } + + Set subresources = findSubresources(index, scannedResources); + if (!subresources.isEmpty()) { + for (DotName locator : subresources) { + reflectiveClass.produce(new ReflectiveClassBuildItem(true, true, locator.toString())); } + // Sub-resource locators are unremovable beans + unremovableBeans.produce( + new UnremovableBeanBuildItem(new UnremovableBeanBuildItem.BeanClassNamesExclusion( + subresources.stream().map(Object::toString).collect(Collectors.toSet())))); } // generate default constructors for suitable concrete @Path classes that don't have them @@ -248,7 +263,7 @@ public void build( if (!scannedResources.isEmpty()) { resteasyInitParameters.put(ResteasyContextParameters.RESTEASY_SCANNED_RESOURCES, - String.join(",", scannedResources)); + scannedResources.keySet().stream().map(Object::toString).collect(Collectors.joining(","))); } resteasyInitParameters.put(ResteasyContextParameters.RESTEASY_SERVLET_MAPPING_PREFIX, path); if (appClass != null) { @@ -262,6 +277,85 @@ public void build( } resteasyServerConfig.produce(new ResteasyServerConfigBuildItem(path, resteasyInitParameters)); + + annotationsTransformer.produce(new AnnotationsTransformerBuildItem(new AnnotationsTransformer() { + + @Override + public boolean appliesTo(Kind kind) { + return kind == Kind.CLASS; + } + + @Override + public void transform(TransformationContext transformationContext) { + ClassInfo clazz = transformationContext.getTarget().asClass(); + if (clazz.classAnnotation(ResteasyDotNames.PATH) != null) { + // Skip root resources - @Path is a bean defining annotation + return; + } + if (BuiltinScope.isIn(clazz.classAnnotations())) { + // Skip classes annotated with built-in scope + return; + } + if (clazz.classAnnotation(ResteasyDotNames.PROVIDER) != null) { + if (clazz.annotations().containsKey(DotNames.INJECT)) { + // A provider with an injection point but no built-in scope is @Singleton + transformationContext.transform().add(BuiltinScope.SINGLETON.getName()).done(); + } + } else if (subresources.contains(clazz.name())) { + // Transform a class annotated with a request method designator + transformationContext.transform() + .add(resteasyConfig.singletonResources ? BuiltinScope.SINGLETON.getName() + : BuiltinScope.REQUEST.getName()) + .done(); + } + } + })); + } + + private Set findSubresources(IndexView index, Map scannedResources) { + // Identify sub-resource candidates + Set subresources = new HashSet<>(); + for (DotName annotation : METHOD_ANNOTATIONS) { + Collection annotationInstances = index.getAnnotations(annotation); + for (AnnotationInstance annotationInstance : annotationInstances) { + DotName declaringClassName = annotationInstance.target().asMethod().declaringClass().name(); + if (scannedResources.containsKey(declaringClassName)) { + // Skip resource classes + continue; + } + subresources.add(declaringClassName); + } + } + // Collect sub-resource locator return types + Set subresourceLocatorTypes = new HashSet<>(); + for (ClassInfo resourceClass : scannedResources.values()) { + ClassInfo clazz = resourceClass; + while (clazz != null) { + for (MethodInfo method : clazz.methods()) { + if (method.hasAnnotation(ResteasyDotNames.PATH)) { + subresourceLocatorTypes.add(method.returnType().name()); + } + } + if (clazz.superName().equals(DotNames.OBJECT)) { + clazz = index.getClassByName(clazz.superName()); + } else { + clazz = null; + } + } + } + // Remove false positives + for (Iterator iterator = subresources.iterator(); iterator.hasNext();) { + DotName subresource = iterator.next(); + for (DotName type : subresourceLocatorTypes) { + // Sub-resource may be a subclass of a locator return type + if (!subresource.equals(type) + && index.getAllKnownSubclasses(type).stream().noneMatch(c -> c.name().equals(subresource))) { + iterator.remove(); + break; + } + } + } + return subresources; } @BuildStep @@ -328,27 +422,6 @@ void beanDefiningAnnotations(BuildProducer bean BuiltinScope.SINGLETON.getName())); } - @BuildStep - AnnotationsTransformerBuildItem annotationTransformer() { - return new AnnotationsTransformerBuildItem(new AnnotationsTransformer() { - - @Override - public boolean appliesTo(Kind kind) { - return kind == Kind.CLASS; - } - - @Override - public void transform(TransformationContext transformationContext) { - ClassInfo clazz = transformationContext.getTarget().asClass(); - if (clazz.classAnnotation(ResteasyDotNames.PROVIDER) != null && clazz.annotations().containsKey(DotNames.INJECT) - && !BuiltinScope.isIn(clazz.classAnnotations())) { - // A provider with an injection point but no built-in scope is @Singleton - transformationContext.transform().add(BuiltinScope.SINGLETON.getName()).done(); - } - } - }); - } - /** * Indicates that JAXB support should be enabled * diff --git a/extensions/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/subresource/MyService.java b/extensions/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/subresource/MyService.java new file mode 100644 index 0000000000000..617228f73231e --- /dev/null +++ b/extensions/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/subresource/MyService.java @@ -0,0 +1,12 @@ +package io.quarkus.resteasy.test.subresource; + +import javax.enterprise.context.ApplicationScoped; + +@ApplicationScoped +public class MyService { + + public String ping() { + return "pong"; + } + +} diff --git a/extensions/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/subresource/PingResource.java b/extensions/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/subresource/PingResource.java new file mode 100644 index 0000000000000..9fccb55785edd --- /dev/null +++ b/extensions/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/subresource/PingResource.java @@ -0,0 +1,16 @@ +package io.quarkus.resteasy.test.subresource; + +import javax.inject.Inject; +import javax.ws.rs.GET; + +public class PingResource { + + @Inject + MyService service; + + @GET + public String ping() { + return service.ping(); + } + +} diff --git a/extensions/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/subresource/PingsResource.java b/extensions/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/subresource/PingsResource.java new file mode 100644 index 0000000000000..b609fd06dca86 --- /dev/null +++ b/extensions/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/subresource/PingsResource.java @@ -0,0 +1,18 @@ +package io.quarkus.resteasy.test.subresource; + +import javax.ws.rs.Path; +import javax.ws.rs.container.ResourceContext; +import javax.ws.rs.core.Context; + +@Path("pings") +public class PingsResource { + + @Context + ResourceContext resourceContext; + + @Path("do") + public PingResource findPing() { + return resourceContext.getResource(PingResource.class); + } + +} diff --git a/extensions/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/subresource/SubresourceLocatorTest.java b/extensions/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/subresource/SubresourceLocatorTest.java new file mode 100644 index 0000000000000..5acdcf8494cdb --- /dev/null +++ b/extensions/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/subresource/SubresourceLocatorTest.java @@ -0,0 +1,24 @@ +package io.quarkus.resteasy.test.subresource; + +import org.hamcrest.Matchers; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; +import io.restassured.RestAssured; + +public class SubresourceLocatorTest { + + @RegisterExtension + static QuarkusUnitTest runner = new QuarkusUnitTest() + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) + .addClasses(PingResource.class, PingsResource.class, MyService.class)); + + @Test + public void testSubresourceLocator() { + RestAssured.when().get("/pings/do").then().body(Matchers.is("pong")); + } + +}