Skip to content

Commit

Permalink
Scan JAX-RS sub-resource locators
Browse files Browse the repository at this point in the history
- resolves quarkusio#3919
  • Loading branch information
mkouba committed Sep 11, 2019
1 parent 0cfe545 commit 6df0385
Show file tree
Hide file tree
Showing 5 changed files with 168 additions and 25 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -151,6 +153,7 @@ public void build(
BuildProducer<BytecodeTransformerBuildItem> transformers,
BuildProducer<ResteasyServerConfigBuildItem> resteasyServerConfig,
BuildProducer<UnremovableBeanBuildItem> unremovableBeans,
BuildProducer<AnnotationsTransformerBuildItem> annotationsTransformer,
List<AdditionalJaxRsResourceDefiningAnnotationBuildItem> additionalJaxRsResourceDefiningAnnotations,
List<AdditionalJaxRsResourceMethodAnnotationsBuildItem> additionalJaxRsResourceMethodAnnotations,
List<AdditionalJaxRsResourceMethodParamAnnotations> additionalJaxRsResourceMethodParamAnnotations,
Expand Down Expand Up @@ -195,7 +198,7 @@ public void build(
appClass = null;
}

Set<String> scannedResources = new HashSet<>();
Map<DotName, ClassInfo> scannedResources = new HashMap<>();
Set<DotName> pathInterfaces = new HashSet<>();
Set<ClassInfo> withoutDefaultCtor = new HashSet<>();
for (AnnotationInstance annotation : allPaths) {
Expand All @@ -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));

Expand All @@ -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<DotName> 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
Expand All @@ -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) {
Expand All @@ -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<DotName> findSubresources(IndexView index, Map<DotName, ClassInfo> scannedResources) {
// Identify sub-resource candidates
Set<DotName> subresources = new HashSet<>();
for (DotName annotation : METHOD_ANNOTATIONS) {
Collection<AnnotationInstance> 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<DotName> 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<DotName> 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
Expand Down Expand Up @@ -328,27 +422,6 @@ void beanDefiningAnnotations(BuildProducer<BeanDefiningAnnotationBuildItem> 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
*
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package io.quarkus.resteasy.test.subresource;

import javax.enterprise.context.ApplicationScoped;

@ApplicationScoped
public class MyService {

public String ping() {
return "pong";
}

}
Original file line number Diff line number Diff line change
@@ -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();
}

}
Original file line number Diff line number Diff line change
@@ -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);
}

}
Original file line number Diff line number Diff line change
@@ -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"));
}

}

0 comments on commit 6df0385

Please sign in to comment.