Skip to content

Commit

Permalink
[Cdi2,Jakarta Cdi] Add step definitions as beans when not discovered (#…
Browse files Browse the repository at this point in the history
…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 <[email protected]>
Co-authored-by: Daniel Beland <[email protected]>
  • Loading branch information
3 people authored Mar 8, 2021
1 parent dde1d4c commit 06e1c08
Show file tree
Hide file tree
Showing 16 changed files with 644 additions and 127 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
74 changes: 62 additions & 12 deletions cdi2/README.md
Original file line number Diff line number Diff line change
@@ -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
<dependencies>
[...]
<dependency>
<groupId>io.cucumber</groupId>
<artifactId>cucumber-cdi2</artifactId>
<version>${cucumber.version}</version>
<scope>test</scope>
</dependency>
[...]
</dependencies>
```

## Setup

Expand All @@ -27,18 +41,54 @@ And for Weld it is:
<dependency>
<groupId>org.jboss.weld.se</groupId>
<artifactId>weld-se-core</artifactId>
<version>3.1.1.Final</version>
<version>3.1.6.Final</version>
<scope>test</scope>
</dependency>
```

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
<dependency>
<groupId>javax.enterprise</groupId>
<artifactId>cdi-api</artifactId>
<version>2.0</version>
</dependency>
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"));
}
}
```
73 changes: 68 additions & 5 deletions cdi2/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,12 @@
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-params</artifactId>
<version>${junit-jupiter.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest-core</artifactId>
Expand All @@ -65,10 +71,21 @@

<profiles>
<profile>
<id>cdi2-openwebbeans</id>
<id>cdi2-weld</id>
<activation>
<activeByDefault>true</activeByDefault>
</activation>
<dependencies>
<dependency>
<groupId>org.jboss.weld.se</groupId>
<artifactId>weld-se-core</artifactId>
<version>${weld-se-core.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
</profile>
<profile>
<id>cdi2-openwebbeans</id>
<dependencies>
<dependency>
<groupId>org.apache.openwebbeans</groupId>
Expand All @@ -85,7 +102,12 @@
</dependencies>
</profile>
<profile>
<id>cdi2-weld</id>
<id>cdi2-all-implementations</id>
<activation>
<property>
<name>env.CI</name>
</property>
</activation>
<dependencies>
<dependency>
<groupId>org.jboss.weld.se</groupId>
Expand All @@ -94,12 +116,53 @@
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.jboss.weld</groupId>
<artifactId>weld-core-impl</artifactId>
<version>${weld-se-core.version}</version>
<groupId>org.apache.openwebbeans</groupId>
<artifactId>openwebbeans-se</artifactId>
<version>${openwebbeans.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.openwebbeans</groupId>
<artifactId>openwebbeans-impl</artifactId>
<version>${openwebbeans.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<executions>
<execution>
<id>default-test</id>
<phase>test</phase>
<goals>
<goal>test</goal>
</goals>
<configuration>
<classpathDependencyExcludes>
<classpathDependencyExclude>org.apache.openwebbeans:openwebbeans-se</classpathDependencyExclude>
<classpathDependencyExclude>org.apache.openwebbeans:openwebbeans-impl</classpathDependencyExclude>
</classpathDependencyExcludes>
</configuration>
</execution>
<execution>
<id>openwebbeans</id>
<phase>test</phase>
<goals>
<goal>test</goal>
</goals>
<configuration>
<classpathDependencyExcludes>
<classpathDependencyExclude>org.jboss.weld.se:weld-se-core</classpathDependencyExclude>
</classpathDependencyExcludes>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
</profiles>

Expand Down
78 changes: 61 additions & 17 deletions cdi2/src/main/java/io/cucumber/cdi2/Cdi2Factory.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,61 +3,66 @@
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<Class<?>> stepClasses = new HashSet<>();

private final Map<Class<?>, 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
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> T getInstance(final Class<T> type) {
final Unmanaged.UnmanagedInstance<?> instance = standaloneInstances.get(type);
public <T> T getInstance(Class<T> type) {
Unmanaged.UnmanagedInstance<?> instance = standaloneInstances.get(type);
if (instance != null) {
return type.cast(instance.get());
}
final Instance<T> selected = container.select(type);
Instance<T> selected = container.select(type);
if (selected.isUnsatisfied()) {
BeanManager beanManager = container.getBeanManager();
Unmanaged<T> unmanaged = new Unmanaged<>(beanManager, type);
Expand All @@ -71,4 +76,43 @@ public <T> T getInstance(final Class<T> type) {
return selected.get();
}

void afterBeanDiscovery(@Observes AfterBeanDiscovery afterBeanDiscovery, BeanManager bm) {
Set<Class<?>> unmanaged = new HashSet<>();
for (Class<?> stepClass : stepClasses) {
discoverUnmanagedTypes(afterBeanDiscovery, bm, unmanaged, stepClass);
}
}

private void discoverUnmanagedTypes(
AfterBeanDiscovery afterBeanDiscovery, BeanManager bm, Set<Class<?>> 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
}

}
Loading

0 comments on commit 06e1c08

Please sign in to comment.