Skip to content

Commit 06e1c08

Browse files
[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 <[email protected]> Co-authored-by: Daniel Beland <[email protected]>
1 parent dde1d4c commit 06e1c08

File tree

16 files changed

+644
-127
lines changed

16 files changed

+644
-127
lines changed

CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
1717

1818
### Fixed
1919
* [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)
20+
* [Cdi2] Add step definitions as beans when not discovered ([#2248](https://github.com/cucumber/cucumber-jvm/pull/2248)) Daniel Beland, M.P. Korstanje)
2021
* [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)
22+
* [Jakarta Cdi] Add step definitions as beans when not discovered ([#2248](https://github.com/cucumber/cucumber-jvm/pull/2248)) Daniel Beland, M.P. Korstanje)
2123

2224
## [6.10.0] (2021-02-14)
2325

cdi2/README.md

+62-12
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,23 @@
11
Cucumber CDI 2
22
==============
33

4-
This module relies on CDI Standalone Edition (CDI SE) API to start/stop a CDI container
5-
and customize it - adding steps. It looks up the beans/steps in CDI and if not available
6-
it instantiates it as POJO with CDI injection support - unmanaged bean.
4+
Use CDI Standalone Edition (CDI SE) API to provide dependency injection in to
5+
steps definitions
6+
7+
Add the `cucumber-cdi2` dependency to your pom.xml:
8+
9+
```xml
10+
<dependencies>
11+
[...]
12+
<dependency>
13+
<groupId>io.cucumber</groupId>
14+
<artifactId>cucumber-cdi2</artifactId>
15+
<version>${cucumber.version}</version>
16+
<scope>test</scope>
17+
</dependency>
18+
[...]
19+
</dependencies>
20+
```
721

822
## Setup
923

@@ -27,18 +41,54 @@ And for Weld it is:
2741
<dependency>
2842
<groupId>org.jboss.weld.se</groupId>
2943
<artifactId>weld-se-core</artifactId>
30-
<version>3.1.1.Final</version>
44+
<version>3.1.6.Final</version>
3145
<scope>test</scope>
3246
</dependency>
3347
```
3448

35-
To ensure the module is compatible with all implementations and future API version, it does not transitively bring the API.
36-
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:
49+
## Usage
3750

38-
```xml
39-
<dependency>
40-
<groupId>javax.enterprise</groupId>
41-
<artifactId>cdi-api</artifactId>
42-
<version>2.0</version>
43-
</dependency>
51+
For each scenario a new CDI container is started. If not present in the
52+
container, step definitions are added as unmanaged beans and dependencies are
53+
injected.
54+
55+
Note: Only step definition classes are added as unmanaged beans if not explicitly
56+
defined. Other support code is not. Consider adding a `beans.xml` to
57+
automatically declare test all classes as beans.
58+
59+
Note: To share state step definitions and other support code must at least be
60+
application scoped.
61+
62+
```java
63+
package com.example.app;
64+
65+
import cucumber.api.java.en.Given;
66+
import cucumber.api.java.en.Then;
67+
68+
import javax.enterprise.context.ApplicationScoped;
69+
import javax.inject.Inject;
70+
71+
import java.util.Collections;
72+
73+
import static org.junit.jupiter.api.Assertions.assertEquals;
74+
75+
public class StepDefinition {
76+
77+
@Inject
78+
private final Belly belly;
79+
80+
public StepDefinitions(Belly belly) {
81+
this.belly = belly;
82+
}
83+
84+
@Given("I have {int} {word} in my belly")
85+
public void I_have_n_things_in_my_belly(int n, String what) {
86+
belly.setContents(Collections.nCopies(n, what));
87+
}
88+
89+
@Then("there are {int} cukes in my belly")
90+
public void checkCukes(int n) {
91+
assertEquals(belly.getContents(), Collections.nCopies(n, "cukes"));
92+
}
93+
}
4494
```

cdi2/pom.xml

+68-5
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,12 @@
5656
<artifactId>junit-jupiter</artifactId>
5757
<scope>test</scope>
5858
</dependency>
59+
<dependency>
60+
<groupId>org.junit.jupiter</groupId>
61+
<artifactId>junit-jupiter-params</artifactId>
62+
<version>${junit-jupiter.version}</version>
63+
<scope>test</scope>
64+
</dependency>
5965
<dependency>
6066
<groupId>org.hamcrest</groupId>
6167
<artifactId>hamcrest-core</artifactId>
@@ -65,10 +71,21 @@
6571

6672
<profiles>
6773
<profile>
68-
<id>cdi2-openwebbeans</id>
74+
<id>cdi2-weld</id>
6975
<activation>
7076
<activeByDefault>true</activeByDefault>
7177
</activation>
78+
<dependencies>
79+
<dependency>
80+
<groupId>org.jboss.weld.se</groupId>
81+
<artifactId>weld-se-core</artifactId>
82+
<version>${weld-se-core.version}</version>
83+
<scope>test</scope>
84+
</dependency>
85+
</dependencies>
86+
</profile>
87+
<profile>
88+
<id>cdi2-openwebbeans</id>
7289
<dependencies>
7390
<dependency>
7491
<groupId>org.apache.openwebbeans</groupId>
@@ -85,7 +102,12 @@
85102
</dependencies>
86103
</profile>
87104
<profile>
88-
<id>cdi2-weld</id>
105+
<id>cdi2-all-implementations</id>
106+
<activation>
107+
<property>
108+
<name>env.CI</name>
109+
</property>
110+
</activation>
89111
<dependencies>
90112
<dependency>
91113
<groupId>org.jboss.weld.se</groupId>
@@ -94,12 +116,53 @@
94116
<scope>test</scope>
95117
</dependency>
96118
<dependency>
97-
<groupId>org.jboss.weld</groupId>
98-
<artifactId>weld-core-impl</artifactId>
99-
<version>${weld-se-core.version}</version>
119+
<groupId>org.apache.openwebbeans</groupId>
120+
<artifactId>openwebbeans-se</artifactId>
121+
<version>${openwebbeans.version}</version>
122+
<scope>test</scope>
123+
</dependency>
124+
<dependency>
125+
<groupId>org.apache.openwebbeans</groupId>
126+
<artifactId>openwebbeans-impl</artifactId>
127+
<version>${openwebbeans.version}</version>
100128
<scope>test</scope>
101129
</dependency>
102130
</dependencies>
131+
<build>
132+
<plugins>
133+
<plugin>
134+
<groupId>org.apache.maven.plugins</groupId>
135+
<artifactId>maven-surefire-plugin</artifactId>
136+
<executions>
137+
<execution>
138+
<id>default-test</id>
139+
<phase>test</phase>
140+
<goals>
141+
<goal>test</goal>
142+
</goals>
143+
<configuration>
144+
<classpathDependencyExcludes>
145+
<classpathDependencyExclude>org.apache.openwebbeans:openwebbeans-se</classpathDependencyExclude>
146+
<classpathDependencyExclude>org.apache.openwebbeans:openwebbeans-impl</classpathDependencyExclude>
147+
</classpathDependencyExcludes>
148+
</configuration>
149+
</execution>
150+
<execution>
151+
<id>openwebbeans</id>
152+
<phase>test</phase>
153+
<goals>
154+
<goal>test</goal>
155+
</goals>
156+
<configuration>
157+
<classpathDependencyExcludes>
158+
<classpathDependencyExclude>org.jboss.weld.se:weld-se-core</classpathDependencyExclude>
159+
</classpathDependencyExcludes>
160+
</configuration>
161+
</execution>
162+
</executions>
163+
</plugin>
164+
</plugins>
165+
</build>
103166
</profile>
104167
</profiles>
105168

cdi2/src/main/java/io/cucumber/cdi2/Cdi2Factory.java

+61-17
Original file line numberDiff line numberDiff line change
@@ -3,61 +3,66 @@
33
import io.cucumber.core.backend.ObjectFactory;
44
import org.apiguardian.api.API;
55

6+
import javax.enterprise.context.spi.CreationalContext;
7+
import javax.enterprise.event.Observes;
68
import javax.enterprise.inject.Instance;
79
import javax.enterprise.inject.se.SeContainer;
810
import javax.enterprise.inject.se.SeContainerInitializer;
11+
import javax.enterprise.inject.spi.AfterBeanDiscovery;
12+
import javax.enterprise.inject.spi.AnnotatedType;
913
import javax.enterprise.inject.spi.BeanManager;
14+
import javax.enterprise.inject.spi.Extension;
15+
import javax.enterprise.inject.spi.InjectionTarget;
1016
import javax.enterprise.inject.spi.Unmanaged;
1117

1218
import java.util.HashMap;
19+
import java.util.HashSet;
1320
import java.util.Map;
21+
import java.util.Set;
1422

1523
@API(status = API.Status.STABLE)
16-
public final class Cdi2Factory implements ObjectFactory {
24+
public final class Cdi2Factory implements ObjectFactory, Extension {
25+
26+
private final Set<Class<?>> stepClasses = new HashSet<>();
1727

1828
private final Map<Class<?>, Unmanaged.UnmanagedInstance<?>> standaloneInstances = new HashMap<>();
19-
private SeContainerInitializer initializer;
2029
private SeContainer container;
2130

2231
@Override
2332
public void start() {
24-
container = getInitializer().initialize();
33+
if (container == null) {
34+
SeContainerInitializer initializer = SeContainerInitializer.newInstance();
35+
initializer.addExtensions(this);
36+
container = initializer.initialize();
37+
}
2538
}
2639

2740
@Override
2841
public void stop() {
2942
if (container != null) {
3043
container.close();
3144
container = null;
32-
initializer = null;
3345
}
34-
for (final Unmanaged.UnmanagedInstance<?> unmanaged : standaloneInstances.values()) {
46+
for (Unmanaged.UnmanagedInstance<?> unmanaged : standaloneInstances.values()) {
3547
unmanaged.preDestroy();
3648
unmanaged.dispose();
3749
}
3850
standaloneInstances.clear();
3951
}
4052

41-
private SeContainerInitializer getInitializer() {
42-
if (initializer == null) {
43-
initializer = SeContainerInitializer.newInstance();
44-
}
45-
return initializer;
46-
}
47-
4853
@Override
49-
public boolean addClass(final Class<?> clazz) {
50-
getInitializer().addBeanClasses(clazz);
54+
public boolean addClass(Class<?> clazz) {
55+
stepClasses.add(clazz);
5156
return true;
5257
}
5358

5459
@Override
55-
public <T> T getInstance(final Class<T> type) {
56-
final Unmanaged.UnmanagedInstance<?> instance = standaloneInstances.get(type);
60+
public <T> T getInstance(Class<T> type) {
61+
Unmanaged.UnmanagedInstance<?> instance = standaloneInstances.get(type);
5762
if (instance != null) {
5863
return type.cast(instance.get());
5964
}
60-
final Instance<T> selected = container.select(type);
65+
Instance<T> selected = container.select(type);
6166
if (selected.isUnsatisfied()) {
6267
BeanManager beanManager = container.getBeanManager();
6368
Unmanaged<T> unmanaged = new Unmanaged<>(beanManager, type);
@@ -71,4 +76,43 @@ public <T> T getInstance(final Class<T> type) {
7176
return selected.get();
7277
}
7378

79+
void afterBeanDiscovery(@Observes AfterBeanDiscovery afterBeanDiscovery, BeanManager bm) {
80+
Set<Class<?>> unmanaged = new HashSet<>();
81+
for (Class<?> stepClass : stepClasses) {
82+
discoverUnmanagedTypes(afterBeanDiscovery, bm, unmanaged, stepClass);
83+
}
84+
}
85+
86+
private void discoverUnmanagedTypes(
87+
AfterBeanDiscovery afterBeanDiscovery, BeanManager bm, Set<Class<?>> unmanaged, Class<?> candidate
88+
) {
89+
if (unmanaged.contains(candidate) || !bm.getBeans(candidate).isEmpty()) {
90+
return;
91+
}
92+
unmanaged.add(candidate);
93+
94+
addBean(afterBeanDiscovery, bm, candidate);
95+
}
96+
97+
@SuppressWarnings({ "unchecked", "rawtypes" })
98+
private void addBean(AfterBeanDiscovery afterBeanDiscovery, BeanManager beanManager, Class<?> clazz) {
99+
AnnotatedType clazzAnnotatedType = beanManager.createAnnotatedType(clazz);
100+
// @formatter:off
101+
InjectionTarget injectionTarget = beanManager
102+
.getInjectionTargetFactory(clazzAnnotatedType)
103+
.createInjectionTarget(null);
104+
// @formatter:on
105+
// @formatter:off
106+
afterBeanDiscovery.addBean()
107+
.read(clazzAnnotatedType)
108+
.createWith(callback -> {
109+
CreationalContext c = (CreationalContext) callback;
110+
Object instance = injectionTarget.produce(c);
111+
injectionTarget.inject(instance, c);
112+
injectionTarget.postConstruct(instance);
113+
return instance;
114+
});
115+
// @formatter:on
116+
}
117+
74118
}

0 commit comments

Comments
 (0)