From 15b8e348628f049887a3b2a75aeca60425b89e95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20Gasterst=C3=A4dt?= <157380642+antagoony@users.noreply.github.com> Date: Sat, 6 Dec 2025 21:23:52 +0100 Subject: [PATCH 01/21] define PicoBackendProviderService and PicoBackend --- .../cucumber/picocontainer/PicoBackend.java | 29 +++++++++++++++++++ .../PicoBackendProviderService.java | 17 +++++++++++ ...cumber.core.backend.BackendProviderService | 1 + 3 files changed, 47 insertions(+) create mode 100644 cucumber-picocontainer/src/main/java/io/cucumber/picocontainer/PicoBackend.java create mode 100644 cucumber-picocontainer/src/main/java/io/cucumber/picocontainer/PicoBackendProviderService.java create mode 100644 cucumber-picocontainer/src/main/resources/META-INF/services/io.cucumber.core.backend.BackendProviderService diff --git a/cucumber-picocontainer/src/main/java/io/cucumber/picocontainer/PicoBackend.java b/cucumber-picocontainer/src/main/java/io/cucumber/picocontainer/PicoBackend.java new file mode 100644 index 0000000000..b09505d5b5 --- /dev/null +++ b/cucumber-picocontainer/src/main/java/io/cucumber/picocontainer/PicoBackend.java @@ -0,0 +1,29 @@ +package io.cucumber.picocontainer; + +import io.cucumber.core.backend.Backend; +import io.cucumber.core.backend.Glue; +import io.cucumber.core.backend.Snippet; + +import java.net.URI; +import java.util.List; + +final class PicoBackend implements Backend { + + @Override + public void loadGlue(Glue glue, List gluePaths) { + } + + @Override + public void buildWorld() { + } + + @Override + public void disposeWorld() { + } + + @Override + public Snippet getSnippet() { + return null; + } + +} diff --git a/cucumber-picocontainer/src/main/java/io/cucumber/picocontainer/PicoBackendProviderService.java b/cucumber-picocontainer/src/main/java/io/cucumber/picocontainer/PicoBackendProviderService.java new file mode 100644 index 0000000000..d47d6f3388 --- /dev/null +++ b/cucumber-picocontainer/src/main/java/io/cucumber/picocontainer/PicoBackendProviderService.java @@ -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) { + return new PicoBackend(); + } + +} diff --git a/cucumber-picocontainer/src/main/resources/META-INF/services/io.cucumber.core.backend.BackendProviderService b/cucumber-picocontainer/src/main/resources/META-INF/services/io.cucumber.core.backend.BackendProviderService new file mode 100644 index 0000000000..682c8c5dcf --- /dev/null +++ b/cucumber-picocontainer/src/main/resources/META-INF/services/io.cucumber.core.backend.BackendProviderService @@ -0,0 +1 @@ +io.cucumber.picocontainer.PicoBackendProviderService From 4990f86039ec2f8da8ceb4c15d7968e215c9e66d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20Gasterst=C3=A4dt?= <157380642+antagoony@users.noreply.github.com> Date: Sat, 6 Dec 2025 23:46:21 +0100 Subject: [PATCH 02/21] specify PicoConfiguration annotation (with API.Status.EXPERIMENTAL) --- .../picocontainer/PicoConfiguration.java | 79 +++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 cucumber-picocontainer/src/main/java/io/cucumber/picocontainer/PicoConfiguration.java diff --git a/cucumber-picocontainer/src/main/java/io/cucumber/picocontainer/PicoConfiguration.java b/cucumber-picocontainer/src/main/java/io/cucumber/picocontainer/PicoConfiguration.java new file mode 100644 index 0000000000..a42c13580a --- /dev/null +++ b/cucumber-picocontainer/src/main/java/io/cucumber/picocontainer/PicoConfiguration.java @@ -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: + * + *

+ * An example (ancillary containing the specific ProviderAdapter as nested + * class) is: + * + *

+ * package some.example;
+ *
+ * import java.sql.*;
+ * import io.cucumber.picocontainer.PicoConfiguration;
+ * import org.picocontainer.injectors.ProviderAdapter;
+ *
+ * @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");
+ *         }
+ *     }
+ *
+ * }
+ * 
+ *

+ * Notes: + *

