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;
}
}