Skip to content

Commit 6113917

Browse files
author
Dan Jasek
committed
Readd @config code. Move it to its own Guice module.
1 parent df973aa commit 6113917

14 files changed

Lines changed: 436 additions & 30 deletions

File tree

README.md

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,54 @@ public class HelloWorldApplication extends Application<HelloWorldConfiguration>
8585
// you must have your health checks inherit from InjectableHealthCheck in order for them to be injected
8686
}
8787
}
88+
```
89+
Configuration data will be auto-injected and named. Use the provided @Config annotation to specify
90+
the path to the configuration data to be injected.
91+
```java
92+
93+
public class HelloWorldConfiguration extends Configuration {
94+
@JsonProperty
95+
private String template;
96+
97+
@JsonProperty
98+
private Person defaultPerson = new Person();
99+
100+
public String getTemplate() { return template; }
101+
102+
public Person getDefaultPerson() { return defaultPerson; }
103+
}
104+
105+
public class Person {
106+
@JsonProperty
107+
private String name = "Stranger";
108+
private String city = "Unknown";
109+
110+
public String getName() { return name; }
111+
}
112+
113+
public class HelloWorldModule extends AbstractModule {
114+
115+
// configuration data is available for injection and named based on the fields in the configuration objects
116+
@Inject
117+
@Config("template")
118+
private String template;
119+
120+
// defaultPerson.name will only be available if the Person class is defined within the package path
121+
// set by addConfigPackages (see below)
122+
@Inject
123+
@Config("defaultPerson.name")
124+
private String defaultName;
125+
126+
// A root config class may also be specified. The path provided will be relative to this root object.
127+
@Inject
128+
@Config(Person.class, "city")
129+
private String defaultCity;
130+
131+
@Override
132+
protected void configure() {
133+
}
134+
}
135+
```
88136

89137
Modules will also be injected before being added. Field injections only, constructor based injections will not be available.
90138
Configuration data and initialization module data will be available for injecting into modules.
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package com.hubspot.dropwizard.guice.ConfigData;
2+
3+
import javax.inject.Qualifier;
4+
import java.lang.annotation.Retention;
5+
import java.lang.annotation.Documented;
6+
import static java.lang.annotation.RetentionPolicy.RUNTIME;
7+
8+
/**
9+
* Guice {@linkplain Qualifier qualifier} that is bound
10+
* to fields in Dropwizard configuration objects.
11+
*/
12+
@Qualifier
13+
@Documented
14+
@Retention(RUNTIME)
15+
public @interface Config {
16+
17+
/** The config path. */
18+
String value();
19+
20+
/** The root config object to which the path is relative */
21+
Class root() default void.class;
22+
}
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
package com.hubspot.dropwizard.guice.ConfigData;
2+
3+
import com.google.common.base.Function;
4+
import com.google.common.base.Joiner;
5+
import com.google.common.base.Preconditions;
6+
import com.google.common.collect.Lists;
7+
import com.google.common.collect.Maps;
8+
import com.google.inject.AbstractModule;
9+
import com.google.inject.Provider;
10+
import io.dropwizard.Configuration;
11+
import org.apache.commons.lang3.ArrayUtils;
12+
import org.apache.commons.lang3.ClassUtils;
13+
import org.apache.commons.lang3.reflect.FieldUtils;
14+
15+
import java.lang.reflect.Field;
16+
import java.util.HashMap;
17+
import java.util.List;
18+
import java.util.Map;
19+
import java.util.Map.Entry;
20+
21+
import static com.google.common.base.Throwables.propagate;
22+
import static java.lang.String.format;
23+
24+
/**
25+
* Binds fields in the configurationClasses. Names field using the
26+
* @Config qualifier.
27+
* @param <T>
28+
*/
29+
public class ConfigDataModule<T extends Configuration> extends AbstractModule {
30+
private final T configuration;
31+
private final String[] configurationPackages;
32+
33+
public ConfigDataModule(T configuration,
34+
String[] configurationPackages) {
35+
this.configuration = Preconditions.checkNotNull(configuration);
36+
Preconditions.checkNotNull(configurationPackages);
37+
this.configurationPackages = ensureTypeInPackages(configuration.getClass(), configurationPackages);
38+
}
39+
40+
private String[] ensureTypeInPackages(Class<?> type, String[] packages) {
41+
String configName = type.getName();
42+
for(String pack : packages) {
43+
if(configName.startsWith(pack)) return packages;
44+
}
45+
return ArrayUtils.add(packages, configName);
46+
}
47+
48+
@Override
49+
protected void configure() {
50+
bindConfigs();
51+
}
52+
53+
private void bindConfigs() {
54+
HashMap<Class, String[]> roots = new HashMap<>();
55+
roots.put(void.class, new String[0]);
56+
bindConfigs(configuration.getClass(), roots, Lists.<Class<?>>newArrayList());
57+
}
58+
@SuppressWarnings("unchecked")
59+
private void bindConfigs(Class<?> config, Map<Class,String[]> roots, List<Class<?>> visited) {
60+
List<Class<?>> classes = Lists.newArrayList(ClassUtils.getAllSuperclasses(config));
61+
classes.add(config);
62+
for(Class<?> cls: classes) {
63+
//Only ever use a given class as a root once. Additional uses will have conflicting paths.
64+
boolean useAsRoot = false;
65+
if(!visited.contains(cls)) {
66+
useAsRoot = true;
67+
visited.add(cls);
68+
}
69+
for(Field field: cls.getDeclaredFields()) {
70+
Class<?> type = field.getType();
71+
final String name = field.getName();
72+
73+
Map<Class, String[]> newRoots = Maps.newHashMap(Maps.transformValues(roots, new Function<String[], String[]>() {
74+
@Override
75+
public String[] apply(String[] path) {
76+
String[] subpath = new String[path.length + 1];
77+
System.arraycopy(path, 0, subpath, 0, path.length);
78+
subpath[path.length] = name;
79+
return subpath;
80+
}
81+
}));
82+
if(useAsRoot) newRoots.put(cls, new String[]{ name });
83+
ConfigElementProvider provider = new ConfigElementProvider(newRoots.get(void.class));
84+
85+
for (Entry<Class, String[]> root : newRoots.entrySet()) {
86+
bind(type)
87+
.annotatedWith(new ConfigImpl(root.getKey(), Joiner.on(".").join(root.getValue())))
88+
.toProvider(provider);
89+
}
90+
91+
if(!type.isEnum() && isInConfigPackage(type))
92+
bindConfigs(type, newRoots, visited);
93+
}
94+
}
95+
}
96+
97+
private boolean isInConfigPackage(Class<?> type) {
98+
String name = type.getName();
99+
if(name == null) return false;
100+
101+
for(String pack : configurationPackages) {
102+
if(name.startsWith(pack)) return true;
103+
}
104+
return false;
105+
}
106+
107+
private class ConfigElementProvider<U> implements Provider<U> {
108+
private final Field[] path;
109+
110+
public ConfigElementProvider(String[] path) {
111+
this.path = new Field[path.length];
112+
113+
Class<?> cls = configuration.getClass();
114+
for(int i=0; i<path.length; i++) {
115+
this.path[i] = findField(cls, path[i]);
116+
cls = this.path[i].getType();
117+
}
118+
}
119+
120+
private Field findField(final Class<?> cls, String name) {
121+
Field f;
122+
Class<?> search = cls;
123+
do {
124+
f = FieldUtils.getDeclaredField(search, name, true);
125+
if(f != null)
126+
return f;
127+
else
128+
search = search.getSuperclass();
129+
130+
} while(!search.equals(Object.class));
131+
132+
throw new IllegalStateException(format("Unable to find field %s on %s", name, cls.getName()));
133+
}
134+
135+
@Override
136+
public U get() {
137+
Object obj = configuration;
138+
for(Field field: path) {
139+
try {
140+
obj = field.get(obj);
141+
if (obj == null) {
142+
return null; // Should cause an injection exception
143+
}
144+
145+
} catch(IllegalAccessException e) {
146+
throw propagate(e);
147+
}
148+
}
149+
150+
return (U) obj;
151+
}
152+
}
153+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package com.hubspot.dropwizard.guice.ConfigData;
2+
3+
import com.hubspot.dropwizard.guice.ConfigData.Config;
4+
5+
import static com.google.common.base.Preconditions.checkNotNull;
6+
7+
import java.io.Serializable;
8+
import java.lang.annotation.Annotation;
9+
10+
public class ConfigImpl implements Config, Serializable {
11+
12+
private final String value;
13+
private final Class root;
14+
15+
public ConfigImpl(String value) {
16+
this.value = checkNotNull(value, "name");
17+
this.root = void.class;
18+
}
19+
20+
public ConfigImpl(Class root, String value) {
21+
this.value = checkNotNull(value, "name");
22+
this.root = checkNotNull(root);
23+
}
24+
25+
public String value() {
26+
return this.value;
27+
}
28+
29+
public Class root() {
30+
return this.root;
31+
}
32+
33+
public int hashCode() {
34+
// This is specified in java.lang.Annotation.
35+
return ((127 * "value".hashCode()) ^ value.hashCode()) +
36+
((127 * "root".hashCode()) ^ root.hashCode());
37+
}
38+
39+
public boolean equals(Object o) {
40+
if (!(o instanceof Config)) {
41+
return false;
42+
}
43+
44+
Config other = (Config) o;
45+
return value.equals(other.value()) &&
46+
root.equals(other.root());
47+
}
48+
49+
public String toString() {
50+
return "@" + Config.class.getName() + "(root=" + root + ", " + "value=" + value + ")";
51+
}
52+
53+
public Class<? extends Annotation> annotationType() {
54+
return Config.class;
55+
}
56+
57+
private static final long serialVersionUID = 0;
58+
}

src/main/java/com/hubspot/dropwizard/guice/GuiceBundle.java

Lines changed: 29 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import java.util.List;
44

55
import com.google.inject.*;
6+
import com.hubspot.dropwizard.guice.ConfigData.ConfigDataModule;
67
import io.dropwizard.setup.Bootstrap;
78
import org.slf4j.Logger;
89
import org.slf4j.LoggerFactory;
@@ -30,6 +31,7 @@ public class GuiceBundle<T extends Configuration> implements ConfiguredBundle<T>
3031
private final List<Module> modules;
3132
private final List<Module> initModules;
3233
private final List<Function<Injector, ServletContextListener>> contextListenerGenerators;
34+
private final String[] configurationPackages;
3335
private final InjectorFactory injectorFactory;
3436

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

4952
/**
5053
* Add a module to the bundle.
@@ -81,6 +84,17 @@ public Builder<T> setConfigClass(Class<T> clazz) {
8184
configurationClass = Optional.of(clazz);
8285
return this;
8386
}
87+
88+
/**
89+
* Sets a list of base packages that may contain configuration objects.
90+
* When config data is bound in the injector, classes within these
91+
* packages will be recursed into.
92+
*/
93+
public Builder<T> addConfigPackages(String... basePackages) {
94+
Preconditions.checkNotNull(basePackages.length > 0);
95+
configurationPackages.addAll(Arrays.asList(basePackages));
96+
return this;
97+
}
8498

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

102116
public GuiceBundle<T> build(Stage s) {
103117
return new GuiceBundle<>(s, autoConfig, modules, initModules, contextListenerGenerators, injectorFactory,
104-
configurationClass);
118+
configurationClass, configurationPackages.toArray(new String[0]));
105119
}
106120

107121
}
@@ -116,17 +130,20 @@ private GuiceBundle(Stage stage,
116130
List<Module> initModules,
117131
List<Function<Injector, ServletContextListener>> contextListenerGenerators,
118132
InjectorFactory injectorFactory,
119-
Optional<Class<T>> configurationClass) {
133+
Optional<Class<T>> configurationClass,
134+
String[] configurationPackages) {
120135
Preconditions.checkNotNull(modules);
121136
Preconditions.checkArgument(!modules.isEmpty());
122137
Preconditions.checkNotNull(contextListenerGenerators);
123138
Preconditions.checkNotNull(stage);
139+
Preconditions.checkNotNull(configurationPackages);
124140
this.modules = modules;
125141
this.initModules = initModules;
126142
this.contextListenerGenerators = contextListenerGenerators;
127143
this.autoConfig = autoConfig;
128144
this.configurationClass = configurationClass;
129145
this.injectorFactory = injectorFactory;
146+
this.configurationPackages = configurationPackages;
130147
this.stage = stage;
131148
}
132149

@@ -165,10 +182,7 @@ public void run(final T configuration, final Environment environment) {
165182
void run(Bootstrap<T> bootstrap, Environment environment, final T configuration) {
166183
initEnvironmentModule();
167184
setEnvironment(bootstrap, environment, configuration);
168-
//The secondary injected modules generally use config data. If we are starting up a command
169-
//that doesn't have a configuration, loading these modules is useless at best.
170-
boolean addModules = configuration != null;
171-
initGuice(environment, addModules);
185+
initGuice(environment, configuration);
172186
Injector injector = getInjector().get();
173187

174188
if(environment != null) {
@@ -203,10 +217,16 @@ private void initEnvironmentModule() {
203217
}
204218
}
205219

206-
private void initGuice(final Environment environment, boolean addModules) {
207-
Injector environmentInjector = initInjector.createChildInjector(dropwizardEnvironmentModule);
220+
@SuppressWarnings("unchecked")
221+
private void initGuice(final Environment environment, T configuration) {
222+
List<Module> envModules = new ArrayList<>();
223+
envModules.add(dropwizardEnvironmentModule);
224+
if(configuration != null) envModules.add(new ConfigDataModule(configuration, configurationPackages));
225+
Injector environmentInjector = initInjector.createChildInjector(envModules);
208226

209-
if(addModules) {
227+
//The secondary injected modules generally use config data. If we are starting up a command
228+
//that doesn't have a configuration, loading these modules is useless at best.
229+
if(configuration != null) {
210230
for (Module module : modules)
211231
environmentInjector.injectMembers(module);
212232

0 commit comments

Comments
 (0)