diff --git a/.gitignore b/.gitignore index e0483b8..ca92ae7 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,4 @@ target/ .project .settings .idea -*.iml \ No newline at end of file +*.iml diff --git a/README.md b/README.md index dd2754b..534033b 100644 --- a/README.md +++ b/README.md @@ -7,60 +7,175 @@ the dropwizard environment upon service start. ### Usage +```xml + + + com.hubspot.dropwizard + dropwizard-guice + 0.7.0.2 + + +``` + Simply install a new instance of the bundle during your service initialization ```java -public class HelloWorldService extends Application { +public class HelloWorldApplication extends Application { + + private GuiceBundle guiceBundle; public static void main(String[] args) throws Exception { - new HelloWorldService().run(args); - } - - @Override - public String getName() { - return "hello-world"; - } - - @Override - public void initialize(Bootstrap bootstrap) { - bootstrap.addBundle(GuiceBundle.newBuilder() - .addModule(new HelloWorldModule()) - .build() - ); - } - - @Override - public void run(HelloWorldConfiguration configuration, final Environment environment) { - environment.jersey().register(HelloWorldResource.class); - environment.healthChecks().register("Template", TemplateHealthCheck.class); - } + new HelloWorldApplication().run(args); + } + + @Override + public void initialize(Bootstrap bootstrap) { + + guiceBundle = GuiceBundle.newBuilder() + .addModule(new HelloWorldModule()) + .setConfigClass(HelloWorldConfiguration.class) + .build(); + + bootstrap.addBundle(guiceBundle); + } + @Override + public String getName() { + return "hello-world"; + } + + @Override + public void run(HelloWorldConfiguration helloWorldConfiguration, Environment environment) throws Exception { + environment.jersey().register(HelloWorldResource.class); + environment.lifecycle().manage(guiceBundle.getInjector().getInstance(TemplateHealthCheck.class)); + } } ``` -Lastly, you can enable auto configuration via package scanning. +You can enable auto configuration via package scanning. +```java +public class HelloWorldApplication extends Application { + + public static void main(String[] args) throws Exception { + new HelloWorldApplication().run(args); + } + + @Override + public void initialize(Bootstrap bootstrap) { + + GuiceBundle guiceBundle = GuiceBundle.newBuilder() + .addModule(new HelloWorldModule()) + .enableAutoConfig(getClass().getPackage().getName()) + .setConfigClass(HelloWorldConfiguration.class) + .build(); + + bootstrap.addBundle(guiceBundle); + // with AutoConfig enabled you don't need to add bundles or commands explicitly here. + // inherit from one of InjectedCommand, InjectedConfiguredCommand, or InjectedEnvironmentCommand + // to get access to all modules during injection. + } + + @Override + public String getName() { + return "hello-world"; + } + + @Override + public void run(HelloWorldConfiguration helloWorldConfiguration, Environment environment) throws Exception { + // now you don't need to add resources, tasks, healthchecks or providers + // you must have your health checks inherit from InjectableHealthCheck in order for them to be injected + } +} + +Modules will also be injected before being added. Field injections only, constructor based injections will not be available. +Configuration data and initialization module data will be available for injecting into modules. ```java -public class HelloWorldService extends Service { + + +public class HelloWorldApplication extends Application { public static void main(String[] args) throws Exception { - new HelloWorldService().run(args); - } - - @Override - public void initialize(Bootstrap bootstrap) { - bootstrap.setName("hello-world"); - bootstrap.addBundle(GuiceBundle.newBuilder() - .addModule(new HelloWorldModule()) - .enableAutoConfig(getClass().getPackage().getName()) - .build() - ); - } - - @Override - public void run(HelloWorldConfiguration configuration, final Environment environment) { - // now you don't need to add resources, tasks, healthchecks or providers - // you must have your health checks inherit from InjectableHealthCheck in order for them to be injected - } + new HelloWorldApplication().run(args); + } + + @Override + public void initialize(Bootstrap bootstrap) { + + GuiceBundle guiceBundle = GuiceBundle.newBuilder() + .addInitModule(new BaseModule()) + // bindings defined in the BaseModule or any configuration data is available for + // injection into HelloWorldModule fields + .addModule(new HelloWorldModule()) + //Any resource, task, bundle, etc within this class path will be included automatically. + .enableAutoConfig(getClass().getPackage().getName()) + //The contents of any config objects within this package path will be auto-injected. + .addConfigPackages(getClass().getPackage().getName()) + .setConfigClass(HelloWorldConfiguration.class) + .build(); + + bootstrap.addBundle(guiceBundle); + } + + @Override + public String getName() { + return "hello-world"; + } + + @Override + public void run(HelloWorldConfiguration helloWorldConfiguration, Environment environment) throws Exception { + } +} +``` + +If you are having trouble accessing your Configuration or Environment inside a Guice Module, you could try using a provider. + +```java +public class HelloWorldModule extends AbstractModule { + + @Override + protected void configure() { + // anything you'd like to configure + } + + @Provides + public SomePool providesSomethingThatNeedsConfiguration(HelloWorldConfiguration configuration) { + return new SomePool(configuration.getPoolName()); + } + + @Provides + public SomeManager providesSomenthingThatNeedsEnvironment(Environment env) { + return new SomeManager(env.getSomethingFromHere())); + } +} +``` + +You can also replace the default Guice `Injector` by implementing your own `InjectorFactory`. For example if you want +to use [Governator](https://github.com/Netflix/governator) you can create the following implementation: + +```java +public class GovernatorInjectorFactory implements InjectorFactory { + + @Override + public Injector create( final Stage stage, final List modules ) { + return LifecycleInjector.builder().inStage( stage ).withModules( modules ).build() + .createInjector(); + } +} +``` + +and then set the InjectorFactory when initializing the GuiceBundle: + +```java +@Override +public void initialize(Bootstrap bootstrap) { + + GuiceBundle guiceBundle = GuiceBundle.newBuilder() + .addModule(new HelloWorldModule()) + .enableAutoConfig(getClass().getPackage().getName()) + .setConfigClass(HelloWorldConfiguration.class) + .setInjectorFactory( new GovernatorInjectorFactory() ) + .build(); + bootstrap.addBundle(guiceBundle); } ``` diff --git a/pom.xml b/pom.xml index f12ae5a..9733521 100644 --- a/pom.xml +++ b/pom.xml @@ -1,13 +1,12 @@ - - 4.0.0 + + 4.0.0 - - org.sonatype.oss - oss-parent - 7 - + + org.sonatype.oss + oss-parent + 7 + @@ -17,63 +16,118 @@ - com.hubspot.dropwizard - dropwizard-guice - 0.7.0-sskrla-SNAPSHOT - Dropwizard Guice Support - Simple library for using Guice DI in a dropwizard service. - https://github.com/HubSpot/dropwizard-guice + com.hubspot.dropwizard + dropwizard-guice + 0.8.1-SNAPSHOT + Dropwizard Guice Support + Simple library for using Guice DI in a dropwizard service. + https://github.com/HubSpot/dropwizard-guice - - - The Apache Software License, Version 2.0 - http://www.apache.org/licenses/LICENSE-2.0.txt - repo - - + + + The Apache Software License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0.txt + repo + + - - scm:git:git@github.com:HubSpot/dropwizard-guice.git - scm:git:git@github.com:HubSpot/dropwizard-guice.git - git@github.com:HubSpot/dropwizard-guice.git - + + scm:git:git@github.com:HubSpot/dropwizard-guice.git + scm:git:git@github.com:HubSpot/dropwizard-guice.git + git@github.com:HubSpot/dropwizard-guice.git + - - - eliast - Elias Torres - elias@hubspot.com - - + + + eliast + Elias Torres + elias@hubspot.com + + - - - io.dropwizard - dropwizard-core - 0.7.0-rc1 - - - com.google.inject.extensions - guice-servlet - 3.0 - - - com.sun.jersey.contribs - jersey-guice - 1.17.1 - - - org.reflections - reflections - 0.9.8 - - - com.google.guava - guava - - - - + + 0.8.0-rc3 + + + + + + com.google.code.findbugs + annotations + 3.0.0 + + + io.dropwizard + dropwizard-core + ${io.dropwizard.version} + + + com.google.code.findbugs + jsr305 + + + + + com.google.inject + guice + 4.0-beta5 + + + com.google.guava + guava + + + + + com.google.inject.extensions + guice-servlet + 4.0-beta5 + + + com.squarespace.jersey2-guice + jersey2-guice + 0.5 + + + org.glassfish.jersey.containers + jersey-container-servlet-core + + + org.glassfish.jersey.containers + jersey-container-servlet + + + + + org.reflections + reflections + 0.9.9 + + + com.google.guava + guava + + + + + com.jayway.restassured + rest-assured + 2.4.0 + test + + + io.dropwizard + dropwizard-testing + ${io.dropwizard.version} + test + + + io.dropwizard + dropwizard-client + ${io.dropwizard.version} + test + + @@ -83,12 +137,11 @@ 2.5 - 1.6 - 1.6 + 1.7 + 1.7 - org.apache.maven.plugins diff --git a/src/main/java/com/hubspot/dropwizard/guice/AutoConfig.java b/src/main/java/com/hubspot/dropwizard/guice/AutoConfig.java index 87a2d16..03b57ea 100644 --- a/src/main/java/com/hubspot/dropwizard/guice/AutoConfig.java +++ b/src/main/java/com/hubspot/dropwizard/guice/AutoConfig.java @@ -1,13 +1,21 @@ package com.hubspot.dropwizard.guice; +import com.google.inject.ConfigurationException; +import com.google.common.base.Function; +import com.google.common.collect.Collections2; import io.dropwizard.Bundle; +import io.dropwizard.ConfiguredBundle; +import io.dropwizard.cli.Command; +import io.dropwizard.cli.ConfiguredCommand; +import io.dropwizard.cli.EnvironmentCommand; import io.dropwizard.lifecycle.Managed; import io.dropwizard.servlets.tasks.Task; import io.dropwizard.setup.Bootstrap; import io.dropwizard.setup.Environment; + import com.google.common.base.Preconditions; import com.google.inject.Injector; -import com.sun.jersey.spi.inject.InjectableProvider; +import org.glassfish.jersey.server.model.Resource; import org.reflections.Reflections; import org.reflections.scanners.SubTypesScanner; import org.reflections.scanners.TypeAnnotationsScanner; @@ -18,7 +26,9 @@ import org.slf4j.LoggerFactory; import javax.ws.rs.Path; +import javax.ws.rs.ext.ParamConverterProvider; import javax.ws.rs.ext.Provider; +import java.util.Collection; import java.util.Set; public class AutoConfig { @@ -44,23 +54,28 @@ public AutoConfig(String... basePackages) { public void run(Environment environment, Injector injector) { addHealthChecks(environment, injector); - addProviders(environment, injector); - addInjectableProviders(environment, injector); - addResources(environment, injector); + addProviders(environment); + addResources(environment); addTasks(environment, injector); addManaged(environment, injector); + addParamConverterProviders(environment); } public void initialize(Bootstrap bootstrap, Injector injector) { addBundles(bootstrap, injector); + addCommands(bootstrap, injector); } private void addManaged(Environment environment, Injector injector) { Set> managedClasses = reflections .getSubTypesOf(Managed.class); for (Class managed : managedClasses) { - environment.lifecycle().manage(injector.getInstance(managed)); - logger.info("Added managed: {}", managed); + try { + environment.lifecycle().manage(injector.getInstance(managed)); + logger.info("Added managed: {}", managed); + } catch (ConfigurationException e) { + logger.warn("Could not get instance of managed: {}", managed); + } } } @@ -68,8 +83,12 @@ private void addTasks(Environment environment, Injector injector) { Set> taskClasses = reflections .getSubTypesOf(Task.class); for (Class task : taskClasses) { - environment.admin().addTask(injector.getInstance(task)); - logger.info("Added task: {}", task); + try { + environment.admin().addTask(injector.getInstance(task)); + logger.info("Added task: {}", task); + } catch (ConfigurationException e) { + logger.warn("Could not get instance of task: {}", task); + } } } @@ -77,24 +96,17 @@ private void addHealthChecks(Environment environment, Injector injector) { Set> healthCheckClasses = reflections .getSubTypesOf(InjectableHealthCheck.class); for (Class healthCheck : healthCheckClasses) { - InjectableHealthCheck instance = injector.getInstance(healthCheck); - environment.healthChecks().register(instance.getName(), instance); - logger.info("Added injectableHealthCheck: {}", healthCheck); - } - } - - @SuppressWarnings("rawtypes") - private void addInjectableProviders(Environment environment, - Injector injector) { - Set> injectableProviders = reflections - .getSubTypesOf(InjectableProvider.class); - for (Class injectableProvider : injectableProviders) { - environment.jersey().register(injectableProvider); - logger.info("Added injectableProvider: {}", injectableProvider); + try { + InjectableHealthCheck instance = injector.getInstance(healthCheck); + environment.healthChecks().register(instance.getName(), instance); + logger.info("Added injectableHealthCheck: {}", healthCheck); + } catch (ConfigurationException e) { + logger.warn("Could not get instance of InjectableHealthCheck: {}", healthCheck); + } } } - private void addProviders(Environment environment, Injector injector) { + private void addProviders(Environment environment) { Set> providerClasses = reflections .getTypesAnnotatedWith(Provider.class); for (Class provider : providerClasses) { @@ -103,21 +115,80 @@ private void addProviders(Environment environment, Injector injector) { } } - private void addResources(Environment environment, Injector injector) { + private void addResources(Environment environment) { Set> resourceClasses = reflections .getTypesAnnotatedWith(Path.class); for (Class resource : resourceClasses) { - environment.jersey().register(resource); - logger.info("Added resource class: {}", resource); + if(Resource.isAcceptable(resource)) { + environment.jersey().register(resource); + logger.info("Added resource class: {}", resource); + } } } + @SuppressWarnings("rawtypes") private void addBundles(Bootstrap bootstrap, Injector injector) { Set> bundleClasses = reflections .getSubTypesOf(Bundle.class); for (Class bundle : bundleClasses) { - bootstrap.addBundle(injector.getInstance(bundle)); - logger.info("Added bundle class {} during bootstrap", bundle); + try { + bootstrap.addBundle(injector.getInstance(bundle)); + logger.info("Added bundle class {} during bootstrap", bundle); + } catch (ConfigurationException e) { + logger.warn("Could not get instance of bundle: {}", bundle); + } + } + Set> configuredBundleClasses = reflections.getSubTypesOf(ConfiguredBundle.class); + for(Class bundle : configuredBundleClasses) + { + if(!bundle.equals(GuiceBundle.class)) + { + try { + bootstrap.addBundle(injector.getInstance(bundle)); + logger.info("Added configured bundle class {} during bootstrap", bundle); + } catch (ConfigurationException e) { + logger.warn("Could not get instance of configured bundle: {}", bundle); + } + } + } + } + + private void addCommands(Bootstrap bootstrap, Injector injector) { + Collection existingCommands = Collections2.transform(bootstrap.getCommands(), + new Function() { + @Override + public Class apply(Command input) { + return input.getClass(); + } + }); + + Set> commandClasses = reflections.getSubTypesOf(Command.class); + //The SubTypesScanner does not resolve the entire ancestry of a class + //This won't get subtyped Commands. If this becomes a problem, a + //replacement Scanner could be written. It is getting a bit ridiculous + //with all the Injected commands as well. + commandClasses.addAll(reflections.getSubTypesOf(ConfiguredCommand.class)); + commandClasses.addAll(reflections.getSubTypesOf(EnvironmentCommand.class)); + commandClasses.addAll(reflections.getSubTypesOf(InjectedCommand.class)); + commandClasses.addAll(reflections.getSubTypesOf(InjectedConfiguredCommand.class)); + commandClasses.addAll(reflections.getSubTypesOf(InjectedEnvironmentCommand.class)); + for(Class command : commandClasses) { + if(existingCommands.contains(command)) continue; + try { + bootstrap.addCommand(injector.getInstance(command)); + logger.info("Added command class {} during bootstrap", command); + } catch (ConfigurationException e) { + logger.warn("Could not get instance of command: {}", command); + } + } + } + + private void addParamConverterProviders(Environment environment) { + Set> providerClasses = reflections + .getSubTypesOf(ParamConverterProvider.class); + for (Class provider : providerClasses) { + environment.jersey().register(provider); + logger.info("Added ParamConverterProvider class: {}", provider); } } } diff --git a/src/main/java/com/hubspot/dropwizard/guice/DropwizardEnvironmentModule.java b/src/main/java/com/hubspot/dropwizard/guice/DropwizardEnvironmentModule.java index 46f3699..4e9df22 100644 --- a/src/main/java/com/hubspot/dropwizard/guice/DropwizardEnvironmentModule.java +++ b/src/main/java/com/hubspot/dropwizard/guice/DropwizardEnvironmentModule.java @@ -1,30 +1,22 @@ package com.hubspot.dropwizard.guice; -import com.google.common.base.Joiner; -import com.google.common.base.Throwables; -import com.google.common.collect.Lists; +import com.google.common.base.Optional; import com.google.inject.*; import com.google.inject.name.Names; import io.dropwizard.Configuration; import io.dropwizard.jetty.MutableServletContextHandler; +import io.dropwizard.setup.Bootstrap; import io.dropwizard.setup.Environment; -import org.apache.commons.lang.ClassUtils; -import org.apache.commons.lang.reflect.FieldUtils; +import net.sourceforge.argparse4j.inf.Namespace; import javax.servlet.ServletContext; -import java.lang.reflect.Field; -import java.lang.reflect.Modifier; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; - -import static com.google.common.base.Throwables.propagate; -import static java.lang.String.format; public class DropwizardEnvironmentModule extends AbstractModule { - private static final String ILLEGAL_DROPWIZARD_MODULE_STATE = "The dropwizard environment has not yet been set. This is likely caused by trying to access the dropwizard environment during the bootstrap phase."; - private T configuration; - private Environment environment; + private static final String ILLEGAL_DROPWIZARD_MODULE_STATE = "The dropwizard environment has not been set. This is likely caused by trying to access the dropwizard environment during the bootstrap phase or during a non-configured command."; + private Optional configuration; + private Optional environment; + private Optional namespace = Optional.absent(); + private Optional> bootstrap; private Class configurationClass; public DropwizardEnvironmentModule(Class configurationClass) { @@ -34,14 +26,15 @@ public DropwizardEnvironmentModule(Class configurationClass) { @Override protected void configure() { Provider provider = new CustomConfigurationProvider(); - bind(configurationClass).toProvider(provider); - if (configurationClass != Configuration.class) { - bind(Configuration.class).toProvider(provider); - } - - bindConfigs(configurationClass, new String[]{}, Lists.>newArrayList()); - - bindContext("application", environment.getApplicationContext()); + if(configuration.isPresent()){ + bind(configurationClass).toProvider(provider); + if (configurationClass != Configuration.class) { + bind(Configuration.class).toProvider(provider); + } + } + if(environment.isPresent()) { + bindContext("application", environment.get().getApplicationContext()); + } } /** @@ -54,102 +47,57 @@ private void bindContext(String name, MutableServletContextHandler context) { .toInstance(context.getServletContext()); } - private void bindConfigs(Class config, String[] path, List> visited) { - List> classes = Lists.newArrayList(ClassUtils.getAllSuperclasses(config)); - classes.add(config); - for(Class cls: classes) { - for(Field field: cls.getDeclaredFields()) { - Class type = field.getType(); - if(isConfigObject(type) && !visited.contains(type)) { - visited.add(type); - - String[] subpath = new String[path.length + 1]; - System.arraycopy(path, 0, subpath, 0, path.length); - subpath[path.length] = field.getName(); - - bind(type) - .annotatedWith(Names.named(Joiner.on(".").join(subpath))) - .toProvider(new ConfigElementProvider(configurationClass, subpath)); + @Deprecated + public void setEnvironmentData(T configuration, Environment environment) { + setEnvironmentData(null, environment, configuration); + } - if(!type.isEnum()) - bindConfigs(type, subpath, visited); - } - } - } + public void setEnvironmentData(Bootstrap bootstrap, + Environment environment, + T configuration) { + this.bootstrap = Optional.fromNullable(bootstrap); + this.configuration = Optional.fromNullable(configuration); + this.environment = Optional.fromNullable(environment); } - protected boolean isConfigObject(Class type) { - return !type.isPrimitive() - && !type.getName().startsWith("java."); + public void setNamespace(Namespace namespace) { + this.namespace = Optional.fromNullable(namespace); } - public void setEnvironmentData(T configuration, Environment environment) { - this.configuration = configuration; - this.environment = environment; - } - @Provides public Environment providesEnvironment() { - if (environment == null) { + if (environment == null || !environment.isPresent()) { throw new ProvisionException(ILLEGAL_DROPWIZARD_MODULE_STATE); } - return environment; + return environment.get(); } + @Provides + public Namespace providesNamespace() { + if (namespace == null || !namespace.isPresent()) { + throw new ProvisionException(ILLEGAL_DROPWIZARD_MODULE_STATE); + } + return namespace.get(); + } + + /** + * Note: This is a raw type. Guice cannot inject the full type due to type erasure + */ + @Provides + public Bootstrap providesBootstrap() { + if (bootstrap == null || !bootstrap.isPresent()) { + throw new ProvisionException(ILLEGAL_DROPWIZARD_MODULE_STATE); + } + return bootstrap.get(); + } + private class CustomConfigurationProvider implements Provider { @Override public T get() { - if (configuration == null) { + if (configuration == null || !configuration.isPresent()) { throw new ProvisionException(ILLEGAL_DROPWIZARD_MODULE_STATE); } - return configuration; + return configuration.get(); } } - - private class ConfigElementProvider implements Provider { - private final Field[] path; - - public ConfigElementProvider(Class configCls, String[] path) { - this.path = new Field[path.length]; - - Class cls = configCls; - for(int i=0; i cls, String name) { - Field f = null; - Class search = cls; - do { - f = FieldUtils.getDeclaredField(search, name, true); - if(f != null) - return f; - else - search = search.getSuperclass(); - - } while(!search.equals(Object.class)); - - throw new IllegalStateException(format("Unable to find field %s on %s", name, cls.getName())); - } - - @Override - public U get() { - Object obj = configuration; - for(Field field: path) { - try { - obj = field.get(obj); - if (obj == null) { - return null; // Should cause an injection exception - } - - } catch(IllegalAccessException e) { - throw propagate(e); - } - } - - return (U) obj; - } - } } diff --git a/src/main/java/com/hubspot/dropwizard/guice/GuiceBundle.java b/src/main/java/com/hubspot/dropwizard/guice/GuiceBundle.java index 2264063..d0f0f9c 100644 --- a/src/main/java/com/hubspot/dropwizard/guice/GuiceBundle.java +++ b/src/main/java/com/hubspot/dropwizard/guice/GuiceBundle.java @@ -1,26 +1,26 @@ package com.hubspot.dropwizard.guice; +import java.util.List; + +import com.google.inject.*; +import io.dropwizard.setup.Bootstrap; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import com.google.common.base.Function; import com.google.common.base.Optional; import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableList; import com.google.common.collect.Lists; -import com.google.inject.Guice; -import com.google.inject.Injector; -import com.google.inject.Module; -import com.google.inject.Stage; -import com.google.inject.servlet.GuiceFilter; -import com.sun.jersey.api.core.ResourceConfig; -import com.sun.jersey.spi.container.servlet.ServletContainer; + import io.dropwizard.Configuration; import io.dropwizard.ConfiguredBundle; -import io.dropwizard.setup.Bootstrap; +import io.dropwizard.cli.Command; import io.dropwizard.setup.Environment; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import javax.annotation.Nullable; +import net.sourceforge.argparse4j.inf.Namespace; import javax.servlet.ServletContextListener; -import java.util.List; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; public class GuiceBundle implements ConfiguredBundle { @@ -28,26 +28,49 @@ public class GuiceBundle implements ConfiguredBundle private final AutoConfig autoConfig; private final List modules; + private final List initModules; private final List> contextListenerGenerators; + private final InjectorFactory injectorFactory; + private Injector initInjector; - private Injector injector; + private Injector finalInjector; private DropwizardEnvironmentModule dropwizardEnvironmentModule; private Optional> configurationClass; private Stage stage; - public static class Builder { private AutoConfig autoConfig; + private List initModules = Lists.newArrayList(); private List modules = Lists.newArrayList(); private List> contextListenerGenerators = Lists.newArrayList(); - private Optional> configurationClass = Optional.>absent(); - + private Optional> configurationClass = Optional.absent(); + private InjectorFactory injectorFactory = new InjectorFactoryImpl(); + + /** + * Add a module to the bundle. + * Module may be injected with configuration and environment data. + * This module will NOT be available for other Bundles and Commands initialized with AutoConfig. + * Modules will also NOT be available when running classic Command and ConfiguredCommands. + * They will be available when using InjectedConfiguredCommand, however. + */ public Builder addModule(Module module) { Preconditions.checkNotNull(module); modules.add(module); return this; } + /** + * Add a module to the bundle. + * Module will not be injected itself. + * This module will be available for other Bundles and Commands + * initialized with AutoConfig. + */ + public Builder addInitModule(Module module) { + Preconditions.checkNotNull(module); + initModules.add(module); + return this; + } + public Builder addServletContextListener(Function contextListenerGenerator) { Preconditions.checkNotNull(contextListenerGenerator); contextListenerGenerators.add(contextListenerGenerator); @@ -58,6 +81,12 @@ public Builder setConfigClass(Class clazz) { configurationClass = Optional.of(clazz); return this; } + + public Builder setInjectorFactory(InjectorFactory factory) { + Preconditions.checkNotNull(factory); + injectorFactory = factory; + return this; + } public Builder enableAutoConfig(String... basePackages) { Preconditions.checkNotNull(basePackages.length > 0); @@ -71,87 +100,130 @@ public GuiceBundle build() { } public GuiceBundle build(Stage s) { - return new GuiceBundle(s, autoConfig, modules, contextListenerGenerators, configurationClass); + return new GuiceBundle<>(s, autoConfig, modules, initModules, contextListenerGenerators, injectorFactory, + configurationClass); } } - + public static Builder newBuilder() { - return new Builder(); + return new Builder<>(); } private GuiceBundle(Stage stage, AutoConfig autoConfig, List modules, + List initModules, List> contextListenerGenerators, + InjectorFactory injectorFactory, Optional> configurationClass) { Preconditions.checkNotNull(modules); Preconditions.checkArgument(!modules.isEmpty()); Preconditions.checkNotNull(contextListenerGenerators); Preconditions.checkNotNull(stage); this.modules = modules; + this.initModules = initModules; this.contextListenerGenerators = contextListenerGenerators; this.autoConfig = autoConfig; this.configurationClass = configurationClass; + this.injectorFactory = injectorFactory; this.stage = stage; } @Override public void initialize(Bootstrap bootstrap) { - initInjector = Guice.createInjector(this.stage); + initInjector(); if (autoConfig != null) { - autoConfig.initialize(bootstrap, injector); + autoConfig.initialize(bootstrap, initInjector); + } + + setupCommands(bootstrap.getCommands()); + } + + @SuppressWarnings("unchecked") + private void setupCommands(Collection commands) { + for(Command c : commands) { + if(c instanceof GuiceCommand) { + ((GuiceCommand) c).setInit(this); + } } } + private void initInjector() { + try { + initInjector = injectorFactory.create(this.stage, ImmutableList.copyOf(this.initModules)); + } catch(Exception ie) { + logger.error("Exception occurred when creating Guice Injector - exiting", ie); + System.exit(1); + } + } + @Override public void run(final T configuration, final Environment environment) { - final GuiceContainer container = initGuice(configuration, environment); - - environment.jersey().replace(new Function() { - @Nullable - @Override - public ServletContainer apply(ResourceConfig resourceConfig) { - return container; + run(null, environment, configuration); + } + void run(Bootstrap bootstrap, Environment environment, final T configuration) { + initEnvironmentModule(); + setEnvironment(bootstrap, environment, configuration); + //The secondary injected modules generally use config data. If we are starting up a command + //that doesn't have a configuration, loading these modules is useless at best. + boolean addModules = configuration != null; + initGuice(environment, addModules); + Injector injector = getInjector().get(); + + if(environment != null) { + JerseyUtil.registerGuiceBound(injector, environment.jersey()); + JerseyUtil.registerGuiceFilter(environment); + + for (Function generator : contextListenerGenerators) { + environment.servlets().addServletListeners(generator.apply(injector)); } - }); - environment.servlets().addFilter("Guice Filter", GuiceFilter.class) - .addMappingForUrlPatterns(null, false, environment.getApplicationContext().getContextPath() + "*"); - for (Function generator : contextListenerGenerators) { - environment.servlets().addServletListeners(generator.apply(injector)); + if (autoConfig != null) { + autoConfig.run(environment, injector); + } } + } - if (autoConfig != null) { - autoConfig.run(environment, injector); - } + @SuppressWarnings("unchecked") + private void setEnvironment(Bootstrap bootstrap, final Environment environment, final T configuration) { + dropwizardEnvironmentModule.setEnvironmentData(bootstrap, environment, configuration); } - private GuiceContainer initGuice(final T configuration, final Environment environment) { - GuiceContainer container = new GuiceContainer(); - container.setResourceConfig(environment.jersey().getResourceConfig()); - JerseyContainerModule jerseyContainerModule = new JerseyContainerModule(container); + void setNamespace(Namespace namespace) { + dropwizardEnvironmentModule.setNamespace(namespace); + } + + private void initEnvironmentModule() { if (configurationClass.isPresent()) { - dropwizardEnvironmentModule = new DropwizardEnvironmentModule(configurationClass.get()); + dropwizardEnvironmentModule = new DropwizardEnvironmentModule<>(configurationClass.get()); } else { - dropwizardEnvironmentModule = new DropwizardEnvironmentModule(Configuration.class); + dropwizardEnvironmentModule = new DropwizardEnvironmentModule<>(Configuration.class); } - dropwizardEnvironmentModule.setEnvironmentData(configuration, environment); - - Injector moduleInjector = initInjector.createChildInjector(dropwizardEnvironmentModule); + } - for(Module module: modules) - moduleInjector.injectMembers(module); + private void initGuice(final Environment environment, boolean addModules) { + Injector environmentInjector = initInjector.createChildInjector(dropwizardEnvironmentModule); - modules.add(jerseyContainerModule); - injector = moduleInjector.createChildInjector(modules); - injector.injectMembers(container); + if(addModules) { + for (Module module : modules) + environmentInjector.injectMembers(module); - return container; + if (environment != null) modules.add(new JerseyModule()); + finalInjector = environmentInjector.createChildInjector(ImmutableList.copyOf(modules)); + } + else finalInjector = environmentInjector; } - public Injector getInjector() { - return injector; + public Provider getInjector() { + //With double injection, it is not safe to simply provide the finalInjector as the correct + //instance will change over time. + return new Provider() { + @Override + public Injector get() { + return (finalInjector != null) ? finalInjector : initInjector; + } + }; } } diff --git a/src/main/java/com/hubspot/dropwizard/guice/GuiceCommand.java b/src/main/java/com/hubspot/dropwizard/guice/GuiceCommand.java new file mode 100644 index 0000000..81d832c --- /dev/null +++ b/src/main/java/com/hubspot/dropwizard/guice/GuiceCommand.java @@ -0,0 +1,7 @@ +package com.hubspot.dropwizard.guice; + +import io.dropwizard.Configuration; + +interface GuiceCommand { + void setInit(GuiceBundle init); +} diff --git a/src/main/java/com/hubspot/dropwizard/guice/GuiceContainer.java b/src/main/java/com/hubspot/dropwizard/guice/GuiceContainer.java deleted file mode 100644 index f923dcc..0000000 --- a/src/main/java/com/hubspot/dropwizard/guice/GuiceContainer.java +++ /dev/null @@ -1,76 +0,0 @@ -package com.hubspot.dropwizard.guice; - -import java.util.Map; - -import javax.servlet.ServletException; -import javax.ws.rs.core.Application; - -import com.google.inject.Inject; -import com.google.inject.Injector; -import com.google.inject.Scope; -import com.google.inject.Singleton; -import com.google.inject.servlet.ServletScopes; -import com.sun.jersey.api.core.DefaultResourceConfig; -import com.sun.jersey.api.core.ResourceConfig; -import com.sun.jersey.core.spi.component.ComponentScope; -import com.sun.jersey.guice.spi.container.GuiceComponentProviderFactory; -import com.sun.jersey.spi.container.WebApplication; -import com.sun.jersey.spi.container.servlet.ServletContainer; -import com.sun.jersey.spi.container.servlet.WebConfig; - -@Singleton -public class GuiceContainer extends ServletContainer { - - private static final long serialVersionUID = 1931878850157940335L; - - @Inject - private Injector injector; - - private WebApplication webapp; - - private ResourceConfig resourceConfig = new DefaultResourceConfig(); - - public class ServletGuiceComponentProviderFactory extends GuiceComponentProviderFactory { - public ServletGuiceComponentProviderFactory(ResourceConfig config, Injector injector) { - super(config, injector); - } - - @Override - public Map createScopeMap() { - Map m = super.createScopeMap(); - - m.put(ServletScopes.REQUEST, ComponentScope.PerRequest); - return m; - } - } - - public GuiceContainer() { - } - - public GuiceContainer(Application app) { - super(app); - } - - public GuiceContainer(Class app) { - super(app); - } - - public void setResourceConfig(ResourceConfig resourceConfig) { - this.resourceConfig = resourceConfig; - } - - @Override - protected ResourceConfig getDefaultResourceConfig(Map props, WebConfig webConfig) throws ServletException { - return resourceConfig; - } - - @Override - protected void initiate(ResourceConfig config, WebApplication webapp) { - this.webapp = webapp; - webapp.initiate(config, new ServletGuiceComponentProviderFactory(config, injector)); - } - - public WebApplication getWebApplication() { - return webapp; - } -} \ No newline at end of file diff --git a/src/main/java/com/hubspot/dropwizard/guice/HK2Linker.java b/src/main/java/com/hubspot/dropwizard/guice/HK2Linker.java new file mode 100644 index 0000000..59da6f7 --- /dev/null +++ b/src/main/java/com/hubspot/dropwizard/guice/HK2Linker.java @@ -0,0 +1,22 @@ +package com.hubspot.dropwizard.guice; + +import com.google.inject.Injector; +import com.squarespace.jersey2.guice.BootstrapUtils; +import org.glassfish.hk2.api.ServiceLocator; +import javax.inject.Inject; + +//Inspired by gwizard-jersey - https://github.com/stickfigure/gwizard +/** + * Binding this as an eager singleton provides the second step of linking Guice back into HK2. + * (the first step was to install the HK2 BootstrapModule in the Guice module). + * + * This needs to happen before anything else related to Jersey starts. + */ +public class HK2Linker { + @Inject + public HK2Linker(Injector injector, ServiceLocator locator) { + BootstrapUtils.link(locator, injector); + BootstrapUtils.install(locator); + } + +} \ No newline at end of file diff --git a/src/main/java/com/hubspot/dropwizard/guice/InjectedCommand.java b/src/main/java/com/hubspot/dropwizard/guice/InjectedCommand.java new file mode 100644 index 0000000..cf584f1 --- /dev/null +++ b/src/main/java/com/hubspot/dropwizard/guice/InjectedCommand.java @@ -0,0 +1,35 @@ +package com.hubspot.dropwizard.guice; + +import io.dropwizard.Configuration; +import io.dropwizard.cli.Command; +import io.dropwizard.setup.Bootstrap; +import net.sourceforge.argparse4j.inf.Namespace; + +/** + * Must be used in conjunction with the GuiceBundle. + * The method annotated with {@link Run} will be injected and run when this command is called. + * The {@link Bootstrap}, and {@link Namespace} will be available for injection. + */ +public abstract class InjectedCommand extends Command implements GuiceCommand { + //I can't figure out how to get the GuiceBundle to work correctly + //without defining a T, which should not be necessary. + private GuiceBundle init; + + protected InjectedCommand(String name, String description) { + super(name, description); + } + + @Override + public void setInit(GuiceBundle init) { + this.init = init; + } + + @Override + final public void run(Bootstrap bootstrap, Namespace namespace) throws Exception { + if(init == null) throw new IllegalStateException("Injected Command run without a GuiceBundle. Was the application initialized correctly?"); + + init.run((Bootstrap)bootstrap, null, null); + init.setNamespace(namespace); + Utils.runRunnable(this, init.getInjector().get()); + } +} \ No newline at end of file diff --git a/src/main/java/com/hubspot/dropwizard/guice/InjectedConfiguredCommand.java b/src/main/java/com/hubspot/dropwizard/guice/InjectedConfiguredCommand.java new file mode 100644 index 0000000..3532ae2 --- /dev/null +++ b/src/main/java/com/hubspot/dropwizard/guice/InjectedConfiguredCommand.java @@ -0,0 +1,37 @@ +package com.hubspot.dropwizard.guice; + +import io.dropwizard.Configuration; +import io.dropwizard.cli.ConfiguredCommand; +import io.dropwizard.setup.Bootstrap; +import net.sourceforge.argparse4j.inf.Namespace; + +/** + * Must be used in conjunction with the GuiceBundle. + * Will load the configuration based Guice modules. + * The method annotated with {@link Run} will be injected and run when this command is called. + * The {link Bootstrap}, {@link Namespace}, and {@link Configuration} will be available for + * injection. + */ +public abstract class InjectedConfiguredCommand extends ConfiguredCommand implements GuiceCommand { + private GuiceBundle init; + + + protected InjectedConfiguredCommand(String name, String description) { + super(name, description); + } + + @Override + public void setInit(GuiceBundle init) { + this.init = init; + } + + @Override + final protected void run(Bootstrap bootstrap, Namespace namespace, T configuration) throws Exception { + if(init == null) throw new IllegalStateException("Injected Command run without a GuiceBundle. Was the application initialized correctly?"); + + init.run(bootstrap, null, configuration); + init.setNamespace(namespace); + Utils.runRunnable(this, init.getInjector().get()); + } +} + diff --git a/src/main/java/com/hubspot/dropwizard/guice/InjectedEnvironmentCommand.java b/src/main/java/com/hubspot/dropwizard/guice/InjectedEnvironmentCommand.java new file mode 100644 index 0000000..4258887 --- /dev/null +++ b/src/main/java/com/hubspot/dropwizard/guice/InjectedEnvironmentCommand.java @@ -0,0 +1,36 @@ +package com.hubspot.dropwizard.guice; + +import io.dropwizard.Application; +import io.dropwizard.Configuration; +import io.dropwizard.cli.EnvironmentCommand; +import io.dropwizard.setup.Environment; +import net.sourceforge.argparse4j.inf.Namespace; + +/** + * Must be used in conjunction with the GuiceBundle. + * Will load the configuration based Guice modules. + * The method annotated with {@link Run} will be injected and run when this command is called. + * The {@link Environment}, {@link Namespace}, and {@link Configuration} will be available for + * injection. + */ +public abstract class InjectedEnvironmentCommand extends EnvironmentCommand implements GuiceCommand { + private GuiceBundle init; + + protected InjectedEnvironmentCommand(Application application, String name, String description) { + super(application, name, description); + } + + @Override + public void setInit(GuiceBundle init) { + this.init = init; + } + + @Override + protected void run(Environment environment, Namespace namespace, T configuration) throws Exception { + if(init == null) throw new IllegalStateException("Injected Command run without a GuiceBundle. Was the application initialized correctly?"); + + //We do not need to run init here, as it was already run by the Dropwizard environment initializer. + init.setNamespace(namespace); + Utils.runRunnable(this, init.getInjector().get()); + } +} diff --git a/src/main/java/com/hubspot/dropwizard/guice/InjectorFactory.java b/src/main/java/com/hubspot/dropwizard/guice/InjectorFactory.java new file mode 100644 index 0000000..7be1e08 --- /dev/null +++ b/src/main/java/com/hubspot/dropwizard/guice/InjectorFactory.java @@ -0,0 +1,19 @@ +package com.hubspot.dropwizard.guice; + +import java.util.List; + +import com.google.inject.Injector; +import com.google.inject.Module; +import com.google.inject.Stage; + +/** + * Factory to create Guice Injector with supplied Stage and List of Modules. + * + * Idea behind separating this out is to enable integrating applications to + * use alternate Guice factories like, - Mycila + * (https://code.google.com/p/mycila/), - Governator + * (https://github.com/Netflix/governator) + */ +public interface InjectorFactory { + Injector create(final Stage stage, final List modules); +} diff --git a/src/main/java/com/hubspot/dropwizard/guice/InjectorFactoryImpl.java b/src/main/java/com/hubspot/dropwizard/guice/InjectorFactoryImpl.java new file mode 100644 index 0000000..339f744 --- /dev/null +++ b/src/main/java/com/hubspot/dropwizard/guice/InjectorFactoryImpl.java @@ -0,0 +1,15 @@ +package com.hubspot.dropwizard.guice; + +import java.util.List; + +import com.google.inject.Guice; +import com.google.inject.Injector; +import com.google.inject.Module; +import com.google.inject.Stage; + +public class InjectorFactoryImpl implements InjectorFactory { + @Override + public Injector create(final Stage stage, final List modules) { + return Guice.createInjector(stage,modules); + } +} diff --git a/src/main/java/com/hubspot/dropwizard/guice/JerseyContainerModule.java b/src/main/java/com/hubspot/dropwizard/guice/JerseyContainerModule.java deleted file mode 100644 index 89ca87e..0000000 --- a/src/main/java/com/hubspot/dropwizard/guice/JerseyContainerModule.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.hubspot.dropwizard.guice; - -import com.sun.jersey.guice.JerseyServletModule; -import com.sun.jersey.spi.container.WebApplication; - -public class JerseyContainerModule extends JerseyServletModule { - private final GuiceContainer container; - - public JerseyContainerModule(final GuiceContainer container) { - this.container = container; - } - - @Override - protected void configureServlets() { - bind(GuiceContainer.class).toInstance(container); - } - - @Override - public WebApplication webApp(com.sun.jersey.guice.spi.container.servlet.GuiceContainer guiceContainer) { - return container.getWebApplication(); - } -} diff --git a/src/main/java/com/hubspot/dropwizard/guice/JerseyModule.java b/src/main/java/com/hubspot/dropwizard/guice/JerseyModule.java new file mode 100644 index 0000000..ea3d268 --- /dev/null +++ b/src/main/java/com/hubspot/dropwizard/guice/JerseyModule.java @@ -0,0 +1,19 @@ +package com.hubspot.dropwizard.guice; + +import com.google.inject.servlet.ServletModule; +import com.squarespace.jersey2.guice.BootstrapModule; +import com.squarespace.jersey2.guice.BootstrapUtils; +import org.glassfish.hk2.api.ServiceLocator; + +//Inspired by gwizard-jersey - https://github.com/stickfigure/gwizard +public class JerseyModule extends ServletModule { + + @Override + protected void configureServlets() { + // The order these operations (including the steps in the linker) are important + ServiceLocator locator = BootstrapUtils.newServiceLocator(); + install(new BootstrapModule(locator)); + + bind(HK2Linker.class).asEagerSingleton(); + } +} diff --git a/src/main/java/com/hubspot/dropwizard/guice/JerseyUtil.java b/src/main/java/com/hubspot/dropwizard/guice/JerseyUtil.java new file mode 100644 index 0000000..c829158 --- /dev/null +++ b/src/main/java/com/hubspot/dropwizard/guice/JerseyUtil.java @@ -0,0 +1,71 @@ +package com.hubspot.dropwizard.guice; + +import com.google.inject.Injector; +import com.google.inject.Key; +import com.google.inject.servlet.GuiceFilter; +import io.dropwizard.jersey.setup.JerseyEnvironment; +import io.dropwizard.setup.Environment; +import org.glassfish.jersey.server.model.Resource; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.ws.rs.Path; +import java.lang.reflect.Type; + +/** + * Functionality used to help link Guice with Jersey + */ +public class JerseyUtil { + + final static Logger logger = LoggerFactory.getLogger(JerseyUtil.class); + + /** + * Registers any Guice-bound providers or root resources. + */ + public static void registerGuiceBound(Injector injector, final JerseyEnvironment environment) { + while (injector != null) { + for (Key key : injector.getBindings().keySet()) { + Type type = key.getTypeLiteral().getType(); + if (type instanceof Class) { + Class c = (Class) type; + if (isProviderClass(c)) { + logger.info("Registering {} as a provider class", c.getName()); + environment.register(c); + } else if (isRootResourceClass(c)) { + // Jersey rejects resources that it doesn't think are acceptable + // Including abstract classes and interfaces, even if there is a valid Guice binding. + if(Resource.isAcceptable(c)) { + logger.info("Registering {} as a root resource class", c.getName()); + environment.register(c); + } else { + logger.warn("Class {} was not registered as a resource. Bind a concrete implementation instead.", c.getName()); + } + } + + } + } + injector = injector.getParent(); + } + } + + private static boolean isProviderClass(Class c) { + return c != null && c.isAnnotationPresent(javax.ws.rs.ext.Provider.class); + } + + private static boolean isRootResourceClass(Class c) { + if (c == null) + return false; + + if (c.isAnnotationPresent(Path.class)) return true; + + for (Class i : c.getInterfaces()) + if (i.isAnnotationPresent(Path.class)) return true; + + return false; + } + + public static void registerGuiceFilter(Environment environment) { + environment.servlets().addFilter("Guice Filter", GuiceFilter.class) + .addMappingForUrlPatterns(null, false, environment.getApplicationContext().getContextPath() + "*"); + } +} diff --git a/src/main/java/com/hubspot/dropwizard/guice/Run.java b/src/main/java/com/hubspot/dropwizard/guice/Run.java new file mode 100644 index 0000000..448df90 --- /dev/null +++ b/src/main/java/com/hubspot/dropwizard/guice/Run.java @@ -0,0 +1,16 @@ +package com.hubspot.dropwizard.guice; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Apply to method in injectable commands. + * Specifies the method to run to kick off the command. + * When the command is run from Dropwizard, this method + * will be called with Guice injected parameters. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface Run { } diff --git a/src/main/java/com/hubspot/dropwizard/guice/Utils.java b/src/main/java/com/hubspot/dropwizard/guice/Utils.java new file mode 100644 index 0000000..1d38d27 --- /dev/null +++ b/src/main/java/com/hubspot/dropwizard/guice/Utils.java @@ -0,0 +1,69 @@ +package com.hubspot.dropwizard.guice; + +import com.google.common.base.Function; +import com.google.common.base.Optional; +import com.google.common.collect.Collections2; +import com.google.inject.ConfigurationException; +import com.google.inject.Injector; +import com.google.inject.Key; +import com.google.inject.TypeLiteral; +import com.google.inject.internal.Annotations; +import com.google.inject.internal.Errors; +import com.google.inject.internal.ErrorsException; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; + +public class Utils { + /** + * Used with {@link GuiceCommand}. Finds a method with the {@link Run} command, injects, and runs it. + */ + public static void runRunnable(Object obj, final Injector injector) throws Exception { + Optional oRun = findRunable(obj.getClass()); + if(!oRun.isPresent()) throw new IllegalStateException("No runnable method found. @Run annotation must be applied to a method."); + Method run = oRun.get(); + + Errors errors = new Errors(run); + List> keys = getMethodKeys(run, errors); + errors.throwConfigurationExceptionIfErrorsExist(); + + run.invoke(obj, Collections2.transform(keys, new Function, Object>() { + @Override + public Object apply(Key input) { + return injector.getInstance(input); + } + }).toArray()); + } + + private static Optional findRunable(Class klass) { + if(klass == Object.class) return Optional.absent(); + for (Method method : klass.getMethods()) { + if (method.getAnnotation(Run.class) != null) + return Optional.of(method); + } + return findRunable(klass.getSuperclass()); + } + + //Lifted from Jukito: https://github.com/ArcBees/Jukito/blob/master/jukito/src/main/java/org/jukito/GuiceUtils.java + private static List> getMethodKeys(Method method, Errors errors) { + Annotation allParameterAnnotations[][] = method.getParameterAnnotations(); + List> result = new ArrayList<>(allParameterAnnotations.length); + Iterator annotationsIterator = Arrays.asList(allParameterAnnotations).iterator(); + TypeLiteral type = TypeLiteral.get(method.getDeclaringClass()); + for (TypeLiteral parameterType : type.getParameterTypes(method)) { + try { + Annotation[] parameterAnnotations = annotationsIterator.next(); + result.add(Annotations.getKey(parameterType, method, parameterAnnotations, errors)); + } catch (ConfigurationException e) { + errors.merge(e.getErrorMessages()); + } catch (ErrorsException e) { + errors.merge(e.getErrors()); + } + } + return result; + } +} diff --git a/src/test/java/com/hubspot/dropwizard/guice/AutoConfigTest.java b/src/test/java/com/hubspot/dropwizard/guice/AutoConfigTest.java new file mode 100644 index 0000000..0020953 --- /dev/null +++ b/src/test/java/com/hubspot/dropwizard/guice/AutoConfigTest.java @@ -0,0 +1,122 @@ +package com.hubspot.dropwizard.guice; + +import com.google.common.collect.ImmutableList; +import com.google.inject.Guice; +import com.google.inject.Injector; +import com.hubspot.dropwizard.guice.objects.*; +import io.dropwizard.Bundle; +import io.dropwizard.jackson.Jackson; +import io.dropwizard.lifecycle.Managed; +import io.dropwizard.lifecycle.setup.LifecycleEnvironment; +import io.dropwizard.servlets.tasks.Task; +import io.dropwizard.setup.AdminEnvironment; +import io.dropwizard.setup.Bootstrap; +import io.dropwizard.setup.Environment; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Spy; +import org.mockito.runners.MockitoJUnitRunner; + +import java.util.Set; +import java.util.SortedSet; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.*; + +@RunWith(MockitoJUnitRunner.class) +public class AutoConfigTest { + + private final Injector injector = Guice.createInjector(new TestModule()); + + @Spy + private Environment environment = new Environment("test env", Jackson.newObjectMapper(), null, null, null); + private AutoConfig autoConfig; + + @Before + public void setUp() { + //when + autoConfig = new AutoConfig(TestModule.class.getPackage().getName()); + } + + @Test + public void addBundlesDuringBootStrap() { + //given + final Bootstrap bootstrap = mock(Bootstrap.class); + when(bootstrap.getCommands()).thenReturn(ImmutableList.of()); + Bundle singletonBundle = injector.getInstance(InjectedBundle.class); + + //when + autoConfig.initialize(bootstrap, injector); + + verify(bootstrap).addBundle(singletonBundle); + } + + @Test + public void addInjectableHealthChecks() { + //when + autoConfig.run(environment, injector); + + // then + SortedSet healthChecks = environment.healthChecks().getNames(); + assertThat(healthChecks).contains(new InjectedHealthCheck().getName()); + } + + @Test + public void addProviders() { + // when + autoConfig.run(environment, injector); + + //then + Set> components = environment.jersey().getResourceConfig().getClasses(); + assertThat(components).containsOnlyOnce(InjectedProvider.class); + } + + @Test + public void addResources() { + //when + autoConfig.run(environment, injector); + + //then + Set> components = environment.jersey().getResourceConfig().getClasses(); + assertThat(components).containsOnlyOnce(ExplicitResource.class); + } + + @Test + public void interfaceResourcesNotAdded() { + //when + autoConfig.run(environment, injector); + injector.getProvider(JitResource.class); + //then + Set> components = environment.jersey().getResourceConfig().getClasses(); + assertThat(components).doesNotContain(ResourceInterface.class); + } + + @Test + public void addTasks() throws Exception { + //given + when(environment.admin()).thenReturn(mock(AdminEnvironment.class)); + + //when + autoConfig.run(environment, injector); + + //then + Task task = injector.getInstance(InjectedTask.class); + assertThat(task.getName()).isEqualTo("test task"); + verify(environment.admin()).addTask(task); + + } + + @Test + public void addManaged() { + //given + Managed managed = injector.getInstance(InjectedManaged.class); + when(environment.lifecycle()).thenReturn(mock(LifecycleEnvironment.class)); + + //when + autoConfig.run(environment, injector); + + //then + verify(environment.lifecycle()).manage(managed); + } +} \ No newline at end of file diff --git a/src/test/java/com/hubspot/dropwizard/guice/GuiceBundleTest.java b/src/test/java/com/hubspot/dropwizard/guice/GuiceBundleTest.java new file mode 100644 index 0000000..a6cd947 --- /dev/null +++ b/src/test/java/com/hubspot/dropwizard/guice/GuiceBundleTest.java @@ -0,0 +1,63 @@ +package com.hubspot.dropwizard.guice; + +import com.google.common.collect.ImmutableList; +import com.google.inject.Injector; +import com.hubspot.dropwizard.guice.objects.TestModule; +import com.squarespace.jersey2.guice.BootstrapUtils; +import io.dropwizard.Configuration; +import io.dropwizard.jackson.Jackson; +import io.dropwizard.setup.Bootstrap; +import io.dropwizard.setup.Environment; +import org.glassfish.hk2.api.ServiceLocator; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; + +import javax.servlet.ServletException; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class GuiceBundleTest { + + @Mock + Environment environment; + + private GuiceBundle guiceBundle; + + @After + public void tearDown() { + BootstrapUtils.reset(); + } + + @Before + public void setUp() { + //given + environment = new Environment("test env", Jackson.newObjectMapper(), null, null, null); + guiceBundle = GuiceBundle.newBuilder() + .addModule(new TestModule()) + .build(); + Bootstrap bootstrap = mock(Bootstrap.class); + when(bootstrap.getCommands()).thenReturn(ImmutableList.of()); + guiceBundle.initialize(bootstrap); + guiceBundle.run(new Configuration(), environment); + } + + @Test + public void createsInjectorWhenInit() throws ServletException { + //then + Injector injector = guiceBundle.getInjector().get(); + assertThat(injector).isNotNull(); + } + + @Test + public void serviceLocatorIsAvaliable () throws ServletException { + ServiceLocator serviceLocator = guiceBundle.getInjector().get().getInstance(ServiceLocator.class); + assertThat(serviceLocator).isNotNull(); + } +} \ No newline at end of file diff --git a/src/test/java/com/hubspot/dropwizard/guice/HK2LinkerTest.java b/src/test/java/com/hubspot/dropwizard/guice/HK2LinkerTest.java new file mode 100644 index 0000000..31edc34 --- /dev/null +++ b/src/test/java/com/hubspot/dropwizard/guice/HK2LinkerTest.java @@ -0,0 +1,46 @@ +package com.hubspot.dropwizard.guice; + +import com.google.inject.Guice; +import com.google.inject.Injector; +import com.hubspot.dropwizard.guice.objects.ExplicitResource; +import com.hubspot.dropwizard.guice.objects.JitResource; +import com.hubspot.dropwizard.guice.objects.TestModule; +import com.squarespace.jersey2.guice.BootstrapUtils; +import org.glassfish.hk2.api.ServiceLocator; +import org.junit.AfterClass; +import org.junit.Test; + +import javax.servlet.ServletException; + +import static org.assertj.core.api.Assertions.assertThat; + +public class HK2LinkerTest { + + final Injector injector = Guice.createInjector(new JerseyModule(), new TestModule()); + final ServiceLocator serviceLocator = injector.getInstance(ServiceLocator.class); + + @AfterClass + public static void tearDown() { + BootstrapUtils.reset(); + } + + @Test + public void explicitGuiceBindingsAreBridgedToHk2() throws ServletException { + // when + ExplicitResource resource = serviceLocator.createAndInitialize(ExplicitResource.class); + + // then + assertThat(resource).isNotNull(); + assertThat(resource.getDAO()).isNotNull(); + } + + @Test + public void jitGuiceBindingsAreBridgedToHk2() throws ServletException { + // when + JitResource resource = serviceLocator.createAndInitialize(JitResource.class); + + // then + assertThat(resource).isNotNull(); + assertThat(resource.getDAO()).isNotNull(); + } +} \ No newline at end of file diff --git a/src/test/java/com/hubspot/dropwizard/guice/InjectedCommandTest.java b/src/test/java/com/hubspot/dropwizard/guice/InjectedCommandTest.java new file mode 100644 index 0000000..a42bf84 --- /dev/null +++ b/src/test/java/com/hubspot/dropwizard/guice/InjectedCommandTest.java @@ -0,0 +1,18 @@ +package com.hubspot.dropwizard.guice; + +import com.hubspot.dropwizard.guice.sample.HelloWorldApplication; +import com.hubspot.dropwizard.guice.sample.command.TestCommand; +import com.hubspot.dropwizard.guice.util.CommandRunner; +import org.junit.Test; + +import static org.junit.Assert.*; + +public class InjectedCommandTest { + + @Test + public void run_test_command() { + new CommandRunner<>(HelloWorldApplication.class, "TestCommand").run(); + assertEquals(HelloWorldApplication.class, TestCommand.bootstrap.getApplication().getClass()); + assertTrue(TestCommand.namespace != null); + } +} diff --git a/src/test/java/com/hubspot/dropwizard/guice/InjectedConfiguredCommandTest.java b/src/test/java/com/hubspot/dropwizard/guice/InjectedConfiguredCommandTest.java new file mode 100644 index 0000000..c0d9895 --- /dev/null +++ b/src/test/java/com/hubspot/dropwizard/guice/InjectedConfiguredCommandTest.java @@ -0,0 +1,20 @@ +package com.hubspot.dropwizard.guice; + +import com.hubspot.dropwizard.guice.sample.HelloWorldApplication; +import com.hubspot.dropwizard.guice.sample.command.TestConfiguredCommand; +import com.hubspot.dropwizard.guice.util.CommandRunner; +import org.junit.Test; + +import static io.dropwizard.testing.ResourceHelpers.resourceFilePath; +import static org.junit.Assert.*; + +public class InjectedConfiguredCommandTest { + @Test + public void run_test_command() { + String configPath = resourceFilePath("hello-world.yml"); + new CommandRunner<>(HelloWorldApplication.class, configPath, "SimpleCommand").run(); + assertEquals("Joe", TestConfiguredCommand.configName); + assertEquals(HelloWorldApplication.class, TestConfiguredCommand.bootstrap.getApplication().getClass()); + assertEquals(configPath, TestConfiguredCommand.namespace.get("file")); + } +} diff --git a/src/test/java/com/hubspot/dropwizard/guice/InjectedEnvironmentCommandTest.java b/src/test/java/com/hubspot/dropwizard/guice/InjectedEnvironmentCommandTest.java new file mode 100644 index 0000000..0b87ea4 --- /dev/null +++ b/src/test/java/com/hubspot/dropwizard/guice/InjectedEnvironmentCommandTest.java @@ -0,0 +1,25 @@ +package com.hubspot.dropwizard.guice; + +import com.hubspot.dropwizard.guice.sample.HelloWorldApplication; +import com.hubspot.dropwizard.guice.sample.command.TestEnvironmentCommand; +import com.hubspot.dropwizard.guice.util.CommandRunner; +import org.junit.Test; + +import static io.dropwizard.testing.ResourceHelpers.resourceFilePath; +import static org.junit.Assert.*; + +public class InjectedEnvironmentCommandTest { + @Test + public void run_simple_command() { + new CommandRunner<>(HelloWorldApplication.class, resourceFilePath("hello-world.yml"), "TestEnvironmentCommand").run(); + assertEquals("Joe", TestEnvironmentCommand.configName); + } + @Test + public void run_test_command() { + String configPath = resourceFilePath("hello-world.yml"); + new CommandRunner<>(HelloWorldApplication.class, configPath, "TestEnvironmentCommand").run(); + assertEquals("Joe", TestEnvironmentCommand.configName); + assertTrue(TestEnvironmentCommand.environment != null); + assertEquals(configPath, TestEnvironmentCommand.namespace.get("file")); + } +} diff --git a/src/test/java/com/hubspot/dropwizard/guice/InjectedIntegrationTest.java b/src/test/java/com/hubspot/dropwizard/guice/InjectedIntegrationTest.java new file mode 100644 index 0000000..81e6b95 --- /dev/null +++ b/src/test/java/com/hubspot/dropwizard/guice/InjectedIntegrationTest.java @@ -0,0 +1,71 @@ +package com.hubspot.dropwizard.guice; + +import com.google.common.io.Resources; +import com.hubspot.dropwizard.guice.objects.TestApplication; +import com.squarespace.jersey2.guice.BootstrapUtils; +import io.dropwizard.Configuration; +import io.dropwizard.client.JerseyClientBuilder; +import io.dropwizard.testing.junit.DropwizardAppRule; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.ClassRule; +import org.junit.Test; + +import javax.ws.rs.client.Client; +import java.io.File; + +import static org.assertj.core.api.Assertions.assertThat; + +public class InjectedIntegrationTest { + + @ClassRule + public static final DropwizardAppRule RULE = + new DropwizardAppRule<>(TestApplication.class, resourceFilePath("test-config.yml")); + + protected static Client client; + + @BeforeClass + public static void setUp() { + client = new JerseyClientBuilder(RULE.getEnvironment()).build("test client"); + } + + @AfterClass + public static void tearDown() { + BootstrapUtils.reset(); + } + + public static String resourceFilePath(String resourceClassPathLocation) { + try { + return new File(Resources.getResource(resourceClassPathLocation).toURI()).getAbsolutePath(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Test + public void shouldGetExplicitMessage() { + + // when + final String message = client.target( + String.format("http://localhost:%d//explicit/message", RULE.getLocalPort())) + .request() + .get(String.class); + + // then + assertThat(message).isEqualTo("this DAO was bound explicitly"); + } + + @Test + public void shouldGetJitMessage() { + + // when + final String message = client.target( + String.format("http://localhost:%d//jit/message", RULE.getLocalPort())) + .request() + .get(String.class); + + // then + assertThat(message).isEqualTo("this DAO was bound just-in-time"); + } + +} diff --git a/src/test/java/com/hubspot/dropwizard/guice/InjectedResourcesTest.java b/src/test/java/com/hubspot/dropwizard/guice/InjectedResourcesTest.java new file mode 100644 index 0000000..45e4e11 --- /dev/null +++ b/src/test/java/com/hubspot/dropwizard/guice/InjectedResourcesTest.java @@ -0,0 +1,41 @@ +package com.hubspot.dropwizard.guice; + +import com.hubspot.dropwizard.guice.objects.ExplicitDAO; +import com.hubspot.dropwizard.guice.objects.ExplicitResource; +import com.squarespace.jersey2.guice.BootstrapUtils; +import io.dropwizard.testing.junit.ResourceTestRule; +import org.junit.AfterClass; +import org.junit.ClassRule; +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * this test is created to address to Null Pointer Exceptions in JerseyTest.teardown() related to ServiceLocator + * See: https://github.com/dropwizard/dropwizard/issues/828 and http://permalink.gmane.org/gmane.comp.java.dropwizard.devel/376 + */ +public class InjectedResourcesTest { + + @ClassRule + public static final ResourceTestRule resources = ResourceTestRule.builder() + .addResource(new ExplicitResource(new ExplicitDAO())) + .build(); + + @Test + public void shouldGetExplicitMessage() { + // when + String message = resources.client().target("/explicit/message").request().get(String.class); + + // then + assertThat(message).isEqualTo("this DAO was bound explicitly"); + } + + @Test + public void shouldGetJitMessage() { + // when + String message = resources.client().target("/explicit/message").request().get(String.class); + + // then + assertThat(message).isEqualTo("this DAO was bound explicitly"); + } +} \ No newline at end of file diff --git a/src/test/java/com/hubspot/dropwizard/guice/IntegrationTest.java b/src/test/java/com/hubspot/dropwizard/guice/IntegrationTest.java new file mode 100644 index 0000000..05533c6 --- /dev/null +++ b/src/test/java/com/hubspot/dropwizard/guice/IntegrationTest.java @@ -0,0 +1,64 @@ +package com.hubspot.dropwizard.guice; + +import com.google.inject.ConfigurationException; +import com.google.inject.Injector; +import com.google.inject.Key; +import com.google.inject.name.Names; +import com.hubspot.dropwizard.guice.sample.HelloWorldApplication; +import com.hubspot.dropwizard.guice.sample.HelloWorldConfiguration; +import com.hubspot.dropwizard.guice.sample.OtherConfig; +import com.jayway.restassured.RestAssured; +import io.dropwizard.testing.junit.DropwizardAppRule; +import org.junit.BeforeClass; +import org.junit.ClassRule; +import org.junit.Test; + +import static io.dropwizard.testing.ResourceHelpers.*; +import static com.jayway.restassured.RestAssured.*; +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; + +public class IntegrationTest { + @ClassRule + public static final DropwizardAppRule RULE = + new DropwizardAppRule<>(HelloWorldApplication.class, resourceFilePath("hello-world.yml")); + + private static Injector injector; + + @BeforeClass + public static void setup() { + RestAssured.baseURI = "http://localhost:" + RULE.getLocalPort(); + injector = ((HelloWorldApplication) RULE.getApplication()).guiceBundle.getInjector().get(); + } + + @Test + public void configuration_injection_in_resource() throws Exception { + get("/v1/hello-world").then().body("content", equalTo("Hello, Joe!")); + } + + @Test + public void value_passed_through_param_converter() throws Exception { + get("/v1/hello-world?name=Bob").then().body("content", equalTo("Hello, Bob!")); + } + + @Test + public void configuration_injection_in_healthcheck() throws Exception { + get("admin/healthcheck").then().body("template.healthy", equalTo(true)); + } + + @Test + public void request_injection_in_resource() throws Exception { + get("/v1/hello-world/ctx").then().statusCode(200); + } + + @Test + public void module_injection_in_resource() throws Exception { + get("/v1/hello-world/sample").then().body(equalTo("foo")); + } + + @Test + public void dependent_module_gets_injected() throws Exception { + assertEquals("More data is: something", + injector.getInstance(Key.get(String.class, Names.named("dependent")))); + } +} diff --git a/src/test/java/com/hubspot/dropwizard/guice/objects/AbstractTask.java b/src/test/java/com/hubspot/dropwizard/guice/objects/AbstractTask.java new file mode 100644 index 0000000..59b48d6 --- /dev/null +++ b/src/test/java/com/hubspot/dropwizard/guice/objects/AbstractTask.java @@ -0,0 +1,9 @@ +package com.hubspot.dropwizard.guice.objects; + +import io.dropwizard.servlets.tasks.Task; + +public abstract class AbstractTask extends Task { + protected AbstractTask(String name) { + super(name); + } +} diff --git a/src/test/java/com/hubspot/dropwizard/guice/objects/ExplicitDAO.java b/src/test/java/com/hubspot/dropwizard/guice/objects/ExplicitDAO.java new file mode 100644 index 0000000..d7cfb59 --- /dev/null +++ b/src/test/java/com/hubspot/dropwizard/guice/objects/ExplicitDAO.java @@ -0,0 +1,9 @@ +package com.hubspot.dropwizard.guice.objects; + +public class ExplicitDAO { + + public String getMessage() { + return "this DAO was bound explicitly"; + } + +} diff --git a/src/test/java/com/hubspot/dropwizard/guice/objects/ExplicitResource.java b/src/test/java/com/hubspot/dropwizard/guice/objects/ExplicitResource.java new file mode 100644 index 0000000..d3ead1f --- /dev/null +++ b/src/test/java/com/hubspot/dropwizard/guice/objects/ExplicitResource.java @@ -0,0 +1,30 @@ +package com.hubspot.dropwizard.guice.objects; + +import javax.inject.Inject; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; + +import static javax.ws.rs.core.MediaType.APPLICATION_JSON; + +@Path("/explicit") +@Produces(APPLICATION_JSON) +public class ExplicitResource { + private final ExplicitDAO dao; + + @Inject + public ExplicitResource(ExplicitDAO dao) { + this.dao = dao; + } + + @GET + @Path("/message") + public String getMessage () { + return dao.getMessage(); + } + + public ExplicitDAO getDAO() { + return dao; + } + +} diff --git a/src/test/java/com/hubspot/dropwizard/guice/objects/InjectedBundle.java b/src/test/java/com/hubspot/dropwizard/guice/objects/InjectedBundle.java new file mode 100644 index 0000000..399f13b --- /dev/null +++ b/src/test/java/com/hubspot/dropwizard/guice/objects/InjectedBundle.java @@ -0,0 +1,20 @@ +package com.hubspot.dropwizard.guice.objects; + +import io.dropwizard.Bundle; +import io.dropwizard.setup.Bootstrap; +import io.dropwizard.setup.Environment; + +import javax.inject.Singleton; + +@Singleton +public class InjectedBundle implements Bundle { + @Override + public void initialize(Bootstrap bootstrap) { + + } + + @Override + public void run(Environment environment) { + + } +} diff --git a/src/test/java/com/hubspot/dropwizard/guice/objects/InjectedHealthCheck.java b/src/test/java/com/hubspot/dropwizard/guice/objects/InjectedHealthCheck.java new file mode 100644 index 0000000..3af4f05 --- /dev/null +++ b/src/test/java/com/hubspot/dropwizard/guice/objects/InjectedHealthCheck.java @@ -0,0 +1,15 @@ +package com.hubspot.dropwizard.guice.objects; + +import com.hubspot.dropwizard.guice.InjectableHealthCheck; + +public class InjectedHealthCheck extends InjectableHealthCheck { + @Override + public String getName() { + return "healthcheck"; + } + + @Override + protected Result check() throws Exception { + return Result.healthy(); + } +} diff --git a/src/test/java/com/hubspot/dropwizard/guice/objects/InjectedManaged.java b/src/test/java/com/hubspot/dropwizard/guice/objects/InjectedManaged.java new file mode 100644 index 0000000..6e84d1b --- /dev/null +++ b/src/test/java/com/hubspot/dropwizard/guice/objects/InjectedManaged.java @@ -0,0 +1,18 @@ +package com.hubspot.dropwizard.guice.objects; + +import io.dropwizard.lifecycle.Managed; + +import javax.inject.Singleton; + +@Singleton +public class InjectedManaged implements Managed { + @Override + public void start() throws Exception { + + } + + @Override + public void stop() throws Exception { + + } +} diff --git a/src/test/java/com/hubspot/dropwizard/guice/objects/InjectedProvider.java b/src/test/java/com/hubspot/dropwizard/guice/objects/InjectedProvider.java new file mode 100644 index 0000000..f700fc9 --- /dev/null +++ b/src/test/java/com/hubspot/dropwizard/guice/objects/InjectedProvider.java @@ -0,0 +1,8 @@ +package com.hubspot.dropwizard.guice.objects; + +import javax.ws.rs.ext.Provider; + +@Provider +public class InjectedProvider { + +} diff --git a/src/test/java/com/hubspot/dropwizard/guice/objects/InjectedTask.java b/src/test/java/com/hubspot/dropwizard/guice/objects/InjectedTask.java new file mode 100644 index 0000000..b4d112f --- /dev/null +++ b/src/test/java/com/hubspot/dropwizard/guice/objects/InjectedTask.java @@ -0,0 +1,22 @@ +package com.hubspot.dropwizard.guice.objects; + +import com.google.common.collect.ImmutableMultimap; +import com.google.inject.Singleton; + +import javax.inject.Inject; +import javax.inject.Named; +import java.io.PrintWriter; + +@Singleton +public class InjectedTask extends AbstractTask { + + @Inject + protected InjectedTask(@Named("TestTaskName") String name) { + super(name); + } + + @Override + public void execute(ImmutableMultimap immutableMultimap, PrintWriter printWriter) throws Exception { + + } +} diff --git a/src/test/java/com/hubspot/dropwizard/guice/objects/JitDAO.java b/src/test/java/com/hubspot/dropwizard/guice/objects/JitDAO.java new file mode 100644 index 0000000..faed4f2 --- /dev/null +++ b/src/test/java/com/hubspot/dropwizard/guice/objects/JitDAO.java @@ -0,0 +1,7 @@ +package com.hubspot.dropwizard.guice.objects; + +public class JitDAO { + public String getMessage() { + return "this DAO was bound just-in-time"; + } +} diff --git a/src/test/java/com/hubspot/dropwizard/guice/objects/JitResource.java b/src/test/java/com/hubspot/dropwizard/guice/objects/JitResource.java new file mode 100644 index 0000000..2caeaa4 --- /dev/null +++ b/src/test/java/com/hubspot/dropwizard/guice/objects/JitResource.java @@ -0,0 +1,31 @@ +package com.hubspot.dropwizard.guice.objects; + +import javax.inject.Inject; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; + +import static javax.ws.rs.core.MediaType.APPLICATION_JSON; + +@Path("/jit") +@Produces(APPLICATION_JSON) +public class JitResource { + + private final JitDAO dao; + + @Inject + public JitResource(JitDAO dao) { + this.dao = dao; + } + + @GET + @Path("/message") + public String getMessage () { + return dao.getMessage(); + } + + public JitDAO getDAO() { + return dao; + } + +} diff --git a/src/test/java/com/hubspot/dropwizard/guice/objects/ResourceInterface.java b/src/test/java/com/hubspot/dropwizard/guice/objects/ResourceInterface.java new file mode 100644 index 0000000..1e31fab --- /dev/null +++ b/src/test/java/com/hubspot/dropwizard/guice/objects/ResourceInterface.java @@ -0,0 +1,7 @@ +package com.hubspot.dropwizard.guice.objects; + +import javax.ws.rs.Path; + +@Path("/interface") +public interface ResourceInterface { +} diff --git a/src/test/java/com/hubspot/dropwizard/guice/objects/TestApplication.java b/src/test/java/com/hubspot/dropwizard/guice/objects/TestApplication.java new file mode 100644 index 0000000..ef44b10 --- /dev/null +++ b/src/test/java/com/hubspot/dropwizard/guice/objects/TestApplication.java @@ -0,0 +1,26 @@ +package com.hubspot.dropwizard.guice.objects; + +import com.hubspot.dropwizard.guice.GuiceBundle; +import io.dropwizard.Application; +import io.dropwizard.Configuration; +import io.dropwizard.setup.Bootstrap; +import io.dropwizard.setup.Environment; + +import static com.hubspot.dropwizard.guice.GuiceBundle.newBuilder; + +public class TestApplication extends Application { + + @Override + public void initialize(final Bootstrap bootstrap) { + final GuiceBundle jersey2GuiceBundle = newBuilder() + .addModule(new TestModule()) + .enableAutoConfig(this.getClass().getPackage().getName()) + .build(); + bootstrap.addBundle(jersey2GuiceBundle); + } + + @Override + public void run(Configuration configuration, Environment environment) throws Exception { + + } +} diff --git a/src/test/java/com/hubspot/dropwizard/guice/objects/TestModule.java b/src/test/java/com/hubspot/dropwizard/guice/objects/TestModule.java new file mode 100644 index 0000000..b0926bf --- /dev/null +++ b/src/test/java/com/hubspot/dropwizard/guice/objects/TestModule.java @@ -0,0 +1,14 @@ +package com.hubspot.dropwizard.guice.objects; + +import com.google.inject.AbstractModule; +import com.google.inject.name.Names; +import com.hubspot.dropwizard.guice.objects.ExplicitDAO; + +public class TestModule extends AbstractModule { + @Override + protected void configure() { + bind(ExplicitDAO.class); + bindConstant().annotatedWith(Names.named("TestTaskName")).to("test task"); + } +} + diff --git a/src/test/java/com/hubspot/dropwizard/guice/sample/HelloWorldApplication.java b/src/test/java/com/hubspot/dropwizard/guice/sample/HelloWorldApplication.java new file mode 100644 index 0000000..dd75eb7 --- /dev/null +++ b/src/test/java/com/hubspot/dropwizard/guice/sample/HelloWorldApplication.java @@ -0,0 +1,40 @@ +package com.hubspot.dropwizard.guice.sample; + +import com.hubspot.dropwizard.guice.GuiceBundle; +import com.hubspot.dropwizard.guice.sample.config.SubConfig; +import com.hubspot.dropwizard.guice.sample.guice.DependentModule; +import com.hubspot.dropwizard.guice.sample.guice.HelloWorldModule; +import io.dropwizard.Application; +import io.dropwizard.setup.Bootstrap; +import io.dropwizard.setup.Environment; + +public class HelloWorldApplication extends Application { + + public GuiceBundle guiceBundle; + + public static void main(String[] args) throws Exception { + new HelloWorldApplication().run(args); + } + + @Override + public void initialize(Bootstrap bootstrap) { + + guiceBundle = GuiceBundle.newBuilder() + .addInitModule(new HelloWorldModule()) + .addModule(new DependentModule()) + .enableAutoConfig(getClass().getPackage().getName()) + .setConfigClass(HelloWorldConfiguration.class) + .build(); + + bootstrap.addBundle(guiceBundle); + } + + @Override + public String getName() { + return "hello-world"; + } + + @Override + public void run(HelloWorldConfiguration helloWorldConfiguration, Environment environment) throws Exception { + } +} diff --git a/src/test/java/com/hubspot/dropwizard/guice/sample/HelloWorldConfiguration.java b/src/test/java/com/hubspot/dropwizard/guice/sample/HelloWorldConfiguration.java new file mode 100644 index 0000000..d9b00c1 --- /dev/null +++ b/src/test/java/com/hubspot/dropwizard/guice/sample/HelloWorldConfiguration.java @@ -0,0 +1,34 @@ +package com.hubspot.dropwizard.guice.sample; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.hubspot.dropwizard.guice.sample.config.SubConfig; +import io.dropwizard.Configuration; +import org.hibernate.validator.constraints.NotEmpty; + +public class HelloWorldConfiguration extends Configuration { + @NotEmpty + @JsonProperty + private String template; + + @NotEmpty + @JsonProperty + private String defaultName = "Stranger"; + + @JsonProperty + private SubConfig subConfig; + + @JsonProperty + private OtherConfig otherConfig; + + public String getTemplate() { + return template; + } + + public String getDefaultName() { + return defaultName; + } + + public SubConfig getSubConfig() { return subConfig; } + + public OtherConfig getOtherConfig() { return otherConfig; } +} diff --git a/src/test/java/com/hubspot/dropwizard/guice/sample/OtherConfig.java b/src/test/java/com/hubspot/dropwizard/guice/sample/OtherConfig.java new file mode 100644 index 0000000..4e2be3f --- /dev/null +++ b/src/test/java/com/hubspot/dropwizard/guice/sample/OtherConfig.java @@ -0,0 +1,13 @@ +//This is in this package to test that +//config files in packages outside the named root will not be +//picked up by the auto-binding functionality. +package com.hubspot.dropwizard.guice.sample; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public class OtherConfig { + @JsonProperty + private String otherData; + + public String getOtherData() { return otherData; } +} diff --git a/src/test/java/com/hubspot/dropwizard/guice/sample/command/TestCommand.java b/src/test/java/com/hubspot/dropwizard/guice/sample/command/TestCommand.java new file mode 100644 index 0000000..fc3f38a --- /dev/null +++ b/src/test/java/com/hubspot/dropwizard/guice/sample/command/TestCommand.java @@ -0,0 +1,28 @@ +package com.hubspot.dropwizard.guice.sample.command; + +import com.google.inject.Injector; +import com.hubspot.dropwizard.guice.InjectedCommand; +import com.hubspot.dropwizard.guice.Run; +import com.hubspot.dropwizard.guice.sample.HelloWorldConfiguration; +import io.dropwizard.setup.Bootstrap; +import net.sourceforge.argparse4j.inf.Namespace; +import net.sourceforge.argparse4j.inf.Subparser; + +public class TestCommand extends InjectedCommand { + public static Bootstrap bootstrap; + public static Namespace namespace; + + public TestCommand() { + super("TestCommand", "A command that does not do much."); + } + + @Run + public void runner(Bootstrap bootstrap, + Namespace namespace) { + TestCommand.bootstrap = bootstrap; + TestCommand.namespace = namespace; + } + + @Override + public void configure(Subparser subparser) { } +} diff --git a/src/test/java/com/hubspot/dropwizard/guice/sample/command/TestConfiguredCommand.java b/src/test/java/com/hubspot/dropwizard/guice/sample/command/TestConfiguredCommand.java new file mode 100644 index 0000000..475a9b5 --- /dev/null +++ b/src/test/java/com/hubspot/dropwizard/guice/sample/command/TestConfiguredCommand.java @@ -0,0 +1,26 @@ +package com.hubspot.dropwizard.guice.sample.command; + +import com.hubspot.dropwizard.guice.InjectedConfiguredCommand; +import com.hubspot.dropwizard.guice.Run; +import com.hubspot.dropwizard.guice.sample.HelloWorldConfiguration; +import io.dropwizard.setup.Bootstrap; +import net.sourceforge.argparse4j.inf.Namespace; + +public class TestConfiguredCommand extends InjectedConfiguredCommand { + public static String configName; + public static Bootstrap bootstrap; + public static Namespace namespace; + + public TestConfiguredCommand() { + super("SimpleCommand", "A command that does not do much."); + } + + @Run + public void run(HelloWorldConfiguration config, + Bootstrap bootstrap, + Namespace namespace) { + TestConfiguredCommand.configName = config.getDefaultName(); + TestConfiguredCommand.bootstrap = bootstrap; + TestConfiguredCommand.namespace = namespace; + } +} diff --git a/src/test/java/com/hubspot/dropwizard/guice/sample/command/TestEnvironmentCommand.java b/src/test/java/com/hubspot/dropwizard/guice/sample/command/TestEnvironmentCommand.java new file mode 100644 index 0000000..74581ee --- /dev/null +++ b/src/test/java/com/hubspot/dropwizard/guice/sample/command/TestEnvironmentCommand.java @@ -0,0 +1,30 @@ +package com.hubspot.dropwizard.guice.sample.command; + +import com.hubspot.dropwizard.guice.InjectedEnvironmentCommand; +import com.hubspot.dropwizard.guice.Run; +import com.hubspot.dropwizard.guice.sample.HelloWorldApplication; +import com.hubspot.dropwizard.guice.sample.HelloWorldConfiguration; +import io.dropwizard.setup.Environment; +import net.sourceforge.argparse4j.inf.Namespace; + +import javax.inject.Inject; + +public class TestEnvironmentCommand extends InjectedEnvironmentCommand { + public static String configName; + public static Environment environment; + public static Namespace namespace; + + @Inject + public TestEnvironmentCommand(HelloWorldApplication app) { + super(app, "TestEnvironmentCommand", "A command that does not do much."); + } + + @Run + public void run(HelloWorldConfiguration config, + Environment environment, + Namespace namespace) { + TestEnvironmentCommand.configName = config.getDefaultName(); + TestEnvironmentCommand.environment = environment; + TestEnvironmentCommand.namespace = namespace; + } +} diff --git a/src/test/java/com/hubspot/dropwizard/guice/sample/config/SubConfig.java b/src/test/java/com/hubspot/dropwizard/guice/sample/config/SubConfig.java new file mode 100644 index 0000000..e33447b --- /dev/null +++ b/src/test/java/com/hubspot/dropwizard/guice/sample/config/SubConfig.java @@ -0,0 +1,12 @@ +//This is in a sub package in order to test that +//config files in packages below the named root will be picked up. +package com.hubspot.dropwizard.guice.sample.config; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public class SubConfig { + @JsonProperty + private String moreData; + + public String getMoreData() { return moreData; } +} diff --git a/src/test/java/com/hubspot/dropwizard/guice/sample/core/Saying.java b/src/test/java/com/hubspot/dropwizard/guice/sample/core/Saying.java new file mode 100644 index 0000000..0449694 --- /dev/null +++ b/src/test/java/com/hubspot/dropwizard/guice/sample/core/Saying.java @@ -0,0 +1,19 @@ +package com.hubspot.dropwizard.guice.sample.core; + +public class Saying { + private final long id; + private final String content; + + public Saying(long id, String content) { + this.id = id; + this.content = content; + } + + public long getId() { + return id; + } + + public String getContent() { + return content; + } +} \ No newline at end of file diff --git a/src/test/java/com/hubspot/dropwizard/guice/sample/guice/ConfigData.java b/src/test/java/com/hubspot/dropwizard/guice/sample/guice/ConfigData.java new file mode 100644 index 0000000..07f963e --- /dev/null +++ b/src/test/java/com/hubspot/dropwizard/guice/sample/guice/ConfigData.java @@ -0,0 +1,17 @@ +package com.hubspot.dropwizard.guice.sample.guice; + +import com.hubspot.dropwizard.guice.sample.HelloWorldConfiguration; + +import javax.inject.Inject; + +//This is broken out in order to test Just In Time binding. +//This class should be available to Resources without an explicit binding statement. +public class ConfigData { + + @Inject + private HelloWorldConfiguration config; + + public String getTemplate() { return config.getTemplate(); } + + public String getDefaultName() { return config.getDefaultName(); } +} diff --git a/src/test/java/com/hubspot/dropwizard/guice/sample/guice/DependentModule.java b/src/test/java/com/hubspot/dropwizard/guice/sample/guice/DependentModule.java new file mode 100644 index 0000000..5274cfb --- /dev/null +++ b/src/test/java/com/hubspot/dropwizard/guice/sample/guice/DependentModule.java @@ -0,0 +1,17 @@ +package com.hubspot.dropwizard.guice.sample.guice; + +import com.google.inject.AbstractModule; +import com.google.inject.name.Names; +import com.hubspot.dropwizard.guice.sample.HelloWorldConfiguration; + +import javax.inject.Inject; + +public class DependentModule extends AbstractModule { + @Inject + private HelloWorldConfiguration config; + + @Override + protected void configure() { + bind(String.class).annotatedWith(Names.named("dependent")).toInstance("More data is: " + config.getSubConfig().getMoreData()); + } +} diff --git a/src/test/java/com/hubspot/dropwizard/guice/sample/guice/HelloWorldModule.java b/src/test/java/com/hubspot/dropwizard/guice/sample/guice/HelloWorldModule.java new file mode 100644 index 0000000..e33219f --- /dev/null +++ b/src/test/java/com/hubspot/dropwizard/guice/sample/guice/HelloWorldModule.java @@ -0,0 +1,21 @@ +package com.hubspot.dropwizard.guice.sample.guice; + +import com.google.inject.AbstractModule; +import com.google.inject.Provides; + +import javax.inject.Named; + +public class HelloWorldModule extends AbstractModule { + + @Override + protected void configure() { + + } + + @Provides + @Named("sample") + public String provideTemplate() { + return "foo"; + } + +} diff --git a/src/test/java/com/hubspot/dropwizard/guice/sample/health/TemplateHealthCheck.java b/src/test/java/com/hubspot/dropwizard/guice/sample/health/TemplateHealthCheck.java new file mode 100644 index 0000000..cce07b6 --- /dev/null +++ b/src/test/java/com/hubspot/dropwizard/guice/sample/health/TemplateHealthCheck.java @@ -0,0 +1,31 @@ +package com.hubspot.dropwizard.guice.sample.health; + +import com.google.inject.Inject; +import com.google.inject.Singleton; +import com.hubspot.dropwizard.guice.InjectableHealthCheck; +import com.hubspot.dropwizard.guice.sample.HelloWorldConfiguration; + +@Singleton +public class TemplateHealthCheck extends InjectableHealthCheck { + + private final String template; + + @Inject + public TemplateHealthCheck(HelloWorldConfiguration config) { + this.template = config.getTemplate(); + } + + @Override + protected Result check() throws Exception { + final String saying = String.format(template, "TEST"); + if (!saying.contains("TEST")) { + return Result.unhealthy("template doesn't include a name"); + } + return Result.healthy(); + } + + @Override + public String getName() { + return "template"; + } +} \ No newline at end of file diff --git a/src/test/java/com/hubspot/dropwizard/guice/sample/jersey/HelloWorldResource.java b/src/test/java/com/hubspot/dropwizard/guice/sample/jersey/HelloWorldResource.java new file mode 100644 index 0000000..d1e0fbc --- /dev/null +++ b/src/test/java/com/hubspot/dropwizard/guice/sample/jersey/HelloWorldResource.java @@ -0,0 +1,78 @@ +package com.hubspot.dropwizard.guice.sample.jersey; + +import com.codahale.metrics.annotation.Timed; +import com.hubspot.dropwizard.guice.sample.guice.ConfigData; +import com.hubspot.dropwizard.guice.sample.core.Saying; +import com.google.common.base.Optional; +import com.google.inject.Inject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.annotation.PreDestroy; +import javax.inject.Named; +import javax.servlet.ServletContext; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; +import javax.ws.rs.core.*; +import javax.ws.rs.core.Response.Status; +import java.util.concurrent.atomic.AtomicLong; + +@Path("/hello-world") +@Produces(MediaType.APPLICATION_JSON) +public class HelloWorldResource { + + final Logger logger = LoggerFactory.getLogger(HelloWorldResource.class); + + private final String template; + private final String defaultName; + private final AtomicLong counter; + private final String sample; + private final HttpHeaders headers; + + @Inject + private Request ctx; + + @Inject + public HelloWorldResource(ConfigData config, + @Named("sample") String sample, + HttpHeaders headers) { + logger.info("Creating a new HelloWorldResource!"); + this.template = config.getTemplate(); + this.defaultName = config.getDefaultName(); + this.counter = new AtomicLong(); + this.sample = sample; + this.headers = headers; + } + + @GET + @Timed + public Saying sayHello(@QueryParam("name") Optional nameInput, @Context ServletContext context) { + logger.info("User-Agent: " + headers.getRequestHeader("User-Agent")); + logger.info(Integer.toString(ctx.hashCode())); + + String name = (nameInput.isPresent()) ? nameInput.get().data : defaultName; + return new Saying(counter.incrementAndGet(), String.format(template, name)); + } + + @GET + @Timed + @Path("ctx") + public Response checkCTX() { + if(ctx != null) return Response.ok().build(); + return Response.status(Status.INTERNAL_SERVER_ERROR).build(); + } + + @GET + @Timed + @Path("sample") + public String getSample() { + return sample; + } + + @PreDestroy + void destroy() { + logger.info("Destroying HelloWorldResource... :("); + } +} \ No newline at end of file diff --git a/src/test/java/com/hubspot/dropwizard/guice/sample/jersey/ParamInput.java b/src/test/java/com/hubspot/dropwizard/guice/sample/jersey/ParamInput.java new file mode 100644 index 0000000..00474e3 --- /dev/null +++ b/src/test/java/com/hubspot/dropwizard/guice/sample/jersey/ParamInput.java @@ -0,0 +1,5 @@ +package com.hubspot.dropwizard.guice.sample.jersey; + +public class ParamInput { + public String data; +} diff --git a/src/test/java/com/hubspot/dropwizard/guice/sample/jersey/SimpleParamConverterProvider.java b/src/test/java/com/hubspot/dropwizard/guice/sample/jersey/SimpleParamConverterProvider.java new file mode 100644 index 0000000..32cedc5 --- /dev/null +++ b/src/test/java/com/hubspot/dropwizard/guice/sample/jersey/SimpleParamConverterProvider.java @@ -0,0 +1,30 @@ +package com.hubspot.dropwizard.guice.sample.jersey; + +import javax.ws.rs.ext.ParamConverter; +import javax.ws.rs.ext.ParamConverterProvider; +import java.lang.annotation.Annotation; +import java.lang.reflect.Type; + +public class SimpleParamConverterProvider implements ParamConverterProvider { + + @Override + public ParamConverter getConverter(Class rawType, Type genericType, Annotation[] annotations) { + if(genericType.equals(ParamInput.class)) { + return new ParamConverter() { + + @Override + public T fromString(String value) { + ParamInput ret = new ParamInput(); + ret.data = value; + return (T) ret; + } + + @Override + public String toString(T value) { + return ((ParamInput) value).data; + } + }; + } + return null; + } +} diff --git a/src/test/java/com/hubspot/dropwizard/guice/util/CommandRunner.java b/src/test/java/com/hubspot/dropwizard/guice/util/CommandRunner.java new file mode 100644 index 0000000..a45762c --- /dev/null +++ b/src/test/java/com/hubspot/dropwizard/guice/util/CommandRunner.java @@ -0,0 +1,127 @@ +package com.hubspot.dropwizard.guice.util; + +import com.google.common.base.Optional; +import com.google.common.collect.ImmutableMap; +import io.dropwizard.Application; +import io.dropwizard.Configuration; +import io.dropwizard.cli.Command; +import io.dropwizard.setup.Bootstrap; +import io.dropwizard.testing.ConfigOverride; +import net.sourceforge.argparse4j.inf.Namespace; + +import java.util.Enumeration; +import java.util.Objects; + +public class CommandRunner { + private final Class> applicationClass; + private final Optional configPath; + private final String commandName; + private final Command command; + private final ConfigOverride[] configOverrides; + + private Application application; + private Bootstrap bootstrap; + private Namespace namespace; + + public CommandRunner(Class> applicationClass, + String configPath, + String commandName, + ConfigOverride... configOverrides) { + this.applicationClass = applicationClass; + this.configPath = Optional.fromNullable(configPath); + this.commandName = commandName; + this.command = null; + this.configOverrides = configOverrides; + } + + public CommandRunner(Class> applicationClass, + String commandName, + ConfigOverride... configOverrides) { + this.applicationClass = applicationClass; + this.configPath = Optional.absent(); + this.commandName = commandName; + this.command = null; + this.configOverrides = configOverrides; + } + + public CommandRunner(Class> applicationClass, + String configPath, + Command command, + ConfigOverride... configOverrides) { + this.applicationClass = applicationClass; + this.configPath = Optional.fromNullable(configPath); + this.commandName = null; + this.command = command; + this.configOverrides = configOverrides; + } + + public CommandRunner(Class> applicationClass, + Command command, + ConfigOverride... configOverrides) { + this.applicationClass = applicationClass; + this.configPath = Optional.absent(); + this.commandName = null; + this.command = command; + this.configOverrides = configOverrides; + } + + private void setConfigOverrides() { + for (ConfigOverride configOverride: configOverrides) { + configOverride.addToSystemProperties(); + } + } + + private void resetConfigOverrides() { + for (Enumeration props = System.getProperties().propertyNames(); props.hasMoreElements();) { + String keyString = (String) props.nextElement(); + if (keyString.startsWith("dw.")) { + System.clearProperty(keyString); + } + } + } + + public Application newApplication() { + try { + return application = applicationClass.newInstance(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + public Bootstrap newBootStrap() { + if(application == null) throw new RuntimeException("Application must be initialized before newBootStrap is called."); + return bootstrap = new Bootstrap<>(application); + } + + private void initialize() { + newApplication(); + newBootStrap(); + if(configPath.isPresent()) + namespace = new Namespace(ImmutableMap.of("file", configPath.get())); + else namespace = new Namespace(ImmutableMap.of()); + + application.initialize(bootstrap); + } + + private Command getCommand(String name) { + if(bootstrap == null) throw new RuntimeException("Must be initialized before getCommand is called."); + for(Command command : bootstrap.getCommands()) { + if(Objects.equals(command.getName(), name)) return command; + } + return null; + } + + public void run() { + setConfigOverrides(); + initialize(); + + try { + if (command != null) command.run(bootstrap, namespace); + else getCommand(commandName).run(bootstrap, namespace); + } catch (Exception e) { + throw new RuntimeException(e); + } + + resetConfigOverrides(); + } +} diff --git a/src/test/resources/hello-world.yml b/src/test/resources/hello-world.yml new file mode 100644 index 0000000..617b216 --- /dev/null +++ b/src/test/resources/hello-world.yml @@ -0,0 +1,12 @@ +template: Hello, %s! +defaultName: Joe + +subConfig: + moreData: something + +otherConfig: + otherData: hidden + +server: + type: simple + applicationContextPath: /v1 \ No newline at end of file diff --git a/src/test/resources/test-config.yml b/src/test/resources/test-config.yml new file mode 100644 index 0000000..733e890 --- /dev/null +++ b/src/test/resources/test-config.yml @@ -0,0 +1,7 @@ +server: + type: simple + applicationContextPath: / + adminContextPath: /admin + connector: + type: http + port: 9999 \ No newline at end of file