classLoader) {
+ return new PicoBackend(container, classLoader);
+ }
+
+}
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:
+ *
+ * - 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:
+ *
+ *
+ * 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:
+ *
+ * - 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}.
+ * - If there is no {@link PicoConfiguration} 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)
+ * 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 ..."
+ *
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.TYPE)
+@API(status = API.Status.EXPERIMENTAL)
+public @interface PicoConfiguration {
+
+ Class extends Provider>[] providers() default {};
+
+ Class extends ProviderAdapter>[] providerAdapters() default {};
+
+}
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..27cb5bd2f8 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,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();
}
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
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 {
+}