diff --git a/README.md b/README.md index 6abafc0..9d7e0fc 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,7 @@ public class HelloWorldApplication extends Application } ``` -Lastly, you can enable auto configuration via package scanning. +You can enable auto configuration via package scanning. ```java public class HelloWorldApplication extends Application { @@ -69,6 +69,9 @@ public class HelloWorldApplication extends Application .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 @@ -83,6 +86,94 @@ public class HelloWorldApplication extends Application } } ``` +Configuration data will be auto-injected and named. Use the provided @Config annotation to specify +the path to the configuration data to be injected. +```java + +public class HelloWorldConfiguration extends Configuration { + @JsonProperty + private String template; + + @JsonProperty + private Person defaultPerson = new Person(); + + public String getTemplate() { return template; } + + public Person getDefaultPerson() { return defaultPerson; } +} + +public class Person { + @JsonProperty + private String name = "Stranger"; + private String city = "Unknown"; + + public String getName() { return name; } +} + +public class HelloWorldModule extends AbstractModule { + + // configuration data is available for injection and named based on the fields in the configuration objects + @Inject + @Config("template") + private String template; + + // defaultPerson.name will only be available if the Person class is defined within the package path + // set by addConfigPackages (see below) + @Inject + @Config("defaultPerson.name") + private String defaultName; + + // A root config class may also be specified. The path provided will be relative to this root object. + @Inject + @Config(Person.class, "city") + private String defaultCity; + + @Override + protected void configure() { + } +} +``` + +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 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() + .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 diff --git a/pom.xml b/pom.xml old mode 100755 new mode 100644 index 03e64b0..9733521 --- a/pom.xml +++ b/pom.xml @@ -109,6 +109,12 @@ + + com.jayway.restassured + rest-assured + 2.4.0 + test + io.dropwizard dropwizard-testing diff --git a/src/main/java/com/hubspot/dropwizard/guice/AutoConfig.java b/src/main/java/com/hubspot/dropwizard/guice/AutoConfig.java index 0cfd714..03b57ea 100644 --- a/src/main/java/com/hubspot/dropwizard/guice/AutoConfig.java +++ b/src/main/java/com/hubspot/dropwizard/guice/AutoConfig.java @@ -1,11 +1,18 @@ 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 org.glassfish.jersey.server.model.Resource; @@ -21,6 +28,7 @@ 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 { @@ -55,15 +63,19 @@ public void run(Environment environment, Injector injector) { public void initialize(Bootstrap bootstrap, Injector injector) { addBundles(bootstrap, injector); - addConfiguredBundles(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); + } } } @@ -71,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); + } } } @@ -80,9 +96,13 @@ 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); + 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); + } } } @@ -106,27 +126,63 @@ private void addResources(Environment environment) { } } + @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); + } } - } - - @SuppressWarnings("unchecked") - private void addConfiguredBundles(Bootstrap bootstrap, Injector injector) { - Set> configuredBundleClasses = reflections - .getSubTypesOf(ConfiguredBundle.class); - for (Class configuredBundle : configuredBundleClasses) { - if (configuredBundle != GuiceBundle.class) { - bootstrap.addBundle(injector.getInstance(configuredBundle)); - logger.info("Added configured bundle class {} during bootstrap", configuredBundle); + 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); diff --git a/src/main/java/com/hubspot/dropwizard/guice/ConfigData/Config.java b/src/main/java/com/hubspot/dropwizard/guice/ConfigData/Config.java new file mode 100644 index 0000000..ff6cf31 --- /dev/null +++ b/src/main/java/com/hubspot/dropwizard/guice/ConfigData/Config.java @@ -0,0 +1,22 @@ +package com.hubspot.dropwizard.guice.ConfigData; + +import javax.inject.Qualifier; +import java.lang.annotation.Retention; +import java.lang.annotation.Documented; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +/** + * Guice {@linkplain Qualifier qualifier} that is bound + * to fields in Dropwizard configuration objects. + */ +@Qualifier +@Documented +@Retention(RUNTIME) +public @interface Config { + + /** The config path. */ + String value(); + + /** The root config object to which the path is relative */ + Class root() default void.class; +} diff --git a/src/main/java/com/hubspot/dropwizard/guice/ConfigData/ConfigDataModule.java b/src/main/java/com/hubspot/dropwizard/guice/ConfigData/ConfigDataModule.java new file mode 100644 index 0000000..a3afbe5 --- /dev/null +++ b/src/main/java/com/hubspot/dropwizard/guice/ConfigData/ConfigDataModule.java @@ -0,0 +1,153 @@ +package com.hubspot.dropwizard.guice.ConfigData; + +import com.google.common.base.Function; +import com.google.common.base.Joiner; +import com.google.common.base.Preconditions; +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import com.google.inject.AbstractModule; +import com.google.inject.Provider; +import io.dropwizard.Configuration; +import org.apache.commons.lang3.ArrayUtils; +import org.apache.commons.lang3.ClassUtils; +import org.apache.commons.lang3.reflect.FieldUtils; + +import java.lang.reflect.Field; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +import static com.google.common.base.Throwables.propagate; +import static java.lang.String.format; + +/** + * Binds fields in the configurationClasses. Names field using the + * @Config qualifier. + * @param + */ +public class ConfigDataModule extends AbstractModule { + private final T configuration; + private final String[] configurationPackages; + + public ConfigDataModule(T configuration, + String[] configurationPackages) { + this.configuration = Preconditions.checkNotNull(configuration); + Preconditions.checkNotNull(configurationPackages); + this.configurationPackages = ensureTypeInPackages(configuration.getClass(), configurationPackages); + } + + private String[] ensureTypeInPackages(Class type, String[] packages) { + String configName = type.getName(); + for(String pack : packages) { + if(configName.startsWith(pack)) return packages; + } + return ArrayUtils.add(packages, configName); + } + + @Override + protected void configure() { + bindConfigs(); + } + + private void bindConfigs() { + HashMap roots = new HashMap<>(); + roots.put(void.class, new String[0]); + bindConfigs(configuration.getClass(), roots, Lists.>newArrayList()); + } + @SuppressWarnings("unchecked") + private void bindConfigs(Class config, Map roots, List> visited) { + List> classes = Lists.newArrayList(ClassUtils.getAllSuperclasses(config)); + classes.add(config); + for(Class cls: classes) { + //Only ever use a given class as a root once. Additional uses will have conflicting paths. + boolean useAsRoot = false; + if(!visited.contains(cls)) { + useAsRoot = true; + visited.add(cls); + } + for(Field field: cls.getDeclaredFields()) { + Class type = field.getType(); + final String name = field.getName(); + + Map newRoots = Maps.newHashMap(Maps.transformValues(roots, new Function() { + @Override + public String[] apply(String[] path) { + String[] subpath = new String[path.length + 1]; + System.arraycopy(path, 0, subpath, 0, path.length); + subpath[path.length] = name; + return subpath; + } + })); + if(useAsRoot) newRoots.put(cls, new String[]{ name }); + ConfigElementProvider provider = new ConfigElementProvider(newRoots.get(void.class)); + + for (Entry root : newRoots.entrySet()) { + bind(type) + .annotatedWith(new ConfigImpl(root.getKey(), Joiner.on(".").join(root.getValue()))) + .toProvider(provider); + } + + if(!type.isEnum() && isInConfigPackage(type)) + bindConfigs(type, newRoots, visited); + } + } + } + + private boolean isInConfigPackage(Class type) { + String name = type.getName(); + if(name == null) return false; + + for(String pack : configurationPackages) { + if(name.startsWith(pack)) return true; + } + return false; + } + + private class ConfigElementProvider implements Provider { + private final Field[] path; + + public ConfigElementProvider(String[] path) { + this.path = new Field[path.length]; + + Class cls = configuration.getClass(); + for(int i=0; i cls, String name) { + Field f; + 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/ConfigData/ConfigImpl.java b/src/main/java/com/hubspot/dropwizard/guice/ConfigData/ConfigImpl.java new file mode 100644 index 0000000..5c0f692 --- /dev/null +++ b/src/main/java/com/hubspot/dropwizard/guice/ConfigData/ConfigImpl.java @@ -0,0 +1,58 @@ +package com.hubspot.dropwizard.guice.ConfigData; + +import com.hubspot.dropwizard.guice.ConfigData.Config; + +import static com.google.common.base.Preconditions.checkNotNull; + +import java.io.Serializable; +import java.lang.annotation.Annotation; + +public class ConfigImpl implements Config, Serializable { + + private final String value; + private final Class root; + + public ConfigImpl(String value) { + this.value = checkNotNull(value, "name"); + this.root = void.class; + } + + public ConfigImpl(Class root, String value) { + this.value = checkNotNull(value, "name"); + this.root = checkNotNull(root); + } + + public String value() { + return this.value; + } + + public Class root() { + return this.root; + } + + public int hashCode() { + // This is specified in java.lang.Annotation. + return ((127 * "value".hashCode()) ^ value.hashCode()) + + ((127 * "root".hashCode()) ^ root.hashCode()); + } + + public boolean equals(Object o) { + if (!(o instanceof Config)) { + return false; + } + + Config other = (Config) o; + return value.equals(other.value()) && + root.equals(other.root()); + } + + public String toString() { + return "@" + Config.class.getName() + "(root=" + root + ", " + "value=" + value + ")"; + } + + public Class annotationType() { + return Config.class; + } + + private static final long serialVersionUID = 0; +} diff --git a/src/main/java/com/hubspot/dropwizard/guice/DropwizardEnvironmentModule.java b/src/main/java/com/hubspot/dropwizard/guice/DropwizardEnvironmentModule.java index 57ace6f..4e9df22 100644 --- a/src/main/java/com/hubspot/dropwizard/guice/DropwizardEnvironmentModule.java +++ b/src/main/java/com/hubspot/dropwizard/guice/DropwizardEnvironmentModule.java @@ -1,16 +1,22 @@ package com.hubspot.dropwizard.guice; +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 com.google.inject.AbstractModule; -import com.google.inject.Provider; -import com.google.inject.Provides; -import com.google.inject.ProvisionException; +import net.sourceforge.argparse4j.inf.Namespace; + +import javax.servlet.ServletContext; 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) { @@ -20,32 +26,78 @@ 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); - } + if(configuration.isPresent()){ + bind(configurationClass).toProvider(provider); + if (configurationClass != Configuration.class) { + bind(Configuration.class).toProvider(provider); + } + } + if(environment.isPresent()) { + bindContext("application", environment.get().getApplicationContext()); + } } + /** + * Bind some of the context objects to be injectable. Annotated with a {@link com.google.inject.name.Names} to + * prevent collisions for any that the {@link com.google.inject.servlet.ServletModule} may bind later. + */ + private void bindContext(String name, MutableServletContextHandler context) { + bind(ServletContext.class) + .annotatedWith(Names.named(name)) + .toInstance(context.getServletContext()); + } + + @Deprecated public void setEnvironmentData(T configuration, Environment environment) { - this.configuration = configuration; - this.environment = environment; + setEnvironmentData(null, environment, configuration); } + public void setEnvironmentData(Bootstrap bootstrap, + Environment environment, + T configuration) { + this.bootstrap = Optional.fromNullable(bootstrap); + this.configuration = Optional.fromNullable(configuration); + this.environment = Optional.fromNullable(environment); + } + + public void setNamespace(Namespace namespace) { + this.namespace = Optional.fromNullable(namespace); + } + @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(); } } } diff --git a/src/main/java/com/hubspot/dropwizard/guice/GuiceBundle.java b/src/main/java/com/hubspot/dropwizard/guice/GuiceBundle.java index d101932..bddc1af 100644 --- a/src/main/java/com/hubspot/dropwizard/guice/GuiceBundle.java +++ b/src/main/java/com/hubspot/dropwizard/guice/GuiceBundle.java @@ -1,20 +1,27 @@ package com.hubspot.dropwizard.guice; +import java.util.List; + +import com.google.inject.*; +import com.hubspot.dropwizard.guice.ConfigData.ConfigDataModule; +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.Injector; -import com.google.inject.Module; -import com.google.inject.Stage; + import io.dropwizard.Configuration; import io.dropwizard.ConfiguredBundle; -import io.dropwizard.setup.Bootstrap; +import io.dropwizard.cli.Command; import io.dropwizard.setup.Environment; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.List; +import net.sourceforge.argparse4j.inf.Namespace; +import javax.servlet.ServletContextListener; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; public class GuiceBundle implements ConfiguredBundle { @@ -22,28 +29,72 @@ public class GuiceBundle implements ConfiguredBundle private final AutoConfig autoConfig; private final List modules; + private final List initModules; + private final List> contextListenerGenerators; + private final String[] configurationPackages; private final InjectorFactory injectorFactory; - private Injector injector; + + private Injector initInjector; + 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 InjectorFactory injectorFactory = new InjectorFactoryImpl(); - + List configurationPackages = new ArrayList<>(); + + /** + * 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); + return this; + } + public Builder setConfigClass(Class clazz) { configurationClass = Optional.of(clazz); return this; } + + /** + * Sets a list of base packages that may contain configuration objects. + * When config data is bound in the injector, classes within these + * packages will be recursed into. + */ + public Builder addConfigPackages(String... basePackages) { + Preconditions.checkNotNull(basePackages.length > 0); + configurationPackages.addAll(Arrays.asList(basePackages)); + return this; + } public Builder setInjectorFactory(InjectorFactory factory) { Preconditions.checkNotNull(factory); @@ -63,7 +114,8 @@ public GuiceBundle build() { } public GuiceBundle build(Stage s) { - return new GuiceBundle<>(s, autoConfig, modules, configurationClass, injectorFactory); + return new GuiceBundle<>(s, autoConfig, modules, initModules, contextListenerGenerators, injectorFactory, + configurationClass, configurationPackages.toArray(new String[0])); } } @@ -72,60 +124,126 @@ public static Builder newBuilder() { return new Builder<>(); } - private GuiceBundle(Stage stage, AutoConfig autoConfig, List modules, Optional> configurationClass, InjectorFactory injectorFactory) { + private GuiceBundle(Stage stage, + AutoConfig autoConfig, + List modules, + List initModules, + List> contextListenerGenerators, + InjectorFactory injectorFactory, + Optional> configurationClass, + String[] configurationPackages) { Preconditions.checkNotNull(modules); Preconditions.checkArgument(!modules.isEmpty()); + Preconditions.checkNotNull(contextListenerGenerators); Preconditions.checkNotNull(stage); + Preconditions.checkNotNull(configurationPackages); this.modules = modules; + this.initModules = initModules; + this.contextListenerGenerators = contextListenerGenerators; this.autoConfig = autoConfig; this.configurationClass = configurationClass; this.injectorFactory = injectorFactory; + this.configurationPackages = configurationPackages; this.stage = stage; } @Override public void initialize(Bootstrap bootstrap) { - if (configurationClass.isPresent()) { - dropwizardEnvironmentModule = new DropwizardEnvironmentModule<>(configurationClass.get()); - } else { - dropwizardEnvironmentModule = new DropwizardEnvironmentModule<>(Configuration.class); + initInjector(); + if (autoConfig != null) { + autoConfig.initialize(bootstrap, initInjector); } - modules.add(new JerseyModule()); - modules.add(dropwizardEnvironmentModule); - initInjector(); + setupCommands(bootstrap.getCommands()); + } - if (autoConfig != null) { - autoConfig.initialize(bootstrap, injector); + @SuppressWarnings("unchecked") + private void setupCommands(Collection commands) { + for(Command c : commands) { + if(c instanceof GuiceCommand) { + ((GuiceCommand) c).setInit(this); + } } } private void initInjector() { try { - injector = injectorFactory.create(this.stage,ImmutableList.copyOf(this.modules)); + 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); - } + logger.error("Exception occurred when creating Guice Injector - exiting", ie); + System.exit(1); + } } @Override public void run(final T configuration, final Environment environment) { - JerseyUtil.registerGuiceBound(injector, environment.jersey()); - JerseyUtil.registerGuiceFilter(environment); - setEnvironment(configuration, environment); + run(null, environment, configuration); + } + void run(Bootstrap bootstrap, Environment environment, final T configuration) { + initEnvironmentModule(); + setEnvironment(bootstrap, environment, configuration); + initGuice(environment, configuration); + 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)); + } + + 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); + } + + + void setNamespace(Namespace namespace) { + dropwizardEnvironmentModule.setNamespace(namespace); + } + + private void initEnvironmentModule() { + if (configurationClass.isPresent()) { + dropwizardEnvironmentModule = new DropwizardEnvironmentModule<>(configurationClass.get()); + } else { + dropwizardEnvironmentModule = new DropwizardEnvironmentModule<>(Configuration.class); } } @SuppressWarnings("unchecked") - private void setEnvironment(final T configuration, final Environment environment) { - dropwizardEnvironmentModule.setEnvironmentData(configuration, environment); + private void initGuice(final Environment environment, T configuration) { + List envModules = new ArrayList<>(); + envModules.add(dropwizardEnvironmentModule); + if(configuration != null) envModules.add(new ConfigDataModule(configuration, configurationPackages)); + Injector environmentInjector = initInjector.createChildInjector(envModules); + + //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. + if(configuration != null) { + for (Module module : modules) + environmentInjector.injectMembers(module); + + 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/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/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 index 216de66..0020953 100644 --- a/src/test/java/com/hubspot/dropwizard/guice/AutoConfigTest.java +++ b/src/test/java/com/hubspot/dropwizard/guice/AutoConfigTest.java @@ -1,5 +1,6 @@ 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.*; @@ -35,13 +36,14 @@ public class AutoConfigTest { @Before public void setUp() { //when - autoConfig = new AutoConfig(getClass().getPackage().getName()); + 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 @@ -84,7 +86,7 @@ public void addResources() { public void interfaceResourcesNotAdded() { //when autoConfig.run(environment, injector); - + injector.getProvider(JitResource.class); //then Set> components = environment.jersey().getResourceConfig().getClasses(); assertThat(components).doesNotContain(ResourceInterface.class); diff --git a/src/test/java/com/hubspot/dropwizard/guice/GuiceBundleTest.java b/src/test/java/com/hubspot/dropwizard/guice/GuiceBundleTest.java index c430d62..a6cd947 100644 --- a/src/test/java/com/hubspot/dropwizard/guice/GuiceBundleTest.java +++ b/src/test/java/com/hubspot/dropwizard/guice/GuiceBundleTest.java @@ -1,5 +1,6 @@ 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; @@ -19,6 +20,7 @@ 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 { @@ -41,6 +43,7 @@ public void setUp() { .addModule(new TestModule()) .build(); Bootstrap bootstrap = mock(Bootstrap.class); + when(bootstrap.getCommands()).thenReturn(ImmutableList.of()); guiceBundle.initialize(bootstrap); guiceBundle.run(new Configuration(), environment); } @@ -48,13 +51,13 @@ public void setUp() { @Test public void createsInjectorWhenInit() throws ServletException { //then - Injector injector = guiceBundle.getInjector(); + Injector injector = guiceBundle.getInjector().get(); assertThat(injector).isNotNull(); } @Test public void serviceLocatorIsAvaliable () throws ServletException { - ServiceLocator serviceLocator = guiceBundle.getInjector().getInstance(ServiceLocator.class); + 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/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/IntegrationTest.java b/src/test/java/com/hubspot/dropwizard/guice/IntegrationTest.java new file mode 100644 index 0000000..ea4baf4 --- /dev/null +++ b/src/test/java/com/hubspot/dropwizard/guice/IntegrationTest.java @@ -0,0 +1,109 @@ +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.ConfigData.ConfigImpl; +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 nested_configuration_injection() throws Exception { + assertEquals("something", + injector.getInstance(Key.get(String.class, new ConfigImpl("subConfig.moreData")))); + } + + @Test + public void nested_configuration_outside_of_scope() { + OtherConfig oc = injector.getInstance(Key.get(OtherConfig.class, new ConfigImpl("otherConfig"))); + assertEquals("hidden",oc.getOtherData()); + //Otherconfig is not within the package scope defined by 'addConfigPackages' + //So its contents should not be avaliable. + try { + injector.getInstance(Key.get(String.class, new ConfigImpl("otherConfig.otherData"))); + assertFalse(true); + } catch(ConfigurationException e) { + } + } + + @Test + public void dependent_module_gets_injected() throws Exception { + assertEquals("More data is: something", + injector.getInstance(Key.get(String.class, Names.named("dependent")))); + } + + @Test + public void nested_config_injection_in_resource() throws Exception { + get("/v1/hello-world/moredata1").then().body(equalTo("something")); + } + + @Test + public void nested_config_injection_from_constructor_in_resource() throws Exception { + get("/v1/hello-world/moredata2").then().body(equalTo("something")); + } + + @Test + public void nested_config_injection_with_main_root_in_resource() throws Exception { + get("/v1/hello-world/moredata3").then().body(equalTo("something")); + } + + @Test + public void nested_config_injection_with_subroot_in_resource() throws Exception { + get("/v1/hello-world/moredata4").then().body(equalTo("something")); + } + + @Test + public void nested_config_injection_from_subconfig_in_resource() throws Exception { + get("/v1/hello-world/moredata5").then().body(equalTo("something")); + } +} 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/ExplicitResource.java b/src/test/java/com/hubspot/dropwizard/guice/objects/ExplicitResource.java index 9decc64..d3ead1f 100644 --- a/src/test/java/com/hubspot/dropwizard/guice/objects/ExplicitResource.java +++ b/src/test/java/com/hubspot/dropwizard/guice/objects/ExplicitResource.java @@ -14,7 +14,7 @@ public class ExplicitResource { @Inject public ExplicitResource(ExplicitDAO dao) { - this.dao = dao;; + this.dao = dao; } @GET diff --git a/src/test/java/com/hubspot/dropwizard/guice/objects/InjectedTask.java b/src/test/java/com/hubspot/dropwizard/guice/objects/InjectedTask.java index 17c2be3..b4d112f 100644 --- a/src/test/java/com/hubspot/dropwizard/guice/objects/InjectedTask.java +++ b/src/test/java/com/hubspot/dropwizard/guice/objects/InjectedTask.java @@ -2,14 +2,13 @@ import com.google.common.collect.ImmutableMultimap; import com.google.inject.Singleton; -import io.dropwizard.servlets.tasks.Task; import javax.inject.Inject; import javax.inject.Named; import java.io.PrintWriter; @Singleton -public class InjectedTask extends Task { +public class InjectedTask extends AbstractTask { @Inject protected InjectedTask(@Named("TestTaskName") String name) { 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..129446c --- /dev/null +++ b/src/test/java/com/hubspot/dropwizard/guice/sample/HelloWorldApplication.java @@ -0,0 +1,41 @@ +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) + .addConfigPackages(SubConfig.class.getPackage().getName()) + .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..de44fdf --- /dev/null +++ b/src/test/java/com/hubspot/dropwizard/guice/sample/command/TestConfiguredCommand.java @@ -0,0 +1,27 @@ +package com.hubspot.dropwizard.guice.sample.command; + +import com.hubspot.dropwizard.guice.ConfigData.Config; +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(@Config("defaultName") String name, + Bootstrap bootstrap, + Namespace namespace) { + TestConfiguredCommand.configName = name; + 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..c401629 --- /dev/null +++ b/src/test/java/com/hubspot/dropwizard/guice/sample/command/TestEnvironmentCommand.java @@ -0,0 +1,31 @@ +package com.hubspot.dropwizard.guice.sample.command; + +import com.hubspot.dropwizard.guice.ConfigData.Config; +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(@Config("defaultName") String name, + Environment environment, + Namespace namespace) { + TestEnvironmentCommand.configName = name; + 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..9fef7c6 --- /dev/null +++ b/src/test/java/com/hubspot/dropwizard/guice/sample/guice/ConfigData.java @@ -0,0 +1,21 @@ +package com.hubspot.dropwizard.guice.sample.guice; + +import com.hubspot.dropwizard.guice.ConfigData.Config; + +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 + @Config("template") + private String template; + + @Inject + @Config("defaultName") + private String defaultName; + + public String getTemplate() { return template; } + + public String getDefaultName() { return defaultName; } +} 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..fb000ba --- /dev/null +++ b/src/test/java/com/hubspot/dropwizard/guice/sample/guice/DependentModule.java @@ -0,0 +1,18 @@ +package com.hubspot.dropwizard.guice.sample.guice; + +import com.google.inject.AbstractModule; +import com.google.inject.Inject; +import com.google.inject.name.Names; +import com.hubspot.dropwizard.guice.ConfigData.Config; + +public class DependentModule extends AbstractModule { + + @Inject + @Config("subConfig.moreData") + private String injectedData; + + @Override + protected void configure() { + bind(String.class).annotatedWith(Names.named("dependent")).toInstance("More data is: " + injectedData); + } +} 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..e364649 --- /dev/null +++ b/src/test/java/com/hubspot/dropwizard/guice/sample/guice/HelloWorldModule.java @@ -0,0 +1,20 @@ +package com.hubspot.dropwizard.guice.sample.guice; + +import com.google.inject.AbstractModule; +import com.google.inject.Provides; +import com.hubspot.dropwizard.guice.ConfigData.Config; + +public class HelloWorldModule extends AbstractModule { + + @Override + protected void configure() { + + } + + @Provides + @Config("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..c96eb3f --- /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.ConfigData.Config; +import com.hubspot.dropwizard.guice.InjectableHealthCheck; + +@Singleton +public class TemplateHealthCheck extends InjectableHealthCheck { + + private final String template; + + @Inject + public TemplateHealthCheck(@Config("template") String template) { + this.template = template; + } + + @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..a3d2f45 --- /dev/null +++ b/src/test/java/com/hubspot/dropwizard/guice/sample/jersey/HelloWorldResource.java @@ -0,0 +1,131 @@ +package com.hubspot.dropwizard.guice.sample.jersey; + +import com.codahale.metrics.annotation.Timed; +import com.hubspot.dropwizard.guice.ConfigData.Config; +import com.hubspot.dropwizard.guice.sample.HelloWorldConfiguration; +import com.hubspot.dropwizard.guice.sample.config.SubConfig; +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.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 + @Config("subConfig.moreData") + private String moreData1; + private String moreData2; + @Inject + @Config(root = HelloWorldConfiguration.class, value = "subConfig.moreData") + private String moreData3; + @Inject + @Config(root = SubConfig.class, value = "moreData") + private String moreData4; + @Inject + @Config("subConfig") + private SubConfig subConfig; + + @Inject + public HelloWorldResource(ConfigData config, + @Config("sample") String sample, + @Config("subConfig.moreData") String moreData2, + HttpHeaders headers) { + logger.info("Creating a new HelloWorldResource!"); + this.template = config.getTemplate(); + this.defaultName = config.getDefaultName(); + this.counter = new AtomicLong(); + this.sample = sample; + this.moreData2 = moreData2; + 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; + } + + @GET + @Timed + @Path("moredata1") + public String getMoreData1() { + return moreData1; + } + + @GET + @Timed + @Path("moredata2") + public String getMoreData2() { + return moreData2; + } + + @GET + @Timed + @Path("moredata3") + public String getMoreData3() { + return moreData3; + } + + @GET + @Timed + @Path("moredata4") + public String getMoreData4() { + return moreData4; + } + + @GET + @Timed + @Path("moredata5") + public String getMoreData5() { + return subConfig.getMoreData(); + } + + @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