Skip to content

Commit

Permalink
Readd @config code. Move it to its own Guice module.
Browse files Browse the repository at this point in the history
  • Loading branch information
Dan Jasek committed Mar 2, 2015
1 parent df973aa commit 6113917
Show file tree
Hide file tree
Showing 14 changed files with 436 additions and 30 deletions.
48 changes: 48 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,54 @@ public class HelloWorldApplication extends Application<HelloWorldConfiguration>
// you must have your health checks inherit from InjectableHealthCheck in order for them to be injected
}
}
```
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.
Expand Down
22 changes: 22 additions & 0 deletions src/main/java/com/hubspot/dropwizard/guice/ConfigData/Config.java
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
@@ -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 <T>
*/
public class ConfigDataModule<T extends Configuration> 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<Class, String[]> roots = new HashMap<>();
roots.put(void.class, new String[0]);
bindConfigs(configuration.getClass(), roots, Lists.<Class<?>>newArrayList());
}
@SuppressWarnings("unchecked")
private void bindConfigs(Class<?> config, Map<Class,String[]> roots, List<Class<?>> visited) {
List<Class<?>> 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<Class, String[]> newRoots = Maps.newHashMap(Maps.transformValues(roots, new Function<String[], String[]>() {
@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<Class, String[]> 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<U> implements Provider<U> {
private final Field[] path;

public ConfigElementProvider(String[] path) {
this.path = new Field[path.length];

Class<?> cls = configuration.getClass();
for(int i=0; i<path.length; i++) {
this.path[i] = findField(cls, path[i]);
cls = this.path[i].getType();
}
}

private Field findField(final Class<?> 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;
}
}
}
Original file line number Diff line number Diff line change
@@ -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<? extends Annotation> annotationType() {
return Config.class;
}

private static final long serialVersionUID = 0;
}
38 changes: 29 additions & 9 deletions src/main/java/com/hubspot/dropwizard/guice/GuiceBundle.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
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;
Expand Down Expand Up @@ -30,6 +31,7 @@ public class GuiceBundle<T extends Configuration> implements ConfiguredBundle<T>
private final List<Module> modules;
private final List<Module> initModules;
private final List<Function<Injector, ServletContextListener>> contextListenerGenerators;
private final String[] configurationPackages;
private final InjectorFactory injectorFactory;

private Injector initInjector;
Expand All @@ -45,6 +47,7 @@ public static class Builder<T extends Configuration> {
private List<Function<Injector, ServletContextListener>> contextListenerGenerators = Lists.newArrayList();
private Optional<Class<T>> configurationClass = Optional.absent();
private InjectorFactory injectorFactory = new InjectorFactoryImpl();
List<String> configurationPackages = new ArrayList<>();

/**
* Add a module to the bundle.
Expand Down Expand Up @@ -81,6 +84,17 @@ public Builder<T> setConfigClass(Class<T> 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<T> addConfigPackages(String... basePackages) {
Preconditions.checkNotNull(basePackages.length > 0);
configurationPackages.addAll(Arrays.asList(basePackages));
return this;
}

public Builder<T> setInjectorFactory(InjectorFactory factory) {
Preconditions.checkNotNull(factory);
Expand All @@ -101,7 +115,7 @@ public GuiceBundle<T> build() {

public GuiceBundle<T> build(Stage s) {
return new GuiceBundle<>(s, autoConfig, modules, initModules, contextListenerGenerators, injectorFactory,
configurationClass);
configurationClass, configurationPackages.toArray(new String[0]));
}

}
Expand All @@ -116,17 +130,20 @@ private GuiceBundle(Stage stage,
List<Module> initModules,
List<Function<Injector, ServletContextListener>> contextListenerGenerators,
InjectorFactory injectorFactory,
Optional<Class<T>> configurationClass) {
Optional<Class<T>> 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;
}

Expand Down Expand Up @@ -165,10 +182,7 @@ public void run(final T configuration, final Environment environment) {
void run(Bootstrap<T> 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);
initGuice(environment, configuration);
Injector injector = getInjector().get();

if(environment != null) {
Expand Down Expand Up @@ -203,10 +217,16 @@ private void initEnvironmentModule() {
}
}

private void initGuice(final Environment environment, boolean addModules) {
Injector environmentInjector = initInjector.createChildInjector(dropwizardEnvironmentModule);
@SuppressWarnings("unchecked")
private void initGuice(final Environment environment, T configuration) {
List<Module> envModules = new ArrayList<>();
envModules.add(dropwizardEnvironmentModule);
if(configuration != null) envModules.add(new ConfigDataModule(configuration, configurationPackages));
Injector environmentInjector = initInjector.createChildInjector(envModules);

if(addModules) {
//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);

Expand Down
Loading

0 comments on commit 6113917

Please sign in to comment.