From 06e1c08ff662c3ca1bb71d45241114ca7a3520fe Mon Sep 17 00:00:00 2001 From: "M.P. Korstanje" Date: Mon, 8 Mar 2021 16:35:39 +0100 Subject: [PATCH] [Cdi2,Jakarta Cdi] Add step definitions as beans when not discovered (#2248) Depending on the CDI implementation used adding bean classes through getInitializer().addBeanClasses(clazz); while also using a beans.xml file to mark all classes in the module as beans results in duplicate bean definitions. By defining step definitions only as beans when not already defined we avoid this problem. Fixes: #2241 Co-authored-by: Daniel Beland Co-authored-by: Daniel Beland --- CHANGELOG.md | 2 + cdi2/README.md | 74 +++++++++-- cdi2/pom.xml | 73 ++++++++++- .../java/io/cucumber/cdi2/Cdi2Factory.java | 78 +++++++++--- .../io/cucumber/cdi2/Cdi2FactoryTest.java | 79 +++++++++--- .../cdi2/IgnoreLocalBeansXmlClassLoader.java | 38 ++++++ .../cdi2/example/BellyStepDefinitions.java | 2 + ...r.properties => junit-platform.properties} | 0 jakarta-cdi/README.md | 87 +++++++++---- jakarta-cdi/pom.xml | 120 ++++++++++++++++-- .../jakarta/cdi/CdiJakartaFactory.java | 79 +++++++++--- .../jakarta/cdi/CdiJakartaFactoryTest.java | 88 ++++++++++--- .../cdi/IgnoreLocalBeansXmlClassLoader.java | 38 ++++++ .../cdi/example/BellyStepDefinitions.java | 4 +- .../src/test/resources/META-INF/beans.xml | 9 +- ...r.properties => junit-platform.properties} | 0 16 files changed, 644 insertions(+), 127 deletions(-) create mode 100644 cdi2/src/test/java/io/cucumber/cdi2/IgnoreLocalBeansXmlClassLoader.java rename cdi2/src/test/resources/{cucumber.properties => junit-platform.properties} (100%) create mode 100644 jakarta-cdi/src/test/java/io/cucumber/jakarta/cdi/IgnoreLocalBeansXmlClassLoader.java rename jakarta-cdi/src/test/resources/{cucumber.properties => junit-platform.properties} (100%) diff --git a/CHANGELOG.md b/CHANGELOG.md index da8d9144f6..2a7179bf07 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,7 +17,9 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ### Fixed * [Cdi2] Correctly cast the UnmanagedInstance values ([#2242](https://github.com/cucumber/cucumber-jvm/pull/2242), [#2244](https://github.com/cucumber/cucumber-jvm/pull/2244) Daniel Beland) + * [Cdi2] Add step definitions as beans when not discovered ([#2248](https://github.com/cucumber/cucumber-jvm/pull/2248)) Daniel Beland, M.P. Korstanje) * [Jakarta Cdi] Correctly cast the UnmanagedInstance values ([#2242](https://github.com/cucumber/cucumber-jvm/pull/2242), [#2248](https://github.com/cucumber/cucumber-jvm/pull/2248) Daniel Beland) + * [Jakarta Cdi] Add step definitions as beans when not discovered ([#2248](https://github.com/cucumber/cucumber-jvm/pull/2248)) Daniel Beland, M.P. Korstanje) ## [6.10.0] (2021-02-14) diff --git a/cdi2/README.md b/cdi2/README.md index b8d2ac9d12..c3cfffe0b4 100644 --- a/cdi2/README.md +++ b/cdi2/README.md @@ -1,9 +1,23 @@ Cucumber CDI 2 ============== -This module relies on CDI Standalone Edition (CDI SE) API to start/stop a CDI container -and customize it - adding steps. It looks up the beans/steps in CDI and if not available -it instantiates it as POJO with CDI injection support - unmanaged bean. +Use CDI Standalone Edition (CDI SE) API to provide dependency injection in to +steps definitions + +Add the `cucumber-cdi2` dependency to your pom.xml: + +```xml + + [...] + + io.cucumber + cucumber-cdi2 + ${cucumber.version} + test + + [...] + +``` ## Setup @@ -27,18 +41,54 @@ And for Weld it is: org.jboss.weld.se weld-se-core - 3.1.1.Final + 3.1.6.Final test ``` -To ensure the module is compatible with all implementations and future API version, it does not transitively bring the API. -If you don't know which one to use, you can import the following one but if you develop CDI code you should already have one provided: +## Usage -```xml - - javax.enterprise - cdi-api - 2.0 - +For each scenario a new CDI container is started. If not present in the +container, step definitions are added as unmanaged beans and dependencies are +injected. + +Note: Only step definition classes are added as unmanaged beans if not explicitly +defined. Other support code is not. Consider adding a `beans.xml` to +automatically declare test all classes as beans. + +Note: To share state step definitions and other support code must at least be +application scoped. + +```java +package com.example.app; + +import cucumber.api.java.en.Given; +import cucumber.api.java.en.Then; + +import javax.enterprise.context.ApplicationScoped; +import javax.inject.Inject; + +import java.util.Collections; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class StepDefinition { + + @Inject + private final Belly belly; + + public StepDefinitions(Belly belly) { + this.belly = belly; + } + + @Given("I have {int} {word} in my belly") + public void I_have_n_things_in_my_belly(int n, String what) { + belly.setContents(Collections.nCopies(n, what)); + } + + @Then("there are {int} cukes in my belly") + public void checkCukes(int n) { + assertEquals(belly.getContents(), Collections.nCopies(n, "cukes")); + } +} ``` diff --git a/cdi2/pom.xml b/cdi2/pom.xml index 43405ae338..cbe346ab03 100644 --- a/cdi2/pom.xml +++ b/cdi2/pom.xml @@ -56,6 +56,12 @@ junit-jupiter test + + org.junit.jupiter + junit-jupiter-params + ${junit-jupiter.version} + test + org.hamcrest hamcrest-core @@ -65,10 +71,21 @@ - cdi2-openwebbeans + cdi2-weld true + + + org.jboss.weld.se + weld-se-core + ${weld-se-core.version} + test + + + + + cdi2-openwebbeans org.apache.openwebbeans @@ -85,7 +102,12 @@ - cdi2-weld + cdi2-all-implementations + + + env.CI + + org.jboss.weld.se @@ -94,12 +116,53 @@ test - org.jboss.weld - weld-core-impl - ${weld-se-core.version} + org.apache.openwebbeans + openwebbeans-se + ${openwebbeans.version} + test + + + org.apache.openwebbeans + openwebbeans-impl + ${openwebbeans.version} test + + + + org.apache.maven.plugins + maven-surefire-plugin + + + default-test + test + + test + + + + org.apache.openwebbeans:openwebbeans-se + org.apache.openwebbeans:openwebbeans-impl + + + + + openwebbeans + test + + test + + + + org.jboss.weld.se:weld-se-core + + + + + + + diff --git a/cdi2/src/main/java/io/cucumber/cdi2/Cdi2Factory.java b/cdi2/src/main/java/io/cucumber/cdi2/Cdi2Factory.java index ec15e0bf22..026c1c5002 100644 --- a/cdi2/src/main/java/io/cucumber/cdi2/Cdi2Factory.java +++ b/cdi2/src/main/java/io/cucumber/cdi2/Cdi2Factory.java @@ -3,25 +3,38 @@ import io.cucumber.core.backend.ObjectFactory; import org.apiguardian.api.API; +import javax.enterprise.context.spi.CreationalContext; +import javax.enterprise.event.Observes; import javax.enterprise.inject.Instance; import javax.enterprise.inject.se.SeContainer; import javax.enterprise.inject.se.SeContainerInitializer; +import javax.enterprise.inject.spi.AfterBeanDiscovery; +import javax.enterprise.inject.spi.AnnotatedType; import javax.enterprise.inject.spi.BeanManager; +import javax.enterprise.inject.spi.Extension; +import javax.enterprise.inject.spi.InjectionTarget; import javax.enterprise.inject.spi.Unmanaged; import java.util.HashMap; +import java.util.HashSet; import java.util.Map; +import java.util.Set; @API(status = API.Status.STABLE) -public final class Cdi2Factory implements ObjectFactory { +public final class Cdi2Factory implements ObjectFactory, Extension { + + private final Set> stepClasses = new HashSet<>(); private final Map, Unmanaged.UnmanagedInstance> standaloneInstances = new HashMap<>(); - private SeContainerInitializer initializer; private SeContainer container; @Override public void start() { - container = getInitializer().initialize(); + if (container == null) { + SeContainerInitializer initializer = SeContainerInitializer.newInstance(); + initializer.addExtensions(this); + container = initializer.initialize(); + } } @Override @@ -29,35 +42,27 @@ public void stop() { if (container != null) { container.close(); container = null; - initializer = null; } - for (final Unmanaged.UnmanagedInstance unmanaged : standaloneInstances.values()) { + for (Unmanaged.UnmanagedInstance unmanaged : standaloneInstances.values()) { unmanaged.preDestroy(); unmanaged.dispose(); } standaloneInstances.clear(); } - private SeContainerInitializer getInitializer() { - if (initializer == null) { - initializer = SeContainerInitializer.newInstance(); - } - return initializer; - } - @Override - public boolean addClass(final Class clazz) { - getInitializer().addBeanClasses(clazz); + public boolean addClass(Class clazz) { + stepClasses.add(clazz); return true; } @Override - public T getInstance(final Class type) { - final Unmanaged.UnmanagedInstance instance = standaloneInstances.get(type); + public T getInstance(Class type) { + Unmanaged.UnmanagedInstance instance = standaloneInstances.get(type); if (instance != null) { return type.cast(instance.get()); } - final Instance selected = container.select(type); + Instance selected = container.select(type); if (selected.isUnsatisfied()) { BeanManager beanManager = container.getBeanManager(); Unmanaged unmanaged = new Unmanaged<>(beanManager, type); @@ -71,4 +76,43 @@ public T getInstance(final Class type) { return selected.get(); } + void afterBeanDiscovery(@Observes AfterBeanDiscovery afterBeanDiscovery, BeanManager bm) { + Set> unmanaged = new HashSet<>(); + for (Class stepClass : stepClasses) { + discoverUnmanagedTypes(afterBeanDiscovery, bm, unmanaged, stepClass); + } + } + + private void discoverUnmanagedTypes( + AfterBeanDiscovery afterBeanDiscovery, BeanManager bm, Set> unmanaged, Class candidate + ) { + if (unmanaged.contains(candidate) || !bm.getBeans(candidate).isEmpty()) { + return; + } + unmanaged.add(candidate); + + addBean(afterBeanDiscovery, bm, candidate); + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + private void addBean(AfterBeanDiscovery afterBeanDiscovery, BeanManager beanManager, Class clazz) { + AnnotatedType clazzAnnotatedType = beanManager.createAnnotatedType(clazz); + // @formatter:off + InjectionTarget injectionTarget = beanManager + .getInjectionTargetFactory(clazzAnnotatedType) + .createInjectionTarget(null); + // @formatter:on + // @formatter:off + afterBeanDiscovery.addBean() + .read(clazzAnnotatedType) + .createWith(callback -> { + CreationalContext c = (CreationalContext) callback; + Object instance = injectionTarget.produce(c); + injectionTarget.inject(instance, c); + injectionTarget.postConstruct(instance); + return instance; + }); + // @formatter:on + } + } diff --git a/cdi2/src/test/java/io/cucumber/cdi2/Cdi2FactoryTest.java b/cdi2/src/test/java/io/cucumber/cdi2/Cdi2FactoryTest.java index c3aea7135d..95d4668f65 100644 --- a/cdi2/src/test/java/io/cucumber/cdi2/Cdi2FactoryTest.java +++ b/cdi2/src/test/java/io/cucumber/cdi2/Cdi2FactoryTest.java @@ -1,10 +1,14 @@ package io.cucumber.cdi2; import io.cucumber.core.backend.ObjectFactory; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import javax.enterprise.context.ApplicationScoped; import javax.enterprise.inject.Vetoed; +import javax.inject.Inject; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.core.Is.is; @@ -12,23 +16,39 @@ import static org.hamcrest.core.IsNot.not; import static org.hamcrest.core.IsNull.notNullValue; import static org.junit.jupiter.api.Assertions.assertAll; -import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; class Cdi2FactoryTest { final ObjectFactory factory = new Cdi2Factory(); + @AfterEach + void stop() { + factory.stop(); + IgnoreLocalBeansXmlClassLoader.restoreClassLoader(); + } + + @Test + void lifecycleIsIdempotent() { + assertDoesNotThrow(factory::stop); + factory.start(); + assertDoesNotThrow(factory::start); + factory.stop(); + assertDoesNotThrow(factory::stop); + } + @Vetoed static class VetoedBean { } - @Test - void shouldCreateNewInstancesForEachScenario() { - factory.addClass(VetoedBean.class); - + @ParameterizedTest + @ValueSource(booleans = { true, false }) + void shouldCreateNewInstancesForEachScenario(boolean ignoreLocalBeansXml) { + IgnoreLocalBeansXmlClassLoader.setClassLoader(ignoreLocalBeansXml); // Scenario 1 factory.start(); + factory.addClass(VetoedBean.class); VetoedBean a1 = factory.getInstance(VetoedBean.class); VetoedBean a2 = factory.getInstance(VetoedBean.class); assertThat(a1, is(equalTo(a2))); @@ -52,30 +72,55 @@ static class ApplicationScopedBean { } - @Test - void shouldCreateApplicationScopedInstance() { + @ParameterizedTest + @ValueSource(booleans = { true, false }) + void shouldCreateApplicationScopedInstance(boolean ignoreLocalBeansXml) { + IgnoreLocalBeansXmlClassLoader.setClassLoader(ignoreLocalBeansXml); factory.addClass(ApplicationScopedBean.class); factory.start(); - ApplicationScopedBean cdiStep = factory.getInstance(ApplicationScopedBean.class); + ApplicationScopedBean bean = factory.getInstance(ApplicationScopedBean.class); assertAll( // assert that it is is a CDI proxy - () -> assertThat(cdiStep.getClass(), not(is(ApplicationScopedBean.class))), - () -> assertThat(cdiStep.getClass().getSuperclass(), is(ApplicationScopedBean.class))); + () -> assertThat(bean.getClass(), not(is(ApplicationScopedBean.class))), + () -> assertThat(bean.getClass().getSuperclass(), is(ApplicationScopedBean.class))); factory.stop(); } - @Test - void shouldCreateUnmanagedInstance() { - factory.addClass(UnmanagedBean.class); + static class UnmanagedBean { + + } + + @ParameterizedTest + @ValueSource(booleans = { true, false }) + void shouldCreateUnmanagedInstance(boolean ignoreLocalBeansXml) { + IgnoreLocalBeansXmlClassLoader.setClassLoader(ignoreLocalBeansXml); factory.start(); - assertNotNull(factory.getInstance(UnmanagedBean.class)); - UnmanagedBean cdiStep = factory.getInstance(UnmanagedBean.class); - assertThat(cdiStep.getClass(), is(UnmanagedBean.class)); + UnmanagedBean bean = factory.getInstance(UnmanagedBean.class); + assertThat(bean.getClass(), is(UnmanagedBean.class)); factory.stop(); } - static class UnmanagedBean { + static class OtherStepDefinitions { + + } + static class StepDefinitions { + + @Inject + OtherStepDefinitions injected; + + } + + @ParameterizedTest + @ValueSource(booleans = { true, false }) + void shouldInjectStepDefinitions(boolean ignoreLocalBeansXml) { + IgnoreLocalBeansXmlClassLoader.setClassLoader(ignoreLocalBeansXml); + factory.addClass(OtherStepDefinitions.class); + factory.addClass(StepDefinitions.class); + factory.start(); + StepDefinitions stepDefinitions = factory.getInstance(StepDefinitions.class); + assertThat(stepDefinitions.injected, is(notNullValue())); + factory.stop(); } } diff --git a/cdi2/src/test/java/io/cucumber/cdi2/IgnoreLocalBeansXmlClassLoader.java b/cdi2/src/test/java/io/cucumber/cdi2/IgnoreLocalBeansXmlClassLoader.java new file mode 100644 index 0000000000..f9f577c240 --- /dev/null +++ b/cdi2/src/test/java/io/cucumber/cdi2/IgnoreLocalBeansXmlClassLoader.java @@ -0,0 +1,38 @@ +package io.cucumber.cdi2; + +import java.io.IOException; +import java.net.URL; +import java.util.Enumeration; + +public class IgnoreLocalBeansXmlClassLoader extends ClassLoader { + + private static final String BEANS_XML_FILE = "META-INF/beans.xml"; + + public IgnoreLocalBeansXmlClassLoader(ClassLoader parent) { + super(parent); + } + + @Override + public Enumeration getResources(String name) throws IOException { + Enumeration enumeration = super.getResources(name); + if (BEANS_XML_FILE.equals(name) && enumeration.hasMoreElements()) { + enumeration.nextElement(); + } + return enumeration; + } + + public static void setClassLoader(boolean ignoreLocalBeansXml) { + ClassLoader threadClassLoader = Thread.currentThread().getContextClassLoader(); + if (ignoreLocalBeansXml && !(threadClassLoader instanceof IgnoreLocalBeansXmlClassLoader)) { + Thread.currentThread().setContextClassLoader(new IgnoreLocalBeansXmlClassLoader(threadClassLoader)); + } + } + + public static void restoreClassLoader() { + ClassLoader threadClassLoader = Thread.currentThread().getContextClassLoader(); + if (threadClassLoader instanceof IgnoreLocalBeansXmlClassLoader) { + Thread.currentThread().setContextClassLoader(threadClassLoader.getParent()); + } + } + +} diff --git a/cdi2/src/test/java/io/cucumber/cdi2/example/BellyStepDefinitions.java b/cdi2/src/test/java/io/cucumber/cdi2/example/BellyStepDefinitions.java index 091f92facd..c6e5447c55 100644 --- a/cdi2/src/test/java/io/cucumber/cdi2/example/BellyStepDefinitions.java +++ b/cdi2/src/test/java/io/cucumber/cdi2/example/BellyStepDefinitions.java @@ -3,10 +3,12 @@ import io.cucumber.java.en.Given; import io.cucumber.java.en.Then; +import javax.enterprise.context.ApplicationScoped; import javax.inject.Inject; import static org.junit.jupiter.api.Assertions.assertEquals; +@ApplicationScoped public class BellyStepDefinitions { @Inject diff --git a/cdi2/src/test/resources/cucumber.properties b/cdi2/src/test/resources/junit-platform.properties similarity index 100% rename from cdi2/src/test/resources/cucumber.properties rename to cdi2/src/test/resources/junit-platform.properties diff --git a/jakarta-cdi/README.md b/jakarta-cdi/README.md index 6ed6920a1a..aeab4a8c8b 100644 --- a/jakarta-cdi/README.md +++ b/jakarta-cdi/README.md @@ -1,17 +1,32 @@ Cucumber CDI Jakarta ==================== -This module relies on CDI Standalone Edition (CDI SE) API to start/stop a CDI container -and customize it - adding steps. It looks up the beans/steps in CDI and if not available -it instantiates it as POJO with CDI injection support - unmanaged bean. +Use CDI Standalone Edition (CDI SE) API to provide dependency injection in to +steps definitions. -IMPORTANT: it uses jakarta flavor of CDI and not javax one. +Add the `cucumber-jakarta-cdi` dependency to your pom.xml: + +```xml + + [...] + + io.cucumber + cucumber-jakarta-cdi + ${cucumber.version} + test + + [...] + +``` + +IMPORTANT: This module uses jakarta flavor of CDI and not javax one. ## Setup -To use it, it is important to provide your CDI SE implementation - likely Weld or Apache OpenWebBeans. +To use it, it is important to provide your CDI SE implementation - likely Weld +or Apache OpenWebBeans. -For Apache OpenWebBeans the dependency is: +#### Apache OpenWebBeans ```xml @@ -67,7 +82,7 @@ For Apache OpenWebBeans the dependency is: ``` -And for Weld it is: +#### Weld ```xml @@ -78,20 +93,48 @@ And for Weld it is: ``` -To ensure the module is compatible with all implementations and future API version, it does not transitively bring the API. -If you don't know which one to use, you can import the following one but if you develop CDI code you should already have one provided: +## Usage -```xml - - jakarta.enterprise - jakarta.enterprise.cdi-api - 3.0.0 - provided - - - jakarta.activation - jakarta.activation-api - 2.0.0 - provided - +For each scenario a new CDI container is started. If not present in the +container, step definitions are added as unmanaged beans and dependencies are +injected. + +Note: Only step definition classes are added as unmanaged beans if not explicitly +defined. Other support code is not. Consider adding a `beans.xml` to +automatically declare test all classes as beans. + +Note: To share state step definitions and other support code must at least be +application scoped. + +```java +package com.example.app; + +import cucumber.api.java.en.Given; +import cucumber.api.java.en.Then; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +import java.util.Collections; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class StepDefinition { + + @Inject + private final Belly belly; + + public StepDefinitions(Belly belly) { + this.belly = belly; + } + + @Given("I have {int} {word} in my belly") + public void I_have_n_things_in_my_belly(int n, String what) { + belly.setContents(Collections.nCopies(n, what)); + } + + @Then("there are {int} cukes in my belly") + public void checkCukes(int n) { + assertEquals(belly.getContents(), Collections.nCopies(n, "cukes")); + } +} ``` diff --git a/jakarta-cdi/pom.xml b/jakarta-cdi/pom.xml index b53eb2d12a..2207ae3f87 100644 --- a/jakarta-cdi/pom.xml +++ b/jakarta-cdi/pom.xml @@ -4,6 +4,7 @@ io.cucumber.cdi.jakarta 2.0.21 + 4.18 3.0.0 2.0.0 4.0.0.Final @@ -56,6 +57,12 @@ junit-jupiter test + + org.junit.jupiter + junit-jupiter-params + ${junit-jupiter.version} + test + org.hamcrest hamcrest-core @@ -65,13 +72,21 @@ - jakarta-cdi-openwebbeans + jakarta-cdi-weld true - - 4.18 - + + + org.jboss.weld.se + weld-se-core + ${weld.version} + test + + + + + jakarta-cdi-openwebbeans org.apache.openwebbeans @@ -126,9 +141,13 @@ - - jakarta-cdi-weld + jakarta-cdi-all-implementations + + + env.CI + + org.jboss.weld.se @@ -137,12 +156,95 @@ test - org.jboss.weld - weld-core-impl - ${weld.version} + org.apache.openwebbeans + openwebbeans-se + ${openwebbeans.version} + jakarta + test + + + * + * + + + + + org.apache.openwebbeans + openwebbeans-impl + ${openwebbeans.version} + test + jakarta + + + * + * + + + + + org.apache.openwebbeans + openwebbeans-spi + ${openwebbeans.version} + test + jakarta + + + * + * + + + + + org.apache.xbean + xbean-finder-shaded + ${xbean.version} + test + + + org.apache.xbean + xbean-asm9-shaded + ${xbean.version} test + + + + org.apache.maven.plugins + maven-surefire-plugin + + + default-test + test + + test + + + + org.apache.openwebbeans:openwebbeans-se + org.apache.openwebbeans:openwebbeans-impl + org.apache.openwebbeans:openwebbeans-spi + org.apache.xbean:xbean-finder-shaded + org.apache.xbean:xbean-asm9-shaded + + + + + openwebbeans + test + + test + + + + org.jboss.weld.se:weld-se-core + + + + + + + diff --git a/jakarta-cdi/src/main/java/io/cucumber/jakarta/cdi/CdiJakartaFactory.java b/jakarta-cdi/src/main/java/io/cucumber/jakarta/cdi/CdiJakartaFactory.java index 47e426369d..f118e2ed26 100644 --- a/jakarta-cdi/src/main/java/io/cucumber/jakarta/cdi/CdiJakartaFactory.java +++ b/jakarta-cdi/src/main/java/io/cucumber/jakarta/cdi/CdiJakartaFactory.java @@ -1,26 +1,39 @@ package io.cucumber.jakarta.cdi; import io.cucumber.core.backend.ObjectFactory; +import jakarta.enterprise.context.spi.CreationalContext; +import jakarta.enterprise.event.Observes; import jakarta.enterprise.inject.Instance; import jakarta.enterprise.inject.se.SeContainer; import jakarta.enterprise.inject.se.SeContainerInitializer; +import jakarta.enterprise.inject.spi.AfterBeanDiscovery; +import jakarta.enterprise.inject.spi.AnnotatedType; import jakarta.enterprise.inject.spi.BeanManager; +import jakarta.enterprise.inject.spi.Extension; +import jakarta.enterprise.inject.spi.InjectionTarget; import jakarta.enterprise.inject.spi.Unmanaged; import org.apiguardian.api.API; import java.util.HashMap; +import java.util.HashSet; import java.util.Map; +import java.util.Set; @API(status = API.Status.STABLE) -public final class CdiJakartaFactory implements ObjectFactory { +public final class CdiJakartaFactory implements ObjectFactory, Extension { + + private final Set> stepClasses = new HashSet<>(); private final Map, Unmanaged.UnmanagedInstance> standaloneInstances = new HashMap<>(); - private SeContainerInitializer initializer; private SeContainer container; @Override public void start() { - container = getInitializer().initialize(); + if (container == null) { + SeContainerInitializer initializer = SeContainerInitializer.newInstance(); + initializer.addExtensions(this); + container = initializer.initialize(); + } } @Override @@ -28,35 +41,27 @@ public void stop() { if (container != null) { container.close(); container = null; - initializer = null; } - for (final Unmanaged.UnmanagedInstance unmanaged : standaloneInstances.values()) { + for (Unmanaged.UnmanagedInstance unmanaged : standaloneInstances.values()) { unmanaged.preDestroy(); unmanaged.dispose(); } standaloneInstances.clear(); } - private SeContainerInitializer getInitializer() { - if (initializer == null) { - initializer = SeContainerInitializer.newInstance(); - } - return initializer; - } - @Override - public boolean addClass(final Class clazz) { - getInitializer().addBeanClasses(clazz); + public boolean addClass(Class clazz) { + stepClasses.add(clazz); return true; } @Override - public T getInstance(final Class type) { - final Unmanaged.UnmanagedInstance instance = standaloneInstances.get(type); + public T getInstance(Class type) { + Unmanaged.UnmanagedInstance instance = standaloneInstances.get(type); if (instance != null) { return type.cast(instance.get()); } - final Instance selected = container.select(type); + Instance selected = container.select(type); if (selected.isUnsatisfied()) { BeanManager beanManager = container.getBeanManager(); Unmanaged unmanaged = new Unmanaged<>(beanManager, type); @@ -69,4 +74,44 @@ public T getInstance(final Class type) { } return selected.get(); } + + void afterBeanDiscovery(@Observes AfterBeanDiscovery afterBeanDiscovery, BeanManager bm) { + Set> unmanaged = new HashSet<>(); + for (Class stepClass : stepClasses) { + discoverUnmanagedTypes(afterBeanDiscovery, bm, unmanaged, stepClass); + } + } + + private void discoverUnmanagedTypes( + AfterBeanDiscovery afterBeanDiscovery, BeanManager bm, Set> unmanaged, Class candidate + ) { + if (unmanaged.contains(candidate) || !bm.getBeans(candidate).isEmpty()) { + return; + } + unmanaged.add(candidate); + + addBean(afterBeanDiscovery, bm, candidate); + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + private void addBean(AfterBeanDiscovery afterBeanDiscovery, BeanManager beanManager, Class clazz) { + AnnotatedType clazzAnnotatedType = beanManager.createAnnotatedType(clazz); + // @formatter:off + InjectionTarget injectionTarget = beanManager + .getInjectionTargetFactory(clazzAnnotatedType) + .createInjectionTarget(null); + // @formatter:on + // @formatter:off + afterBeanDiscovery.addBean() + .read(clazzAnnotatedType) + .createWith(callback -> { + CreationalContext c = (CreationalContext) callback; + Object instance = injectionTarget.produce(c); + injectionTarget.inject(instance, c); + injectionTarget.postConstruct(instance); + return instance; + }); + // @formatter:on + } + } diff --git a/jakarta-cdi/src/test/java/io/cucumber/jakarta/cdi/CdiJakartaFactoryTest.java b/jakarta-cdi/src/test/java/io/cucumber/jakarta/cdi/CdiJakartaFactoryTest.java index 4d85870210..9abb149e73 100644 --- a/jakarta-cdi/src/test/java/io/cucumber/jakarta/cdi/CdiJakartaFactoryTest.java +++ b/jakarta-cdi/src/test/java/io/cucumber/jakarta/cdi/CdiJakartaFactoryTest.java @@ -3,7 +3,11 @@ import io.cucumber.core.backend.ObjectFactory; import jakarta.enterprise.context.ApplicationScoped; import jakarta.enterprise.inject.Vetoed; +import jakarta.inject.Inject; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.core.Is.is; @@ -11,23 +15,39 @@ import static org.hamcrest.core.IsNot.not; import static org.hamcrest.core.IsNull.notNullValue; import static org.junit.jupiter.api.Assertions.assertAll; -import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; class CdiJakartaFactoryTest { final ObjectFactory factory = new CdiJakartaFactory(); + @AfterEach + void stop() { + factory.stop(); + IgnoreLocalBeansXmlClassLoader.restoreClassLoader(); + } + + @Test + void lifecycleIsIdempotent() { + assertDoesNotThrow(factory::stop); + factory.start(); + assertDoesNotThrow(factory::start); + factory.stop(); + assertDoesNotThrow(factory::stop); + } + @Vetoed static class VetoedBean { } - @Test - void shouldCreateNewInstancesForEachScenario() { - factory.addClass(VetoedBean.class); - + @ParameterizedTest + @ValueSource(booleans = { true, false }) + void shouldCreateNewInstancesForEachScenario(boolean ignoreLocalBeansXml) { + IgnoreLocalBeansXmlClassLoader.setClassLoader(ignoreLocalBeansXml); // Scenario 1 factory.start(); + factory.addClass(VetoedBean.class); VetoedBean a1 = factory.getInstance(VetoedBean.class); VetoedBean a2 = factory.getInstance(VetoedBean.class); assertThat(a1, is(equalTo(a2))); @@ -41,9 +61,9 @@ void shouldCreateNewInstancesForEachScenario() { // VetoedBean makes it possible to compare the object outside the // scenario/application scope assertAll( - () -> assertThat(a1, is(notNullValue())), - () -> assertThat(a1, is(not(equalTo(b1)))), - () -> assertThat(b1, is(not(equalTo(a1))))); + () -> assertThat(a1, is(notNullValue())), + () -> assertThat(a1, is(not(equalTo(b1)))), + () -> assertThat(b1, is(not(equalTo(a1))))); } @ApplicationScoped @@ -51,29 +71,55 @@ static class ApplicationScopedBean { } - @Test - void shouldCreateApplicationScopedInstance() { + @ParameterizedTest + @ValueSource(booleans = { true, false }) + void shouldCreateApplicationScopedInstance(boolean ignoreLocalBeansXml) { + IgnoreLocalBeansXmlClassLoader.setClassLoader(ignoreLocalBeansXml); factory.addClass(ApplicationScopedBean.class); factory.start(); - ApplicationScopedBean cdiStep = factory.getInstance(ApplicationScopedBean.class); + ApplicationScopedBean bean = factory.getInstance(ApplicationScopedBean.class); assertAll( - // assert that it is is a CDI proxy - () -> assertThat(cdiStep.getClass(), not(is(ApplicationScopedBean.class))), - () -> assertThat(cdiStep.getClass().getSuperclass(), is(ApplicationScopedBean.class))); + // assert that it is is a CDI proxy + () -> assertThat(bean.getClass(), not(is(ApplicationScopedBean.class))), + () -> assertThat(bean.getClass().getSuperclass(), is(ApplicationScopedBean.class))); factory.stop(); } - @Test - void shouldCreateUnmanagedInstance() { - factory.addClass(UnmanagedBean.class); + static class UnmanagedBean { + + } + + @ParameterizedTest + @ValueSource(booleans = { true, false }) + void shouldCreateUnmanagedInstance(boolean ignoreLocalBeansXml) { + IgnoreLocalBeansXmlClassLoader.setClassLoader(ignoreLocalBeansXml); factory.start(); - assertNotNull(factory.getInstance(UnmanagedBean.class)); - UnmanagedBean cdiStep = factory.getInstance(UnmanagedBean.class); - assertThat(cdiStep.getClass(), is(UnmanagedBean.class)); + UnmanagedBean bean = factory.getInstance(UnmanagedBean.class); + assertThat(bean.getClass(), is(UnmanagedBean.class)); factory.stop(); } - static class UnmanagedBean { + static class OtherStepDefinitions { + + } + + static class StepDefinitions { + + @Inject + OtherStepDefinitions injected; } + + @ParameterizedTest + @ValueSource(booleans = { true, false }) + void shouldInjectStepDefinitions(boolean ignoreLocalBeansXml) { + IgnoreLocalBeansXmlClassLoader.setClassLoader(ignoreLocalBeansXml); + factory.addClass(OtherStepDefinitions.class); + factory.addClass(StepDefinitions.class); + factory.start(); + StepDefinitions stepDefinitions = factory.getInstance(StepDefinitions.class); + assertThat(stepDefinitions.injected, is(notNullValue())); + factory.stop(); + } + } diff --git a/jakarta-cdi/src/test/java/io/cucumber/jakarta/cdi/IgnoreLocalBeansXmlClassLoader.java b/jakarta-cdi/src/test/java/io/cucumber/jakarta/cdi/IgnoreLocalBeansXmlClassLoader.java new file mode 100644 index 0000000000..0b37c16039 --- /dev/null +++ b/jakarta-cdi/src/test/java/io/cucumber/jakarta/cdi/IgnoreLocalBeansXmlClassLoader.java @@ -0,0 +1,38 @@ +package io.cucumber.jakarta.cdi; + +import java.io.IOException; +import java.net.URL; +import java.util.Enumeration; + +public class IgnoreLocalBeansXmlClassLoader extends ClassLoader { + + private static final String BEANS_XML_FILE = "META-INF/beans.xml"; + + public IgnoreLocalBeansXmlClassLoader(ClassLoader parent) { + super(parent); + } + + @Override + public Enumeration getResources(String name) throws IOException { + Enumeration enumeration = super.getResources(name); + if (BEANS_XML_FILE.equals(name) && enumeration.hasMoreElements()) { + enumeration.nextElement(); + } + return enumeration; + } + + public static void setClassLoader(boolean ignoreLocalBeansXml) { + ClassLoader threadClassLoader = Thread.currentThread().getContextClassLoader(); + if (ignoreLocalBeansXml && !(threadClassLoader instanceof IgnoreLocalBeansXmlClassLoader)) { + Thread.currentThread().setContextClassLoader(new IgnoreLocalBeansXmlClassLoader(threadClassLoader)); + } + } + + public static void restoreClassLoader() { + ClassLoader threadClassLoader = Thread.currentThread().getContextClassLoader(); + if (threadClassLoader instanceof IgnoreLocalBeansXmlClassLoader) { + Thread.currentThread().setContextClassLoader(threadClassLoader.getParent()); + } + } + +} diff --git a/jakarta-cdi/src/test/java/io/cucumber/jakarta/cdi/example/BellyStepDefinitions.java b/jakarta-cdi/src/test/java/io/cucumber/jakarta/cdi/example/BellyStepDefinitions.java index a723019520..e3eb142704 100644 --- a/jakarta-cdi/src/test/java/io/cucumber/jakarta/cdi/example/BellyStepDefinitions.java +++ b/jakarta-cdi/src/test/java/io/cucumber/jakarta/cdi/example/BellyStepDefinitions.java @@ -2,12 +2,12 @@ import io.cucumber.java.en.Given; import io.cucumber.java.en.Then; -import jakarta.enterprise.inject.Vetoed; +import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; import static org.junit.jupiter.api.Assertions.assertEquals; -@Vetoed +@ApplicationScoped public class BellyStepDefinitions { @Inject diff --git a/jakarta-cdi/src/test/resources/META-INF/beans.xml b/jakarta-cdi/src/test/resources/META-INF/beans.xml index ea3cf1c048..350652e748 100644 --- a/jakarta-cdi/src/test/resources/META-INF/beans.xml +++ b/jakarta-cdi/src/test/resources/META-INF/beans.xml @@ -1,8 +1,7 @@ - +