All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.google.sitebricks.options.OptionsModule Maven / Gradle / Ivy

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 [email protected] (Dhanji R. Prasanna)
 */
public class OptionsModule extends AbstractModule {
  private final Map options;

  private final List> optionClasses = new ArrayList>();

  public OptionsModule(String[] commandLine, Iterable> freeOptions) {
    options = new HashMap(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 freeOptionMap : freeOptions) {
      options.putAll(freeOptionMap);
    }
  }

  public OptionsModule(String[] commandLine) {
    this(commandLine, ImmutableList.>of());
  }

  public OptionsModule(Iterable> 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> toMaps(ResourceBundle[] freeOptions) {
    List> maps = Lists.newArrayList();
    for (ResourceBundle bundle : freeOptions) {
      Map asMap = Maps.newHashMap();
      Enumeration keys = bundle.getKeys();
      while (keys.hasMoreElements()) {
        String key = keys.nextElement();
        asMap.put(key, bundle.getString(key));
      }

      maps.add(asMap);
    }
    return maps;
  }

  private static Iterable> toMaps(Properties[] freeOptions) {
    List> 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 concreteOptions =
          new HashMap(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 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 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;
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy