-
-
Notifications
You must be signed in to change notification settings - Fork 2k
My Approach for "Support Provider instances with Pico Container" #3128
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 21 commits
15b8e34
4990f86
aa987be
7caf720
4bdfd55
5e857d3
ed440b4
ae8732c
48e0aaf
cfd1bad
bfb86b5
9cc04cf
02acb37
5ff1b2d
b442c9f
0dd91ad
e47c54d
05c19f2
50a5083
e3f3062
f58342e
7b946bb
d57a4b3
0d6216a
8211a0c
3ac388d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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. | ||
| * <p> | ||
| * An example is: | ||
| * | ||
| * <pre> | ||
| * 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"); | ||
| * } | ||
| * } | ||
| * </pre> | ||
| * <p> | ||
| * In order to re-use existing {@link Provider}s, you can refer to those like | ||
| * this: | ||
| * | ||
| * <pre> | ||
| * package some.example; | ||
| * | ||
| * import io.cucumber.picocontainer.CucumberPicoProvider; | ||
| * import some.other.namespace.SomeExistingProvider.class; | ||
| * | ||
| * @CucumberPicoProvider(providers = { SomeExistingProvider.class }) | ||
| * public class MyCucumberPicoProviders { | ||
| * } | ||
| * </pre> | ||
| * <p> | ||
| * Notes: | ||
| * <ul> | ||
| * <li>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}.</li> | ||
| * <li>If there is no {@link CucumberPicoProvider} annotation at all then | ||
| * (beside the basic preparation) no additional PicoContainer preparation will | ||
| * be done.</li> | ||
| * <li>Cucumber PicoContainer uses PicoContainer's {@link MutablePicoContainer} | ||
| * internally. Doing so, all {@link #providers() Providers} will be added by | ||
| * {@link MutablePicoContainer#addAdapter(org.picocontainer.ComponentAdapter) | ||
| * MutablePicoContainer#addAdapter(new ProviderAdapter(provider))}. (If any of | ||
| * the providers additionally extends | ||
| * {@link org.picocontainer.injectors.ProviderAdapter ProviderAdapter} then | ||
| * these will be added directly without being wrapped again.)</li> | ||
| * <li>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 ..."</li> | ||
| * </ul> | ||
| */ | ||
| @Retention(RetentionPolicy.RUNTIME) | ||
| @Target(ElementType.TYPE) | ||
| @API(status = API.Status.EXPERIMENTAL) | ||
| public @interface CucumberPicoProvider { | ||
|
|
||
| Class<? extends Provider>[] providers() default {}; | ||
|
|
||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<ClassLoader> classLoaderSupplier) { | ||
| this.container = container; | ||
| this.classFinder = new ClasspathScanner(classLoaderSupplier); | ||
| } | ||
|
|
||
| @Override | ||
| public void loadGlue(Glue glue, List<URI> gluePaths) { | ||
| gluePaths.stream() | ||
| .filter(gluePath -> CLASSPATH_SCHEME.equals(gluePath.getScheme())) | ||
| .map(ClasspathSupport::packageName) | ||
| .map(classFinder::scanForClassesInPackage) | ||
| .flatMap(Collection::stream) | ||
| .filter(clazz -> clazz.isAnnotationPresent(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; | ||
| } | ||
|
|
||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| package io.cucumber.picocontainer; | ||
|
|
||
| import io.cucumber.core.backend.Backend; | ||
| import io.cucumber.core.backend.BackendProviderService; | ||
| import io.cucumber.core.backend.Container; | ||
| import io.cucumber.core.backend.Lookup; | ||
|
|
||
| import java.util.function.Supplier; | ||
|
|
||
| public final class PicoBackendProviderService implements BackendProviderService { | ||
|
|
||
| @Override | ||
| public Backend create(Lookup lookup, Container container, Supplier<ClassLoader> classLoader) { | ||
| return new PicoBackend(container, classLoader); | ||
| } | ||
|
|
||
| } |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -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<Class<?>> providers = new HashSet<>(); | ||||||
| Set<Class<?>> 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" | ||||||
|
||||||
| "PicoContainer Provider classes. Please ensure that at least one the following requirements is satisfied:\n" | |
| "PicoContainer Provider classes. Please ensure that at least one the following requirements are satisfied:\n" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The text has changed even more. There are five requirements now, and all must be satisfied.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| io.cucumber.picocontainer.PicoBackendProviderService |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would make sense to split
@CucumberPicoProviderannotated and other glue classes here. It would clean up the logic instartand prevent constructor dependencies being added to the container..There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There are two collections now, one for the
Providerclasses, one for the "normal" component classes.