diff --git a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/ArcContainerImpl.java b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/ArcContainerImpl.java index 678c6466f9c6f2..e6adca367160b0 100644 --- a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/ArcContainerImpl.java +++ b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/ArcContainerImpl.java @@ -24,9 +24,11 @@ import javax.enterprise.context.Dependent; import javax.enterprise.context.Destroyed; import javax.enterprise.context.Initialized; +import javax.enterprise.event.Event; import javax.enterprise.inject.AmbiguousResolutionException; import javax.enterprise.inject.Any; import javax.enterprise.inject.Default; +import javax.enterprise.inject.Instance; import javax.enterprise.inject.spi.Bean; import javax.enterprise.inject.spi.BeanManager; import javax.enterprise.inject.spi.CDI; @@ -107,6 +109,9 @@ public ArcContainerImpl() { transitiveInterceptorBindings.put(entry.getKey(), entry.getValue()); } } + // register built-in beans + addBuiltInBeans(); + Collections.sort(interceptors, (i1, i2) -> Integer.compare(i2.getPriority(), i1.getPriority())); resolved = new ComputingCache<>(this::resolve); @@ -118,6 +123,13 @@ public ArcContainerImpl() { } } + private void addBuiltInBeans() { + // BeanManager, Event, Instance + beans.add(new BeanManagerBean()); + beans.add(new EventBean()); + beans.add(new InstanceBean()); + } + void init() { requireRunning(); // Fire an event with qualifier @Initialized(ApplicationScoped.class) @@ -575,13 +587,24 @@ private void requireRunning() { private static final class Resolvable { + private static final Set BUILT_IN_TYPES = new HashSet<>(Arrays.asList(Event.class, Instance.class)); + private static final Annotation[] ANY_QUALIFIER = new Annotation[] { Any.Literal.INSTANCE }; + final Type requiredType; final Annotation[] qualifiers; Resolvable(Type requiredType, Annotation[] qualifiers) { - this.requiredType = requiredType; - this.qualifiers = qualifiers; + // if the type is any of BUILT_IN_TYPES, the resolution simplifies type to raw type and ignores qualifiers + // this is so that every injection point matches the bean we provide for that type + Type rawType = Reflections.getRawType(requiredType); + if (BUILT_IN_TYPES.contains(rawType)) { + this.requiredType = rawType; + this.qualifiers = ANY_QUALIFIER; + } else { + this.requiredType = requiredType; + this.qualifiers = qualifiers; + } } @Override diff --git a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/BeanManagerBean.java b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/BeanManagerBean.java new file mode 100644 index 00000000000000..9b0368f81eaf10 --- /dev/null +++ b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/BeanManagerBean.java @@ -0,0 +1,34 @@ +package io.quarkus.arc; + +import java.lang.reflect.Type; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; +import javax.enterprise.context.spi.CreationalContext; +import javax.enterprise.inject.spi.BeanManager; + +public class BeanManagerBean extends BuiltInBean { + + private static final Set BM_TYPES = Collections + .unmodifiableSet(new HashSet<>(Arrays.asList(Object.class, BeanManager.class))); + + @Override + public Set getTypes() { + return BM_TYPES; + } + + @Override + public BeanManager get(CreationalContext creationalContext) { + BeanManager instance = new BeanManagerProvider<>().get(creationalContext); + CreationalContextImpl.addDependencyToParent((InjectableBean) InjectionPointProvider.get().getBean(), + instance, creationalContext); + return instance; + } + + @Override + public Class getBeanClass() { + return BeanManagerImpl.class; + } + +} diff --git a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/BuiltInBean.java b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/BuiltInBean.java new file mode 100644 index 00000000000000..04f3d0a4b7d44c --- /dev/null +++ b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/BuiltInBean.java @@ -0,0 +1,45 @@ +package io.quarkus.arc; + +import java.lang.annotation.Annotation; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import javax.enterprise.context.Dependent; +import javax.enterprise.context.spi.CreationalContext; + +/** + * Common class for all built-in beans. + * + */ +public abstract class BuiltInBean implements InjectableBean { + + @Override + public String getIdentifier() { + return sha1(toString()); + } + + @Override + public Class getScope() { + return Dependent.class; + } + + @Override + public T create(CreationalContext creationalContext) { + return get(creationalContext); + } + + // copy of io.quarkus.arc.processor.Hashes#sha1 + private String sha1(String value) { + try { + MessageDigest md = MessageDigest.getInstance("SHA-1"); + byte[] digest = md.digest(value.getBytes(StandardCharsets.UTF_8)); + StringBuilder sb = new StringBuilder(40); + for (int i = 0; i < digest.length; ++i) { + sb.append(Integer.toHexString((digest[i] & 0xFF) | 0x100).substring(1, 3)); + } + return sb.toString(); + } catch (NoSuchAlgorithmException e) { + throw new IllegalStateException(e); + } + } +} diff --git a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/EventBean.java b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/EventBean.java new file mode 100644 index 00000000000000..785e00153aa9d6 --- /dev/null +++ b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/EventBean.java @@ -0,0 +1,37 @@ +package io.quarkus.arc; + +import java.lang.reflect.Type; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; +import javax.enterprise.context.spi.CreationalContext; +import javax.enterprise.event.Event; +import javax.enterprise.inject.spi.InjectionPoint; +import javax.enterprise.util.TypeLiteral; + +public class EventBean extends BuiltInBean> { + + public static final Set EVENT_TYPES = Collections + .unmodifiableSet(new HashSet<>(Arrays.asList(Event.class, new TypeLiteral>() { + }.getType(), Object.class))); + + @Override + public Set getTypes() { + return EVENT_TYPES; + } + + @Override + public Event get(CreationalContext> creationalContext) { + // Obtain current IP to get the required type and qualifiers + InjectionPoint ip = InjectionPointProvider.get(); + EventImpl instance = new EventImpl<>(ip.getType(), ip.getQualifiers()); + CreationalContextImpl.addDependencyToParent((InjectableBean>) ip.getBean(), instance, creationalContext); + return instance; + } + + @Override + public Class getBeanClass() { + return EventImpl.class; + } +} diff --git a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/InstanceBean.java b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/InstanceBean.java new file mode 100644 index 00000000000000..e28be698afef23 --- /dev/null +++ b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/InstanceBean.java @@ -0,0 +1,38 @@ +package io.quarkus.arc; + +import java.lang.reflect.Type; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; +import javax.enterprise.context.spi.CreationalContext; +import javax.enterprise.inject.Instance; +import javax.enterprise.inject.spi.InjectionPoint; +import javax.enterprise.util.TypeLiteral; + +public class InstanceBean extends BuiltInBean> { + + public static final Set INSTANCE_TYPES = Collections + .unmodifiableSet(new HashSet<>(Arrays.asList(Instance.class, new TypeLiteral>() { + }.getType(), Object.class))); + + @Override + public Set getTypes() { + return INSTANCE_TYPES; + } + + @Override + public Class getBeanClass() { + return InstanceImpl.class; + } + + @Override + public Instance get(CreationalContext> creationalContext) { + // Obtain current IP to get the required type and qualifiers + InjectionPoint ip = InjectionPointProvider.get(); + InstanceImpl> instance = new InstanceImpl>((InjectableBean) ip.getBean(), ip.getType(), + ip.getQualifiers(), (CreationalContextImpl) creationalContext, Collections.EMPTY_SET, ip.getMember(), 0); + CreationalContextImpl.addDependencyToParent((InjectableBean>) ip.getBean(), instance, creationalContext); + return instance; + } +} diff --git a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/Reflections.java b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/Reflections.java index 0b8d301b68d17c..3cd36737f2f23a 100644 --- a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/Reflections.java +++ b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/Reflections.java @@ -1,9 +1,15 @@ package io.quarkus.arc; +import java.lang.reflect.Array; import java.lang.reflect.Constructor; import java.lang.reflect.Field; +import java.lang.reflect.GenericArrayType; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.lang.reflect.TypeVariable; +import java.lang.reflect.WildcardType; import java.util.Arrays; /** @@ -109,4 +115,41 @@ static T cast(Object obj) { return (T) obj; } + public static Class getRawType(Type type) { + if (type instanceof Class) { + return (Class) type; + } + if (type instanceof ParameterizedType) { + if (((ParameterizedType) type).getRawType() instanceof Class) { + return (Class) ((ParameterizedType) type).getRawType(); + } + } + if (type instanceof TypeVariable) { + TypeVariable variable = (TypeVariable) type; + Type[] bounds = variable.getBounds(); + return getBound(bounds); + } + if (type instanceof WildcardType) { + WildcardType wildcard = (WildcardType) type; + return getBound(wildcard.getUpperBounds()); + } + if (type instanceof GenericArrayType) { + GenericArrayType genericArrayType = (GenericArrayType) type; + Class rawType = getRawType(genericArrayType.getGenericComponentType()); + if (rawType != null) { + return (Class) Array.newInstance(rawType, 0).getClass(); + } + } + return null; + } + + @SuppressWarnings("unchecked") + private static Class getBound(Type[] bounds) { + if (bounds.length == 0) { + return (Class) Object.class; + } else { + return getRawType(bounds[0]); + } + } + } diff --git a/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/builtin/beans/BuiltInBeansAreResolvableTest.java b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/builtin/beans/BuiltInBeansAreResolvableTest.java new file mode 100644 index 00000000000000..29e267ebbeff51 --- /dev/null +++ b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/builtin/beans/BuiltInBeansAreResolvableTest.java @@ -0,0 +1,223 @@ +package io.quarkus.arc.test.builtin.beans; + +import io.quarkus.arc.Arc; +import io.quarkus.arc.CreationalContextImpl; +import io.quarkus.arc.test.ArcTestContainer; +import java.lang.annotation.Annotation; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.reflect.Member; +import java.lang.reflect.Type; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; +import javax.enterprise.context.ApplicationScoped; +import javax.enterprise.event.Event; +import javax.enterprise.event.Observes; +import javax.enterprise.inject.Any; +import javax.enterprise.inject.Instance; +import javax.enterprise.inject.spi.Annotated; +import javax.enterprise.inject.spi.Bean; +import javax.enterprise.inject.spi.BeanManager; +import javax.enterprise.inject.spi.InjectionPoint; +import javax.enterprise.util.AnnotationLiteral; +import javax.enterprise.util.TypeLiteral; +import javax.inject.Qualifier; +import org.junit.Assert; +import org.junit.Rule; +import org.junit.Test; + +public class BuiltInBeansAreResolvableTest { + + @Rule + public ArcTestContainer container = ArcTestContainer.builder().beanClasses(DummyBean.class, DummyQualifier.class).build(); + + @Test + public void testBeanManagerBean() { + // use dynamic resolution to test it + Instance instance = Arc.container().beanManager().createInstance(); + // basic BM is resolvable + Assert.assertTrue(instance.select(BeanManager.class).isResolvable()); + // invoke something on the BM + Assert.assertEquals(1, instance.select(BeanManager.class).get().getBeans(DummyBean.class).size()); + // you shouldn't be able to select BM with qualifiers + Assert.assertFalse(instance.select(BeanManager.class, new DummyQualifier.Literal()).isResolvable()); + } + + @Test + public void testEventBean() { + // use dynamic resolution to test it + Instance instance = Arc.container().beanManager().createInstance(); + // event with no qualifier and raw type + Instance rawNoQualifier = instance.select(Event.class); + Assert.assertTrue(rawNoQualifier.isResolvable()); + DummyBean.resetCounters(); + rawNoQualifier.get().fire(new Object()); + Assert.assertEquals(0, DummyBean.noQualifiers); + Assert.assertEquals(0, DummyBean.qualifierTimesTriggered); + Assert.assertEquals(1, DummyBean.objectTimesTriggered); + + // event with type and no qualifier + Instance> typedEventNoQualifier = instance.select(new TypeLiteral>() { + }); + Assert.assertTrue(typedEventNoQualifier.isResolvable()); + DummyBean.resetCounters(); + typedEventNoQualifier.get().fire("foo"); + Assert.assertEquals(1, DummyBean.noQualifiers); + Assert.assertEquals(0, DummyBean.qualifierTimesTriggered); + Assert.assertEquals(1, DummyBean.objectTimesTriggered); + + // event with type and qualifier + Instance> typedEventWithQualifier = instance.select(new TypeLiteral>() { + }, new DummyQualifier.Literal()); + Assert.assertTrue(typedEventWithQualifier.isResolvable()); + DummyBean.resetCounters(); + typedEventWithQualifier.get().fire("foo"); + Assert.assertEquals(1, DummyBean.noQualifiers); + Assert.assertEquals(1, DummyBean.qualifierTimesTriggered); + Assert.assertEquals(1, DummyBean.objectTimesTriggered); + + } + + @Test + public void testInstanceBean() { + BeanManager bm = Arc.container().beanManager(); + + // verify all selections have a backing bean + Set> instanceBeans = bm.getBeans(Instance.class); + Assert.assertEquals(1, instanceBeans.size()); + Set> typedInstanceBeans = bm.getBeans(new TypeLiteral>() { + }.getType()); + Assert.assertEquals(1, typedInstanceBeans.size()); + Set> typedQualifiedInstanceBean = bm.getBeans(new TypeLiteral>() { + }.getType(), new DummyQualifier.Literal()); + Assert.assertEquals(1, typedQualifiedInstanceBean.size()); + + InjectionPoint dummyIp = new InjectionPoint() { + + @Override + public Type getType() { + return new TypeLiteral>() { + }.getType(); + } + + @Override + public Set getQualifiers() { + return new HashSet<>(); + } + + @Override + public Bean getBean() { + return null; + } + + @Override + public Member getMember() { + return null; + } + + @Override + public Annotated getAnnotated() { + return null; + } + + @Override + public boolean isDelegate() { + return false; + } + + @Override + public boolean isTransient() { + return false; + } + }; + + InjectionPoint dummyIpWithQualifiers = new InjectionPoint() { + + @Override + public Type getType() { + return new TypeLiteral>() { + }.getType(); + } + + @Override + public Set getQualifiers() { + return new HashSet<>(Arrays.asList(Any.Literal.INSTANCE, new DummyQualifier.Literal())); + } + + @Override + public Bean getBean() { + return null; + } + + @Override + public Member getMember() { + return null; + } + + @Override + public Annotated getAnnotated() { + return null; + } + + @Override + public boolean isDelegate() { + return false; + } + + @Override + public boolean isTransient() { + return false; + } + }; + + Instance dummyInstance = (Instance) bm.getInjectableReference(dummyIp, + new CreationalContextImpl<>(null)); + Assert.assertFalse(dummyInstance.isResolvable()); // not resolvable, no qualifier + dummyInstance.select(new DummyQualifier.Literal()).get().ping(); + + Instance dummyInstanceWithQualifier = (Instance) bm.getInjectableReference(dummyIpWithQualifiers, + new CreationalContextImpl<>(null)); + Assert.assertTrue(dummyInstanceWithQualifier.isResolvable()); // resolvable, qualifier is present + dummyInstanceWithQualifier.get().ping(); + } + + @Retention(RetentionPolicy.RUNTIME) + @Qualifier + @interface DummyQualifier { + + @SuppressWarnings("all") + public static class Literal extends AnnotationLiteral implements DummyQualifier { + } + } + + @ApplicationScoped + @DummyQualifier + static class DummyBean { + + public static int noQualifiers = 0; + public static int qualifierTimesTriggered = 0; + public static int objectTimesTriggered = 0; + + public void observeEvent(@Observes String payload) { + noQualifiers++; + } + + public void observeQualifiedEvent(@Observes @DummyQualifier String payload) { + qualifierTimesTriggered++; + } + + public void observeBasicEvent(@Observes @Any Object objPayload) { + objectTimesTriggered++; + } + + public static void resetCounters() { + noQualifiers = 0; + qualifierTimesTriggered = 0; + objectTimesTriggered = 0; + } + + public void ping() { + }; + } +}