package ru.vyarus.dropwizard.guice; import com.google.common.base.Preconditions; import com.google.common.base.Stopwatch; import com.google.common.collect.Sets; import com.google.inject.Injector; import com.google.inject.Module; import com.google.inject.Stage; import io.dropwizard.Configuration; import io.dropwizard.ConfiguredBundle; import io.dropwizard.setup.Bootstrap; import io.dropwizard.setup.Environment; import ru.vyarus.dropwizard.guice.bundle.DefaultBundleLookup; import ru.vyarus.dropwizard.guice.bundle.GuiceyBundleLookup; import ru.vyarus.dropwizard.guice.bundle.lookup.VoidBundleLookup; import ru.vyarus.dropwizard.guice.injector.DefaultInjectorFactory; import ru.vyarus.dropwizard.guice.injector.InjectorFactory; import ru.vyarus.dropwizard.guice.injector.lookup.InjectorLookup; import ru.vyarus.dropwizard.guice.module.GuiceSupportModule; import ru.vyarus.dropwizard.guice.module.context.ConfigurationContext; import ru.vyarus.dropwizard.guice.module.context.debug.DiagnosticBundle; import ru.vyarus.dropwizard.guice.module.context.debug.report.diagnostic.DiagnosticConfig; import ru.vyarus.dropwizard.guice.module.context.debug.report.tree.ContextTreeConfig; import ru.vyarus.dropwizard.guice.module.context.option.Option; import ru.vyarus.dropwizard.guice.module.installer.CoreInstallersBundle; import ru.vyarus.dropwizard.guice.module.installer.FeatureInstaller; import ru.vyarus.dropwizard.guice.module.installer.WebInstallersBundle; import ru.vyarus.dropwizard.guice.module.installer.bundle.GuiceyBundle; import ru.vyarus.dropwizard.guice.module.installer.internal.CommandSupport; import ru.vyarus.dropwizard.guice.module.installer.scanner.ClasspathScanner; import ru.vyarus.dropwizard.guice.module.installer.util.BundleSupport; import ru.vyarus.dropwizard.guice.module.jersey.debug.HK2DebugBundle; import ru.vyarus.dropwizard.guice.module.support.BootstrapAwareModule; import ru.vyarus.dropwizard.guice.module.support.ConfigurationAwareModule; import ru.vyarus.dropwizard.guice.module.support.EnvironmentAwareModule; import javax.servlet.DispatcherType; import java.util.Arrays; import java.util.EnumSet; import java.util.Map; import static ru.vyarus.dropwizard.guice.GuiceyOptions.*; import static ru.vyarus.dropwizard.guice.module.context.stat.Stat.*; /** * Bundle enables guice integration for dropwizard. Guice context is configured in initialization phase, * but actual injector is created on run phase, This approach provides greater configuration options, because during * initialization configuration and environment objects are not available. Bootstrap, Environment and Configuration * objects will be available in guice context. But if you need them in module (for example to get * configuration parameters), implement one of *AwareModule interfaces (e.g. * {@link ru.vyarus.dropwizard.guice.module.support.ConfigurationAwareModule}). * <p> * You can use auto scan to automatically install features. To enable auto scan you must configure package (or * packages) to search in. To know all supported features look * {@link ru.vyarus.dropwizard.guice.module.installer.FeatureInstaller} implementations. Installers are * extendable mechanism: they are resolved by scanning classpath, so you can add new installers in your code and * classpath scanning will find them and activate. Also, installers could be disabled (for example, if you want to * replace existing installer, you will disable it in builder and implement your own - auto scan will find it and * activate). * <p> * Any class may be hidden from auto scanning with {@code @InvisibleForScanner} annotation. * <p> * Commands may use injection too, but only fields injection. You can register command manually and their fields * will be injected or you can activate auto scan for commands in builder (disabled by default). If auto scan * for commands enabled, they will be instantiated with default no-arg constructor. * <p> * Resources are registered using jersey integration module. GuiceFilter is registered for both contexts to provide * request and session scopes support. * <p> * Lifecycle: * <ul> * <li>Bundle configured</li> * <li>Bundle initialization started</li> * <li>If commands scan enabled, commands resolved from classpath and registered in Bootstrap</li> * <li>Bundle run started</li> * <li>If auto scan enabled, scan classpath for feature installers and perform one more scan with registered * installers to find extensions</li> * <li>Perform {@link GuiceyBundle} lookup with registered {@link GuiceyBundleLookup}</li> * <li>Guice injector created</li> * <li>Register all extensions found by installers</li> * <li>Perform injections for all registered environment commands (because only environment commands runs bundles)</li> * </ul> * <p> * Project was originally inspired by <a href="https://github.com/HubSpot/dropwizard-guice">dropwizard-guice</a> * project. And because of this, project name was changed to dropwizard-guicey. * * @param <T> configuration type * @author Vyacheslav Rusakov * @see ru.vyarus.dropwizard.guice.module.GuiceyConfigurationInfo for configuratio diagnostic * @since 31.08.2014 */ @SuppressWarnings({"PMD.ExcessiveImports", "PMD.TooManyMethods", "PMD.ExcessiveClassLength"}) public final class GuiceBundle<T extends Configuration> implements ConfiguredBundle<T> { private Injector injector; private final ConfigurationContext context = new ConfigurationContext(); private InjectorFactory injectorFactory = new DefaultInjectorFactory(); private GuiceyBundleLookup bundleLookup = new DefaultBundleLookup(); private Bootstrap bootstrap; private ClasspathScanner scanner; GuiceBundle() { // Bundle should be instantiated only from builder } @Override public void initialize(final Bootstrap bootstrap) { final Stopwatch timer = context.stat().timer(GuiceyTime); final String[] packages = context.option(ScanPackages); final boolean searchCommands = context.option(SearchCommands); final boolean scanEnabled = packages.length > 0; if (searchCommands) { Preconditions.checkState(scanEnabled, "Commands search could not be performed, because auto scan was not activated"); } // have to remember bootstrap in order to inject it into modules this.bootstrap = bootstrap; if (scanEnabled) { scanner = new ClasspathScanner(Sets.newHashSet(Arrays.asList(packages)), context.stat()); if (searchCommands) { CommandSupport.registerCommands(bootstrap, scanner, context); } } timer.stop(); } @Override public void run(final T configuration, final Environment environment) throws Exception { final Stopwatch timer = context.stat().timer(GuiceyTime); if (context.option(UseCoreInstallers)) { context.registerBundles(new CoreInstallersBundle()); } configureFromBundles(configuration, environment); context.registerModules(new GuiceSupportModule(scanner, context)); configureModules(configuration, environment); createInjector(environment); afterInjectorCreation(); timer.stop(); } /** * @return created injector instance or fail if injector not yet created */ public Injector getInjector() { return Preconditions.checkNotNull(injector, "Guice not initialized"); } /** * Apply configuration from registered bundles. If dropwizard bundles support is enabled, lookup them too. * * @param configuration configuration object * @param environment environment object */ private void configureFromBundles(final T configuration, final Environment environment) { final Stopwatch timer = context.stat().timer(BundleTime); final Stopwatch resolutionTimer = context.stat().timer(BundleResolutionTime); if (context.option(ConfigureFromDropwizardBundles)) { context.registerDwBundles(BundleSupport.findBundles(bootstrap, GuiceyBundle.class)); } context.registerLookupBundles(bundleLookup.lookup()); resolutionTimer.stop(); BundleSupport.processBundles(context, configuration, environment, bootstrap.getApplication()); timer.stop(); } /** * Post-process registered modules by injecting bootstrap, configuration and environment objects. * * @param configuration configuration object * @param environment environment object */ @SuppressWarnings("unchecked") private void configureModules(final T configuration, final Environment environment) { for (Module mod : context.getModules()) { if (mod instanceof BootstrapAwareModule) { ((BootstrapAwareModule) mod).setBootstrap(bootstrap); } if (mod instanceof ConfigurationAwareModule) { ((ConfigurationAwareModule<T>) mod).setConfiguration(configuration); } if (mod instanceof EnvironmentAwareModule) { ((EnvironmentAwareModule) mod).setEnvironment(environment); } } } private void createInjector(final Environment environment) { final Stopwatch timer = context.stat().timer(InjectorCreationTime); injector = injectorFactory.createInjector(context.option(InjectorStage), context.getModules()); // registering as managed to cleanup injector on application stop environment.lifecycle().manage( InjectorLookup.registerInjector(bootstrap.getApplication(), injector)); timer.stop(); } @SuppressWarnings("unchecked") private void afterInjectorCreation() { CommandSupport.initCommands(bootstrap.getCommands(), injector, context.stat()); if (scanner != null) { scanner.cleanup(); } } /** * @param <T> configuration type * @return builder instance to construct bundle */ public static <T extends Configuration> Builder<T> builder() { return new Builder<T>(); } /** * Builder encapsulates bundle configuration options. * * @param <T> configuration type */ public static class Builder<T extends Configuration> { private final GuiceBundle<T> bundle = new GuiceBundle<T>(); /** * Options is a generic mechanism to provide internal configuration values for guicey and 3rd party bundles. * See {@link GuiceyOptions} as options example. Bundles may define their own enums in the same way to * use options mechanism. * <p> * Options intended to be used for development time specific configurations (most likely * low level options to slightly change behaviour). Also, in contrast to internal booleans * (e.g. in main bundle), options are accessible everywhere and may be used by other 3rd party module or * simply for reporting. * <p> * Options may be set only on application level. Guicey bundles could access option values through * {@linkplain ru.vyarus.dropwizard.guice.module.installer.bundle.GuiceyBootstrap#option(Enum) * bootstrap object}. Guice service could access options through * {@linkplain ru.vyarus.dropwizard.guice.module.context.option.Options options bean}. Installers * could use {@link ru.vyarus.dropwizard.guice.module.installer.option.WithOptions} to get access to options. * Options metadata for reporting is available through * {@linkplain ru.vyarus.dropwizard.guice.module.context.option.OptionsInfo guice bean}. * <p> * Options may be used instead of shortcut methods when configuration value is dynamic (without options * it would not be impossible to configure without breaking builder flow and additional if statements). * <p> * Note: each option declares exact option type and provided value is checked for type compatibility. * * @param option option enum * @param value option value (not null) * @param <K> helper type for option signature definition * @return builder instance for chained calls * @throws NullPointerException is null value provided * @throws IllegalArgumentException if provided value incompatible with option type * @see Option for more details * @see GuiceyOptions * @see ru.vyarus.dropwizard.guice.module.installer.InstallersOptions */ public <K extends Enum & Option> Builder<T> option(final K option, final Object value) { bundle.context.setOption(option, value); return this; } /** * Sets multiple options at once. No options lookup mechanism provided out of the box (like for bundles), * but it's very easy to implement custom lookup solution (e.g. for providing specific options in tests). * This method would be useful for such lookup implementations. * <p> * Note: {@link Option} type is not mixed with enum in declaration to simplify usage, * but used enums must be correct options. * * @param options options map (not null) * @param <K> helper type for option signature definition * @return builder instance for chained calls * @throws NullPointerException is null value provided for any option * @throws IllegalArgumentException if any provided value incompatible with option type * @see #option(Enum, Object) for more info */ @SuppressWarnings("unchecked") public <K extends Enum & Option> Builder<T> options(final Map<Enum, Object> options) { ((Map<K, Object>) (Map) options).forEach(this::option); return this; } /** * Configures custom {@link InjectorFactory}. Required by some guice extensions like governator. * * @param injectorFactory custom guice injector factory * @return builder instance for chained calls */ public Builder<T> injectorFactory(final InjectorFactory injectorFactory) { bundle.injectorFactory = injectorFactory; return this; } /** * Configure custom {@link GuiceyBundleLookup}. Lookup provides an easy way to indirectly install * {@link GuiceyBundle} bundles. Default implementation support lookup by system property. * * @param bundleLookup custom bundle lookup implementation * @return builder instance for chained calls * @see DefaultBundleLookup */ public Builder<T> bundleLookup(final GuiceyBundleLookup bundleLookup) { bundle.bundleLookup = bundleLookup; return this; } /** * Disables default bundle lookup. * * @return builder instance for chained calls */ public Builder<T> disableBundleLookup() { return bundleLookup(new VoidBundleLookup()); } /** * Enables auto scan feature. * When enabled, all core installers are registered automatically. * * @param basePackages packages to scan extensions in * @return builder instance for chained calls * @see GuiceyOptions#ScanPackages */ public Builder<T> enableAutoConfig(final String... basePackages) { Preconditions.checkState(basePackages.length > 0, "Specify at least one package to scan"); return option(ScanPackages, basePackages); } /** * All registered modules must be of unique type (all modules registered). If two or more modules of the * same type registered, only first instance will be used. * <p> * NOTE: if module implements *AwareModule interfaces, objects will be set just before configuration start. * * @param modules one or more juice modules * @return builder instance for chained calls * @see ru.vyarus.dropwizard.guice.module.support.BootstrapAwareModule * @see ru.vyarus.dropwizard.guice.module.support.ConfigurationAwareModule * @see ru.vyarus.dropwizard.guice.module.support.EnvironmentAwareModule * @see ru.vyarus.dropwizard.guice.module.support.DropwizardAwareModule */ public Builder<T> modules(final Module... modules) { Preconditions.checkState(modules.length > 0, "Specify at least one module"); bundle.context.registerModules(modules); return this; } /** * NOTE: will not scan if auto scan not enabled (packages not configured * with {@link #enableAutoConfig(String...)}). * <p> * Enables commands classpath search. All found commands are instantiated and registered in * bootstrap. Default constructor is used for simple commands, but {@link io.dropwizard.cli.EnvironmentCommand} * must have constructor with {@link io.dropwizard.Application} argument. * <p> * By default, commands search is disabled. * * @return builder instance for chained calls * @see CommandSupport * @see GuiceyOptions#SearchCommands */ public Builder<T> searchCommands() { return option(SearchCommands, true); } /** * Disables automatic {@link CoreInstallersBundle} registration (no installers will be registered by default). * * @return builder instance for chained calls * @see GuiceyOptions#UseCoreInstallers */ public Builder<T> noDefaultInstallers() { return option(UseCoreInstallers, false); } /** * Shortcut to install {@link WebInstallersBundle}. Web installers are not available by default to * reduce default installers count. Web installers use default servlet api annotations to install * guice-aware servlets, filters and listeners. In many cases it will be more useful than using * guice servlet modules. * <p> * When use web installers to declare servlets and filters, guice servlet modules support may be disabled * with {@link #noGuiceFilter()}. This will safe some startup time and removed extra guice filters. * * @return builder instance for chained calls * @see WebInstallersBundle */ public Builder<T> useWebInstallers() { bundle.context.registerBundles(new WebInstallersBundle()); return this; } /** * Disables {@link com.google.inject.servlet.GuiceFilter} registration for both application and admin contexts. * Without guice filter registered, guice {@link com.google.inject.servlet.ServletModule} registrations * are useless (because they can't be used). So guice servlet modules support will be disabled: * no request or session scopes may be used and registrations of servlet modules will be denied * (binding already declared exception). Even with disabled guice filter, request and response * objects provider injections still may be used in resources (will work through hk provider). * <p> * Guice servlets initialization took ~50ms, so injector creation will be a bit faster after disabling. * <p> * Enable {@link #useWebInstallers()} web installers to use instead of guice servlet modules for servlets * and filters registration. * * @return builder instance for chained calls * @see GuiceyOptions#GuiceFilterRegistration */ public Builder<T> noGuiceFilter() { return option(GuiceFilterRegistration, EnumSet.noneOf(DispatcherType.class)); } /** * @param installers feature installer types to disable * @return builder instance for chained calls */ @SafeVarargs public final Builder<T> disableInstallers(final Class<? extends FeatureInstaller>... installers) { bundle.context.disableInstallers(installers); return this; } /** * Feature installers registered automatically when auto scan enabled, * but if you don't want to use it, you can register installers manually (note: without auto scan default * installers will not be registered). * <p>Also, could be used to add installers from packages not included in auto scanning.</p> * * @param installers feature installer classes to register * @return builder instance for chained calls */ @SafeVarargs public final Builder<T> installers(final Class<? extends FeatureInstaller>... installers) { bundle.context.registerInstallers(installers); return this; } /** * Beans could be registered automatically when auto scan enabled, * but if you don't want to use it, you can register beans manually. * <p>Guice injector will instantiate beans and registered installers will be used to recognize and * properly register provided extension beans.</p> * <p>Also, could be used to add beans from packages not included in auto scanning.</p> * <p>NOTE: startup will fail if bean not recognized by installers.</p> * <p>NOTE: Don't register commands here: either enable auto scan, which will install commands automatically * or register command directly to bootstrap object and dependencies will be injected to it after * injector creation.</p> * * @param extensionClasses extension bean classes to register * @return builder instance for chained calls */ public Builder<T> extensions(final Class<?>... extensionClasses) { bundle.context.registerExtensions(extensionClasses); return this; } /** * Guicey bundles are mainly useful for extensions (to group installers and extensions installation without * auto scan). Its very like dropwizard bundles. * <p> * It's also possible to use dropwizard bundles as guicey bundles: bundle must implement * {@link GuiceyBundle} and {@link #configureFromDropwizardBundles()} must be enabled * (disabled by default). This allows using dropwizard bundles as universal extension point. * <p> * Duplicate bundles are filtered automatically: bundles of the same type considered duplicate. * * @param bundles guicey bundles * @return builder instance for chained calls */ public Builder<T> bundles(final GuiceyBundle... bundles) { bundle.context.registerBundles(bundles); return this; } /** * Enables registered dropwizard bundles check if they implement {@link GuiceyBundle} and register them as * guicey bundles. This allows using dropwizard bundles as universal extension point. * <p> * Disabled by default. * * @return builder instance for chained calls * @see GuiceyOptions#ConfigureFromDropwizardBundles */ public Builder<T> configureFromDropwizardBundles() { return option(ConfigureFromDropwizardBundles, true); } /** * Enables binding of interfaces implemented by configuration class to configuration instance * in guice context. Only interfaces directly implemented by any configuration class in configuration * classes hierarchy. Interfaces from java.* and groovy.* packages are skipped. * This is useful to support {@code HasSomeConfiguration} interfaces convention. * <p> * When disabled, only classes in configuration hierarchy are registered (e.g. in case * {@code MyConfiguration extends MyBaseConfiguration extends Configuration}, all 3 classes would be bound. * <p> * Disabled by default. * * @return builder instance for chained calls * @see GuiceyOptions#BindConfigurationInterfaces */ public Builder<T> bindConfigurationInterfaces() { return option(BindConfigurationInterfaces, true); } /** * Enables strict control of beans instantiation context: all beans must be instantiated by guice, except * beans annotated with {@link ru.vyarus.dropwizard.guice.module.installer.feature.jersey.HK2Managed}. * When bean instantiated in wrong context exception would be thrown. * <p> * It is useful if you write your own installers or to simply ensure correctness in doubtful cases. * <p> * Do not use for production! It is intended to be used mostly in tests or to diagnose problems * during development. * <p> * To implicitly enable this check in all tests use * {@code PropertyBundleLookup.enableBundles(HK2DebugBundle.class)}. * * @return builder instance for chained calls * @see HK2DebugBundle */ public Builder<T> strictScopeControl() { bundle.context.registerBundles(new HK2DebugBundle()); return this; } /** * Print additional diagnostic logs with startup statistics, installed bundles, installers and resolved * extensions and configuration tree. * <p> * Statistics shows mainly where guice spent most time. Configuration info is * useful for configuration problems resolution. * Also, logs useful for better understanding how guicey works. * <p> * If custom logging format is required use {@link DiagnosticBundle} directly. * <p> * Bundle could be enabled indirectly with bundle lookup mechanism (e.g. with system property * {@code PropertyBundleLookup.enableBundles(DiagnosticBundle.class)}). * <p> * NOTE: Can't be used together with {@link #printAvailableInstallers()}. * * @return builder instance for chained calls * @see DiagnosticBundle */ public Builder<T> printDiagnosticInfo() { bundle.context.registerBundles(new DiagnosticBundle()); return this; } /** * Prints all registered (not disabled) installers with registration source. Useful to see all supported * extension types when multiple guicey bundles registered and available features become not obvious * from application class. * <p> * In contrast to {@link #printDiagnosticInfo()} shows all installers (including installers not used by * application extensions). Installers report intended only to show available installers and will not * show duplicate installers registrations or installers disabling (use diagnostic reporting for * all configuration aspects). * <p> * NOTE: Can't be used together with {@link #printDiagnosticInfo()}. Both serve different purposes: * available installers - to see what can be used and diagnostic info - to solve configuration problems * or better understand current configuration. * * @return builder instance for chained calls * @see DiagnosticBundle */ public Builder<T> printAvailableInstallers() { bundle.context.registerBundles( DiagnosticBundle.builder() .printConfiguration(new DiagnosticConfig() .printInstallers() .printNotUsedInstallers()) .printContextTree(new ContextTreeConfig() .hideCommands() .hideDuplicateRegistrations() .hideEmptyBundles() .hideExtensions() .hideModules()) .build()); return this; } /** * @param stage stage to run injector with * @return bundle instance * @see GuiceyOptions#InjectorStage */ public GuiceBundle<T> build(final Stage stage) { option(InjectorStage, stage); return build(); } /** * @return bundle instance with implicit PRODUCTION stage * @see GuiceyOptions#InjectorStage */ public GuiceBundle<T> build() { return bundle; } } }