diff --git a/docs/src/main/asciidoc/cdi-integration.adoc b/docs/src/main/asciidoc/cdi-integration.adoc index fe92dd294ba6a..a68baa17eeadc 100644 --- a/docs/src/main/asciidoc/cdi-integration.adoc +++ b/docs/src/main/asciidoc/cdi-integration.adoc @@ -486,6 +486,21 @@ if (foo.getHandle().getBean().isActive()) { } ---- +If you want to consume only active beans, you can inject an `InjectableInstance<>` and call `getActive()` to get the single instance or `listActive()` to get all instances: + +[source,java] +---- +import io.quarkus.arc.InjectableInstance; + +@Inject +@Any +InjectableInstance foos; + +for (Foo foo : foos.listActive()) + ... +} +---- + [[synthetic_observers]] == Use Case - Synthetic Observers 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/InjectableInstance.java b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/InjectableInstance.java index f3bff0a4ffc73..9d80f6f228753 100644 --- a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/InjectableInstance.java +++ b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/InjectableInstance.java @@ -2,11 +2,14 @@ import java.lang.annotation.Annotation; import java.util.Iterator; +import java.util.List; import java.util.stream.Stream; import java.util.stream.StreamSupport; import jakarta.enterprise.context.Dependent; +import jakarta.enterprise.inject.AmbiguousResolutionException; import jakarta.enterprise.inject.Instance; +import jakarta.enterprise.inject.UnsatisfiedResolutionException; import jakarta.enterprise.util.TypeLiteral; /** @@ -75,11 +78,37 @@ default T orElse(T other) { * If there is exactly one bean that matches the required type and qualifiers, returns the instance, otherwise returns * {@code null}. * - * @param other * @return the bean instance or {@code null} */ default T orNull() { return orElse(null); } + /** + * Returns exactly one instance of an {@linkplain InjectableBean#checkActive() active} bean that matches + * the required type and qualifiers. If no active bean matches, or if more than one active bean matches, + * throws an exception. + * + * @return the single instance of an active matching bean + */ + default T getActive() { + List list = listActive(); + if (list.size() == 1) { + return list.get(0); + } else if (list.isEmpty()) { + throw new UnsatisfiedResolutionException("No active bean found"); + } else { + throw new AmbiguousResolutionException("More than one active bean found: " + list); + } + } + + /** + * Returns the list of instances of {@linkplain InjectableBean#checkActive() active} beans that match + * the required type and qualifiers, sorter in priority order (higher priority goes first). + * + * @return the list of instances of matching active beans + */ + default List listActive() { + return handlesStream().filter(it -> it.getBean().isActive()).map(InstanceHandle::get).toList(); + } } diff --git a/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/instance/ActiveMultipleTest.java b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/instance/ActiveMultipleTest.java new file mode 100644 index 0000000000000..5904753fd7728 --- /dev/null +++ b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/instance/ActiveMultipleTest.java @@ -0,0 +1,228 @@ +package io.quarkus.arc.test.instance; + +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.assertThrows; + +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.AmbiguousResolutionException; +import jakarta.enterprise.inject.Any; +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.ActiveResult; +import io.quarkus.arc.Arc; +import io.quarkus.arc.BeanCreator; +import io.quarkus.arc.BeanDestroyer; +import io.quarkus.arc.InjectableInstance; +import io.quarkus.arc.SyntheticCreationalContext; +import io.quarkus.arc.test.ArcTestContainer; +import io.quarkus.arc.test.MyQualifier; + +public class ActiveMultipleTest { + @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 testListActive() { + Consumer consumer = Arc.container().select(Consumer.class).get(); + + List activeServices = consumer.services.listActive(); + assertEquals(2, activeServices.size()); + assertThatExceptionOfType(UnsupportedOperationException.class) + .isThrownBy(() -> activeServices.remove(0)); + // ServiceDelta has higher priority + Service delta = activeServices.get(0); + assertEquals("delta", delta.ping()); + assertEquals("alpha", activeServices.get(1).ping()); + assertNotNull(delta.getInjectionPoint()); + assertEquals(Service.class, delta.getInjectionPoint().getType()); + } + + @Test + public void testGetActive() { + Consumer consumer = Arc.container().select(Consumer.class).get(); + + assertThrows(AmbiguousResolutionException.class, () -> { + consumer.services.getActive(); + }); + } + + @Singleton + public static class Consumer { + @Inject + @Any + InjectableInstance services; + } + + 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/instance/ActiveSingleTest.java b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/instance/ActiveSingleTest.java new file mode 100644 index 0000000000000..308c2f96d3923 --- /dev/null +++ b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/instance/ActiveSingleTest.java @@ -0,0 +1,114 @@ +package io.quarkus.arc.test.instance; + +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.List; +import java.util.function.Supplier; + +import jakarta.enterprise.inject.Any; +import jakarta.inject.Inject; +import jakarta.inject.Singleton; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.arc.ActiveResult; +import io.quarkus.arc.Arc; +import io.quarkus.arc.BeanCreator; +import io.quarkus.arc.InjectableInstance; +import io.quarkus.arc.SyntheticCreationalContext; +import io.quarkus.arc.test.ArcTestContainer; + +public class ActiveSingleTest { + @RegisterExtension + public ArcTestContainer container = ArcTestContainer.builder() + .beanClasses(Service.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) + .scope(Singleton.class) + .checkActive(NeverActive.class) + .creator(BravoCreator.class) + .done(); + }) + .build(); + + @Test + public void testListActive() { + Consumer consumer = Arc.container().select(Consumer.class).get(); + + List activeServices = consumer.services.listActive(); + assertEquals(1, activeServices.size()); + assertThatExceptionOfType(UnsupportedOperationException.class) + .isThrownBy(() -> activeServices.remove(0)); + Service alpha = activeServices.get(0); + assertEquals("alpha", alpha.ping()); + } + + @Test + public void testGetActive() { + Consumer consumer = Arc.container().select(Consumer.class).get(); + + assertEquals("alpha", consumer.services.getActive().ping()); + } + + @Singleton + public static class Consumer { + @Inject + @Any + InjectableInstance services; + } + + interface Service { + String ping(); + } + + 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 { + @Override + public String ping() { + return "bravo"; + } + } + + static class BravoCreator implements BeanCreator { + @Override + public ServiceBravo create(SyntheticCreationalContext context) { + return new ServiceBravo(); + } + } + + static class AlwaysActive implements Supplier { + @Override + public ActiveResult get() { + return ActiveResult.active(); + } + } + + static class NeverActive implements Supplier { + @Override + public ActiveResult get() { + return ActiveResult.inactive(""); + } + } +}