Skip to content

Commit

Permalink
Properly support lookup with qualifiers, create test coverage for it
Browse files Browse the repository at this point in the history
  • Loading branch information
manovotn committed Sep 1, 2023
1 parent 9c09351 commit e3ddebe
Show file tree
Hide file tree
Showing 18 changed files with 368 additions and 28 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ public Collection<AnnotatedMethod<? super X>> getInvokableMethods() {
@Override
public InvokerBuilder<Invoker<X, ?>> createInvoker(AnnotatedMethod<? super X> annotatedMethod) {
checkWithinObserverNotification();
return new InvokerBuilderImpl<>(getBean().getType(), annotatedMethod.getJavaMember());
return new InvokerBuilderImpl<>(getBean().getType(), annotatedMethod.getJavaMember(), getBeanManager());
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,6 @@ public Collection<AnnotatedMethod<? super Object>> getInvokableMethods() {
@Override
public InvokerBuilder<Invoker<Object, ?>> createInvoker(AnnotatedMethod<? super Object> annotatedMethod) {
checkWithinObserverNotification();
return new InvokerBuilderImpl<>(getBean().getEjbDescriptor().getBeanClass(), annotatedMethod.getJavaMember());
return new InvokerBuilderImpl<>(getBean().getEjbDescriptor().getBeanClass(), annotatedMethod.getJavaMember(), getBeanManager());
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package org.jboss.weld.invokable;

import jakarta.enterprise.inject.spi.BeanManager;
import jakarta.enterprise.invoke.InvokerBuilder;

import java.lang.reflect.Method;
Expand All @@ -13,18 +14,20 @@ abstract class AbstractInvokerBuilder<T> implements InvokerBuilder<T> {

boolean instanceLookup;
boolean[] argLookup;
BeanManager beanManager;
TransformerMetadata instanceTransformer;
TransformerMetadata returnValueTransformer;
TransformerMetadata exceptionTransformer;
TransformerMetadata invocationWrapper;
final TransformerMetadata[] argTransformers;

// TODO Class is rawtype otherwise we cannot use it from InvokerInfoBuilder, can we improve this?
public AbstractInvokerBuilder(Class beanClass, Method method) {
public AbstractInvokerBuilder(Class beanClass, Method method, BeanManager beanManager) {
this.beanClass = beanClass;
this.method = method;
this.argLookup = new boolean[method.getParameters().length];
this.argTransformers = new TransformerMetadata[method.getParameters().length];
this.beanManager = beanManager;
}


Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
package org.jboss.weld.invokable;

import jakarta.enterprise.inject.spi.BeanManager;
import jakarta.enterprise.invoke.Invoker;

import java.lang.reflect.Method;

public class InvokerBuilderImpl<T> extends AbstractInvokerBuilder {

public InvokerBuilderImpl(Class<T> beanClass, Method method) {
super(beanClass, method);
public InvokerBuilderImpl(Class<T> beanClass, Method method, BeanManager beanManager) {
super(beanClass, method, beanManager);
}

@Override
Expand Down
78 changes: 58 additions & 20 deletions impl/src/main/java/org/jboss/weld/invokable/InvokerImpl.java
Original file line number Diff line number Diff line change
@@ -1,17 +1,22 @@
package org.jboss.weld.invokable;

import jakarta.enterprise.context.Dependent;
import jakarta.enterprise.inject.Default;
import jakarta.enterprise.inject.Instance;
import jakarta.enterprise.inject.build.compatible.spi.InvokerInfo;
import jakarta.enterprise.inject.spi.BeanManager;
import jakarta.enterprise.inject.spi.CDI;
import jakarta.enterprise.inject.spi.DeploymentException;
import jakarta.enterprise.invoke.Invoker;
import jakarta.inject.Named;

import java.lang.annotation.Annotation;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Parameter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
Expand All @@ -23,19 +28,20 @@ public class InvokerImpl<T, R> implements Invoker<T, R>, InvokerInfo {
private final MethodHandle beanMethodHandle;
private final MethodHandle invocationWrapper;
private final Method method;
private final boolean instanceLookup;
private final boolean[] argLookup;
// null if no lookup is to be done
private final Annotation[] instanceLookupQualifiers;
private final Annotation[][] argLookupQualifiers;
private final boolean hasInstanceTransformer;
private final Class<T> beanClass;
private final InvokerCleanupActions cleanupActions;

// this variant is only used for invocation wrapper and assumes fully-initialized state
private InvokerImpl(MethodHandle beanMethodHandle, boolean instanceLookup, boolean[] argLookup,
private InvokerImpl(MethodHandle beanMethodHandle, Annotation[] instanceLookupQualifiers, Annotation[][] argLookupQualifiers,
boolean hasInstanceTransformer, InvokerCleanupActions cleanupActions,
Class<T> beanClass, Method method) {
this.beanMethodHandle = beanMethodHandle;
this.instanceLookup = instanceLookup;
this.argLookup = argLookup;
this.instanceLookupQualifiers = instanceLookupQualifiers;
this.argLookupQualifiers = argLookupQualifiers;
this.hasInstanceTransformer = hasInstanceTransformer;
this.cleanupActions = cleanupActions;
this.beanClass = beanClass;
Expand All @@ -46,11 +52,21 @@ private InvokerImpl(MethodHandle beanMethodHandle, boolean instanceLookup, boole
protected InvokerImpl(AbstractInvokerBuilder<T> builder) {
// needs to be initialized first, as can be used when creating method handles
this.cleanupActions = new InvokerCleanupActions();

this.instanceLookup = builder.instanceLookup;
this.argLookup = builder.argLookup;
this.beanClass = builder.beanClass;
this.method = builder.method;
if (builder.instanceLookup) {
this.instanceLookupQualifiers = extractInstanceQualifiers(beanClass, builder.beanManager);
} else {
this.instanceLookupQualifiers = null;
}
this.argLookupQualifiers = new Annotation[builder.argLookup.length][];
for (int i = 0; i < builder.argLookup.length; i++) {
// positions that remain null are those for which we don't perform lookup
if (builder.argLookup[i]) {
this.argLookupQualifiers[i] = extractParamQualifiers(method.getParameters()[i], builder.beanManager);
}
}


// handles transformers
MethodHandle[] argTransformers = new MethodHandle[builder.argTransformers.length];
Expand Down Expand Up @@ -247,25 +263,25 @@ public R invoke(T instance, Object[] arguments) {
// if there is an invocation wrapper, just invoke the wrapper immediately
if (this.invocationWrapper != null) {
try {
return (R) invocationWrapper.invoke(instance, arguments, new InvokerImpl<>(beanMethodHandle, instanceLookup,
argLookup, hasInstanceTransformer, cleanupActions, beanClass, method));
return (R) invocationWrapper.invoke(instance, arguments, new InvokerImpl<>(beanMethodHandle, instanceLookupQualifiers,
argLookupQualifiers, hasInstanceTransformer, cleanupActions, beanClass, method));
} catch (Throwable e) {
// invocation wrapper or the method itself threw an exception, we just rethrow
throw new RuntimeException(e);
}
}

// TODO do we want to verify that the T instance is the exact class of the bean?
if (arguments.length != argLookup.length) {
if (arguments.length != method.getParameterCount()) {
// TODO better exception
throw new IllegalArgumentException("Wrong number of args for invoker! Expected: " + argLookup.length + " Provided: " + arguments);
throw new IllegalArgumentException("Wrong number of args for invoker! Expected: " + method.getParameterCount() + " Provided: " + arguments.length);
}
T beanInstance = instance;
Instance<Object> cdiLookup = CDI.current().getBeanManager().createInstance();
if (instanceLookup) {
if (instanceLookupQualifiers != null) {
// instance lookup set, ignore the parameter we got and lookup the instance
// standard CDI resolution errors can occur
beanInstance = getInstance(cdiLookup, beanClass);
beanInstance = getInstance(cdiLookup, beanClass, instanceLookupQualifiers);
} else {
// arg should not be null unless the method is (a) static, (b) has instance transformer, (c) has instance lookup
if (beanInstance == null && !Modifier.isStatic(method.getModifiers()) && !hasInstanceTransformer) {
Expand All @@ -274,10 +290,10 @@ public R invoke(T instance, Object[] arguments) {
}
}

List<Object> methodArgs = new ArrayList<>(argLookup.length + 1);
for (int i = 0; i < argLookup.length; i++) {
if (argLookup[i]) {
methodArgs.add(i, getInstance(cdiLookup, beanMethodHandle.type().parameterType(Modifier.isStatic(method.getModifiers()) ? i : i + 1)));
List<Object> methodArgs = new ArrayList<>(argLookupQualifiers.length + 1);
for (int i = 0; i < argLookupQualifiers.length; i++) {
if (argLookupQualifiers[i] != null) {
methodArgs.add(i, getInstance(cdiLookup, beanMethodHandle.type().parameterType(Modifier.isStatic(method.getModifiers()) ? i : i + 1), argLookupQualifiers[i]));
} else {
methodArgs.add(i, arguments[i]);
}
Expand All @@ -304,12 +320,34 @@ public R invoke(T instance, Object[] arguments) {
}
}

private <TYPE> TYPE getInstance(Instance<Object> lookup, Class<TYPE> classToLookup) {
Instance.Handle<TYPE> handle = lookup.select(classToLookup).getHandle();
private <TYPE> TYPE getInstance(Instance<Object> lookup, Class<TYPE> classToLookup, Annotation... qualifiers) {
Instance.Handle<TYPE> handle = lookup.select(classToLookup, qualifiers).getHandle();
this.cleanupActions.addInstanceHandle(handle);
return handle.get();
}

private Annotation[] extractInstanceQualifiers(Class<?> beanClass, BeanManager bm) {
return extractQualifiers(beanClass.getAnnotations(), bm);
}

private Annotation[] extractParamQualifiers(Parameter parameter, BeanManager bm) {
return extractQualifiers(parameter.getAnnotations(), bm);
}

private Annotation[] extractQualifiers(Annotation[] annotations, BeanManager bm) {
List<Annotation> qualifiers = new ArrayList<>();
for (Annotation a : annotations) {
if (bm.isQualifier(a.annotationType())) {
qualifiers.add(a);
}
}
// add default when there are no qualifiers or just @Named
if (qualifiers.size() == 0 || (qualifiers.size() == 1 && qualifiers.get(0).annotationType().equals(Named.class))) {
qualifiers.add(Default.Literal.INSTANCE);
}
return qualifiers.toArray(new Annotation[]{});
}

private class InvokerCleanupActions implements Consumer<Runnable> {

private List<Runnable> cleanupTasks = new ArrayList<>();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
package org.jboss.weld.invokable;

import jakarta.enterprise.inject.build.compatible.spi.InvokerInfo;
import jakarta.enterprise.inject.spi.BeanManager;

import java.lang.reflect.Method;

public class InvokerInfoBuilder extends AbstractInvokerBuilder<InvokerInfo> {

public InvokerInfoBuilder(Class<?> beanClass, Method method) {
super(beanClass, method);
public InvokerInfoBuilder(Class<?> beanClass, Method method, BeanManager beanManager) {
super(beanClass, method, beanManager);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package org.jboss.weld.tests.invokable.lookup;

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.inject.Produces;

@ApplicationScoped
public class BeanProducer {

@Produces
@MyQualifier1("foo")
@MyQualifier4("binding")
public String produce1 () {
return MyQualifier1.class.getSimpleName() + MyQualifier4.class.getSimpleName();
}

@Produces
@MyQualifier2
public String produce2 () {
return MyQualifier2.class.getSimpleName();
}

@Produces
@MyQualifier5
public String produceAmbig1 () {
throw new IllegalStateException("Ambiguous producer should never be invoked");
}

@Produces
@MyQualifier5
public String produceAmbig2 () {
throw new IllegalStateException("Ambiguous producer should never be invoked");
}

@Produces
public String producePlain () {
throw new IllegalStateException("No qualifier producer should never be invoked");
}

@Produces
@ToBeQualifier
public String produceQualified () {
return ToBeQualifier.class.getSimpleName();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package org.jboss.weld.tests.invokable.lookup;

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.invoke.Invokable;

@ApplicationScoped
@MyQualifier1("myBean")
@Invokable
public class InvokableBean {

public String instanceLookup() {
return InvokableBean.class.getSimpleName();
}

// there are two producers providing a bean for the first argument
public String ambiguousLookup(@MyQualifier5 String a) {
return a;
}

// there is no bean with @MyQualifier3
public String unsatisfiedLookup(@MyQualifier3 String a) {
return a;
}

public String correctLookup (@MyQualifier1("noMatter") @MyQualifier4("binding") String a, @MyQualifier2 String b) {
return a + b;
}

public String lookupWithRegisteredQualifier (@ToBeQualifier String a) {
return a;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package org.jboss.weld.tests.invokable.lookup;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

import jakarta.enterprise.inject.AmbiguousResolutionException;
import jakarta.enterprise.inject.UnsatisfiedResolutionException;
import jakarta.enterprise.inject.spi.Extension;
import jakarta.inject.Inject;
import org.jboss.arquillian.container.test.api.Deployment;
import org.jboss.arquillian.junit.Arquillian;
import org.jboss.shrinkwrap.api.Archive;
import org.jboss.shrinkwrap.api.BeanArchive;
import org.jboss.shrinkwrap.api.ShrinkWrap;
import org.jboss.weld.test.util.Utils;
import org.junit.Test;
import org.junit.runner.RunWith;

@RunWith(Arquillian.class)
public class InvokableMethodLookupTest {

@Deployment
public static Archive getDeployment() {
return ShrinkWrap.create(BeanArchive.class, Utils.getDeploymentNameAsHash(InvokableMethodLookupTest.class))
.addPackage(InvokableMethodLookupTest.class.getPackage())
.addAsServiceProvider(Extension.class, InvokerRegistreringExtension.class);
}

@Inject
InvokerRegistreringExtension extension;

@Inject
@MyQualifier1("abc")
InvokableBean bean;

@Test
public void testInstanceLookupWithQualifiers() {
Object invokerResult = extension.getInstanceLookupInvoker().invoke(null, new Object[]{});
assertTrue(invokerResult instanceof String);
assertEquals(InvokableBean.class.getSimpleName(), invokerResult);
}

@Test
public void testCorrectArqLookupWithQualifiers() {
Object invokerResult = extension.getCorrectLookupInvoker().invoke(bean, new Object[]{null, null});
assertTrue(invokerResult instanceof String);
assertEquals(MyQualifier1.class.getSimpleName() + MyQualifier4.class.getSimpleName() + MyQualifier2.class.getSimpleName(), invokerResult);
}

@Test
public void testLookupWithRegisteredQualifier() {
Object invokerResult = extension.getLookupWithRegisteredQualifier().invoke(bean, new Object[]{null});
assertTrue(invokerResult instanceof String);
assertEquals(ToBeQualifier.class.getSimpleName(), invokerResult);
}

@Test
public void testUnsatisfiedLookupWithQualifier() {
try {
Object invokerResult = extension.getUnsatisfiedLookupInvoker().invoke(bean, new Object[]{null});
fail();
} catch (UnsatisfiedResolutionException e) {
// expected
}
}

@Test
public void testAmbigLookupWithQualifiers() {
try {
Object invokerResult = extension.getAmbiguousLookupInvoker().invoke(bean, new Object[]{null});
fail();
} catch (AmbiguousResolutionException e) {
// expected
}
}
}

0 comments on commit e3ddebe

Please sign in to comment.