Skip to content
Open
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
15b8e34
define PicoBackendProviderService and PicoBackend
antagoony Dec 6, 2025
4990f86
specify PicoConfiguration annotation (with API.Status.EXPERIMENTAL)
antagoony Dec 6, 2025
aa987be
implement PicoConfiguration awareness (add all Provider/ProviderAdapter)
antagoony Dec 6, 2025
7caf720
stop conditionally only
antagoony Dec 6, 2025
4bdfd55
add CHANGELOG entry
antagoony Dec 7, 2025
5e857d3
add README description and example
antagoony Dec 7, 2025
ed440b4
add PR reference
antagoony Dec 7, 2025
ae8732c
rename @PicoConfiguration to @CucumberPicoProvider
antagoony Dec 7, 2025
48e0aaf
remove provisioning of separate #providerAdapters()
antagoony Dec 7, 2025
cfd1bad
rename example/test class to clarify its intention
antagoony Dec 16, 2025
bfb86b5
enable package-internal re-use
antagoony Dec 16, 2025
9cc04cf
add both, referenced Pico-Providers and annotated Pico-Providers
antagoony Dec 16, 2025
02acb37
keep distinction of Provider/ProviderAdapter internally
antagoony Dec 16, 2025
5ff1b2d
not all component-adapters may be an instance of Cached
antagoony Dec 16, 2025
b442c9f
rename example/test class to clarify its intention
antagoony Dec 16, 2025
0dd91ad
insinuate cascading pico-provider construction
antagoony Dec 16, 2025
e47c54d
extend pico-backend tests
antagoony Dec 16, 2025
05c19f2
add check for meaningful CucumberPicoProvider annotation
antagoony Dec 16, 2025
50a5083
revise JavaDoc of @CucumberPicoProvider
antagoony Dec 16, 2025
e3f3062
revise README.md according to current @CucumberPicoProvider
antagoony Dec 17, 2025
f58342e
move CHANGELOG entry to current [Unreleased] section
antagoony Dec 17, 2025
7b946bb
remove pointer to further PicoContainer Provider`s
antagoony Dec 18, 2025
d57a4b3
because instantiated by default constructor, such constructor must exist
antagoony Dec 18, 2025
0d6216a
distinguish provider classes and component classes
antagoony Dec 18, 2025
8211a0c
add further requirements to be sure Provider class can be instantiated
antagoony Dec 18, 2025
3ac388d
rename/re-sort unit tests according PicoBackend
antagoony Dec 19, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added
- [Java] Support Provider instances with Pico Container ([#2879](https://github.com/cucumber/cucumber-jvm/issues/2879), [#3128](https://github.com/cucumber/cucumber-jvm/pull/3128) Stefan Gasterstädt)

## [7.33.0] - 2025-12-09
### Added
- [Java] Add `Scenario.getLanguage()` to return the current language ([#3124](https://github.com/cucumber/cucumber-jvm/pull/3124) Stefan Gasterstädt)
Expand Down
24 changes: 24 additions & 0 deletions cucumber-picocontainer/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,3 +123,27 @@ customization. If you want to customize your dependency injection context,
it is recommended to provide your own implementation of
`io.cucumber.core.backend.ObjectFactory` and make it available through
SPI.

However it is possible to configure additional PicoContainer `Provider`s. For
example, some step definition classes might require a database connection as a
constructor argument.

```java
package com.example.app;

import java.sql.*;
import io.cucumber.picocontainer.CucumberPicoProvider;
import org.picocontainer.injectors.Provider;

