/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

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