Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- [Core] Upload Cucumber Reports with Gzip encoding ([#3115](https://github.com/cucumber/cucumber-jvm/pull/3115))
- [Java] Add `Scenario.getLanguage()` to return the current language ([#3124](https://github.com/cucumber/cucumber-jvm/pull/3124) Stefan Gasterstädt)
- [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.32.0] - 2025-11-21
### Changed
Expand Down
26 changes: 26 additions & 0 deletions cucumber-picocontainer/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,3 +123,29 @@ 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 and/or
`ProviderAdapter`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.PicoConfiguration;
import org.picocontainer.injectors.ProviderAdapter;

@PicoConfiguration(providerAdapters = { ExamplePicoConfiguration.DatabaseConnectionProvider.class })
public class ExamplePicoConfiguration {

public static class DatabaseConnectionProvider extends ProviderAdapter {
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,57 @@
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 static io.cucumber.core.resource.ClasspathSupport.CLASSPATH_SCHEME;
import static java.util.Arrays.stream;

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(PicoConfiguration.class))
.distinct()
.forEach(picoConfig -> {
PicoConfiguration configuration = picoConfig.getAnnotation(PicoConfiguration.class);
stream(configuration.providers()).forEach(container::addClass);
stream(configuration.providerAdapters()).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
@@ -0,0 +1,79 @@
package io.cucumber.picocontainer;

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

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
* configuration. At the moment this covers:
* <ul>
* <li>a list of classes conforming the PicoContainer's {@link Provider}
* interface,</li>
* <li>a list of classes conforming the PicoContainer's {@link ProviderAdapter}
* interface.</li>
* </ul>
* <p>
* An example (ancillary containing the specific ProviderAdapter as nested
* class) is:
*
* <pre>
* package some.example;
*
* import java.sql.*;
* import io.cucumber.picocontainer.PicoConfiguration;
* import org.picocontainer.injectors.ProviderAdapter;
*
* &#64;PicoConfiguration(providerAdapters = { MyPicoConfiguration.DatabaseConnectionProvider.class })
* public class MyPicoConfiguration {
*
* public static class DatabaseConnectionProvider extends ProviderAdapter {
* 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>
* Notes:
* <ul>
* <li>Currently, there is no limitation to the number of
* {@link PicoConfiguration} annotations. All of these annotations will be
* considered when preparing the {@link org.picocontainer.PicoContainer
* PicoContainer}.</li>
* <li>If there is no {@link PicoConfiguration} 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))} and all
* {@link #providerAdapters() ProviderAdapters} will be added by
* {@link MutablePicoContainer#addAdapter(org.picocontainer.ComponentAdapter)
* MutablePicoContainer#addAdapter(adapter)}.</li>
* <li>For each class there can be only one
* {@link Provider}/{@link ProviderAdapter}. 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 PicoConfiguration {

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

Class<? extends ProviderAdapter>[] providerAdapters() default {};

}
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,8 +35,29 @@ public void start() {
.withCaching()
.withLifecycle()
.build();
Set<Class<?>> providers = new HashSet<>();
Set<Class<?>> providedClasses = new HashSet<>();
for (Class<?> clazz : classes) {
pico.addComponent(clazz);
if (isProviderAdapter(clazz)) {
providers.add(clazz);
Class<?> providedClass = addProviderAdapter(clazz);
providedClasses.add(providedClass);
} else if (isProvider(clazz)) {
providers.add(clazz);
Class<?> providedClass = addProvider(clazz);
providedClasses.add(providedClass);
}
}
for (Class<?> clazz : classes) {
// do not add the classes that represent a picocontainer
// ProviderAdapter/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,
Expand All @@ -45,9 +70,40 @@ public void start() {
pico.start();
}

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

private Class<?> addProviderAdapter(Class<?> clazz) {
try {
ProviderAdapter adapter = (ProviderAdapter) clazz.getDeclaredConstructor().newInstance();
pico.addAdapter(adapter);
return adapter.getComponentImplementation();
} catch (ReflectiveOperationException | IllegalArgumentException | SecurityException | PicoException e) {
throw new CucumberBackendException(e.getMessage(), e);
}
}

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

private Class<?> addProvider(Class<?> clazz) {
try {
Provider provider = (Provider) clazz.getDeclaredConstructor().newInstance();
ProviderAdapter adapter = new ProviderAdapter(provider);
pico.addAdapter(adapter);
return adapter.getComponentImplementation();
} 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();
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
io.cucumber.picocontainer.PicoBackendProviderService
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package io.cucumber.picocontainer;

import io.cucumber.core.backend.Glue;
import io.cucumber.core.backend.ObjectFactory;
import io.cucumber.picocontainer.annotationconfig.ConnectionProvider;
import io.cucumber.picocontainer.annotationconfig.DatabaseConnectionProvider;
import io.cucumber.picocontainer.annotationconfig.ExamplePicoConfiguration;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import java.net.URI;

import static java.lang.Thread.currentThread;
import static java.util.Arrays.asList;
import static java.util.Collections.singletonList;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;

@ExtendWith(MockitoExtension.class)
class PicoBackendTest {

@Mock
private Glue glue;

@Mock
private ObjectFactory factory;

private PicoBackend backend;

@BeforeEach
void createBackend() {
this.backend = new PicoBackend(this.factory, currentThread()::getContextClassLoader);
}

@Test
void considers_but_does_not_add_annotated_configuration() {
backend.loadGlue(glue, singletonList(URI.create("classpath:io/cucumber/picocontainer/annotationconfig")));
backend.buildWorld();
verify(factory, never()).addClass(ExamplePicoConfiguration.class);
}

@Test
void adds_provider_classes() {
backend.loadGlue(glue, singletonList(URI.create("classpath:io/cucumber/picocontainer/annotationconfig")));
backend.buildWorld();
verify(factory).addClass(ConnectionProvider.class);
}

@Test
void adds_provideradapter_classes() {
backend.loadGlue(glue, singletonList(URI.create("classpath:io/cucumber/picocontainer/annotationconfig")));
backend.buildWorld();
verify(factory).addClass(DatabaseConnectionProvider.class);
}

@Test
void finds_configured_classes_only_once_when_scanning_twice() {
backend.loadGlue(glue, asList(
URI.create("classpath:io/cucumber/picocontainer/annotationconfig"),
URI.create("classpath:io/cucumber/picocontainer/annotationconfig")));
backend.buildWorld();
verify(factory, never()).addClass(ExamplePicoConfiguration.class);
verify(factory, times(1)).addClass(ConnectionProvider.class);
verify(factory, times(1)).addClass(DatabaseConnectionProvider.class);
}

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

import org.picocontainer.injectors.Provider;

import java.net.HttpURLConnection;

public class ConnectionProvider implements Provider {

public HttpURLConnection provide() {
throw new UnsupportedOperationException("Intentionally not supported to detect any premature injection.");
}

}
Loading
Loading