PageRenderTime 185ms CodeModel.GetById 172ms app.highlight 10ms RepoModel.GetById 1ms app.codeStats 0ms

/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
  1package com.google.sitebricks.options;
  2
  3import com.google.common.collect.ImmutableList;
  4import com.google.common.collect.Lists;
  5import com.google.common.collect.Maps;
  6import com.google.inject.AbstractModule;
  7import com.google.inject.Inject;
  8import net.sf.cglib.proxy.Enhancer;
  9import net.sf.cglib.proxy.MethodInterceptor;
 10import net.sf.cglib.proxy.MethodProxy;
 11
 12import java.lang.reflect.InvocationHandler;
 13import java.lang.reflect.Method;
 14import java.lang.reflect.Modifier;
 15import java.lang.reflect.Proxy;
 16import java.util.*;
 17import java.util.logging.Logger;
 18
 19/**
 20 * @author dhanji@gmail.com (Dhanji R. Prasanna)
 21 */
 22public class OptionsModule extends AbstractModule {
 23  private final Map<String, String> options;
 24
 25  private final List<Class<?>> optionClasses = new ArrayList<Class<?>>();
 26
 27  public OptionsModule(String[] commandLine, Iterable<Map<String, String>> freeOptions) {
 28    options = new HashMap<String, String>(commandLine.length);
 29    for (String option : commandLine) {
 30      if (option.startsWith("--") && option.length() > 2) {
 31        option = option.substring(2);
 32
 33        String[] pair = option.split("=", 2);
 34        if (pair.length == 1) {
 35          options.put(pair[0], Boolean.TRUE.toString());
 36        } else {
 37          options.put(pair[0], pair[1]);
 38        }
 39      }
 40    }
 41
 42    for (Map<String, String> freeOptionMap : freeOptions) {
 43      options.putAll(freeOptionMap);
 44    }
 45  }
 46
 47  public OptionsModule(String[] commandLine) {
 48    this(commandLine, ImmutableList.<Map<String, String>>of());
 49  }
 50
 51  public OptionsModule(Iterable<Map<String, String>> freeOptions) {
 52    this(new String[0], freeOptions);
 53  }
 54
 55  public OptionsModule(Properties... freeOptions) {
 56    this(new String[0], toMaps(freeOptions));
 57  }
 58
 59  public OptionsModule(ResourceBundle... freeOptions) {
 60    this(new String[0], toMaps(freeOptions));
 61  }
 62
 63  private static Iterable<Map<String, String>> toMaps(ResourceBundle[] freeOptions) {
 64    List<Map<String, String>> maps = Lists.newArrayList();
 65    for (ResourceBundle bundle : freeOptions) {
 66      Map<String, String> asMap = Maps.newHashMap();
 67      Enumeration<String> keys = bundle.getKeys();
 68      while (keys.hasMoreElements()) {
 69        String key = keys.nextElement();
 70        asMap.put(key, bundle.getString(key));
 71      }
 72
 73      maps.add(asMap);
 74    }
 75    return maps;
 76  }
 77
 78  private static Iterable<Map<String, String>> toMaps(Properties[] freeOptions) {
 79    List<Map<String, String>> maps = Lists.newArrayList();
 80    for (Properties freeOption : freeOptions) {
 81      maps.add(Maps.fromProperties(freeOption));
 82    }
 83    return maps;
 84  }
 85
 86  @Override
 87  protected final void configure() {
 88    // Analyze options classes.
 89    for (Class<?> optionClass : optionClasses) {
 90
 91      // If using abstract classes, detect cglib.
 92      if (Modifier.isAbstract(optionClass.getModifiers())) {
 93        try {
 94          Class.forName("net.sf.cglib.proxy.Enhancer");
 95        } catch (ClassNotFoundException e) {
 96          String message = String.format("Cannot use abstract @Option classes unless Cglib is on the classpath, " +
 97              "[%s] was abstract. Hint: add Cglib 2.0.2 or better to classpath",
 98              optionClass.getName());
 99          Logger.getLogger(Options.class.getName()).severe(message);
100          addError(message);
101        }
102      }
103
104      String namespace = optionClass.getAnnotation(Options.class).value();
105      if (!namespace.isEmpty())
106        namespace += ".";
107
108      // Construct a map that will contain the values needed to back the interface.
109      final Map<String, String> concreteOptions =
110          new HashMap<String, String>(optionClass.getDeclaredMethods().length);
111      boolean skipClass = false;
112      for (Method method : optionClass.getDeclaredMethods()) {
113        String key = namespace + method.getName();
114
115        String value = options.get(key);
116
117        // Gather all the errors regarding @Options methods that have no specified config.
118        if (null == value && Modifier.isAbstract(method.getModifiers())) {
119          addError("Option '%s' specified in type [%s] is unavailable in provided configuration",
120              key,
121              optionClass);
122          skipClass = true;
123          break;
124        }
125
126        // TODO Can we validate that the value is coercible into the return type correctly?
127        concreteOptions.put(method.getName(), value);
128      }
129
130      if (!skipClass) {
131        Object instance;
132        if (optionClass.isInterface()) {
133          instance = createJdkProxyHandler(optionClass, concreteOptions);
134        } else {
135          instance = createCglibHandler(optionClass, concreteOptions);
136        }
137
138        bindToInstance(optionClass, instance);
139      }
140    }
141  }
142
143  @SuppressWarnings("unchecked")
144  private void bindToInstance(Class optionClass, Object instance) {
145    bind(optionClass).toInstance(instance);
146  }
147
148  private Object createJdkProxyHandler(Class<?> optionClass,
149                                       final Map<String, String> concreteOptions) {
150    final InvocationHandler handler = new InvocationHandler() {
151      @Inject
152      OptionTypeConverter converter;
153
154      @Override
155      public Object invoke(Object o, Method method, Object[] objects) throws Throwable {
156        return converter.convert(concreteOptions.get(method.getName()), method.getReturnType());
157      }
158    };
159    requestInjection(handler);
160    return Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(),
161        new Class<?>[]{optionClass}, handler);
162  }
163
164  private Object createCglibHandler(Class<?> optionClass,
165                                    final Map<String, String> concreteOptions) {
166    MethodInterceptor interceptor = new MethodInterceptor() {
167      @Inject
168      OptionTypeConverter converter;
169
170      @Override
171      public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy)
172          throws Throwable {
173        String value = concreteOptions.get(method.getName());
174        if (null == value) {
175          // Return the default value by calling the original method.
176          return methodProxy.invokeSuper(o, objects);
177        }
178        return converter.convert(value, method.getReturnType());
179      }
180    };
181    requestInjection(interceptor);
182    return Enhancer.create(optionClass, interceptor);
183  }
184
185  public OptionsModule options(Class<?> clazz) {
186    if (!clazz.isInterface() && !Modifier.isAbstract(clazz.getModifiers())) {
187      throw new IllegalArgumentException(String.format("%s must be an interface or abstract class",
188          clazz.getName()));
189    }
190
191    if (!clazz.isAnnotationPresent(Options.class)) {
192      throw new IllegalArgumentException(String.format("%s must be annotated with @Options",
193          clazz.getName()));
194    }
195
196    optionClasses.add(clazz);
197    return this;
198  }
199}