/sitebricks-options/src/main/java/com/google/sitebricks/options/OptionsModule.java
http://github.com/dhanji/sitebricks · Java · 199 lines · 159 code · 31 blank · 9 comment · 25 complexity · 75a81a7d96cc35b78b1ab3a4955ef9b3 MD5 · raw file
- package com.google.sitebricks.options;
- import com.google.common.collect.ImmutableList;
- import com.google.common.collect.Lists;
- import com.google.common.collect.Maps;
- import com.google.inject.AbstractModule;
- import com.google.inject.Inject;
- import net.sf.cglib.proxy.Enhancer;
- import net.sf.cglib.proxy.MethodInterceptor;
- import net.sf.cglib.proxy.MethodProxy;
- import java.lang.reflect.InvocationHandler;
- import java.lang.reflect.Method;
- import java.lang.reflect.Modifier;
- import java.lang.reflect.Proxy;
- import java.util.*;
- import java.util.logging.Logger;
- /**
- * @author dhanji@gmail.com (Dhanji R. Prasanna)
- */
- public class OptionsModule extends AbstractModule {
- private final Map<String, String> options;
- private final List<Class<?>> optionClasses = new ArrayList<Class<?>>();
- public OptionsModule(String[] commandLine, Iterable<Map<String, String>> freeOptions) {
- options = new HashMap<String, String>(commandLine.length);
- for (String option : commandLine) {
- if (option.startsWith("--") && option.length() > 2) {
- option = option.substring(2);
- String[] pair = option.split("=", 2);
- if (pair.length == 1) {
- options.put(pair[0], Boolean.TRUE.toString());
- } else {
- options.put(pair[0], pair[1]);
- }
- }
- }
- for (Map<String, String> freeOptionMap : freeOptions) {
- options.putAll(freeOptionMap);
- }
- }
- public OptionsModule(String[] commandLine) {
- this(commandLine, ImmutableList.<Map<String, String>>of());
- }
- public OptionsModule(Iterable<Map<String, String>> freeOptions) {
- this(new String[0], freeOptions);
- }
- public OptionsModule(Properties... freeOptions) {
- this(new String[0], toMaps(freeOptions));
- }
- public OptionsModule(ResourceBundle... freeOptions) {
- this(new String[0], toMaps(freeOptions));
- }
- private static Iterable<Map<String, String>> toMaps(ResourceBundle[] freeOptions) {
- List<Map<String, String>> maps = Lists.newArrayList();
- for (ResourceBundle bundle : freeOptions) {
- Map<String, String> asMap = Maps.newHashMap();
- Enumeration<String> keys = bundle.getKeys();
- while (keys.hasMoreElements()) {
- String key = keys.nextElement();
- asMap.put(key, bundle.getString(key));
- }
- maps.add(asMap);
- }
- return maps;
- }
- private static Iterable<Map<String, String>> toMaps(Properties[] freeOptions) {
- List<Map<String, String>> maps = Lists.newArrayList();
- for (Properties freeOption : freeOptions) {
- maps.add(Maps.fromProperties(freeOption));
- }
- return maps;
- }
- @Override
- protected final void configure() {
- // Analyze options classes.
- for (Class<?> optionClass : optionClasses) {
- // If using abstract classes, detect cglib.
- if (Modifier.isAbstract(optionClass.getModifiers())) {
- try {
- Class.forName("net.sf.cglib.proxy.Enhancer");
- } catch (ClassNotFoundException e) {
- String message = String.format("Cannot use abstract @Option classes unless Cglib is on the classpath, " +
- "[%s] was abstract. Hint: add Cglib 2.0.2 or better to classpath",
- optionClass.getName());
- Logger.getLogger(Options.class.getName()).severe(message);
- addError(message);
- }
- }
- String namespace = optionClass.getAnnotation(Options.class).value();
- if (!namespace.isEmpty())
- namespace += ".";
- // Construct a map that will contain the values needed to back the interface.
- final Map<String, String> concreteOptions =
- new HashMap<String, String>(optionClass.getDeclaredMethods().length);
- boolean skipClass = false;
- for (Method method : optionClass.getDeclaredMethods()) {
- String key = namespace + method.getName();
- String value = options.get(key);
- // Gather all the errors regarding @Options methods that have no specified config.
- if (null == value && Modifier.isAbstract(method.getModifiers())) {
- addError("Option '%s' specified in type [%s] is unavailable in provided configuration",
- key,
- optionClass);
- skipClass = true;
- break;
- }
- // TODO Can we validate that the value is coercible into the return type correctly?
- concreteOptions.put(method.getName(), value);
- }
- if (!skipClass) {
- Object instance;
- if (optionClass.isInterface()) {
- instance = createJdkProxyHandler(optionClass, concreteOptions);
- } else {
- instance = createCglibHandler(optionClass, concreteOptions);
- }
- bindToInstance(optionClass, instance);
- }
- }
- }
- @SuppressWarnings("unchecked")
- private void bindToInstance(Class optionClass, Object instance) {
- bind(optionClass).toInstance(instance);
- }
- private Object createJdkProxyHandler(Class<?> optionClass,
- final Map<String, String> concreteOptions) {
- final InvocationHandler handler = new InvocationHandler() {
- @Inject
- OptionTypeConverter converter;
- @Override
- public Object invoke(Object o, Method method, Object[] objects) throws Throwable {
- return converter.convert(concreteOptions.get(method.getName()), method.getReturnType());
- }
- };
- requestInjection(handler);
- return Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(),
- new Class<?>[]{optionClass}, handler);
- }
- private Object createCglibHandler(Class<?> optionClass,
- final Map<String, String> concreteOptions) {
- MethodInterceptor interceptor = new MethodInterceptor() {
- @Inject
- OptionTypeConverter converter;
- @Override
- public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy)
- throws Throwable {
- String value = concreteOptions.get(method.getName());
- if (null == value) {
- // Return the default value by calling the original method.
- return methodProxy.invokeSuper(o, objects);
- }
- return converter.convert(value, method.getReturnType());
- }
- };
- requestInjection(interceptor);
- return Enhancer.create(optionClass, interceptor);
- }
- public OptionsModule options(Class<?> clazz) {
- if (!clazz.isInterface() && !Modifier.isAbstract(clazz.getModifiers())) {
- throw new IllegalArgumentException(String.format("%s must be an interface or abstract class",
- clazz.getName()));
- }
- if (!clazz.isAnnotationPresent(Options.class)) {
- throw new IllegalArgumentException(String.format("%s must be annotated with @Options",
- clazz.getName()));
- }
- optionClasses.add(clazz);
- return this;
- }
- }