package com.netflix.fabricator.guice;
import java.lang.reflect.Modifier;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import com.google.common.base.Preconditions;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.google.inject.AbstractModule;
import com.google.inject.Module;
import com.google.inject.Provider;
import com.google.inject.TypeLiteral;
import com.google.inject.multibindings.MapBinder;
import com.google.inject.name.Names;
import com.google.inject.util.Types;
import com.netflix.fabricator.Builder;
import com.netflix.fabricator.ComponentType;
import com.netflix.fabricator.annotations.Default;
import com.netflix.fabricator.annotations.TypeImplementation;
import com.netflix.fabricator.annotations.Type;
import com.netflix.fabricator.component.ComponentFactory;
import com.netflix.fabricator.component.ComponentManager;
import com.netflix.governator.guice.lazy.LazySingletonScope;
/**
* Utility class for creating a binding between a type string name and an
* implementation using the builder pattern.
*
* @author elandau
*
* @param <T>
*/
public class ComponentModuleBuilder<T> {
private Map<String, Provider<ComponentFactory<T>>> bindings = Maps.newHashMap();
private Set<String> ids = Sets.newHashSet();
private Map<String, T> instances = Maps.newHashMap();
private Class<? extends ComponentManager> managerClass;
private String typeName;
public Module build(final Class<T> type) {
return new AbstractModule() {
@Override
protected void configure() {
TypeLiteral componentFactoryTypeLiteral = TypeLiteral.get(Types.newParameterizedType(ComponentFactory.class, type));
if (managerClass != null) {
TypeLiteral<ComponentType<T>> componentType = (TypeLiteral<ComponentType<T>>) TypeLiteral.get(Types.newParameterizedType(ComponentType.class, type));
if (typeName == null) {
Type typeAnnot = type.getAnnotation(Type.class);
Preconditions.checkNotNull(typeAnnot, "Missing @Type annotation for " + type.getCanonicalName());
bind(componentType)
.toInstance(new ComponentType<T>(typeAnnot.value()));
}
else {
bind(componentType)
.toInstance(new ComponentType<T>(typeName));
}
TypeLiteral<ComponentManager<T>> managerType = (TypeLiteral<ComponentManager<T>>) TypeLiteral.get(Types.newParameterizedType(ComponentManager.class, type));
TypeLiteral<ComponentManager<T>> managerTypeImpl = (TypeLiteral<ComponentManager<T>>) TypeLiteral.get(Types.newParameterizedType(managerClass, type));
bind(managerType)
.to(managerTypeImpl)
.in(LazySingletonScope.get());
if (!Modifier.isAbstract(type.getModifiers() )) {
bind(componentFactoryTypeLiteral)
.annotatedWith(Default.class)
.toProvider(new GuiceBindingComponentFactoryProvider<T>((Class<T>) type))
.in(LazySingletonScope.get());
}
}
// Create the multi binder for this type.
MapBinder<String, ComponentFactory<T>> factories = (MapBinder<String, ComponentFactory<T>>) MapBinder.newMapBinder(
binder(),
TypeLiteral.get(String.class),
componentFactoryTypeLiteral
);
// Add different sub types to the multi binder
for (Entry<String, Provider<ComponentFactory<T>>> entry : bindings.entrySet()) {
factories.addBinding(entry.getKey()).toProvider(entry.getValue());
}
// Add specific named ids
for (String id : ids) {
bind(type)
.annotatedWith(Names.named(id))
.toProvider(new NamedInstanceProvider(id, TypeLiteral.get(Types.newParameterizedType(ComponentManager.class, type))));
}
// Add externally provided named instances
for (Entry<String, T> entry : instances.entrySet()) {
bind(type)
.annotatedWith(Names.named(entry.getKey()))
.toInstance(entry.getValue());
}
}
};
}
/**
* Identifies a specific subclass of the component type. The mapper will create
* an instance of class 'type' whenever it sees the value 'id' for the type
* field in the configuration specification (i.e. .properties or .json data)
*
* @param subTypeName
* @param subType
*/
public ComponentModuleBuilder<T> implementation(String subTypeName, Class<? extends T> subType) {
bindings.put(subTypeName, new GuiceBindingComponentFactoryProvider<T>(subType));
return this;
}
public ComponentModuleBuilder<T> implementation(Class<? extends T> type) {
TypeImplementation subType = type.getAnnotation(TypeImplementation.class);
Preconditions.checkNotNull(subType, "Missing @TypeImplementation for class " + type.getCanonicalName());
bindings.put(subType.value(), new GuiceBindingComponentFactoryProvider<T>((Class<T>) type));
return this;
}
public ComponentModuleBuilder<T> factory(String subType, final Class<? extends ComponentFactory<T>> factory) {
bindings.put(subType, new ComponentFactoryFactoryProvider<T>(factory));
return this;
}
public ComponentModuleBuilder<T> typename(String typeName) {
this.typeName = typeName;
return this;
}
public ComponentModuleBuilder<T> manager(Class<? extends ComponentManager> clazz) {
managerClass = clazz;
return this;
}
/**
* Specify a builder (as a Factory) on which configuration will be mapped and the
* final object created when the builder's build() method is called. Use this
* when you don't have access to the implementing class.
*
* Example usage,
*
* install(new ComponentMouldeBuilder<SomeComponent>()
* .builder("type", MyCompomentBuilder.class)
* .build();
*
* public class MyComponentBuilder implements Builder<SomeComponent> {
* public MyComponentBuilder withSomeProperty(String value) {
* ...
* }
*
* ...
*
* public SomeComponent build() {
* return new SomeComponentImpl(...);
* }
* }
*
* @param type
* @param builder
* @return
*/
public ComponentModuleBuilder<T> builder(String type, Class<? extends Builder<T>> builder) {
bindings.put(type, new GuiceBindingComponentFactoryProvider<T>(builder));
return this;
}
/**
* Indicate a specific instance for id. This makes it possible to inject an instance
* using @Named('id') instead of the ComponentManager
*
* @param id
* @return
*/
public ComponentModuleBuilder<T> named(String id) {
ids.add(id);
return this;
}
/**
* A a named instance of an existing component. No configuration will be done here.
* @param id
* @param instance
* @return
*/
public ComponentModuleBuilder<T> named(String id, T instance) {
instances.put(id, instance);
return this;
}
}