package com.netflix.fabricator;
import com.google.common.base.CaseFormat;
import com.google.common.base.Preconditions;
import com.google.common.collect.Maps;
import com.google.inject.Inject;
import com.netflix.fabricator.component.ComponentFactory;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.annotation.Nullable;
import java.lang.reflect.Method;
import java.util.Map;
import java.util.Map.Entry;
/**
* Utility class for creating a binding between a type string name and an
* implementation using the builder pattern.
*
* TODO: PostConstruct and PreDestroy
*
* @author elandau
*
* @param <T>
*
*/
public class BindingComponentFactory<T> {
private static final Logger LOG = LoggerFactory.getLogger(BindingComponentFactory.class);
private static final String BUILDER_METHOD_NAME = "builder";
private static final String BUILD_METHOD_NAME = "build";
private static final String ID_METHOD_SUFFIX = "Id"; // TODO: Make this configurable
private static final String WITH_METHOD_PREFIX = "with";
private static final String SET_METHOD_PREFIX = "set";
private final ComponentFactory<T> factory;
public static interface Instantiator {
public Object create(ConfigurationNode config) throws Exception ;
}
/**
* Class determined to be the builder
*/
private Class<?> builderClass;
/**
* Map of all object properties keyed by property name.
*
* TODO: What about multiple methods for the same property?
*/
private final Map<String, PropertyInfo> properties;
/**
* Implementation determined to be best method for instantiating an instance of T or it's builder
*/
private Instantiator instantiator;
private PropertyBinderResolver binderResolver;
public BindingComponentFactory(final Class<?> clazz, PropertyBinderResolver binderResolver, final InjectionSpi injector) {
this.binderResolver = binderResolver;
try {
// Check if this class is a Builder<T> in which case just create
// an instance of the builder and treat it as a builder with builder
// method get().
if (Builder.class.isAssignableFrom(clazz)) {
this.builderClass = clazz;
this.instantiator = new Instantiator() {
public Object create(@Nullable ConfigurationNode config) {
return (Builder<?>) injector.getInstance(clazz);
}
};
}
// Check if there is a builder() method that returns an instance of the
// builder.
else {
final Method method = clazz.getMethod(BUILDER_METHOD_NAME);
this.builderClass = method.getReturnType();
this.instantiator = new Instantiator() {
public Object create(ConfigurationNode config) throws Exception {
Object obj = method.invoke(null, (Object[])null);
injector.injectMembers(obj);
return obj;
}
};
}
}
catch (Exception e) {
// Otherwise, look for a Builder inner class of T
for (final Class<?> inner : clazz.getClasses()) {
if (inner.getSimpleName().equals("Builder")) {
this.builderClass = inner;
this.instantiator = new Instantiator() {
public Object create(ConfigurationNode config) {
return injector.getInstance(inner);
}
};
break;
}
}
}
Preconditions.checkNotNull(builderClass, "No builder class found for " + clazz.getCanonicalName());
properties = makePropertiesMap(builderClass);
this.factory = new ComponentFactory<T>() {
@SuppressWarnings("unchecked")
@Override
public T create(ConfigurationNode config) {
try {
// 1. Create an instance of the builder. This still will also do basic
// dependency injection using @Inject. Named injections will be handled
// by the configuration mapping phase
Object builder = instantiator.create(config);
// 2. Set the 'id'
mapId(builder, config);
// 3. Apply configuration
mapConfiguration(builder, config);
// 4. call build()
Method buildMethod = builder.getClass().getMethod(BUILD_METHOD_NAME);
return (T) buildMethod.invoke(builder);
} catch (Exception e) {
throw new RuntimeException(String.format("Error creating component '%s' of type '%s'", config.getId(), clazz.getName()), e);
}
}
@Override
public Map<String, PropertyInfo> getProperties() {
return properties;
}
@Override
public Class<?> getRawType() {
return clazz;
}
};
}
private void mapId(Object builder, ConfigurationNode config) throws Exception {
if (config.getId() != null) {
Method idMethod = null;
try {
idMethod = builder.getClass().getMethod(WITH_METHOD_PREFIX + ID_METHOD_SUFFIX, String.class);
}catch (NoSuchMethodException e) {
// OK to ignore
}
if(idMethod == null) {
try {
idMethod = builder.getClass().getMethod(SET_METHOD_PREFIX + ID_METHOD_SUFFIX, String.class);
}catch (NoSuchMethodException e) {
// OK to ignore
}
}
if (idMethod != null) {
idMethod.invoke(builder, config.getId());
} else {
LOG.trace("cannot find id method");
}
}
}
/**
* Perform the actual configuration mapping by iterating through all parameters
* and applying the config.
*
* @param obj
* @param config
* @throws Exception
*/
private void mapConfiguration(Object obj, ConfigurationNode node) throws Exception {
for (Entry<String, PropertyInfo> prop : properties.entrySet()) {
ConfigurationNode child = node.getChild(prop.getKey());
if (child != null) {
try {
prop.getValue().apply(obj, child);
}
catch (Exception e) {
throw new Exception("Failed to map property: " + prop.getKey(), e);
}
}
}
}
private static boolean hasInjectAnnotation(Method method) {
return method.isAnnotationPresent(Inject.class) ||
method.isAnnotationPresent(javax.inject.Inject.class);
}
private static String getPropertyName(Method method) {
if (method.getName().startsWith(WITH_METHOD_PREFIX)) {
return CaseFormat.UPPER_CAMEL.to(
CaseFormat.LOWER_CAMEL,
StringUtils.substringAfter(method.getName(), WITH_METHOD_PREFIX));
}
if (method.getName().startsWith(SET_METHOD_PREFIX)) {
return CaseFormat.UPPER_CAMEL.to(
CaseFormat.LOWER_CAMEL,
StringUtils.substringAfter(method.getName(), SET_METHOD_PREFIX));
}
return null;
}
/**
* Return all deduced properties.
*
* @param builderClass
* @return
*/
private Map<String, PropertyInfo> makePropertiesMap(Class<?> builderClass) {
Map<String, PropertyInfo> properties = Maps.newHashMap();
// Iterate through each method and try to match a property
for (final Method method : builderClass.getMethods()) {
// Skip methods that do real DI. These will have been injected at object creation
if (hasInjectAnnotation(method)) {
continue;
}
// Deduce property name from the method
final String propertyName = getPropertyName(method);
if (propertyName == null) {
continue;
}
// Only support methods with a single parameter.
// TODO: Might want to support methods that take TimeUnit
Class<?>[] types = method.getParameterTypes();
if (types.length != 1) {
continue;
}
PropertyInfo prop = new PropertyInfo(propertyName);
PropertyBinder binding = binderResolver.get(method);
if (binding != null) {
prop.addBinding(binding);
properties.put(propertyName, prop);
}
}
return properties;
}
public ComponentFactory<T> get() {
return factory;
}
}