Skip to content

Commit

Permalink
Revisit the non-standard behavior of ArcContainer#instanceSupplier()
Browse files Browse the repository at this point in the history
Modify RR and Undertow processors to add @typed to the resources they discover as beans
  • Loading branch information
manovotn committed Oct 10, 2022
1 parent bda2ef3 commit 300fb32
Show file tree
Hide file tree
Showing 6 changed files with 139 additions and 17 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,12 @@ default <T> T instance(Class<T> type, Annotation... qualifiers) {
}

/**
* Note that if there are multiple sub classes of the given type this will return the exact match. This means
* that this can be used to directly instantiate superclasses of other beans without causing problems. This behavior differs
* to standard CDI rules where an ambiguous dependency would exist.
* Returns an instance factory for given bean type and qualifiers.
* <p/>
* This method follows standard CDI rules meaning that if there are two or more beans, an ambiguous dependency
* exception is thrown.
* Note that the factory itself is still allowed to return {@code null} if there is no matching bean which allows
* for fallback implementations.
*
* @param type
* @param qualifiers
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@
import javax.ws.rs.ext.MessageBodyReader;
import javax.ws.rs.ext.MessageBodyWriter;
import javax.ws.rs.ext.Providers;
import javax.ws.rs.ext.ReaderInterceptor;
import javax.ws.rs.ext.WriterInterceptor;

import org.eclipse.microprofile.config.Config;
import org.eclipse.microprofile.config.ConfigProvider;
Expand All @@ -57,6 +59,8 @@
import org.jboss.resteasy.reactive.common.core.Serialisers;
import org.jboss.resteasy.reactive.common.core.SingletonBeanFactory;
import org.jboss.resteasy.reactive.common.model.InjectableBean;
import org.jboss.resteasy.reactive.common.model.InterceptorContainer;
import org.jboss.resteasy.reactive.common.model.PreMatchInterceptorContainer;
import org.jboss.resteasy.reactive.common.model.ResourceClass;
import org.jboss.resteasy.reactive.common.model.ResourceDynamicFeature;
import org.jboss.resteasy.reactive.common.model.ResourceFeature;
Expand Down Expand Up @@ -718,6 +722,75 @@ private FilterClassIntrospector createFilterClassIntrospector() {
return ab.get();
}

// We want to add @Typed to resources and providers so that they can be resolved as CDI bean using purely their
// class as a bean type. This removes any ambiguity that potential subclasses may have.
@BuildStep
public void transformEndpoints(
ResourceScanningResultBuildItem resourceScanningResultBuildItem,
ResourceInterceptorsBuildItem resourceInterceptorsBuildItem,
BuildProducer<io.quarkus.arc.deployment.AnnotationsTransformerBuildItem> annotationsTransformer) {

// all found resources and sub-resources
Set<DotName> allResources = new HashSet<>();
allResources.addAll(resourceScanningResultBuildItem.getResult().getScannedResources().keySet());
allResources.addAll(resourceScanningResultBuildItem.getResult().getPossibleSubResources().keySet());

// discovered filters and interceptors
Set<String> filtersAndInterceptors = new HashSet<>();
InterceptorContainer<ReaderInterceptor> readerInterceptors = resourceInterceptorsBuildItem.getResourceInterceptors()
.getReaderInterceptors();
readerInterceptors.getNameResourceInterceptors().forEach(i -> filtersAndInterceptors.add(i.getClassName()));
readerInterceptors.getGlobalResourceInterceptors().forEach(i -> filtersAndInterceptors.add(i.getClassName()));
InterceptorContainer<WriterInterceptor> writerInterceptors = resourceInterceptorsBuildItem.getResourceInterceptors()
.getWriterInterceptors();
writerInterceptors.getNameResourceInterceptors().forEach(i -> filtersAndInterceptors.add(i.getClassName()));
writerInterceptors.getGlobalResourceInterceptors().forEach(i -> filtersAndInterceptors.add(i.getClassName()));
PreMatchInterceptorContainer<ContainerRequestFilter> containerRequestFilters = resourceInterceptorsBuildItem
.getResourceInterceptors().getContainerRequestFilters();
containerRequestFilters.getPreMatchInterceptors().forEach(i -> filtersAndInterceptors.add(i.getClassName()));
containerRequestFilters.getNameResourceInterceptors().forEach(i -> filtersAndInterceptors.add(i.getClassName()));
containerRequestFilters.getGlobalResourceInterceptors().forEach(i -> filtersAndInterceptors.add(i.getClassName()));
InterceptorContainer<ContainerResponseFilter> containerResponseFilters = resourceInterceptorsBuildItem
.getResourceInterceptors().getContainerResponseFilters();
containerResponseFilters.getGlobalResourceInterceptors().forEach(i -> filtersAndInterceptors.add(i.getClassName()));
containerResponseFilters.getNameResourceInterceptors().forEach(i -> filtersAndInterceptors.add(i.getClassName()));

annotationsTransformer.produce(new io.quarkus.arc.deployment.AnnotationsTransformerBuildItem(
new io.quarkus.arc.processor.AnnotationsTransformer() {

@Override
public boolean appliesTo(AnnotationTarget.Kind kind) {
return kind == AnnotationTarget.Kind.CLASS;
}

@Override
public void transform(TransformationContext context) {
ClassInfo clazz = context.getTarget().asClass();
// check if the class is one of resources/sub-resources
if (allResources.contains(clazz.name())
&& clazz.declaredAnnotation(ResteasyReactiveDotNames.TYPED) == null) {
context.transform().add(createTypedAnnotationInstance(clazz)).done();
return;
}
// check if the class is one of providers, either explicitly declaring the annotation
// or discovered as resource interceptor or filter
if ((clazz.declaredAnnotation(ResteasyReactiveDotNames.PROVIDER) != null
|| filtersAndInterceptors.contains(clazz.name().toString()))
&& clazz.declaredAnnotation(ResteasyReactiveDotNames.TYPED) == null) {
// Add @Typed(MyResource.class)
context.transform().add(createTypedAnnotationInstance(clazz)).done();
}
}
}));
}

private AnnotationInstance createTypedAnnotationInstance(ClassInfo clazz) {
return AnnotationInstance.create(ResteasyReactiveDotNames.TYPED, clazz,
new AnnotationValue[] { AnnotationValue.createArrayValue("value",
new AnnotationValue[] { AnnotationValue.createClassValue("value",
Type.create(clazz.name(), Type.Kind.CLASS)) }) });
}

private Collection<DotName> additionalContextTypes(List<ContextTypeBuildItem> contextTypeBuildItems) {
if (contextTypeBuildItems.isEmpty()) {
return CONTEXT_TYPES;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ public class ChunkedResponseTest {
static QuarkusUnitTest runner = new QuarkusUnitTest()
.withApplicationRoot((jar) -> jar
.addClasses(HelloResource.class)
.addAsResource(new StringAsset("quarkus.rest.output-buffer-size = 256"),
.addAsResource(new StringAsset("quarkus.resteasy-reactive.output-buffer-size = 256"),
"application.properties"));

@Test
Expand Down Expand Up @@ -68,7 +68,22 @@ public String helloSmall() {
}

@Provider
public static final class CustomStringMessageBodyWriter extends ServerStringMessageBodyHandler {
public static class CustomStringMessageBodyWriter extends ServerStringMessageBodyHandler {

@Override
public void writeResponse(Object o, Type genericType, ServerRequestContext context)
throws WebApplicationException {

try (OutputStream stream = context.getOrCreateOutputStream()) {
stream.write(((String) o).getBytes());
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
}

@Provider
public static final class CustomStringMessageBodyWriter2 extends CustomStringMessageBodyWriter {

@Override
public void writeResponse(Object o, Type genericType, ServerRequestContext context)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import javax.annotation.security.RunAs;
import javax.enterprise.context.ApplicationScoped;
import javax.enterprise.context.SessionScoped;
import javax.enterprise.inject.Typed;
import javax.inject.Inject;
import javax.servlet.ServletContainerInitializer;
import javax.servlet.ServletContext;
Expand Down Expand Up @@ -78,12 +79,14 @@
import org.jboss.metadata.web.spec.WebResourceCollectionMetaData;

import io.quarkus.arc.deployment.AdditionalBeanBuildItem;
import io.quarkus.arc.deployment.AnnotationsTransformerBuildItem;
import io.quarkus.arc.deployment.BeanContainerBuildItem;
import io.quarkus.arc.deployment.ConfigInjectionStaticInitBuildItem;
import io.quarkus.arc.deployment.ContextRegistrationPhaseBuildItem;
import io.quarkus.arc.deployment.ContextRegistrationPhaseBuildItem.ContextConfiguratorBuildItem;
import io.quarkus.arc.deployment.CustomScopeBuildItem;
import io.quarkus.arc.deployment.SyntheticBeanBuildItem;
import io.quarkus.arc.processor.AnnotationsTransformer;
import io.quarkus.deployment.Capabilities;
import io.quarkus.deployment.Capability;
import io.quarkus.deployment.Feature;
Expand Down Expand Up @@ -142,6 +145,7 @@ public class UndertowBuildStep {
public static final DotName DECLARE_ROLES = DotName.createSimple(DeclareRoles.class.getName());
public static final DotName MULTIPART_CONFIG = DotName.createSimple(MultipartConfig.class.getName());
public static final DotName SERVLET_SECURITY = DotName.createSimple(ServletSecurity.class.getName());
public static final DotName TYPED = DotName.createSimple(Typed.class.getName());
protected static final String SERVLET_CONTAINER_INITIALIZER = "META-INF/services/javax.servlet.ServletContainerInitializer";
protected static final DotName HANDLES_TYPES = DotName.createSimple(HandlesTypes.class.getName());

Expand Down Expand Up @@ -335,6 +339,40 @@ private boolean hasSecurityCapability(final Capabilities capabilities) {
return capabilities.isCapabilityWithPrefixPresent(Capability.SECURITY);
}

@BuildStep
public void addTypedAnnotations(
BuildProducer<AnnotationsTransformerBuildItem> annotationsTransformer) {

annotationsTransformer.produce(new io.quarkus.arc.deployment.AnnotationsTransformerBuildItem(
new AnnotationsTransformer() {

@Override
public boolean appliesTo(AnnotationTarget.Kind kind) {
return kind == AnnotationTarget.Kind.CLASS;
}

@Override
public void transform(TransformationContext context) {
ClassInfo clazz = context.getTarget().asClass();
if (clazz.declaredAnnotation(WEB_SERVLET) != null
|| clazz.declaredAnnotation(WEB_FILTER) != null
|| clazz.declaredAnnotation(WEB_LISTENER) != null) {
if (clazz.declaredAnnotation(TYPED) == null) {
// Add @Typed(MyResource.class)
context.transform().add(createTypedAnnotationInstance(clazz)).done();
}
}
}
}));
}

private AnnotationInstance createTypedAnnotationInstance(ClassInfo clazz) {
return AnnotationInstance.create(TYPED, clazz,
new AnnotationValue[] { AnnotationValue.createArrayValue("value",
new AnnotationValue[] { AnnotationValue.createClassValue("value",
Type.create(clazz.name(), Type.Kind.CLASS)) }) });
}

@Record(STATIC_INIT)
@BuildStep()
public ServletDeploymentManagerBuildItem build(List<ServletBuildItem> servlets,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -241,20 +241,11 @@ public <T> Supplier<InstanceHandle<T>> instanceSupplier(Class<T> type, Annotatio
qualifiers = new Annotation[] { Default.Literal.INSTANCE };
}
Set<InjectableBean<?>> resolvedBeans = resolved.getValue(new Resolvable(type, qualifiers));
Set<InjectableBean<?>> filteredBean = resolvedBeans;
if (resolvedBeans.size() > 1) {
//if there are multiple beans we look for an exact match
//this method is only called with the exact type required
//so ignoring subclasses is the correct behaviour
filteredBean = new HashSet<>();
for (InjectableBean<?> i : resolvedBeans) {
if (i.getBeanClass().equals(type)) {
filteredBean.add(i);
}
}
throw new AmbiguousResolutionException("Beans: " + resolvedBeans);
}
InjectableBean<T> bean = filteredBean.size() != 1 ? null
: (InjectableBean<T>) filteredBean.iterator().next();
InjectableBean<T> bean = resolvedBeans.size() != 1 ? null
: (InjectableBean<T>) resolvedBeans.iterator().next();
if (bean == null) {
return null;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import javax.annotation.Priority;
import javax.enterprise.context.ApplicationScoped;
import javax.enterprise.context.RequestScoped;
import javax.enterprise.inject.Typed;
import javax.enterprise.inject.Vetoed;
import javax.inject.Inject;
import javax.inject.Singleton;
Expand Down Expand Up @@ -166,6 +167,7 @@ public final class ResteasyReactiveDotNames {
public static final DotName DEFAULT_VALUE = DotName.createSimple(DefaultValue.class.getName());
public static final DotName NAME_BINDING = DotName.createSimple(NameBinding.class.getName());
public static final DotName VETOED = DotName.createSimple(Vetoed.class.getName());
public static final DotName TYPED = DotName.createSimple(Typed.class.getName());
public static final DotName APPLICATION_SCOPED = DotName.createSimple(ApplicationScoped.class.getName());
public static final DotName SINGLETON = DotName.createSimple(Singleton.class.getName());
public static final DotName REQUEST_SCOPED = DotName.createSimple(RequestScoped.class.getName());
Expand Down

0 comments on commit 300fb32

Please sign in to comment.