package com.netflix.governator.guice;
import java.lang.reflect.Constructor;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableList.Builder;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.google.inject.AbstractModule;
import com.google.inject.Injector;
import com.google.inject.Module;
import com.google.inject.ProvisionException;
import com.google.inject.spi.Dependency;
import com.google.inject.spi.InjectionPoint;
import com.netflix.governator.annotations.Modules;
/**
* The {@link ModuleListBuilder} keeps track of modules and their transitive dependencies
* and provides a mechanism to replace or exclude modules.
*
* When {@link build()} is called a list of modules is created and modules will be ordered
* in the order in which they were added while allowing for dependent modules to be listed
* first.
*
* TODO: Provide exclude source
* TODO: Provide include source
* TODO: Force include
* TODO: Guard against circular dependencies
*
* @author elandau
*/
public class ModuleListBuilder {
private static final Logger LOG = LoggerFactory.getLogger(ModuleListBuilder.class);
/**
* Internal class to track either a module class or instance
* @author elandau
*/
public class ModuleProvider {
Class<? extends Module> type;
Module instance;
boolean isExcluded = false;
public ModuleProvider(Class<? extends Module> type) {
this.type = type;
instance = null;
}
public ModuleProvider(Module instance) {
this.instance = instance;
if (instance instanceof AbstractModule) {
type = null;
}
}
public void setInstance(Module module) {
Preconditions.checkState(instance == null, "Instance already exists");
this.instance = module;
}
public Module getInstance(Injector injector) throws Exception {
try {
// Already created
if (instance != null) {
return instance;
}
LOG.info("Getting instace of : " + type.getName());
if (excludes.contains(type)) {
LOG.info("Module '" + type.getName() + "' is excluded");
return null;
}
// Create all of this modules dependencies. This includes both @Modules and injected
// dependencies
for (Class<? extends Module> dep : getIncludeList()) {
ModuleProvider provider = includes.get(dep);
provider.getInstance(injector);
}
// If @Inject is present then instantiate using that constructor and manually inject
// the dependencies. Note that a null will be injected for excluded modules
try {
InjectionPoint ip = InjectionPoint.forConstructorOf(type);
if (ip != null) {
Constructor<?> c = (Constructor<?>) ip.getMember();
c.setAccessible(true);
List<Dependency<?>> deps = ip.getDependencies();
if (!deps.isEmpty()) {
Object[] args = new Object[deps.size()];
for (Dependency<?> dep : deps) {
Class<?> type = dep.getKey().getTypeLiteral().getRawType();
if (Module.class.isAssignableFrom(type)) {
args[dep.getParameterIndex()] = includes.get(dep.getKey().getTypeLiteral().getRawType()).getInstance(injector);
}
else {
args[dep.getParameterIndex()] = injector.getInstance(dep.getKey());
}
}
c.setAccessible(true);
instance = (Module) c.newInstance(args);
}
else {
instance = (Module) c.newInstance();
}
}
else {
// Empty constructor
Constructor<?> c = type.getConstructor();
if (c != null) {
c.setAccessible(true);
instance = (Module) c.newInstance();
}
// Default constructor
else {
instance = type.newInstance();
}
}
injector.injectMembers(instance);
return instance;
}
catch (Exception e) {
throw new ProvisionException("Failed to create module '" + type.getName() + "'", e);
}
}
finally {
if (instance != null) {
registerModule(instance);
}
}
}
private List<Class<? extends Module>> getIncludeList() {
// Look for @Modules(includes={..})
Builder<Class<? extends Module>> builder = ImmutableList.<Class<? extends Module>>builder();
if (type != null) {
Modules annot = type.getAnnotation(Modules.class);
if (annot != null && annot.include() != null) {
builder.add(annot.include());
}
// Look for injected modules
for (Dependency<?> dep : InjectionPoint.forConstructorOf(type).getDependencies()) {
Class<?> depType = dep.getKey().getTypeLiteral().getRawType();
if (Module.class.isAssignableFrom(depType)) {
builder.add((Class<? extends Module>) depType);
}
}
}
return builder.build();
}
private List<Class<? extends Module>> getExcludeList() {
Builder<Class<? extends Module>> builder = ImmutableList.<Class<? extends Module>>builder();
if (type != null) {
Modules annot = type.getAnnotation(Modules.class);
if (annot != null && annot.exclude() != null) {
builder.add(annot.exclude());
}
}
return builder.build();
}
}
// List of all identified Modules in the order in which they were added and identified
private List<ModuleProvider> providers = Lists.newArrayList();
// Map of seen class to the provider. Note that this map will not include any module
// that is a simple
private Map<Class<? extends Module>, ModuleProvider> includes = Maps.newIdentityHashMap();
// Set of module classes to exclude
private Set<Class<? extends Module>> excludes = Sets.newIdentityHashSet();
// Final list of modules to install
private List<Module> resolvedModules = Lists.newArrayList();
// Lookup of resolved modules for duplicate check
private Set<Class<? extends Module>> resolvedModuleLookup = Sets.newIdentityHashSet();
public ModuleListBuilder includeModules(Iterable<? extends Module> modules) {
for (Module module : modules) {
include (module);
}
return this;
}
public ModuleListBuilder include(Iterable<Class<? extends Module>> modules) {
for (Class<? extends Module> module : modules) {
include (module);
}
return this;
}
public ModuleListBuilder include(final Module m) {
ModuleProvider provider = new ModuleProvider(m);
if (!m.getClass().isAnonymousClass()) {
// Do nothing if already exists
if (includes.containsKey(m.getClass())) {
return this;
}
includes.put(m.getClass(), provider);
// Get all @Modules and injected dependencies of this module
for (Class<? extends Module> dep : provider.getIncludeList()) {
// Circular dependencies will be caught by includes.containsKey() above
include(dep, false);
}
for (Class<? extends Module> dep : provider.getExcludeList()) {
exclude(dep);
}
}
// Add to list of known modules. We do this after all dependencies
// have been added
providers.add(provider);
return this;
}
public ModuleListBuilder include(final Class<? extends Module> m) {
include(m, true);
return this;
}
private void include(final Class<? extends Module> m, boolean addToProviders) {
ModuleProvider provider = new ModuleProvider(m);
if (!m.getClass().isAnonymousClass()) {
// Do nothing if already exists
if (includes.containsKey(m.getClass())) {
return;
}
includes.put(m, provider);
// Get all @Modules and injected dependencies of this module
for (Class<? extends Module> dep : provider.getIncludeList()) {
// Circular dependencies will be caught by includes.containsKey() above
include(dep, false);
}
for (Class<? extends Module> dep : provider.getExcludeList()) {
exclude(dep);
}
}
// Add to list of known modules. We do this after all dependencies
// have been added
if (addToProviders) {
providers.add(provider);
}
}
public ModuleListBuilder exclude(Class<? extends Module> m) {
excludes.add(m);
return this;
}
public ModuleListBuilder exclude(Iterable<Class<? extends Module>> modules) {
for (Class<? extends Module> module : modules) {
excludes.add(module);
}
return this;
}
private void registerModule(Module m) {
if (m.getClass().isAnonymousClass()) {
resolvedModules.add(m);
}
else {
if (!resolvedModuleLookup.contains(m.getClass())) {
LOG.info("Adding module '" + m.getClass().getName());
resolvedModules.add(m);
resolvedModuleLookup.add(m.getClass());
}
}
}
List<Module> build(Injector injector) throws Exception {
for (ModuleProvider provider : providers) {
provider.getInstance(injector);
}
return resolvedModules;
}
}