+ */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +@API(status = API.Status.EXPERIMENTAL) +public @interface PicoConfiguration { + + Class[] providers() default {}; + + Class[] providerAdapters() default {}; + +} From aa987be95e2c49c259117ac7b79dee884740205d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20Gasterst=C3=A4dt?= <157380642+antagoony@users.noreply.github.com> Date: Sun, 7 Dec 2025 00:31:20 +0100 Subject: [PATCH 03/21] implement PicoConfiguration awareness (add all Provider/ProviderAdapter) --- cucumber-picocontainer/pom.xml | 7 ++ .../cucumber/picocontainer/PicoBackend.java | 28 ++++++++ .../PicoBackendProviderService.java | 2 +- .../cucumber/picocontainer/PicoFactory.java | 56 ++++++++++++++- .../picocontainer/PicoBackendTest.java | 71 +++++++++++++++++++ .../annotationconfig/ConnectionProvider.java | 13 ++++ .../DatabaseConnectionProvider.java | 13 ++++ .../ExamplePicoConfiguration.java | 7 ++ 8 files changed, 195 insertions(+), 2 deletions(-) create mode 100644 cucumber-picocontainer/src/test/java/io/cucumber/picocontainer/PicoBackendTest.java create mode 100644 cucumber-picocontainer/src/test/java/io/cucumber/picocontainer/annotationconfig/ConnectionProvider.java create mode 100644 cucumber-picocontainer/src/test/java/io/cucumber/picocontainer/annotationconfig/DatabaseConnectionProvider.java create mode 100644 cucumber-picocontainer/src/test/java/io/cucumber/picocontainer/annotationconfig/ExamplePicoConfiguration.java diff --git a/cucumber-picocontainer/pom.xml b/cucumber-picocontainer/pom.xml index c29f43932f..5b90c60a5b 100644 --- a/cucumber-picocontainer/pom.xml +++ b/cucumber-picocontainer/pom.xml @@ -16,6 +16,7 @@ 2.15.2 1.1.2 5.14.1 + 5.20.0 @@ -72,6 +73,12 @@ junit-vintage-engine test + + org.mockito + mockito-junit-jupiter + ${mockito.version} + test + diff --git a/cucumber-picocontainer/src/main/java/io/cucumber/picocontainer/PicoBackend.java b/cucumber-picocontainer/src/main/java/io/cucumber/picocontainer/PicoBackend.java index b09505d5b5..71000e6038 100644 --- a/cucumber-picocontainer/src/main/java/io/cucumber/picocontainer/PicoBackend.java +++ b/cucumber-picocontainer/src/main/java/io/cucumber/picocontainer/PicoBackend.java @@ -1,16 +1,44 @@ 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 classLoaderSupplier) { + this.container = container; + this.classFinder = new ClasspathScanner(classLoaderSupplier); + } + @Override public void loadGlue(Glue glue, List 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 diff --git a/cucumber-picocontainer/src/main/java/io/cucumber/picocontainer/PicoBackendProviderService.java b/cucumber-picocontainer/src/main/java/io/cucumber/picocontainer/PicoBackendProviderService.java index d47d6f3388..93da27a830 100644 --- a/cucumber-picocontainer/src/main/java/io/cucumber/picocontainer/PicoBackendProviderService.java +++ b/cucumber-picocontainer/src/main/java/io/cucumber/picocontainer/PicoBackendProviderService.java @@ -11,7 +11,7 @@ public final class PicoBackendProviderService implements BackendProviderService @Override public Backend create(Lookup lookup, Container container, Supplier classLoader) { - return new PicoBackend(); + return new PicoBackend(container, classLoader); } } diff --git a/cucumber-picocontainer/src/main/java/io/cucumber/picocontainer/PicoFactory.java b/cucumber-picocontainer/src/main/java/io/cucumber/picocontainer/PicoFactory.java index 67213438c4..a38368d55d 100644 --- a/cucumber-picocontainer/src/main/java/io/cucumber/picocontainer/PicoFactory.java +++ b/cucumber-picocontainer/src/main/java/io/cucumber/picocontainer/PicoFactory.java @@ -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; @@ -31,8 +35,29 @@ public void start() { .withCaching() .withLifecycle() .build(); + Set> providers = new HashSet<>(); + Set> 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, @@ -45,6 +70,35 @@ 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(); diff --git a/cucumber-picocontainer/src/test/java/io/cucumber/picocontainer/PicoBackendTest.java b/cucumber-picocontainer/src/test/java/io/cucumber/picocontainer/PicoBackendTest.java new file mode 100644 index 0000000000..12c0e5e7dd --- /dev/null +++ b/cucumber-picocontainer/src/test/java/io/cucumber/picocontainer/PicoBackendTest.java @@ -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); + } + +} diff --git a/cucumber-picocontainer/src/test/java/io/cucumber/picocontainer/annotationconfig/ConnectionProvider.java b/cucumber-picocontainer/src/test/java/io/cucumber/picocontainer/annotationconfig/ConnectionProvider.java new file mode 100644 index 0000000000..12d31b3bf6 --- /dev/null +++ b/cucumber-picocontainer/src/test/java/io/cucumber/picocontainer/annotationconfig/ConnectionProvider.java @@ -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."); + } + +} diff --git a/cucumber-picocontainer/src/test/java/io/cucumber/picocontainer/annotationconfig/DatabaseConnectionProvider.java b/cucumber-picocontainer/src/test/java/io/cucumber/picocontainer/annotationconfig/DatabaseConnectionProvider.java new file mode 100644 index 0000000000..36eaf1e884 --- /dev/null +++ b/cucumber-picocontainer/src/test/java/io/cucumber/picocontainer/annotationconfig/DatabaseConnectionProvider.java @@ -0,0 +1,13 @@ +package io.cucumber.picocontainer.annotationconfig; + +import org.picocontainer.injectors.ProviderAdapter; + +import java.sql.Connection; + +public class DatabaseConnectionProvider extends ProviderAdapter { + + public Connection provide() { + throw new UnsupportedOperationException("Intentionally not supported to detect any premature injection."); + } + +} diff --git a/cucumber-picocontainer/src/test/java/io/cucumber/picocontainer/annotationconfig/ExamplePicoConfiguration.java b/cucumber-picocontainer/src/test/java/io/cucumber/picocontainer/annotationconfig/ExamplePicoConfiguration.java new file mode 100644 index 0000000000..2aab9bffd4 --- /dev/null +++ b/cucumber-picocontainer/src/test/java/io/cucumber/picocontainer/annotationconfig/ExamplePicoConfiguration.java @@ -0,0 +1,7 @@ +package io.cucumber.picocontainer.annotationconfig; + +import io.cucumber.picocontainer.PicoConfiguration; + +@PicoConfiguration(providers = ConnectionProvider.class, providerAdapters = DatabaseConnectionProvider.class) +public class ExamplePicoConfiguration { +} From 7caf720dfc17037e003eb135b55f51d6448fefd2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20Gasterst=C3=A4dt?= <157380642+antagoony@users.noreply.github.com> Date: Sun, 7 Dec 2025 00:31:45 +0100 Subject: [PATCH 04/21] stop conditionally only --- .../src/main/java/io/cucumber/picocontainer/PicoFactory.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cucumber-picocontainer/src/main/java/io/cucumber/picocontainer/PicoFactory.java b/cucumber-picocontainer/src/main/java/io/cucumber/picocontainer/PicoFactory.java index a38368d55d..27cb5bd2f8 100644 --- a/cucumber-picocontainer/src/main/java/io/cucumber/picocontainer/PicoFactory.java +++ b/cucumber-picocontainer/src/main/java/io/cucumber/picocontainer/PicoFactory.java @@ -101,7 +101,9 @@ private Class addProvider(Class clazz) { @Override public void stop() { - pico.stop(); + if (pico.getLifecycleState().isStarted()) { + pico.stop(); + } pico.dispose(); } From 4bdfd55876124a32d01d40a3c69cc2ff1b3c4f21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20Gasterst=C3=A4dt?= <157380642+antagoony@users.noreply.github.com> Date: Sun, 7 Dec 2025 01:01:59 +0100 Subject: [PATCH 05/21] add CHANGELOG entry --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e87af0ad66..2d995a5fbf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [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) +- [Java] Support Provider instances with Pico Container ([#2879](https://github.com/cucumber/cucumber-jvm/issues/2879) Stefan Gasterstädt) ### Changed - [Core] Upload Cucumber Reports with Gzip encoding ([#3115](https://github.com/cucumber/cucumber-jvm/pull/3115)) From 5e857d33218f9101953767862761768e56184bfa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20Gasterst=C3=A4dt?= <157380642+antagoony@users.noreply.github.com> Date: Sun, 7 Dec 2025 01:15:53 +0100 Subject: [PATCH 06/21] add README description and example --- cucumber-picocontainer/README.md | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/cucumber-picocontainer/README.md b/cucumber-picocontainer/README.md index 430f88ac72..951476d994 100644 --- a/cucumber-picocontainer/README.md +++ b/cucumber-picocontainer/README.md @@ -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"); + } + } + +} +``` From ed440b40fb68dd7d3253e21e7bc67504774fb6fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20Gasterst=C3=A4dt?= <157380642+antagoony@users.noreply.github.com> Date: Sun, 7 Dec 2025 01:19:21 +0100 Subject: [PATCH 07/21] add PR reference --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d995a5fbf..1d85b6bfb5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,7 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [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) -- [Java] Support Provider instances with Pico Container ([#2879](https://github.com/cucumber/cucumber-jvm/issues/2879) 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) ### Changed - [Core] Upload Cucumber Reports with Gzip encoding ([#3115](https://github.com/cucumber/cucumber-jvm/pull/3115)) From ae8732cf3f9a8e303d1f7d4d95bbbab82e54f436 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20Gasterst=C3=A4dt?= <157380642+antagoony@users.noreply.github.com> Date: Sun, 7 Dec 2025 23:04:14 +0100 Subject: [PATCH 08/21] rename @PicoConfiguration to @CucumberPicoProvider --- cucumber-picocontainer/README.md | 4 ++-- ...figuration.java => CucumberPicoProvider.java} | 16 ++++++++-------- .../io/cucumber/picocontainer/PicoBackend.java | 4 ++-- .../ExamplePicoConfiguration.java | 4 ++-- 4 files changed, 14 insertions(+), 14 deletions(-) rename cucumber-picocontainer/src/main/java/io/cucumber/picocontainer/{PicoConfiguration.java => CucumberPicoProvider.java} (84%) diff --git a/cucumber-picocontainer/README.md b/cucumber-picocontainer/README.md index 951476d994..d312e425d1 100644 --- a/cucumber-picocontainer/README.md +++ b/cucumber-picocontainer/README.md @@ -132,10 +132,10 @@ database connection as a constructor argument. package com.example.app; import java.sql.*; -import io.cucumber.picocontainer.PicoConfiguration; +import io.cucumber.picocontainer.CucumberPicoProvider; import org.picocontainer.injectors.ProviderAdapter; -@PicoConfiguration(providerAdapters = { ExamplePicoConfiguration.DatabaseConnectionProvider.class }) +@CucumberPicoProvider(providerAdapters = { ExampleCucumberPicoProvider.DatabaseConnectionProvider.class }) public class ExamplePicoConfiguration { public static class DatabaseConnectionProvider extends ProviderAdapter { diff --git a/cucumber-picocontainer/src/main/java/io/cucumber/picocontainer/PicoConfiguration.java b/cucumber-picocontainer/src/main/java/io/cucumber/picocontainer/CucumberPicoProvider.java similarity index 84% rename from cucumber-picocontainer/src/main/java/io/cucumber/picocontainer/PicoConfiguration.java rename to cucumber-picocontainer/src/main/java/io/cucumber/picocontainer/CucumberPicoProvider.java index a42c13580a..d7fec27260 100644 --- a/cucumber-picocontainer/src/main/java/io/cucumber/picocontainer/PicoConfiguration.java +++ b/cucumber-picocontainer/src/main/java/io/cucumber/picocontainer/CucumberPicoProvider.java @@ -27,11 +27,11 @@ * package some.example; * * import java.sql.*; - * import io.cucumber.picocontainer.PicoConfiguration; + * import io.cucumber.picocontainer.CucumberPicoProvider; * import org.picocontainer.injectors.ProviderAdapter; * - * @PicoConfiguration(providerAdapters = { MyPicoConfiguration.DatabaseConnectionProvider.class }) - * public class MyPicoConfiguration { + * @CucumberPicoProvider(providerAdapters = { MyCucumberPicoProvider.DatabaseConnectionProvider.class }) + * public class MyCucumberPicoProvider { * * public static class DatabaseConnectionProvider extends ProviderAdapter { * public Connection provide() throws ClassNotFoundException, ReflectiveOperationException, SQLException { @@ -48,12 +48,12 @@ * Notes: *
    *
  • Currently, there is no limitation to the number of - * {@link PicoConfiguration} annotations. All of these annotations will be + * {@link CucumberPicoProvider} annotations. All of these annotations will be * considered when preparing the {@link org.picocontainer.PicoContainer * PicoContainer}.
  • - *
  • If there is no {@link PicoConfiguration} annotation at all then (beside - * the basic preparation) no additional PicoContainer preparation will be - * done.
  • + *
  • If there is no {@link CucumberPicoProvider} annotation at all then + * (beside the basic preparation) no additional PicoContainer preparation will + * be done.
  • *
  • Cucumber PicoContainer uses PicoContainer's {@link MutablePicoContainer} * internally. Doing so, all {@link #providers() Providers} will be added by * {@link MutablePicoContainer#addAdapter(org.picocontainer.ComponentAdapter) @@ -70,7 +70,7 @@ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) @API(status = API.Status.EXPERIMENTAL) -public @interface PicoConfiguration { +public @interface CucumberPicoProvider { Class[] providers() default {}; diff --git a/cucumber-picocontainer/src/main/java/io/cucumber/picocontainer/PicoBackend.java b/cucumber-picocontainer/src/main/java/io/cucumber/picocontainer/PicoBackend.java index 71000e6038..356ae8355f 100644 --- a/cucumber-picocontainer/src/main/java/io/cucumber/picocontainer/PicoBackend.java +++ b/cucumber-picocontainer/src/main/java/io/cucumber/picocontainer/PicoBackend.java @@ -32,10 +32,10 @@ public void loadGlue(Glue glue, List gluePaths) { .map(ClasspathSupport::packageName) .map(classFinder::scanForClassesInPackage) .flatMap(Collection::stream) - .filter(clazz -> clazz.isAnnotationPresent(PicoConfiguration.class)) + .filter(clazz -> clazz.isAnnotationPresent(CucumberPicoProvider.class)) .distinct() .forEach(picoConfig -> { - PicoConfiguration configuration = picoConfig.getAnnotation(PicoConfiguration.class); + CucumberPicoProvider configuration = picoConfig.getAnnotation(CucumberPicoProvider.class); stream(configuration.providers()).forEach(container::addClass); stream(configuration.providerAdapters()).forEach(container::addClass); }); diff --git a/cucumber-picocontainer/src/test/java/io/cucumber/picocontainer/annotationconfig/ExamplePicoConfiguration.java b/cucumber-picocontainer/src/test/java/io/cucumber/picocontainer/annotationconfig/ExamplePicoConfiguration.java index 2aab9bffd4..d660a51c3e 100644 --- a/cucumber-picocontainer/src/test/java/io/cucumber/picocontainer/annotationconfig/ExamplePicoConfiguration.java +++ b/cucumber-picocontainer/src/test/java/io/cucumber/picocontainer/annotationconfig/ExamplePicoConfiguration.java @@ -1,7 +1,7 @@ package io.cucumber.picocontainer.annotationconfig; -import io.cucumber.picocontainer.PicoConfiguration; +import io.cucumber.picocontainer.CucumberPicoProvider; -@PicoConfiguration(providers = ConnectionProvider.class, providerAdapters = DatabaseConnectionProvider.class) +@CucumberPicoProvider(providers = ConnectionProvider.class, providerAdapters = DatabaseConnectionProvider.class) public class ExamplePicoConfiguration { } From 48e0aafcd4cd673b34fa8fc2812e0c5e511f8885 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20Gasterst=C3=A4dt?= <157380642+antagoony@users.noreply.github.com> Date: Sun, 7 Dec 2025 23:16:21 +0100 Subject: [PATCH 09/21] remove provisioning of separate #providerAdapters() --- .../picocontainer/CucumberPicoProvider.java | 29 +++++++------------ .../cucumber/picocontainer/PicoBackend.java | 1 - .../ExamplePicoConfiguration.java | 2 +- 3 files changed, 12 insertions(+), 20 deletions(-) diff --git a/cucumber-picocontainer/src/main/java/io/cucumber/picocontainer/CucumberPicoProvider.java b/cucumber-picocontainer/src/main/java/io/cucumber/picocontainer/CucumberPicoProvider.java index d7fec27260..ffda554f09 100644 --- a/cucumber-picocontainer/src/main/java/io/cucumber/picocontainer/CucumberPicoProvider.java +++ b/cucumber-picocontainer/src/main/java/io/cucumber/picocontainer/CucumberPicoProvider.java @@ -3,7 +3,6 @@ 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; @@ -16,24 +15,21 @@ *
      *
    • a list of classes conforming the PicoContainer's {@link Provider} * interface,
    • - *
    • a list of classes conforming the PicoContainer's {@link ProviderAdapter} - * interface.
    • *
    *

    - * An example (ancillary containing the specific ProviderAdapter as nested - * class) is: + * An example (ancillary containing the specific Provider as nested class) is: * *

      * package some.example;
      *
      * import java.sql.*;
      * import io.cucumber.picocontainer.CucumberPicoProvider;
    - * import org.picocontainer.injectors.ProviderAdapter;
    + * import org.picocontainer.injectors.Provider;
      *
    - * @CucumberPicoProvider(providerAdapters = { MyCucumberPicoProvider.DatabaseConnectionProvider.class })
    + * @CucumberPicoProvider(providers = { MyCucumberPicoProvider.DatabaseConnectionProvider.class })
      * public class MyCucumberPicoProvider {
      *
    - *     public static class DatabaseConnectionProvider extends ProviderAdapter {
    + *     public static 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
    @@ -57,14 +53,13 @@
      * 
  • 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)}.
  • - *
  • 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 ..."
  • + * 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.)
  • + *
  • 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 ..."
  • *
*/ @Retention(RetentionPolicy.RUNTIME) @@ -74,6 +69,4 @@ Class[] providers() default {}; - Class[] providerAdapters() default {}; - } diff --git a/cucumber-picocontainer/src/main/java/io/cucumber/picocontainer/PicoBackend.java b/cucumber-picocontainer/src/main/java/io/cucumber/picocontainer/PicoBackend.java index 356ae8355f..1b3e5ae4b6 100644 --- a/cucumber-picocontainer/src/main/java/io/cucumber/picocontainer/PicoBackend.java +++ b/cucumber-picocontainer/src/main/java/io/cucumber/picocontainer/PicoBackend.java @@ -37,7 +37,6 @@ public void loadGlue(Glue glue, List gluePaths) { .forEach(picoConfig -> { CucumberPicoProvider configuration = picoConfig.getAnnotation(CucumberPicoProvider.class); stream(configuration.providers()).forEach(container::addClass); - stream(configuration.providerAdapters()).forEach(container::addClass); }); } diff --git a/cucumber-picocontainer/src/test/java/io/cucumber/picocontainer/annotationconfig/ExamplePicoConfiguration.java b/cucumber-picocontainer/src/test/java/io/cucumber/picocontainer/annotationconfig/ExamplePicoConfiguration.java index d660a51c3e..ab0c01261c 100644 --- a/cucumber-picocontainer/src/test/java/io/cucumber/picocontainer/annotationconfig/ExamplePicoConfiguration.java +++ b/cucumber-picocontainer/src/test/java/io/cucumber/picocontainer/annotationconfig/ExamplePicoConfiguration.java @@ -2,6 +2,6 @@ import io.cucumber.picocontainer.CucumberPicoProvider; -@CucumberPicoProvider(providers = ConnectionProvider.class, providerAdapters = DatabaseConnectionProvider.class) +@CucumberPicoProvider(providers = { ConnectionProvider.class, DatabaseConnectionProvider.class }) public class ExamplePicoConfiguration { } From cfd1badfbf0a7d35a48c1aad0b08f71fba71d055 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20Gasterst=C3=A4dt?= <157380642+antagoony@users.noreply.github.com> Date: Tue, 16 Dec 2025 10:09:09 +0100 Subject: [PATCH 10/21] rename example/test class to clarify its intention --- .../annotationconfig/ExamplePicoConfiguration.java | 2 +- .../{ConnectionProvider.java => URLConnectionProvider.java} | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename cucumber-picocontainer/src/test/java/io/cucumber/picocontainer/annotationconfig/{ConnectionProvider.java => URLConnectionProvider.java} (84%) diff --git a/cucumber-picocontainer/src/test/java/io/cucumber/picocontainer/annotationconfig/ExamplePicoConfiguration.java b/cucumber-picocontainer/src/test/java/io/cucumber/picocontainer/annotationconfig/ExamplePicoConfiguration.java index ab0c01261c..73f73cadba 100644 --- a/cucumber-picocontainer/src/test/java/io/cucumber/picocontainer/annotationconfig/ExamplePicoConfiguration.java +++ b/cucumber-picocontainer/src/test/java/io/cucumber/picocontainer/annotationconfig/ExamplePicoConfiguration.java @@ -2,6 +2,6 @@ import io.cucumber.picocontainer.CucumberPicoProvider; -@CucumberPicoProvider(providers = { ConnectionProvider.class, DatabaseConnectionProvider.class }) +@CucumberPicoProvider(providers = { URLConnectionProvider.class, DatabaseConnectionProvider.class }) public class ExamplePicoConfiguration { } diff --git a/cucumber-picocontainer/src/test/java/io/cucumber/picocontainer/annotationconfig/ConnectionProvider.java b/cucumber-picocontainer/src/test/java/io/cucumber/picocontainer/annotationconfig/URLConnectionProvider.java similarity index 84% rename from cucumber-picocontainer/src/test/java/io/cucumber/picocontainer/annotationconfig/ConnectionProvider.java rename to cucumber-picocontainer/src/test/java/io/cucumber/picocontainer/annotationconfig/URLConnectionProvider.java index 12d31b3bf6..18c9fd6486 100644 --- a/cucumber-picocontainer/src/test/java/io/cucumber/picocontainer/annotationconfig/ConnectionProvider.java +++ b/cucumber-picocontainer/src/test/java/io/cucumber/picocontainer/annotationconfig/URLConnectionProvider.java @@ -4,7 +4,7 @@ import java.net.HttpURLConnection; -public class ConnectionProvider implements Provider { +public class URLConnectionProvider implements Provider { public HttpURLConnection provide() { throw new UnsupportedOperationException("Intentionally not supported to detect any premature injection."); From bfb86b5300d9746af1710551c6945c9d4c0b4934 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20Gasterst=C3=A4dt?= <157380642+antagoony@users.noreply.github.com> Date: Tue, 16 Dec 2025 10:11:26 +0100 Subject: [PATCH 11/21] enable package-internal re-use --- .../src/main/java/io/cucumber/picocontainer/PicoFactory.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cucumber-picocontainer/src/main/java/io/cucumber/picocontainer/PicoFactory.java b/cucumber-picocontainer/src/main/java/io/cucumber/picocontainer/PicoFactory.java index 27cb5bd2f8..0592d27998 100644 --- a/cucumber-picocontainer/src/main/java/io/cucumber/picocontainer/PicoFactory.java +++ b/cucumber-picocontainer/src/main/java/io/cucumber/picocontainer/PicoFactory.java @@ -70,7 +70,7 @@ public void start() { pico.start(); } - private boolean isProviderAdapter(Class clazz) { + static boolean isProviderAdapter(Class clazz) { return ProviderAdapter.class.isAssignableFrom(clazz); } @@ -84,7 +84,7 @@ private Class addProviderAdapter(Class clazz) { } } - private boolean isProvider(Class clazz) { + static boolean isProvider(Class clazz) { return Provider.class.isAssignableFrom(clazz); } From 9cc04cf42d878f05324a346522735b8eedcda619 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20Gasterst=C3=A4dt?= <157380642+antagoony@users.noreply.github.com> Date: Tue, 16 Dec 2025 10:13:17 +0100 Subject: [PATCH 12/21] add both, referenced Pico-Providers and annotated Pico-Providers --- cucumber-picocontainer/README.md | 2 +- .../io/cucumber/picocontainer/PicoBackend.java | 17 +++++++++++++---- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/cucumber-picocontainer/README.md b/cucumber-picocontainer/README.md index d312e425d1..6f9bde49a8 100644 --- a/cucumber-picocontainer/README.md +++ b/cucumber-picocontainer/README.md @@ -135,9 +135,9 @@ import java.sql.*; import io.cucumber.picocontainer.CucumberPicoProvider; import org.picocontainer.injectors.ProviderAdapter; -@CucumberPicoProvider(providerAdapters = { ExampleCucumberPicoProvider.DatabaseConnectionProvider.class }) public class ExamplePicoConfiguration { + @CucumberPicoProvider public static class DatabaseConnectionProvider extends ProviderAdapter { public Connection provide() throws ClassNotFoundException, ReflectiveOperationException, SQLException { // Connecting to MySQL Using the JDBC DriverManager Interface diff --git a/cucumber-picocontainer/src/main/java/io/cucumber/picocontainer/PicoBackend.java b/cucumber-picocontainer/src/main/java/io/cucumber/picocontainer/PicoBackend.java index 1b3e5ae4b6..7990f2c4b7 100644 --- a/cucumber-picocontainer/src/main/java/io/cucumber/picocontainer/PicoBackend.java +++ b/cucumber-picocontainer/src/main/java/io/cucumber/picocontainer/PicoBackend.java @@ -11,9 +11,12 @@ 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 { @@ -33,11 +36,17 @@ public void loadGlue(Glue glue, List gluePaths) { .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(picoConfig -> { - CucumberPicoProvider configuration = picoConfig.getAnnotation(CucumberPicoProvider.class); - stream(configuration.providers()).forEach(container::addClass); - }); + .forEach(container::addClass); } @Override From 02acb370c2d4694466845e60f697bf6d8e359c01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20Gasterst=C3=A4dt?= <157380642+antagoony@users.noreply.github.com> Date: Tue, 16 Dec 2025 10:15:24 +0100 Subject: [PATCH 13/21] keep distinction of Provider/ProviderAdapter internally --- .../cucumber/picocontainer/PicoFactory.java | 44 ++++++------------- 1 file changed, 14 insertions(+), 30 deletions(-) diff --git a/cucumber-picocontainer/src/main/java/io/cucumber/picocontainer/PicoFactory.java b/cucumber-picocontainer/src/main/java/io/cucumber/picocontainer/PicoFactory.java index 0592d27998..15615268c9 100644 --- a/cucumber-picocontainer/src/main/java/io/cucumber/picocontainer/PicoFactory.java +++ b/cucumber-picocontainer/src/main/java/io/cucumber/picocontainer/PicoFactory.java @@ -38,23 +38,19 @@ public void start() { Set> providers = new HashSet<>(); Set> providedClasses = new HashSet<>(); for (Class clazz : classes) { - if (isProviderAdapter(clazz)) { + if (isProvider(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); + ProviderAdapter adapter = adapterForProviderClass(clazz); + pico.addAdapter(adapter); + providedClasses.add(adapter.getComponentImplementation()); } } 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'") + // 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); } @@ -70,30 +66,18 @@ public void start() { pico.start(); } - static 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); - } - } - static boolean isProvider(Class clazz) { return Provider.class.isAssignableFrom(clazz); } - private Class addProvider(Class clazz) { + static boolean isProviderAdapter(Class clazz) { + return ProviderAdapter.class.isAssignableFrom(clazz); + } + + private static ProviderAdapter adapterForProviderClass(Class clazz) { try { Provider provider = (Provider) clazz.getDeclaredConstructor().newInstance(); - ProviderAdapter adapter = new ProviderAdapter(provider); - pico.addAdapter(adapter); - return adapter.getComponentImplementation(); + return isProviderAdapter(clazz) ? (ProviderAdapter) provider : new ProviderAdapter(provider); } catch (ReflectiveOperationException | IllegalArgumentException | SecurityException | PicoException e) { throw new CucumberBackendException(e.getMessage(), e); } From 5ff1b2d15b286f1bf0d5e0fb9d66f02e2aad20ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20Gasterst=C3=A4dt?= <157380642+antagoony@users.noreply.github.com> Date: Tue, 16 Dec 2025 10:16:09 +0100 Subject: [PATCH 14/21] not all component-adapters may be an instance of Cached --- .../main/java/io/cucumber/picocontainer/PicoFactory.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/cucumber-picocontainer/src/main/java/io/cucumber/picocontainer/PicoFactory.java b/cucumber-picocontainer/src/main/java/io/cucumber/picocontainer/PicoFactory.java index 15615268c9..fdaff1c01b 100644 --- a/cucumber-picocontainer/src/main/java/io/cucumber/picocontainer/PicoFactory.java +++ b/cucumber-picocontainer/src/main/java/io/cucumber/picocontainer/PicoFactory.java @@ -60,8 +60,11 @@ public void start() { // 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(); } From b442c9f37d699d4c60397d50c48fd5a0bd1b643f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20Gasterst=C3=A4dt?= <157380642+antagoony@users.noreply.github.com> Date: Tue, 16 Dec 2025 10:17:40 +0100 Subject: [PATCH 15/21] rename example/test class to clarify its intention --- .../java/io/cucumber/picocontainer/PicoBackendTest.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cucumber-picocontainer/src/test/java/io/cucumber/picocontainer/PicoBackendTest.java b/cucumber-picocontainer/src/test/java/io/cucumber/picocontainer/PicoBackendTest.java index 12c0e5e7dd..07cef185dd 100644 --- a/cucumber-picocontainer/src/test/java/io/cucumber/picocontainer/PicoBackendTest.java +++ b/cucumber-picocontainer/src/test/java/io/cucumber/picocontainer/PicoBackendTest.java @@ -2,9 +2,9 @@ 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 io.cucumber.picocontainer.annotationconfig.URLConnectionProvider; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -47,7 +47,7 @@ void considers_but_does_not_add_annotated_configuration() { void adds_provider_classes() { backend.loadGlue(glue, singletonList(URI.create("classpath:io/cucumber/picocontainer/annotationconfig"))); backend.buildWorld(); - verify(factory).addClass(ConnectionProvider.class); + verify(factory).addClass(URLConnectionProvider.class); } @Test @@ -64,7 +64,7 @@ void finds_configured_classes_only_once_when_scanning_twice() { 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(URLConnectionProvider.class); verify(factory, times(1)).addClass(DatabaseConnectionProvider.class); } From 0dd91ad46e69b92cc48e5627a3deb1686e4958bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20Gasterst=C3=A4dt?= <157380642+antagoony@users.noreply.github.com> Date: Tue, 16 Dec 2025 10:22:52 +0100 Subject: [PATCH 16/21] insinuate cascading pico-provider construction --- .../annotationconfig/ExamplePicoConfiguration.java | 12 +++++++++++- .../annotationconfig/URLConnectionProvider.java | 3 ++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/cucumber-picocontainer/src/test/java/io/cucumber/picocontainer/annotationconfig/ExamplePicoConfiguration.java b/cucumber-picocontainer/src/test/java/io/cucumber/picocontainer/annotationconfig/ExamplePicoConfiguration.java index 73f73cadba..384d56e3c4 100644 --- a/cucumber-picocontainer/src/test/java/io/cucumber/picocontainer/annotationconfig/ExamplePicoConfiguration.java +++ b/cucumber-picocontainer/src/test/java/io/cucumber/picocontainer/annotationconfig/ExamplePicoConfiguration.java @@ -1,7 +1,17 @@ package io.cucumber.picocontainer.annotationconfig; import io.cucumber.picocontainer.CucumberPicoProvider; +import org.picocontainer.injectors.Provider; -@CucumberPicoProvider(providers = { URLConnectionProvider.class, DatabaseConnectionProvider.class }) +import java.net.URL; + +@CucumberPicoProvider(providers = { ExamplePicoConfiguration.NestedUrlProvider.class, URLConnectionProvider.class, DatabaseConnectionProvider.class }) public class ExamplePicoConfiguration { + + public static class NestedUrlProvider implements Provider { + public URL provide() { + throw new UnsupportedOperationException("Intentionally not supported to detect any premature injection."); + } + } + } diff --git a/cucumber-picocontainer/src/test/java/io/cucumber/picocontainer/annotationconfig/URLConnectionProvider.java b/cucumber-picocontainer/src/test/java/io/cucumber/picocontainer/annotationconfig/URLConnectionProvider.java index 18c9fd6486..d408a4b9d6 100644 --- a/cucumber-picocontainer/src/test/java/io/cucumber/picocontainer/annotationconfig/URLConnectionProvider.java +++ b/cucumber-picocontainer/src/test/java/io/cucumber/picocontainer/annotationconfig/URLConnectionProvider.java @@ -3,10 +3,11 @@ import org.picocontainer.injectors.Provider; import java.net.HttpURLConnection; +import java.net.URL; public class URLConnectionProvider implements Provider { - public HttpURLConnection provide() { + public HttpURLConnection provide(URL url) { throw new UnsupportedOperationException("Intentionally not supported to detect any premature injection."); } From e47c54df1c983e4d654e8222ed15238fd1301267 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20Gasterst=C3=A4dt?= <157380642+antagoony@users.noreply.github.com> Date: Tue, 16 Dec 2025 10:24:17 +0100 Subject: [PATCH 17/21] extend pico-backend tests --- .../picocontainer/PicoBackendTest.java | 27 ++++++++++++++----- .../ExamplePicoConfiguration.java | 3 ++- .../annotationconfig/UrlToUriProvider.java | 16 +++++++++++ 3 files changed, 39 insertions(+), 7 deletions(-) create mode 100644 cucumber-picocontainer/src/test/java/io/cucumber/picocontainer/annotationconfig/UrlToUriProvider.java diff --git a/cucumber-picocontainer/src/test/java/io/cucumber/picocontainer/PicoBackendTest.java b/cucumber-picocontainer/src/test/java/io/cucumber/picocontainer/PicoBackendTest.java index 07cef185dd..939209a876 100644 --- a/cucumber-picocontainer/src/test/java/io/cucumber/picocontainer/PicoBackendTest.java +++ b/cucumber-picocontainer/src/test/java/io/cucumber/picocontainer/PicoBackendTest.java @@ -5,6 +5,7 @@ import io.cucumber.picocontainer.annotationconfig.DatabaseConnectionProvider; import io.cucumber.picocontainer.annotationconfig.ExamplePicoConfiguration; import io.cucumber.picocontainer.annotationconfig.URLConnectionProvider; +import io.cucumber.picocontainer.annotationconfig.UrlToUriProvider; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -38,23 +39,35 @@ void createBackend() { @Test void considers_but_does_not_add_annotated_configuration() { - backend.loadGlue(glue, singletonList(URI.create("classpath:io/cucumber/picocontainer/annotationconfig"))); + 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"))); + void adds_referenced_provider_classes() { + backend.loadGlue(glue, + singletonList(URI.create("classpath:io/cucumber/picocontainer/annotationconfig"))); backend.buildWorld(); verify(factory).addClass(URLConnectionProvider.class); + verify(factory).addClass(DatabaseConnectionProvider.class); } @Test - void adds_provideradapter_classes() { - backend.loadGlue(glue, singletonList(URI.create("classpath:io/cucumber/picocontainer/annotationconfig"))); + void adds_selfsufficient_provider_classes() { + backend.loadGlue(glue, + singletonList(URI.create("classpath:io/cucumber/picocontainer/annotationconfig"))); backend.buildWorld(); - verify(factory).addClass(DatabaseConnectionProvider.class); + verify(factory).addClass(ExamplePicoConfiguration.NestedUrlProvider.class); + } + + @Test + void adds_nested_provider_classes() { + backend.loadGlue(glue, + singletonList(URI.create("classpath:io/cucumber/picocontainer/annotationconfig"))); + backend.buildWorld(); + verify(factory).addClass(UrlToUriProvider.class); } @Test @@ -66,6 +79,8 @@ void finds_configured_classes_only_once_when_scanning_twice() { verify(factory, never()).addClass(ExamplePicoConfiguration.class); verify(factory, times(1)).addClass(URLConnectionProvider.class); verify(factory, times(1)).addClass(DatabaseConnectionProvider.class); + verify(factory, times(1)).addClass(ExamplePicoConfiguration.NestedUrlProvider.class); + verify(factory, times(1)).addClass(UrlToUriProvider.class); } } diff --git a/cucumber-picocontainer/src/test/java/io/cucumber/picocontainer/annotationconfig/ExamplePicoConfiguration.java b/cucumber-picocontainer/src/test/java/io/cucumber/picocontainer/annotationconfig/ExamplePicoConfiguration.java index 384d56e3c4..6493a03088 100644 --- a/cucumber-picocontainer/src/test/java/io/cucumber/picocontainer/annotationconfig/ExamplePicoConfiguration.java +++ b/cucumber-picocontainer/src/test/java/io/cucumber/picocontainer/annotationconfig/ExamplePicoConfiguration.java @@ -5,9 +5,10 @@ import java.net.URL; -@CucumberPicoProvider(providers = { ExamplePicoConfiguration.NestedUrlProvider.class, URLConnectionProvider.class, DatabaseConnectionProvider.class }) +@CucumberPicoProvider(providers = { URLConnectionProvider.class, DatabaseConnectionProvider.class }) public class ExamplePicoConfiguration { + @CucumberPicoProvider public static class NestedUrlProvider implements Provider { public URL provide() { throw new UnsupportedOperationException("Intentionally not supported to detect any premature injection."); diff --git a/cucumber-picocontainer/src/test/java/io/cucumber/picocontainer/annotationconfig/UrlToUriProvider.java b/cucumber-picocontainer/src/test/java/io/cucumber/picocontainer/annotationconfig/UrlToUriProvider.java new file mode 100644 index 0000000000..4aa6c27dfa --- /dev/null +++ b/cucumber-picocontainer/src/test/java/io/cucumber/picocontainer/annotationconfig/UrlToUriProvider.java @@ -0,0 +1,16 @@ +package io.cucumber.picocontainer.annotationconfig; + +import io.cucumber.picocontainer.CucumberPicoProvider; +import org.picocontainer.injectors.Provider; + +import java.net.URI; +import java.net.URL; + +@CucumberPicoProvider +public class UrlToUriProvider implements Provider { + + public URI provide(URL url) { + throw new UnsupportedOperationException("Intentionally not supported to detect any premature injection."); + } + +} From 05c19f2c077ba1bf61d1ef29027a65effdc48d5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20Gasterst=C3=A4dt?= <157380642+antagoony@users.noreply.github.com> Date: Tue, 16 Dec 2025 12:08:04 +0100 Subject: [PATCH 18/21] add check for meaningful CucumberPicoProvider annotation --- .../io/cucumber/picocontainer/PicoFactory.java | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/cucumber-picocontainer/src/main/java/io/cucumber/picocontainer/PicoFactory.java b/cucumber-picocontainer/src/main/java/io/cucumber/picocontainer/PicoFactory.java index fdaff1c01b..b5dec00429 100644 --- a/cucumber-picocontainer/src/main/java/io/cucumber/picocontainer/PicoFactory.java +++ b/cucumber-picocontainer/src/main/java/io/cucumber/picocontainer/PicoFactory.java @@ -96,12 +96,29 @@ public void stop() { @Override public boolean addClass(Class clazz) { + checkMeaningfulPicoAnnotation(clazz); if (isInstantiable(clazz) && classes.add(clazz)) { 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)) { + 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" + + + "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 getInstance(Class type) { return pico.getComponent(type); From 50a5083e0e6b916ffab563694b08b025c2b33f24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20Gasterst=C3=A4dt?= <157380642+antagoony@users.noreply.github.com> Date: Tue, 16 Dec 2025 13:01:54 +0100 Subject: [PATCH 19/21] revise JavaDoc of @CucumberPicoProvider --- .../picocontainer/CucumberPicoProvider.java | 38 +++++++++++-------- 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/cucumber-picocontainer/src/main/java/io/cucumber/picocontainer/CucumberPicoProvider.java b/cucumber-picocontainer/src/main/java/io/cucumber/picocontainer/CucumberPicoProvider.java index ffda554f09..e9ac842e4e 100644 --- a/cucumber-picocontainer/src/main/java/io/cucumber/picocontainer/CucumberPicoProvider.java +++ b/cucumber-picocontainer/src/main/java/io/cucumber/picocontainer/CucumberPicoProvider.java @@ -11,13 +11,9 @@ /** * This annotation is used to provide some additional PicoContainer - * configuration. At the moment this covers: - *
    - *
  • a list of classes conforming the PicoContainer's {@link Provider} - * interface,
  • - *
+ * {@link Provider} classes. *

- * An example (ancillary containing the specific Provider as nested class) is: + * An example is: * *

  * package some.example;
@@ -26,18 +22,28 @@
  * import io.cucumber.picocontainer.CucumberPicoProvider;
  * import org.picocontainer.injectors.Provider;
  *
- * @CucumberPicoProvider(providers = { MyCucumberPicoProvider.DatabaseConnectionProvider.class })
- * public class MyCucumberPicoProvider {
- *
- *     public static 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");
- *         }
+ * @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");
  *     }
+ * }
+ * 
+ *

+ * In order to re-use existing {@link Provider}s, you can refer to those like + * this: + * + *

+ * package some.example;
+ *
+ * import io.cucumber.picocontainer.CucumberPicoProvider;
+ * import some.other.namespace.SomeExistingProvider.class;
  *
+ * @CucumberPicoProvider(providers = { SomeExistingProvider.class })
+ * public class MyCucumberPicoProviders {
  * }
  * 
*

From e3f306241370880ee766d96b81193bfad49a64a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20Gasterst=C3=A4dt?= <157380642+antagoony@users.noreply.github.com> Date: Wed, 17 Dec 2025 15:41:52 +0100 Subject: [PATCH 20/21] revise README.md according to current @CucumberPicoProvider --- cucumber-picocontainer/README.md | 28 +++++++++++++--------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/cucumber-picocontainer/README.md b/cucumber-picocontainer/README.md index 6f9bde49a8..881e7064b1 100644 --- a/cucumber-picocontainer/README.md +++ b/cucumber-picocontainer/README.md @@ -124,27 +124,25 @@ 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. +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.ProviderAdapter; - -public class ExamplePicoConfiguration { - - @CucumberPicoProvider - 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"); - } +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"); } } From f58342edfc0c67b327f6751fd40c93a5d248ab54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20Gasterst=C3=A4dt?= <157380642+antagoony@users.noreply.github.com> Date: Wed, 17 Dec 2025 17:47:24 +0100 Subject: [PATCH 21/21] move CHANGELOG entry to current [Unreleased] section --- CHANGELOG.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1d85b6bfb5..421fe3305f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,10 +11,12 @@ 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) -- [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) ### Changed - [Core] Upload Cucumber Reports with Gzip encoding ([#3115](https://github.com/cucumber/cucumber-jvm/pull/3115))