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..1cc367ebdb
--- /dev/null
+++ b/cucumber-picocontainer/src/main/java/io/cucumber/picocontainer/CucumberPicoProvider.java
@@ -0,0 +1,61 @@
+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");
+ * }
+ * }
+ *
+ *
+ * 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 Provider}s 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 {
+}
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..422c88a5ec
--- /dev/null
+++ b/cucumber-picocontainer/src/main/java/io/cucumber/picocontainer/PicoBackend.java
@@ -0,0 +1,52 @@
+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;
+
+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))
+ .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..4f9713c1a0 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;
@@ -16,6 +20,7 @@
public final class PicoFactory implements ObjectFactory {
private final Set> classes = new HashSet<>();
+ private final Set> providers = new HashSet<>();
private MutablePicoContainer pico;
private static boolean isInstantiable(Class> clazz) {
@@ -31,34 +36,103 @@ public void start() {
.withCaching()
.withLifecycle()
.build();
+ Set> providedClasses = new HashSet<>();
+ for (Class clazz : providers) {
+ ProviderAdapter adapter = adapterForProviderClass(clazz);
+ pico.addAdapter(adapter);
+ providedClasses.add(adapter.getComponentImplementation());
+ }
for (Class> clazz : classes) {
- pico.addComponent(clazz);
+ // do not add classes that are already provided (otherwise this
+ // causes exceptional situations, e.g. PicoCompositionException
+ // with message "Duplicate Keys not allowed. Duplicate for
+ // 'class XXX'")
+ if (!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 hasCucumberPicoProvider(Class> clazz) {
+ return clazz.isAnnotationPresent(CucumberPicoProvider.class);
+ }
+
+ 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 = 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) {
- if (isInstantiable(clazz) && classes.add(clazz)) {
- addConstructorDependencies(clazz);
+ if (hasCucumberPicoProvider(clazz)) {
+ providers.add(checkProperPicoProvider(clazz));
+ } else {
+ if (isInstantiable(clazz) && classes.add(clazz)) {
+ addConstructorDependencies(clazz);
+ }
}
return true;
}
+ private static boolean hasDefaultConstructor(Class> clazz) {
+ for (Constructor> constructor : clazz.getDeclaredConstructors()) {
+ if (constructor.getParameterCount() == 0) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @SuppressWarnings("unchecked")
+ private static Class checkProperPicoProvider(Class> clazz) {
+ if (!isProvider(clazz) || !isInstantiable(clazz) || !hasDefaultConstructor(clazz)) {
+ throw new CucumberBackendException(String.format("" +
+ "Glue class %1$s was annotated with @CucumberPicoProvider; marking it as a candidate for declaring a"
+ +
+ "PicoContainer Provider instance. Please ensure that all of the following requirements are satisfied:\n"
+ +
+ "1) the class implements org.picocontainer.injectors.Provider\n" +
+ "2) the class is public\n" +
+ "3) the class is not abstract\n" +
+ "4) the class provides a default constructor\n" +
+ "5) if nested, the class is static.",
+ clazz.getName()));
+ }
+ return (Class) clazz;
+ }
+
@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..88ee4b1b7e
--- /dev/null
+++ b/cucumber-picocontainer/src/test/java/io/cucumber/picocontainer/PicoBackendTest.java
@@ -0,0 +1,85 @@
+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.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(ExamplePicoConfiguration.NestedUrlConnectionProvider.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(ExamplePicoConfiguration.NestedUrlProvider.class);
+ verify(factory, times(1)).addClass(ExamplePicoConfiguration.NestedUrlConnectionProvider.class);
+ verify(factory, times(1)).addClass(UrlToUriProvider.class);
+ verify(factory, times(1)).addClass(DatabaseConnectionProvider.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..034df01299
--- /dev/null
+++ b/cucumber-picocontainer/src/test/java/io/cucumber/picocontainer/annotationconfig/DatabaseConnectionProvider.java
@@ -0,0 +1,15 @@
+package io.cucumber.picocontainer.annotationconfig;
+
+import io.cucumber.picocontainer.CucumberPicoProvider;
+import org.picocontainer.injectors.ProviderAdapter;
+
+import java.sql.Connection;
+
+@CucumberPicoProvider
+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..89780000f3
--- /dev/null
+++ b/cucumber-picocontainer/src/test/java/io/cucumber/picocontainer/annotationconfig/ExamplePicoConfiguration.java
@@ -0,0 +1,25 @@
+package io.cucumber.picocontainer.annotationconfig;
+
+import io.cucumber.picocontainer.CucumberPicoProvider;
+import org.picocontainer.injectors.Provider;
+
+import java.net.HttpURLConnection;
+import java.net.URL;
+
+public class ExamplePicoConfiguration {
+
+ @CucumberPicoProvider
+ public static class NestedUrlProvider implements Provider {
+ public URL provide() {
+ throw new UnsupportedOperationException("Intentionally not supported to detect any premature injection.");
+ }
+ }
+
+ @CucumberPicoProvider
+ public static class NestedUrlConnectionProvider 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.");
+ }
+
+}