/*
* Copyright (c) 2008-2015 Maxifier Ltd. All Rights Reserved.
*/
package com.maxifier.guice.bootstrap;
import com.google.common.base.Splitter;
import com.google.inject.*;
import com.google.inject.name.Names;
import com.google.inject.util.Modules;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Provider;
import java.io.File;
import java.util.*;
import static com.google.common.base.Preconditions.checkNotNull;
import static java.util.Collections.unmodifiableMap;
import static java.util.Collections.unmodifiableSet;
/**
* Builds main application injector using set of modules and plugins.
* <p>Look at individual method's descriptions.</p>
*
* @author Konstantin Lyamshin (2015-11-04 17:12)
*/
public class InjectorBuilder {
private static final Logger logger = LoggerFactory.getLogger(InjectorBuilder.class);
private final HashMap<String, ModuleBundle> bundles = new HashMap<String, ModuleBundle>();
private final HashSet<Module> modules = new HashSet<Module>();
private final HashSet<Module> plugins = new HashSet<Module>();
private Properties configuration = new Properties();
private PluginLoader pluginLoader;
private ClassLoader classLoader;
/**
* Sets configuration properties
*/
public InjectorBuilder withConfiguration(Properties configuration) {
this.configuration = checkNotNull(configuration, "Configuration not specified");
return this;
}
/**
* Sets directory for plugin loading
*
* @see PluginLoader#PluginLoader(File)
*/
public InjectorBuilder withPluginDir(File pluginDir) {
this.pluginLoader = new PluginLoader(pluginDir);
if (classLoader != null) {
pluginLoader.setClassLoader(classLoader);
}
return this;
}
/**
* Sets directory for plugin loading
*
* @see PluginLoader#PluginLoader(File, boolean)
*/
public InjectorBuilder withPluginDir(File pluginDir, boolean flat) {
this.pluginLoader = new PluginLoader(pluginDir, flat);
if (classLoader != null) {
pluginLoader.setClassLoader(classLoader);
}
return this;
}
/**
* Sets classloader for module classes and parent for plugin loaders
*
* @see PluginLoader#setClassLoader(ClassLoader)
*/
public InjectorBuilder withClassLoader(ClassLoader classLoader) {
this.classLoader = checkNotNull(classLoader, "ClassLoader not specified");
if (pluginLoader != null) {
pluginLoader.setClassLoader(classLoader);
}
return this;
}
/**
* Appends application module
*/
public InjectorBuilder withModule(Module module) {
if (!modules.add(checkNotNull(module, "Module not specified"))) {
logger.error("Skipping duplicated module {} with class {}", module, module.getClass().getName());
}
return this;
}
/**
* Reads module bundle from configuration by name and appends it to application modules
*/
public InjectorBuilder withModuleBundle(String bundleName) {
return withModuleBundle(toBundle(bundleName));
}
/**
* Reads module bundle from configuration by full enum name and appends it to application modules
*/
public InjectorBuilder withModuleBundle(Enum<?> bundleEnum) {
return withModuleBundle(toBundle(toName(bundleEnum)));
}
/**
* Loads bundle modules and appends them to application modules
* @see ModuleBundle Module bootstrapping process
* @see #buildConfigurationInjector() Instantiation injector
*/
public InjectorBuilder withModuleBundle(ModuleBundle bundle) {
if (bundles.containsKey(bundle.name())) {
logger.error("Skipping duplicated bundle {} with class {}", bundle.name(), bundle.getClass().getName());
return this;
}
ClassLoader classLoader = this.classLoader != null? this.classLoader: this.getClass().getClassLoader();
Injector injector = buildConfigurationInjector();
ArrayList<Module> modules = new ArrayList<Module>();
for (String moduleName : bundle.modules()) {
try {
Class<?> moduleClass = classLoader.loadClass(moduleName);
Module module = loadModule(injector, moduleClass);
if (module != Modules.EMPTY_MODULE) {
modules.add(module);
}
} catch (ClassNotFoundException e) {
throw new IllegalArgumentException("Can't load module class", e);
}
}
bundles.put(bundle.name(), bundle);
for (Module module : modules) {
withModule(module);
}
return this;
}
/**
* Appends plugin module
*/
public InjectorBuilder withPlugin(Module module) {
if (!plugins.add(checkNotNull(module, "Module not specified"))) {
logger.error("Skipping duplicated plugin {} with class {}", module, module.getClass().getName());
}
return this;
}
/**
* Reads module bundle from configuration by name and appends it to plugins
*/
public InjectorBuilder withPluginBundle(String bundleName) {
return withPluginBundle(toBundle(bundleName));
}
/**
* Reads module bundle from configuration by full enum name and appends it to plugins
*/
public InjectorBuilder withPluginBundle(Enum<?> bundleEnum) {
return withPluginBundle(toBundle(toName(bundleEnum)));
}
/**
* Loads bundle modules and appends them to plugin
* <p>Plugin modules loaded in a child ClassLoaders. To build them you must initialize
* class path using {@link #withPluginDir(File)}.</p>
*
* @see PluginLoader#loadPlugin(Injector, String)
* @see ModuleBundle Module bootstrapping process
* @see #buildConfigurationInjector() Instantiation injector
*/
public InjectorBuilder withPluginBundle(ModuleBundle bundle) {
if (pluginLoader == null) {
throw new IllegalStateException("Plugin directory not specified");
}
Injector injector = buildConfigurationInjector();
ArrayList<Module> plugins = new ArrayList<Module>();
for (String moduleName : bundle.modules()) {
Module plugin = pluginLoader.loadPlugin(injector, moduleName);
if (plugin != Modules.EMPTY_MODULE) {
plugins.add(plugin);
}
}
for (Module plugin : plugins) {
withPlugin(plugin);
}
return this;
}
/**
* Builds special configuration injector which is used for instantiation modules and plugins.
* <p>Injector contains:</p>
* <ul>
* <li>{@code Properties} binding for current configuration.</li>
* <li>{@code @Named String} bindings for individual parameters.</li>
* <li>{@code Map<String, ModuleBundle>} for current module bundles.</li>
* <li>{@code Set<Module>} for current application modules.</li>
* </ul>
*/
public Injector buildConfigurationInjector() {
return Guice.createInjector(new AbstractModule() {
@Override
protected void configure() {
Names.bindProperties(binder(), configuration);
bind(Properties.class).toInstance(new Properties(configuration)); // use default properties to protect configuration from changes
bind(new TypeLiteral<Map<String, ModuleBundle>>(){}).toInstance(unmodifiableMap(bundles));
bind(new TypeLiteral<Set<Module>>(){}).toInstance(unmodifiableSet(modules));
}
});
}
/**
* Builds combined application module.
* <p>Returned module represent all modules registered by this object overridden by registered plugins.</p>
*/
public Module buildApplicationModule() {
return plugins.isEmpty()
? Modules.combine(modules) // No overrides for more clear guice error messages
: Modules.override(modules).with(plugins);
}
/**
* Builds combined application module and creates injector using it.
*/
public Injector buildApplicationInjector(Stage stage) {
for (String bundle : bundles.keySet()) {
logger.debug("Installing {} module bundle", bundle);
}
for (Module plugin : plugins) {
logger.debug("Installing {} plugin", plugin);
}
return Guice.createInjector(stage, buildApplicationModule());
}
private static String toName(Enum<?> bundleEnum) {
return bundleEnum.getDeclaringClass().getName() + '.' + bundleEnum.name();
}
private ModuleBundle toBundle(final String bundleName) {
String bundle = configuration.getProperty(bundleName);
if (bundle == null) {
throw new IllegalArgumentException("Bundle " + bundleName + " not found in configuration");
}
final Iterable<String> modules = Splitter.on(',')
.trimResults()
.omitEmptyStrings()
.split(bundle);
return new ModuleBundle() {
@Override
public String name() {
return bundleName;
}
@Override
public Iterable<String> modules() {
return modules;
}
@Override
public String toString() {
return String.format("ModuleBundle{%s}", bundleName);
}
};
}
static Module loadModule(Injector injector, Class<?> moduleClass) {
try {
Object module = injector.getInstance(moduleClass);
if (module instanceof Provider) {
module = ((Provider) module).get();
}
if (module instanceof Module) {
return (Module) module;
}
throw new IllegalArgumentException("Class doesn't implement Module or Provider<Module>: " + moduleClass.getName());
} catch (ConfigurationException e) {
throw new IllegalArgumentException("Can't load module class", e);
} catch (ProvisionException e) {
throw new IllegalArgumentException("Can't load module class", e);
}
}
}