diff --git a/extensions/arc/runtime/src/main/java/io/quarkus/arc/runtime/BeanContainer.java b/extensions/arc/runtime/src/main/java/io/quarkus/arc/runtime/BeanContainer.java index 737a99bd19da4..df187a9e0c94c 100644 --- a/extensions/arc/runtime/src/main/java/io/quarkus/arc/runtime/BeanContainer.java +++ b/extensions/arc/runtime/src/main/java/io/quarkus/arc/runtime/BeanContainer.java @@ -20,9 +20,12 @@ default T instance(Class 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. + *

+ * 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 diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/ResteasyReactiveProcessor.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/ResteasyReactiveProcessor.java index e239f253d6bcf..d7ccf1c31926f 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/ResteasyReactiveProcessor.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/ResteasyReactiveProcessor.java @@ -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; @@ -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; @@ -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 annotationsTransformer) { + + // all found resources and sub-resources + Set allResources = new HashSet<>(); + allResources.addAll(resourceScanningResultBuildItem.getResult().getScannedResources().keySet()); + allResources.addAll(resourceScanningResultBuildItem.getResult().getPossibleSubResources().keySet()); + + // discovered filters and interceptors + Set filtersAndInterceptors = new HashSet<>(); + InterceptorContainer readerInterceptors = resourceInterceptorsBuildItem.getResourceInterceptors() + .getReaderInterceptors(); + readerInterceptors.getNameResourceInterceptors().forEach(i -> filtersAndInterceptors.add(i.getClassName())); + readerInterceptors.getGlobalResourceInterceptors().forEach(i -> filtersAndInterceptors.add(i.getClassName())); + InterceptorContainer writerInterceptors = resourceInterceptorsBuildItem.getResourceInterceptors() + .getWriterInterceptors(); + writerInterceptors.getNameResourceInterceptors().forEach(i -> filtersAndInterceptors.add(i.getClassName())); + writerInterceptors.getGlobalResourceInterceptors().forEach(i -> filtersAndInterceptors.add(i.getClassName())); + PreMatchInterceptorContainer 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 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 additionalContextTypes(List contextTypeBuildItems) { if (contextTypeBuildItems.isEmpty()) { return CONTEXT_TYPES; diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/response/ChunkedResponseTest.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/response/ChunkedResponseTest.java index 4af67db4b5c03..1bd68e6a2d9bb 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/response/ChunkedResponseTest.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/response/ChunkedResponseTest.java @@ -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 @@ -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) diff --git a/extensions/undertow/deployment/src/main/java/io/quarkus/undertow/deployment/UndertowBuildStep.java b/extensions/undertow/deployment/src/main/java/io/quarkus/undertow/deployment/UndertowBuildStep.java index 68f18b5765624..6758ef0245712 100644 --- a/extensions/undertow/deployment/src/main/java/io/quarkus/undertow/deployment/UndertowBuildStep.java +++ b/extensions/undertow/deployment/src/main/java/io/quarkus/undertow/deployment/UndertowBuildStep.java @@ -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; @@ -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; @@ -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()); @@ -335,6 +339,40 @@ private boolean hasSecurityCapability(final Capabilities capabilities) { return capabilities.isCapabilityWithPrefixPresent(Capability.SECURITY); } + @BuildStep + public void addTypedAnnotations( + BuildProducer 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 servlets, diff --git a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/ArcContainerImpl.java b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/ArcContainerImpl.java index 71cfa5cf8cc6d..94230917eb52c 100644 --- a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/ArcContainerImpl.java +++ b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/ArcContainerImpl.java @@ -241,20 +241,11 @@ public Supplier> instanceSupplier(Class type, Annotatio qualifiers = new Annotation[] { Default.Literal.INSTANCE }; } Set> resolvedBeans = resolved.getValue(new Resolvable(type, qualifiers)); - Set> 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 bean = filteredBean.size() != 1 ? null - : (InjectableBean) filteredBean.iterator().next(); + InjectableBean bean = resolvedBeans.size() != 1 ? null + : (InjectableBean) resolvedBeans.iterator().next(); if (bean == null) { return null; } diff --git a/independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/ResteasyReactiveDotNames.java b/independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/ResteasyReactiveDotNames.java index 6c9a24d14986d..c86e07b6d04ac 100644 --- a/independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/ResteasyReactiveDotNames.java +++ b/independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/ResteasyReactiveDotNames.java @@ -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; @@ -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());