diff --git a/cucumber-picocontainer/src/main/java/io/cucumber/picocontainer/CucumberPicoProvider.java b/cucumber-picocontainer/src/main/java/io/cucumber/picocontainer/CucumberPicoProvider.java
new file mode 100644
index 0000000000..e9ac842e4e
--- /dev/null
+++ b/cucumber-picocontainer/src/main/java/io/cucumber/picocontainer/CucumberPicoProvider.java
@@ -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.
+ *
+ * An example is:
+ *
+ *
+ * package some.example;
+ *
+ * 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");
+ * }
+ * }
+ *
+ *
+ * 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 {
+ * }
+ *
+ *
+ * Notes:
+ *
+ * - 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}.
+ * - 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)
+ * 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)
+@Target(ElementType.TYPE)
+@API(status = API.Status.EXPERIMENTAL)
+public @interface CucumberPicoProvider {
+
+ Class extends Provider>[] 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
new file mode 100644
index 0000000000..7990f2c4b7
--- /dev/null
+++ b/cucumber-picocontainer/src/main/java/io/cucumber/picocontainer/PicoBackend.java
@@ -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 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(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;
+ }
+
+}
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..93da27a830
--- /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(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..b5dec00429 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,34 +35,90 @@ public void start() {
.withCaching()
.withLifecycle()
.build();
+ Set> providers = new HashSet<>();
+ Set> 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)) {
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);
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..939209a876
--- /dev/null
+++ b/cucumber-picocontainer/src/test/java/io/cucumber/picocontainer/PicoBackendTest.java
@@ -0,0 +1,86 @@
+package io.cucumber.picocontainer;
+
+import io.cucumber.core.backend.Glue;
+import io.cucumber.core.backend.ObjectFactory;
+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;
+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_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_selfsufficient_provider_classes() {
+ backend.loadGlue(glue,
+ singletonList(URI.create("classpath:io/cucumber/picocontainer/annotationconfig")));
+ backend.buildWorld();
+ 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
+ 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(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/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..6493a03088
--- /dev/null
+++ b/cucumber-picocontainer/src/test/java/io/cucumber/picocontainer/annotationconfig/ExamplePicoConfiguration.java
@@ -0,0 +1,18 @@
+package io.cucumber.picocontainer.annotationconfig;
+
+import io.cucumber.picocontainer.CucumberPicoProvider;
+import org.picocontainer.injectors.Provider;
+
+import java.net.URL;
+
+@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/URLConnectionProvider.java b/cucumber-picocontainer/src/test/java/io/cucumber/picocontainer/annotationconfig/URLConnectionProvider.java
new file mode 100644
index 0000000000..d408a4b9d6
--- /dev/null
+++ b/cucumber-picocontainer/src/test/java/io/cucumber/picocontainer/annotationconfig/URLConnectionProvider.java
@@ -0,0 +1,14 @@
+package io.cucumber.picocontainer.annotationconfig;
+
+import org.picocontainer.injectors.Provider;
+
+import java.net.HttpURLConnection;
+import java.net.URL;
+
+public class URLConnectionProvider implements Provider {
+
+ public HttpURLConnection provide(URL url) {
+ 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.");
+ }
+
+}