diff --git a/docs/src/main/asciidoc/cdi-integration.adoc b/docs/src/main/asciidoc/cdi-integration.adoc index fe92dd294ba6a..21e2e1c319f65 100644 --- a/docs/src/main/asciidoc/cdi-integration.adoc +++ b/docs/src/main/asciidoc/cdi-integration.adoc @@ -419,6 +419,7 @@ public class TestRecorder { ---- <1> Pass a contextual reference of `Bar` to the constructor of `Foo`. +[[inactive_synthetic_beans]] === Inactive Synthetic Beans In the case when one needs to register multiple synthetic beans at build time but only wants a subset of them active at runtime, it is useful to be able to mark a synthetic bean as _inactive_. @@ -486,6 +487,23 @@ if (foo.getHandle().getBean().isActive()) { } ---- +If you want to consume only active beans, you can inject a `List<>` of the beans with the `@Active` qualifier: + +[source,java] +---- +import io.quarkus.arc.Active; + +@Inject +@Active +List foos; + +for (Foo foo : foos) + ... +} +---- + +This is similar to the `@All` qualifier, but filters out the inactive beans. + [[synthetic_observers]] == Use Case - Synthetic Observers diff --git a/docs/src/main/asciidoc/cdi-reference.adoc b/docs/src/main/asciidoc/cdi-reference.adoc index 69d4c96b9525b..2a432c13bc4a0 100644 --- a/docs/src/main/asciidoc/cdi-reference.adoc +++ b/docs/src/main/asciidoc/cdi-reference.adoc @@ -449,7 +449,7 @@ An _unused_ bean: * is not eligible for injection to any injection point in the dependency tree of _unremovable_ beans, and * does not declare any producer which is eligible for injection to any injection point in the dependency tree, and * is not eligible for injection into any `jakarta.enterprise.inject.Instance` or `jakarta.inject.Provider` injection point, and -* is not eligible for injection into any <`>> injection point. +* is not eligible for injection into any <`>> or `@Inject @Active List<>` injection point. Unused interceptors and decorators are not associated with any bean. @@ -1054,6 +1054,10 @@ NOTE: Neither a type variable nor a wildcard is a legal type parameter for an `@ TIP: It is also possible to obtain the list of all bean instance handles programmatically via the `Arc.container().listAll()` methods. +NOTE: if you use link:cdi-integration#inactive_synthetic_beans[inactive beans], you can use the `@Active` qualifier. +It works just like the `@All` qualifier, but filters out inactive beans. +The `Arc.container().listAll()` methods also have a filtering alternative: `listActive()`. + === Ignoring Class-Level Interceptor Bindings for Methods and Constructors If a managed bean declares interceptor binding annotations on the class level, the corresponding `@AroundInvoke` interceptors will apply to all business methods. diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanArchives.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanArchives.java index 12c9f10614ccd..46a5a1bc1a3f6 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanArchives.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanArchives.java @@ -37,6 +37,7 @@ import org.jboss.jandex.Type; import org.jboss.logging.Logger; +import io.quarkus.arc.Active; import io.quarkus.arc.All; import io.quarkus.arc.Lock; import io.quarkus.arc.impl.ActivateRequestContextInterceptor; @@ -85,6 +86,7 @@ private static IndexView buildAdditionalIndex() { index(indexer, Model.class.getName()); index(indexer, Lock.class.getName()); index(indexer, All.class.getName()); + index(indexer, Active.class.getName()); index(indexer, Identified.class.getName()); // Arc built-in beans index(indexer, ActivateRequestContextInterceptor.class.getName()); diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BuiltinBean.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BuiltinBean.java index 8cc9155f269f7..0111a5db232c0 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BuiltinBean.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BuiltinBean.java @@ -63,8 +63,8 @@ public enum BuiltinBean { DotNames.OBJECT), EVENT_METADATA(Generator.NOOP, BuiltinBean::cdiAndRawTypeMatches, BuiltinBean::validateEventMetadata, DotNames.EVENT_METADATA), - LIST(BuiltinBean::generateListBytecode, - (ip, names) -> cdiAndRawTypeMatches(ip, DotNames.LIST) && ip.getRequiredQualifier(DotNames.ALL) != null, + LIST(BuiltinBean::generateListBytecode, (ip, names) -> cdiAndRawTypeMatches(ip, DotNames.LIST) + && (ip.getRequiredQualifier(DotNames.ALL) != null || ip.getRequiredQualifier(DotNames.ACTIVE) != null), BuiltinBean::validateList, DotNames.LIST), INTERCEPTION_PROXY(BuiltinBean::generateInterceptionProxyBytecode, BuiltinBean::cdiAndRawTypeMatches, BuiltinBean::validateInterceptionProxy, @@ -375,6 +375,7 @@ private static void generateListBytecode(GeneratorContext ctx) { // List or List ResultHandle requiredType; ResultHandle usesInstanceHandle; + ResultHandle onlyActive; Type type = ctx.injectionPoint.getType().asParameterizedType().arguments().get(0); if (type.name().equals(DotNames.INSTANCE_HANDLE)) { requiredType = Types.getTypeHandle(mc, type.asParameterizedType().arguments().get(0)); @@ -383,6 +384,11 @@ private static void generateListBytecode(GeneratorContext ctx) { requiredType = Types.getTypeHandle(mc, type); usesInstanceHandle = mc.load(false); } + if (ctx.injectionPoint.getRequiredQualifier(DotNames.ACTIVE) != null) { + onlyActive = mc.load(true); + } else { + onlyActive = mc.load(false); + } ResultHandle qualifiers = BeanGenerator.collectInjectionPointQualifiers( ctx.beanDeployment, @@ -408,13 +414,12 @@ private static void generateListBytecode(GeneratorContext ctx) { default: throw new IllegalStateException("Unsupported target info: " + ctx.targetInfo); } - ResultHandle listProvider = ctx.constructor.newInstance( - MethodDescriptor.ofConstructor(ListProvider.class, java.lang.reflect.Type.class, java.lang.reflect.Type.class, - Set.class, - InjectableBean.class, Set.class, Member.class, int.class, boolean.class, boolean.class), + ResultHandle listProvider = ctx.constructor.newInstance(MethodDescriptor.ofConstructor(ListProvider.class, + java.lang.reflect.Type.class, java.lang.reflect.Type.class, Set.class, InjectableBean.class, + Set.class, Member.class, int.class, boolean.class, boolean.class, boolean.class), requiredType, injectionPointType, qualifiers, beanHandle, annotationsHandle, javaMemberHandle, - ctx.constructor.load(ctx.injectionPoint.getPosition()), - ctx.constructor.load(ctx.injectionPoint.isTransient()), usesInstanceHandle); + ctx.constructor.load(ctx.injectionPoint.getPosition()), ctx.constructor.load(ctx.injectionPoint.isTransient()), + usesInstanceHandle, onlyActive); ResultHandle listProviderSupplier = ctx.constructor.newInstance( MethodDescriptors.FIXED_VALUE_SUPPLIER_CONSTRUCTOR, listProvider); ctx.constructor.writeInstanceField( @@ -460,13 +465,20 @@ private static void validateList(ValidatorContext ctx) { ctx.errors.accept(new DefinitionException( "An injection point of raw type is defined: " + ctx.injectionPoint.getTargetInfo())); } else { + String qualifier; + if (ctx.injectionPoint.getRequiredQualifier(DotNames.ALL) != null) { + qualifier = "@All"; + } else { + qualifier = "@Active"; + } + // Note that at this point we can be sure that the required type is List<> Type typeParam = ctx.injectionPoint.getType().asParameterizedType().arguments().get(0); if (typeParam.kind() == Type.Kind.WILDCARD_TYPE) { if (ctx.injectionPoint.isSynthetic()) { ctx.errors.accept(new DefinitionException( - "Wildcard is not a legal type argument for a synthetic @All List injection point used in: " - + ctx.injectionTarget.toString())); + "Wildcard is not a legal type argument for a synthetic " + qualifier + + " List injection point used in: " + ctx.injectionTarget.toString())); return; } ClassInfo declaringClass; @@ -477,7 +489,8 @@ private static void validateList(ValidatorContext ctx) { } if (isKotlinClass(declaringClass)) { ctx.errors.accept(new DefinitionException( - "kotlin.collections.List cannot be used together with the @All qualifier, please use MutableList or java.util.List instead: " + "kotlin.collections.List cannot be used together with the " + qualifier + + " qualifier, please use MutableList or java.util.List instead: " + ctx.injectionPoint.getTargetInfo())); } else { ctx.errors.accept(new DefinitionException( @@ -487,6 +500,13 @@ private static void validateList(ValidatorContext ctx) { ctx.errors.accept(new DefinitionException( "Type variable is not a legal type argument for: " + ctx.injectionPoint.getTargetInfo())); } + + if (ctx.injectionPoint.getRequiredQualifier(DotNames.ALL) != null + && ctx.injectionPoint.getRequiredQualifier(DotNames.ACTIVE) != null) { + ctx.errors.accept(new DefinitionException( + "Only one of @All and @Active may be declared on a List<> injection point: " + + ctx.injectionPoint.getTargetInfo())); + } } } diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/DotNames.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/DotNames.java index bdfdd2a990e83..d6a68d4079262 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/DotNames.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/DotNames.java @@ -57,6 +57,7 @@ import org.jboss.jandex.ClassInfo; import org.jboss.jandex.DotName; +import io.quarkus.arc.Active; import io.quarkus.arc.All; import io.quarkus.arc.ArcInvocationContext; import io.quarkus.arc.BindingsSource; @@ -139,6 +140,7 @@ public final class DotNames { public static final DotName VETOED_PRODUCER = create(VetoedProducer.class); public static final DotName LIST = create(List.class); public static final DotName ALL = create(All.class); + public static final DotName ACTIVE = create(Active.class); public static final DotName IDENTIFIED = create(Identified.class); public static final DotName INSTANCE_HANDLE = create(InstanceHandle.class); public static final DotName NO_CLASS_INTERCEPTORS = create(NoClassInterceptors.class); diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/UnusedBeans.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/UnusedBeans.java index 8512a0a621e80..928607ac220a0 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/UnusedBeans.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/UnusedBeans.java @@ -36,7 +36,7 @@ static Set findRemovableBeans(BeanResolver beanResolver, Collection injection points - // - @All List<> injection points + // - @All/@Active List<> injection points Set injected = new HashSet<>(); List instanceInjectionPoints = new ArrayList<>(); List listAllInjectionPoints = new ArrayList<>(); @@ -58,7 +58,7 @@ static Set findRemovableBeans(BeanResolver beanResolver, Collection qualifiers = new HashSet<>(injectionPoint.getRequiredQualifiers()); for (Iterator it = qualifiers.iterator(); it.hasNext();) { AnnotationInstance qualifier = it.next(); - if (qualifier.name().equals(DotNames.ALL)) { + if (qualifier.name().equals(DotNames.ALL) || qualifier.name().equals(DotNames.ACTIVE)) { it.remove(); } } @@ -112,11 +112,11 @@ static Set findRemovableBeans(BeanResolver beanResolver, Collection + // @All/@Active List for (TypeAndQualifiers tq : listAllInjectionPoints) { if (Beans.hasQualifiers(bean, tq.qualifiers) && beanResolver.matchesType(bean, tq.type)) { - LOG.debugf("Unremovable - @All List: %s", bean); + LOG.debugf("Unremovable - @All/@Active List: %s", bean); continue test; } } diff --git a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/Active.java b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/Active.java new file mode 100644 index 0000000000000..dd59864e5ca45 --- /dev/null +++ b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/Active.java @@ -0,0 +1,80 @@ +package io.quarkus.arc; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; +import java.util.List; + +import jakarta.enterprise.inject.Any; +import jakarta.enterprise.inject.Instance; +import jakarta.enterprise.util.AnnotationLiteral; +import jakarta.inject.Qualifier; + +/** + * The container provides a synthetic bean for an injection point with the required type {@link List} and the required qualifier + * {@link Active}. The injected instance is an immutable list of the contextual references of the disambiguated beans, + * without beans that are currently {@linkplain InjectableBean#checkActive() inactive}. + * + *
+ * @ApplicationScoped
+ * public class Processor {
+ *
+ *     @Inject
+ *     @Active
+ *     List<Service> services;
+ * }
+ * 
+ * + * If the injection point declares no other qualifier then {@link Any} is used, i.e. the behavior is equivalent to + * {@code @Inject @Any Instance services} and subsequent filtering on the active status. The semantics is the same + * as for the {@link Instance#iterator()}, i.e. the container attempts to resolve ambiguities. In general, if multiple beans + * are eligible then the container eliminates all beans that are: + *
    + *
  • not alternatives, except for producer methods and fields of beans that are alternatives,
  • + *
  • default beans.
  • + *
+ * + * You can also inject a list of bean instances wrapped in {@link InstanceHandle}. This can be useful if you need to inspect the + * bean metadata. + * + *
+ * @ApplicationScoped
+ * public class Processor {
+ *
+ *     @Inject
+ *     @Active
+ *     List<InstanceHandle<Service>> services;
+ *
+ *     void doSomething() {
+ *         for (InstanceHandle<Service> handle : services) {
+ *             if (handle.getBean().getScope().equals(Dependent.class)) {
+ *                 handle.get().process();
+ *                 break;
+ *             }
+ *         }
+ *     }
+ * }
+ * 
+ * + * The list is sorted by {@link InjectableBean#getPriority()}. Higher priority goes first. + * + * @see jakarta.annotation.Priority + */ +@Qualifier +@Retention(RUNTIME) +@Target({ TYPE, FIELD, METHOD, PARAMETER }) +public @interface Active { + /** + * Supports inline instantiation of this qualifier. + */ + final class Literal extends AnnotationLiteral implements Active { + public static final Literal INSTANCE = new Literal(); + + private static final long serialVersionUID = 1L; + } +} diff --git a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/All.java b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/All.java index c1f2c5c108262..273bbc3e18cfb 100644 --- a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/All.java +++ b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/All.java @@ -62,7 +62,7 @@ * * The list is sorted by {@link InjectableBean#getPriority()}. Higher priority goes first. * - * @see Priority + * @see jakarta.annotation.Priority */ @Qualifier @Retention(RUNTIME) diff --git a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/ArcContainer.java b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/ArcContainer.java index 1ed13a4a95040..4846f1aeff58b 100644 --- a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/ArcContainer.java +++ b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/ArcContainer.java @@ -170,6 +170,46 @@ public interface ArcContainer { */ List> listAll(Type type, Annotation... qualifiers); + /** + * List all active beans matching the required type and qualifiers. + *

+ * Instances of dependent scoped beans should be explicitly destroyed with {@link InstanceHandle#destroy()}. + *

+ * The list is sorted by {@link InjectableBean#getPriority()}. Higher priority goes first. + * + * @param + * @param type + * @param qualifiers + * @return the list of handles for the disambiguated active beans + * @see Active + */ + List> listActive(Class type, Annotation... qualifiers); + + /** + * List all active beans matching the required type and qualifiers. + *

+ * Instances of dependent scoped beans should be explicitly destroyed with {@link InstanceHandle#destroy()}. + *

+ * The list of is sorted by {@link InjectableBean#getPriority()}. Higher priority goes first. + * + * @param + * @param type + * @param qualifiers + * @return the list of handles for the disambiguated active beans + * @see Active + */ + List> listActive(TypeLiteral type, Annotation... qualifiers); + + /** + * + * @param + * @param type + * @param qualifiers + * @return the list of handles for the disambiguated active beans + * @see #listActive(Class, Annotation...) + */ + List> listActive(Type type, Annotation... qualifiers); + /** * Returns true if Arc container is running. * This can be used as a quick check to determine CDI availability in Quarkus. diff --git a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/InjectableBean.java b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/InjectableBean.java index 28492d467e301..f5c456479a604 100644 --- a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/InjectableBean.java +++ b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/InjectableBean.java @@ -136,7 +136,7 @@ default boolean isSuppressed() { * If no priority annotation is used then a bean has the priority of value 0. *

* This priority is used to sort the resolved beans when performing programmatic lookup via - * {@link Instance} or when injecting a list of beans by means of the {@link All} qualifier. + * {@link Instance} or when injecting a list of beans by means of the {@link All} or {@link Active} qualifier. * * @return the priority * @see jakarta.annotation.Priority 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 c63710b5cbd86..f0e9bb658bcbe 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 @@ -362,7 +362,23 @@ public List> listAll(TypeLiteral type, Annotation... qu @Override public List> listAll(Type type, Annotation... qualifiers) { return Instances.listOfHandles(CurrentInjectionPointProvider.EMPTY_SUPPLIER, type, Set.of(qualifiers), - new CreationalContextImpl<>(null)); + new CreationalContextImpl<>(null), false); + } + + @Override + public List> listActive(Class type, Annotation... qualifiers) { + return listActive((Type) type, qualifiers); + } + + @Override + public List> listActive(TypeLiteral type, Annotation... qualifiers) { + return listActive(type.getType(), qualifiers); + } + + @Override + public List> listActive(Type type, Annotation... qualifiers) { + return Instances.listOfHandles(CurrentInjectionPointProvider.EMPTY_SUPPLIER, type, Set.of(qualifiers), + new CreationalContextImpl<>(null), true); } @Override diff --git a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/Instances.java b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/Instances.java index f65b917fd3f5d..3817ca4b197b7 100644 --- a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/Instances.java +++ b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/Instances.java @@ -57,15 +57,27 @@ private static List> resolveAllBeans(Type requiredType, Set> onlyActive(List> beans) { + List> activeBeans = new ArrayList<>(beans.size()); + for (InjectableBean bean : beans) { + if (bean.isActive()) { + activeBeans.add(bean); + } + } + return activeBeans; + } + @SuppressWarnings("unchecked") public static List listOf(InjectableBean targetBean, Type injectionPointType, Type requiredType, - Set requiredQualifiers, - CreationalContext creationalContext, Set annotations, Member javaMember, int position, - boolean isTransient) { + Set requiredQualifiers, CreationalContext creationalContext, Set annotations, + Member javaMember, int position, boolean isTransient, boolean onlyActive) { List> beans = resolveAllBeans(requiredType, requiredQualifiers); if (beans.isEmpty()) { return Collections.emptyList(); } + if (onlyActive) { + beans = onlyActive(beans); + } List list = new ArrayList<>(beans.size()); InjectionPoint prev = InjectionPointProvider.setCurrent(creationalContext, new InjectionPointImpl(injectionPointType, requiredType, requiredQualifiers, targetBean, annotations, javaMember, position, isTransient)); @@ -81,9 +93,8 @@ public static List listOf(InjectableBean targetBean, Type injectionPoi } public static List> listOfHandles(InjectableBean targetBean, Type injectionPointType, - Type requiredType, Set requiredQualifiers, - CreationalContext creationalContext, Set annotations, Member javaMember, int position, - boolean isTransient) { + Type requiredType, Set requiredQualifiers, CreationalContext creationalContext, + Set annotations, Member javaMember, int position, boolean isTransient, boolean onlyActive) { Supplier supplier = new Supplier() { @Override public InjectionPoint get() { @@ -91,17 +102,19 @@ public InjectionPoint get() { annotations, javaMember, position, isTransient); } }; - return listOfHandles(supplier, requiredType, requiredQualifiers, creationalContext); + return listOfHandles(supplier, requiredType, requiredQualifiers, creationalContext, onlyActive); } @SuppressWarnings("unchecked") public static List> listOfHandles(Supplier injectionPoint, Type requiredType, - Set requiredQualifiers, - CreationalContext creationalContext) { + Set requiredQualifiers, CreationalContext creationalContext, boolean onlyActive) { List> beans = resolveAllBeans(requiredType, requiredQualifiers); if (beans.isEmpty()) { return Collections.emptyList(); } + if (onlyActive) { + beans = onlyActive(beans); + } List> list = new ArrayList<>(beans.size()); for (InjectableBean bean : beans) { list.add(getHandle(CreationalContextImpl.unwrap(creationalContext), (InjectableBean) bean, injectionPoint)); diff --git a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/ListProvider.java b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/ListProvider.java index 80c72a90d8283..3eaebb793f91b 100644 --- a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/ListProvider.java +++ b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/ListProvider.java @@ -8,6 +8,7 @@ import jakarta.enterprise.context.spi.CreationalContext; +import io.quarkus.arc.Active; import io.quarkus.arc.All; import io.quarkus.arc.InjectableBean; import io.quarkus.arc.InjectableReferenceProvider; @@ -23,33 +24,34 @@ public class ListProvider implements InjectableReferenceProvider> { private final int position; private final boolean isTransient; private final boolean needsInstanceHandle; + private final boolean onlyActive; public ListProvider(Type requiredType, Type injectionPointType, Set qualifiers, InjectableBean targetBean, Set annotations, - Member javaMember, int position, boolean isTransient, boolean needsInstanceHandle) { + Member javaMember, int position, boolean isTransient, boolean needsInstanceHandle, boolean onlyActive) { this.requiredType = requiredType; this.injectionPointType = injectionPointType; this.qualifiers = qualifiers; - // the @All annotation is not a qualifier of the instances we need to resolve + // the @All/@Active annotations are not qualifiers of the instances we need to resolve this.qualifiers.remove(All.Literal.INSTANCE); + this.qualifiers.remove(Active.Literal.INSTANCE); this.targetBean = targetBean; this.annotations = annotations; this.javaMember = javaMember; this.position = position; this.isTransient = isTransient; this.needsInstanceHandle = needsInstanceHandle; + this.onlyActive = onlyActive; } @Override public List get(CreationalContext> creationalContext) { if (needsInstanceHandle) { return Instances.listOfHandles(targetBean, injectionPointType, requiredType, qualifiers, creationalContext, - annotations, javaMember, - position, isTransient); + annotations, javaMember, position, isTransient, onlyActive); } else { return Instances.listOf(targetBean, injectionPointType, requiredType, qualifiers, creationalContext, annotations, - javaMember, - position, isTransient); + javaMember, position, isTransient, onlyActive); } } } diff --git a/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/all/ListActiveTest.java b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/all/ListActiveTest.java new file mode 100644 index 0000000000000..01c027e70b9ae --- /dev/null +++ b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/all/ListActiveTest.java @@ -0,0 +1,251 @@ +package io.quarkus.arc.test.all; + +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; +import java.util.Map; +import java.util.function.Supplier; + +import jakarta.enterprise.context.Dependent; +import jakarta.enterprise.context.spi.CreationalContext; +import jakarta.enterprise.inject.spi.InjectionPoint; +import jakarta.inject.Inject; +import jakarta.inject.Singleton; + +import org.jboss.jandex.AnnotationInstance; +import org.jboss.jandex.ClassType; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.arc.Active; +import io.quarkus.arc.ActiveResult; +import io.quarkus.arc.Arc; +import io.quarkus.arc.BeanCreator; +import io.quarkus.arc.BeanDestroyer; +import io.quarkus.arc.InstanceHandle; +import io.quarkus.arc.SyntheticCreationalContext; +import io.quarkus.arc.test.ArcTestContainer; +import io.quarkus.arc.test.MyQualifier; +import io.quarkus.arc.test.TestLiteral; + +public class ListActiveTest { + @RegisterExtension + public ArcTestContainer container = ArcTestContainer.builder() + .beanClasses(Service.class, MyQualifier.class, Consumer.class) + .beanRegistrars(context -> { + context.configure(ServiceAlpha.class) + .types(Service.class, ServiceAlpha.class) + .scope(Singleton.class) + .checkActive(AlwaysActive.class) + .creator(AlphaCreator.class) + .done(); + + context.configure(ServiceBravo.class) + .types(Service.class, ServiceBravo.class) + .qualifiers(AnnotationInstance.builder(MyQualifier.class).build()) + .scope(Dependent.class) + .priority(5) + .addInjectionPoint(ClassType.create(InjectionPoint.class)) + .checkActive(NeverActive.class) + .creator(BravoCreator.class) + .destroyer(BravoDestroyer.class) + .done(); + + context.configure(ServiceCharlie.class) + .types(Service.class, ServiceCharlie.class) + .scope(Singleton.class) + .checkActive(NeverActive.class) + .creator(CharlieCreator.class) + .done(); + + context.configure(ServiceDelta.class) + .types(Service.class, ServiceDelta.class) + .qualifiers(AnnotationInstance.builder(MyQualifier.class).build()) + .scope(Dependent.class) + .priority(10) + .addInjectionPoint(ClassType.create(InjectionPoint.class)) + .checkActive(AlwaysActive.class) + .creator(DeltaCreator.class) + .destroyer(DeltaDestroyer.class) + .done(); + }) + .build(); + + @Test + public void testSelectAll() { + verifyHandleInjection(Arc.container().listActive(Service.class), Object.class); + } + + @Test + public void testInjectAllList() { + Consumer consumer = Arc.container().select(Consumer.class).get(); + verifyHandleInjection(consumer.activeHandles, Service.class); + verifyInjection(consumer.activeServices, Service.class); + + assertThatExceptionOfType(IllegalArgumentException.class) + .isThrownBy(() -> Arc.container().listActive(Service.class, new TestLiteral())); + } + + private void verifyHandleInjection(List> services, Class expectedInjectionPointType) { + assertEquals(2, services.size()); + assertThatExceptionOfType(UnsupportedOperationException.class) + .isThrownBy(() -> services.remove(0)); + // ServiceDelta has higher priority + InstanceHandle deltaHandle = services.get(0); + Service delta = deltaHandle.get(); + assertEquals("delta", delta.ping()); + assertEquals("alpha", services.get(1).get().ping()); + assertEquals(Dependent.class, deltaHandle.getBean().getScope()); + assertNotNull(delta.getInjectionPoint()); + assertEquals(expectedInjectionPointType, delta.getInjectionPoint().getType()); + deltaHandle.destroy(); + assertTrue(DeltaDestroyer.DESTROYED); + assertThatExceptionOfType(IllegalStateException.class).isThrownBy(deltaHandle::get); + } + + private void verifyInjection(List services, Class expectedInjectionPointType) { + assertEquals(2, services.size()); + assertThatExceptionOfType(UnsupportedOperationException.class) + .isThrownBy(() -> services.remove(0)); + // ServiceDelta has higher priority + Service delta = services.get(0); + assertEquals("delta", delta.ping()); + assertEquals("alpha", services.get(1).ping()); + assertNotNull(delta.getInjectionPoint()); + assertEquals(expectedInjectionPointType, delta.getInjectionPoint().getType()); + } + + @Singleton + public static class Consumer { + @Inject + @Active + List activeServices; + + @Inject + @Active + List> activeHandles; + } + + interface Service { + String ping(); + + default InjectionPoint getInjectionPoint() { + return null; + } + } + + static class ServiceAlpha implements Service { + public String ping() { + return "alpha"; + } + } + + static class AlphaCreator implements BeanCreator { + @Override + public ServiceAlpha create(SyntheticCreationalContext context) { + return new ServiceAlpha(); + } + } + + static class ServiceBravo implements Service { + private final InjectionPoint injectionPoint; + + ServiceBravo(InjectionPoint injectionPoint) { + this.injectionPoint = injectionPoint; + } + + public String ping() { + return "bravo"; + } + + @Override + public InjectionPoint getInjectionPoint() { + return injectionPoint; + } + } + + static class BravoCreator implements BeanCreator { + @Override + public ServiceBravo create(SyntheticCreationalContext context) { + InjectionPoint ip = context.getInjectedReference(InjectionPoint.class); + return new ServiceBravo(ip); + } + } + + static class BravoDestroyer implements BeanDestroyer { + static boolean DESTROYED = false; + + @Override + public void destroy(ServiceBravo instance, CreationalContext creationalContext, + Map params) { + DESTROYED = true; + } + } + + static class ServiceCharlie implements Service { + @Override + public String ping() { + return "charlie"; + } + } + + static class CharlieCreator implements BeanCreator { + @Override + public ServiceCharlie create(SyntheticCreationalContext context) { + return new ServiceCharlie(); + } + } + + static class ServiceDelta implements Service { + private final InjectionPoint injectionPoint; + + ServiceDelta(InjectionPoint injectionPoint) { + this.injectionPoint = injectionPoint; + } + + @Override + public String ping() { + return "delta"; + } + + @Override + public InjectionPoint getInjectionPoint() { + return injectionPoint; + } + } + + static class DeltaCreator implements BeanCreator { + @Override + public ServiceDelta create(SyntheticCreationalContext context) { + InjectionPoint ip = context.getInjectedReference(InjectionPoint.class); + return new ServiceDelta(ip); + } + } + + static class DeltaDestroyer implements BeanDestroyer { + static boolean DESTROYED = false; + + @Override + public void destroy(ServiceDelta instance, CreationalContext creationalContext, + Map params) { + DESTROYED = true; + } + } + + static class AlwaysActive implements Supplier { + @Override + public ActiveResult get() { + return ActiveResult.active(); + } + } + + static class NeverActive implements Supplier { + @Override + public ActiveResult get() { + return ActiveResult.inactive(""); + } + } +} diff --git a/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/buildextension/beans/SyntheticInjectionPointListActiveInvalidTest.java b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/buildextension/beans/SyntheticInjectionPointListActiveInvalidTest.java new file mode 100644 index 0000000000000..bd6cb533e1606 --- /dev/null +++ b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/buildextension/beans/SyntheticInjectionPointListActiveInvalidTest.java @@ -0,0 +1,60 @@ +package io.quarkus.arc.test.buildextension.beans; + +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; + +import jakarta.enterprise.inject.spi.DefinitionException; +import jakarta.enterprise.util.TypeLiteral; + +import org.jboss.jandex.AnnotationInstance; +import org.jboss.jandex.ClassType; +import org.jboss.jandex.ParameterizedType; +import org.jboss.jandex.WildcardType; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.arc.Active; +import io.quarkus.arc.BeanCreator; +import io.quarkus.arc.SyntheticCreationalContext; +import io.quarkus.arc.test.ArcTestContainer; + +public class SyntheticInjectionPointListActiveInvalidTest { + @RegisterExtension + public ArcTestContainer container = ArcTestContainer.builder() + .beanRegistrars(context -> { + context.configure(SyntheticBean.class) + .types(ClassType.create(SyntheticBean.class)) + .creator(SynthBeanCreator.class) + .addInjectionPoint(ParameterizedType.create(List.class, WildcardType.UNBOUNDED), + AnnotationInstance.builder(Active.class).build()) + .unremovable() + .done(); + }) + .shouldFail() + .build(); + + @Test + public void trigger() { + Throwable error = container.getFailure(); + assertNotNull(error); + assertInstanceOf(DefinitionException.class, error); + assertTrue(error.getMessage().contains( + "Wildcard is not a legal type argument for a synthetic @Active List injection point")); + } + + static class SyntheticBean { + SyntheticBean(List list) { + } + } + + static class SynthBeanCreator implements BeanCreator { + @Override + public SyntheticBean create(SyntheticCreationalContext context) { + return new SyntheticBean(context.getInjectedReference(new TypeLiteral<>() { + }, Active.Literal.INSTANCE)); + } + } +} diff --git a/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/buildextension/beans/SyntheticInjectionPointListActiveTest.java b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/buildextension/beans/SyntheticInjectionPointListActiveTest.java new file mode 100644 index 0000000000000..864c326142d64 --- /dev/null +++ b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/buildextension/beans/SyntheticInjectionPointListActiveTest.java @@ -0,0 +1,111 @@ +package io.quarkus.arc.test.buildextension.beans; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import java.util.List; +import java.util.function.Supplier; + +import jakarta.enterprise.util.TypeLiteral; +import jakarta.inject.Singleton; + +import org.jboss.jandex.AnnotationInstance; +import org.jboss.jandex.ClassType; +import org.jboss.jandex.ParameterizedType; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.arc.Active; +import io.quarkus.arc.ActiveResult; +import io.quarkus.arc.Arc; +import io.quarkus.arc.BeanCreator; +import io.quarkus.arc.SyntheticCreationalContext; +import io.quarkus.arc.test.ArcTestContainer; +import io.quarkus.arc.test.MyQualifier; + +public class SyntheticInjectionPointListActiveTest { + @RegisterExtension + public ArcTestContainer container = ArcTestContainer.builder() + .beanClasses(MyQualifier.class) + .beanRegistrars(context -> { + context.configure(MyBean.class) + .types(MyBean.class) + .qualifiers(AnnotationInstance.builder(MyQualifier.class).build()) + .scope(Singleton.class) + .checkActive(AlwaysActive.class) + .creator(MyBeanCreator.class) + .done(); + + context.configure(MyBean.class) + .types(MyBean.class) + .scope(Singleton.class) + .checkActive(NeverActive.class) + .creator(MyBeanCreator.class) + .done(); + + context.configure(SyntheticBean.class) + .types(ClassType.create(SyntheticBean.class)) + .creator(SyntheticBeanCreator.class) + .addInjectionPoint(ParameterizedType.create(List.class, ClassType.create(MyBean.class)), + AnnotationInstance.builder(Active.class).build()) + .unremovable() + .done(); + }) + .build(); + + @Test + public void testListActiveInjection() { + SyntheticBean syntheticBean = Arc.container().instance(SyntheticBean.class).get(); + assertNotNull(syntheticBean); + List list = syntheticBean.getList(); + assertNotNull(list); + assertEquals(1, list.size()); + } + + static class MyBean { + public String ping() { + return MyBean.class.getSimpleName(); + } + } + + static class MyBeanCreator implements BeanCreator { + @Override + public MyBean create(SyntheticCreationalContext context) { + return new MyBean(); + } + } + + static class SyntheticBean { + private List list; + + SyntheticBean(List list) { + this.list = list; + } + + List getList() { + return list; + } + } + + static class SyntheticBeanCreator implements BeanCreator { + @Override + public SyntheticBean create(SyntheticCreationalContext context) { + return new SyntheticBean(context.getInjectedReference(new TypeLiteral<>() { + }, Active.Literal.INSTANCE)); + } + } + + static class AlwaysActive implements Supplier { + @Override + public ActiveResult get() { + return ActiveResult.active(); + } + } + + static class NeverActive implements Supplier { + @Override + public ActiveResult get() { + return ActiveResult.inactive(""); + } + } +} diff --git a/test-framework/junit5-component/src/main/java/io/quarkus/test/component/QuarkusComponentTestExtension.java b/test-framework/junit5-component/src/main/java/io/quarkus/test/component/QuarkusComponentTestExtension.java index d45e7bb5d90b7..7a956ec589406 100644 --- a/test-framework/junit5-component/src/main/java/io/quarkus/test/component/QuarkusComponentTestExtension.java +++ b/test-framework/junit5-component/src/main/java/io/quarkus/test/component/QuarkusComponentTestExtension.java @@ -90,6 +90,7 @@ import org.junit.jupiter.params.ParameterizedTest; import org.mockito.Mock; +import io.quarkus.arc.Active; import io.quarkus.arc.All; import io.quarkus.arc.Arc; import io.quarkus.arc.ArcContainer; @@ -330,7 +331,7 @@ public Object resolveParameter(ParameterContext parameterContext, ExtensionConte injectedParams.add(instance); return instance; } else if (isListAllInjectionPoint(requiredType, qualifiers, parameterContext.getParameter())) { - // Special handling for @Inject @All List<> + // Special handling for @Inject @All/@Active List<> Collection> unsetHandles = new ArrayList<>(); Object ret = handleListAll(requiredType, qualifiers, container, unsetHandles); unsetHandles.forEach(injectedParams::add); @@ -703,10 +704,10 @@ public void register(RegistrationContext registrationContext) { Type requiredType = injectionPoint.getRequiredType(); Set requiredQualifiers = injectionPoint.getRequiredQualifiers(); if (builtin == BuiltinBean.LIST) { - // @All List -> Delta + // @All/@Active List -> Delta requiredType = requiredType.asParameterizedType().arguments().get(0); requiredQualifiers = new HashSet<>(requiredQualifiers); - requiredQualifiers.removeIf(q -> q.name().equals(DotNames.ALL)); + requiredQualifiers.removeIf(q -> q.name().equals(DotNames.ALL) || q.name().equals(DotNames.ACTIVE)); if (requiredQualifiers.isEmpty()) { requiredQualifiers.add(AnnotationInstance.builder(DotNames.DEFAULT).build()); } @@ -1113,7 +1114,7 @@ public FieldInjector(Field field, Object testInstance) throws Exception { injectedInstance = instance; unsetAction = instance::destroy; } else if (isListAllInjectionPoint(requiredType, qualifiers, field)) { - // Special handling for @Injec @All List + // Special handling for @Inject @All/@Active List List> unsetHandles = new ArrayList<>(); injectedInstance = handleListAll(requiredType, qualifiers, container, unsetHandles); unsetAction = () -> destroyDependentHandles(unsetHandles); @@ -1176,19 +1177,22 @@ void destroyDependentHandles(List> handles) { private static Object handleListAll(java.lang.reflect.Type requiredType, Annotation[] qualifiers, ArcContainer container, Collection> unsetHandles) { - // Remove @All and add @Default if empty + // Remove @All/@Active and add @Default if empty Set qualifiersSet = new HashSet<>(); Collections.addAll(qualifiersSet, qualifiers); qualifiersSet.remove(All.Literal.INSTANCE); + boolean onlyActive = qualifiersSet.remove(Active.Literal.INSTANCE); if (qualifiersSet.isEmpty()) { qualifiers = new Annotation[] { Default.Literal.INSTANCE }; } else { qualifiers = qualifiersSet.toArray(new Annotation[] {}); } - List> handles = container.listAll(getFirstActualTypeArgument(requiredType), qualifiers); + List> handles = onlyActive + ? container.listActive(getFirstActualTypeArgument(requiredType), qualifiers) + : container.listAll(getFirstActualTypeArgument(requiredType), qualifiers); unsetHandles.addAll(handles); return isTypeArgumentInstanceHandle(requiredType) ? handles - : handles.stream().map(InstanceHandle::get).collect(Collectors.toUnmodifiableList()); + : handles.stream().map(InstanceHandle::get).toList(); } @SuppressWarnings("unchecked") @@ -1210,7 +1214,8 @@ private static boolean isListRequiredType(java.lang.reflect.Type type) { static boolean isListAllInjectionPoint(java.lang.reflect.Type requiredType, Annotation[] qualifiers, AnnotatedElement annotatedElement) { - if (qualifiers.length > 0 && Arrays.stream(qualifiers).anyMatch(All.Literal.INSTANCE::equals)) { + if (qualifiers.length > 0 && Arrays.stream(qualifiers).anyMatch( + it -> All.Literal.INSTANCE.equals(it) || Active.Literal.INSTANCE.equals(it))) { if (!isListRequiredType(requiredType)) { throw new IllegalStateException("Invalid injection point type: " + annotatedElement); } @@ -1219,11 +1224,9 @@ static boolean isListAllInjectionPoint(java.lang.reflect.Type requiredType, Anno return false; } - static final DotName ALL_NAME = DotName.createSimple(All.class); - static void adaptListAllQualifiers(Set qualifiers) { - // Remove @All and add @Default if empty - qualifiers.removeIf(a -> a.name().equals(ALL_NAME)); + // Remove @All/@Active and add @Default if empty + qualifiers.removeIf(a -> a.name().equals(DotNames.ALL) || a.name().equals(DotNames.ACTIVE)); if (qualifiers.isEmpty()) { qualifiers.add(AnnotationInstance.builder(Default.class).build()); }