@CucumberPicoProvider
public class DatabaseConnectionProvider implements Provider {

public Connection provide() throws ClassNotFoundException, ReflectiveOperationException, SQLException {
// Connecting to MySQL Using the JDBC DriverManager Interface
// https://dev.mysql.com/doc/connector-j/en/connector-j-usagenotes-connect-drivermanager.html
Class.forName("com.mysql.cj.jdbc.Driver").getDeclaredConstructor().newInstance();
return DriverManager.getConnection("jdbc:mysql://localhost:3306/mydb", "mydbuser", "mydbpassword");
}

}
```
7 changes: 7 additions & 0 deletions cucumber-picocontainer/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
<picocontainer.version>2.15.2</picocontainer.version>
<apiguardian-api.version>1.1.2</apiguardian-api.version>
<junit-jupiter.version>5.14.1</junit-jupiter.version>
<mockito.version>5.20.0</mockito.version>
</properties>

<dependencyManagement>
Expand Down Expand Up @@ -72,6 +73,12 @@
<artifactId>junit-vintage-engine</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<version>${mockito.version}</version>
<scope>test</scope>
</dependency>
</dependencies>

<build>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package io.cucumber.picocontainer;

import org.apiguardian.api.API;
import org.picocontainer.MutablePicoContainer;
import org.picocontainer.injectors.Provider;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
* This annotation is used to provide some additional PicoContainer
* {@link Provider} classes.
* <p>
* An example is:
*
* <pre>
* package some.example;
*
* import java.sql.*;
* import io.cucumber.picocontainer.CucumberPicoProvider;
* import org.picocontainer.injectors.Provider;
*
* &#64;CucumberPicoProvider
* public class DatabaseConnectionProvider implements Provider {
* public Connection provide() throws ClassNotFoundException, ReflectiveOperationException, SQLException {
* // Connecting to MySQL Using the JDBC DriverManager Interface
* // https://dev.mysql.com/doc/connector-j/en/connector-j-usagenotes-connect-drivermanager.html
* Class.forName("com.mysql.cj.jdbc.Driver").getDeclaredConstructor().newInstance();
* return DriverManager.getConnection("jdbc:mysql://localhost:3306/mydb", "mydbuser", "mydbpassword");
* }
* }
* </pre>
* <p>
* In order to re-use existing {@link Provider}s, you can refer to those like
* this:
*
* <pre>
* package some.example;
*
* import io.cucumber.picocontainer.CucumberPicoProvider;
* import some.other.namespace.SomeExistingProvider.class;
*
* &#64;CucumberPicoProvider(providers = { SomeExistingProvider.class })
* public class MyCucumberPicoProviders {
* }
* </pre>
* <p>
* Notes:
* <ul>
* <li>Currently, there is no limitation to the number of
* {@link CucumberPicoProvider} annotations. All of these annotations will be
* considered when preparing the {@link org.picocontainer.PicoContainer
* PicoContainer}.</li>
* <li>If there is no {@link CucumberPicoProvider} annotation at all then
* (beside the basic preparation) no additional PicoContainer preparation will
* be done.</li>
* <li>Cucumber PicoContainer uses PicoContainer's {@link MutablePicoContainer}
* internally. Doing so, all {@link #providers() Providers} will be added by
* {@link MutablePicoContainer#addAdapter(org.picocontainer.ComponentAdapter)
* MutablePicoContainer#addAdapter(new ProviderAdapter(provider))}. (If any of
* the providers additionally extends
* {@link org.picocontainer.injectors.ProviderAdapter ProviderAdapter} then
* these will be added directly without being wrapped again.)</li>
* <li>For each class there can be only one {@link Provider}. Otherwise an
* according exception will be thrown (e.g. {@code PicoCompositionException}
* with message "Duplicate Keys not allowed ..."</li>
* </ul>
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@API(status = API.Status.EXPERIMENTAL)
public @interface CucumberPicoProvider {

Class<? extends Provider>[] providers() default {};

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package io.cucumber.picocontainer;

import io.cucumber.core.backend.Backend;
import io.cucumber.core.backend.Container;
import io.cucumber.core.backend.Glue;
import io.cucumber.core.backend.Snippet;
import io.cucumber.core.resource.ClasspathScanner;
import io.cucumber.core.resource.ClasspathSupport;

import java.net.URI;
import java.util.Collection;
import java.util.List;
import java.util.function.Supplier;
import java.util.stream.Stream;

import static io.cucumber.core.resource.ClasspathSupport.CLASSPATH_SCHEME;
import static io.cucumber.picocontainer.PicoFactory.isProvider;
import static java.util.Arrays.stream;
import static java.util.stream.Stream.concat;

final class PicoBackend implements Backend {

private final Container container;
private final ClasspathScanner classFinder;

PicoBackend(Container container, Supplier<ClassLoader> classLoaderSupplier) {
this.container = container;
this.classFinder = new ClasspathScanner(classLoaderSupplier);
}

@Override
public void loadGlue(Glue glue, List<URI> gluePaths) {
gluePaths.stream()
.filter(gluePath -> CLASSPATH_SCHEME.equals(gluePath.getScheme()))
.map(ClasspathSupport::packageName)
.map(classFinder::scanForClassesInPackage)
.flatMap(Collection::stream)
.filter(clazz -> clazz.isAnnotationPresent(CucumberPicoProvider.class))
.flatMap(clazz -> {
CucumberPicoProvider annotation = clazz.getAnnotation(CucumberPicoProvider.class);
if (isProvider(clazz)) {
return concat(Stream.of(clazz), stream(annotation.providers()));
} else {
return stream(annotation.providers());
}

})
.distinct()
.forEach(container::addClass);
}

@Override
public void buildWorld() {
}

@Override
public void disposeWorld() {
}

@Override
public Snippet getSnippet() {
return null;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package io.cucumber.picocontainer;

import io.cucumber.core.backend.Backend;
import io.cucumber.core.backend.BackendProviderService;
import io.cucumber.core.backend.Container;
import io.cucumber.core.backend.Lookup;

import java.util.function.Supplier;

public final class PicoBackendProviderService implements BackendProviderService {

@Override
public Backend create(Lookup lookup, Container container, Supplier<ClassLoader> classLoader) {
return new PicoBackend(container, classLoader);
}

}
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
package io.cucumber.picocontainer;

import io.cucumber.core.backend.CucumberBackendException;
import io.cucumber.core.backend.ObjectFactory;
import org.apiguardian.api.API;
import org.picocontainer.MutablePicoContainer;
import org.picocontainer.PicoBuilder;
import org.picocontainer.PicoException;
import org.picocontainer.behaviors.Cached;
import org.picocontainer.injectors.Provider;
import org.picocontainer.injectors.ProviderAdapter;
import org.picocontainer.lifecycle.DefaultLifecycleState;

import java.lang.reflect.Constructor;
Expand All @@ -31,34 +35,90 @@ public void start() {
.withCaching()
.withLifecycle()
.build();
Set<Class<?>> providers = new HashSet<>();
Set<Class<?>> providedClasses = new HashSet<>();
for (Class<?> clazz : classes) {
pico.addComponent(clazz);
if (isProvider(clazz)) {
providers.add(clazz);
ProviderAdapter adapter = adapterForProviderClass(clazz);
pico.addAdapter(adapter);
providedClasses.add(adapter.getComponentImplementation());
}
}
for (Class<?> clazz : classes) {
// do not add the classes that represent a picocontainer
// Provider, and also do not add those raw classes that are
// already provided (otherwise this causes exceptional
// situations, e.g. PicoCompositionException with message
// "Duplicate Keys not allowed. Duplicate for 'class XXX'")
if (!providers.contains(clazz) && !providedClasses.contains(clazz)) {
pico.addComponent(clazz);
}
}
} else {
// we already get a pico container which is in "disposed" lifecycle,
// so recycle it by defining a new lifecycle and removing all
// instances
pico.setLifecycleState(new DefaultLifecycleState());
pico.getComponentAdapters()
.forEach(cached -> ((Cached<?>) cached).flush());
pico.getComponentAdapters().forEach(adapters -> {
if (adapters instanceof Cached) {
((Cached<?>) adapters).flush();
}
});
}
pico.start();
}

static boolean isProvider(Class<?> clazz) {
return Provider.class.isAssignableFrom(clazz);
}

static boolean isProviderAdapter(Class<?> clazz) {
return ProviderAdapter.class.isAssignableFrom(clazz);
}

private static ProviderAdapter adapterForProviderClass(Class<?> clazz) {
try {
Provider provider = (Provider) clazz.getDeclaredConstructor().newInstance();
return isProviderAdapter(clazz) ? (ProviderAdapter) provider : new ProviderAdapter(provider);
} catch (ReflectiveOperationException | IllegalArgumentException | SecurityException | PicoException e) {
throw new CucumberBackendException(e.getMessage(), e);
}
}

@Override
public void stop() {
pico.stop();
if (pico.getLifecycleState().isStarted()) {
pico.stop();
}
pico.dispose();
}

@Override
public boolean addClass(Class<?> clazz) {
checkMeaningfulPicoAnnotation(clazz);
if (isInstantiable(clazz) && classes.add(clazz)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would make sense to split @CucumberPicoProvider annotated and other glue classes here. It would clean up the logic in start and prevent constructor dependencies being added to the container..

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are two collections now, one for the Provider classes, one for the "normal" component classes.

addConstructorDependencies(clazz);
}
return true;
}

private static void checkMeaningfulPicoAnnotation(Class<?> clazz) {
if (clazz.isAnnotationPresent(CucumberPicoProvider.class)) {
CucumberPicoProvider annotation = clazz.getAnnotation(CucumberPicoProvider.class);
if (!isProvider(clazz) && (annotation.providers().length == 0)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The class must also have a zero arg constructor IIRC.

Copy link
Contributor Author

@antagoony antagoony Dec 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are right, because that constructor is used when instantiating the Provider class within PicoFactory#adapterForProviderClass(Class). This requirement (and others) will be checked now (see PicoFactory#checkProperPicoProvider(Class)).

throw new CucumberBackendException(String.format("" +
"Glue class %1$s was annotated with @CucumberPicoProvider; marking it as a candidate for declaring "
+
"PicoContainer Provider classes. Please ensure that at least one the following requirements is satisfied:\n"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"PicoContainer Provider classes. Please ensure that at least one the following requirements is satisfied:\n"
"PicoContainer Provider classes. Please ensure that at least one the following requirements are satisfied:\n"

Copy link
Contributor Author

@antagoony antagoony Dec 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The text has changed even more. There are five requirements now, and all must be satisfied.

+
"1) the class implements org.picocontainer.injectors.Provider\n" +
"2) the annotation #providers() refers to at least one class implementing org.picocontainer.injectors.Provider",
clazz.getName()));
}
}
}

@Override
public <T> T getInstance(Class<T> type) {
return pico.getComponent(type);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
io.cucumber.picocontainer.PicoBackendProviderService
Loading
Loading