/*
* Copyright (C) 2011 Rhegium Team
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.rhegium.api.bootstrap;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Map.Entry;
import java.util.logging.Handler;
import java.util.logging.LogManager;
import org.rhegium.api.lifecycle.LifecycleAware;
import org.rhegium.api.lifecycle.LifecycleManager;
import org.rhegium.api.modules.FrameworkPlugin;
import org.rhegium.api.modules.PluginDependency;
import org.rhegium.api.modules.PluginManager;
import org.rhegium.internal.injector.ProvisionInterceptorFactory;
import org.rhegium.internal.modules.FrameworkFirewallingClassLoader;
import org.rhegium.internal.modules.ResolvablePluginDependency;
import org.rhegium.internal.modules.PluginClassLoader;
import org.rhegium.internal.modules.PluginContextHelper;
import org.rhegium.internal.modules.PluginDescriptor;
import org.rhegium.internal.modules.PluginThreadContext;
import org.rhegium.internal.utils.ReflectionUtils;
import org.rhegium.internal.utils.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.bridge.SLF4JBridgeHandler;
import com.google.inject.AbstractModule;
import com.google.inject.Binding;
import com.google.inject.Guice;
import com.google.inject.Injector;
import com.google.inject.Key;
import com.google.inject.Module;
import com.google.inject.name.Names;
public abstract class AbstractBootstrapper implements Bootstrapper {
private static final Logger LOG = LoggerFactory.getLogger(AbstractBootstrapper.class);
private static final String STANDARD_CONFIGURATION_FOLDER = "conf";
private static final String PROPERTIES_BOOTSTRAP_PRIVILEGED_PACKAGES = "bootstrap.framework.privileged.packages";
private static final String PROPERTIES_BOOTSTRAP_FRAMEWORK_MODULES = "bootstrap.framework.modules";
private static final String PROPERTIES_BOOTSTRAP_PLUGIN_FOLDER = "bootstrap.plugin.folder";
private static final String PROPERTIES_BOOTSTRAP_WORK_FOLDER = "bootstrap.work.folder";
private static final String STANDARD_PLUGIN_LIBRARY_FOLDER = "lib/plugins";
private static final String STANDARD_WORK_FOLDER = "work";
private FrameworkFirewallingClassLoader firewallingClassLoader;
@Override
public void start(String[] args, ClassLoader classLoader) throws Exception {
// Deactivate java.util.logging
deactivateJULI();
// Prepare startup
preStartup(args);
// Read properties
final Properties properties = loadProperties();
if (LOG.isDebugEnabled()) {
LOG.debug("Adding ClassLoader to prevent access to base classes...");
}
firewallingClassLoader = new FrameworkFirewallingClassLoader(classLoader, loadMultiStringProperty(properties, PROPERTIES_BOOTSTRAP_PRIVILEGED_PACKAGES));
if (LOG.isDebugEnabled()) {
LOG.debug("Actual ClassLoader Hierarchy: " + ReflectionUtils.buildClassLoaderHierachy(firewallingClassLoader));
}
final File pluginPath = findPluginsPath(properties);
final File workPath = findWorkPath(properties);
// Find all plugins
final Collection<ResolvablePluginDependency> pluginDescriptors = PluginContextHelper.precheckAndReorderPluginDescriptors(buildPluginDescriptors(
firewallingClassLoader, pluginPath, workPath));
// Add buddy classloaders
addBuddyClassLoaders(pluginDescriptors);
final Collection<Module> modules = loadAndBuildFrameworkModules(properties, classLoader);
final Collection<FrameworkPlugin> plugins = new ArrayList<FrameworkPlugin>();
for (final ResolvablePluginDependency pluginDescriptor : pluginDescriptors) {
LOG.info(StringUtils.join(" ", "Creating bundle ", pluginDescriptor.getName(),
(!pluginDescriptor.isApiBundle() ? StringUtils.join(" ", "by using Class '", pluginDescriptor.getPluginClass().getCanonicalName())
: " (API-Bundle)"), "..."));
if (!pluginDescriptor.isApiBundle()) {
final FrameworkPlugin plugin = pluginDescriptor.getPluginClass().newInstance();
// Add plugin
plugins.add(plugin);
LOG.info(StringUtils.join(" ", "Configuring Injector for Plugin '", plugin.getName(), "'..."));
final Module module = new PluginThreadContext<Module>(pluginDescriptor.getPluginClassLoader()) {
@Override
public Module run() {
return plugin.configure();
}
}.execute();
if (module != null) {
modules.add(module);
}
}
}
// Bind configuration path
modules.add(new AbstractModule() {
@Override
protected void configure() {
bindConstant().annotatedWith(Names.named("configurationBase")).to(new File(getConfigurationBase()).getAbsolutePath());
}
});
// Building Guice injector and retrieve the implementation instance of
// ILifecycleManager to start it up
final Injector injector = Guice.createInjector(new ProvisionInterceptorFactory().install(modules).build());
// Inject yourself to fulfill possible needs in post startup code
injector.injectMembers(this);
// Get PluginManager
final PluginManager pluginManager = injector.getInstance(PluginManager.class);
// Get LifecycleManager
final LifecycleManager lifecycleManager = injector.getInstance(LifecycleManager.class);
if (pluginManager instanceof PluginManager) {
final PluginManager pm = (PluginManager) pluginManager;
for (final FrameworkPlugin plugin : plugins) {
LOG.info(StringUtils.join("", "Registering Plugin '", plugin.getName(), "'..."));
pm.registerPlugin(plugin);
}
}
// Register all LifecycleAware keys
Map<Key<?>, Binding<?>> bindings = injector.getBindings();
for (Entry<Key<?>, Binding<?>> entry : bindings.entrySet()) {
Class<?> type = entry.getKey().getTypeLiteral().getRawType();
if (LifecycleAware.class.isAssignableFrom(type)) {
Object instance = injector.getInstance(entry.getKey());
lifecycleManager.registerLifecycleAware((LifecycleAware) instance);
}
}
// Startup framework
LOG.info("Initialize plugins...");
lifecycleManager.initialized();
LOG.info("Startup plugins...");
lifecycleManager.start();
// Final steps after framework startup
postStartup();
}
protected abstract void preStartup(String[] args) throws Exception;
protected abstract void postStartup() throws Exception;
private void deactivateJULI() {
LOG.info("Redirect java.util.logging to SLF4J just before start...");
final java.util.logging.Logger rootLogger = LogManager.getLogManager().getLogger("");
for (final Handler handler : rootLogger.getHandlers()) {
rootLogger.removeHandler(handler);
}
SLF4JBridgeHandler.install();
}
private Properties loadProperties() throws IOException {
final File configDirectory = new File(getConfigurationBase());
final Properties properties = new Properties();
properties.load(new FileReader(new File(configDirectory, "framework.properties")));
return properties;
}
private String getConfigurationBase() {
String configurationBase = System.getProperty("org.rhegium.configurationBase");
if (configurationBase == null) {
configurationBase = STANDARD_CONFIGURATION_FOLDER;
}
return configurationBase;
}
private File findPluginsPath(final Properties properties) {
final String pluginsFolder = getProperty(properties, PROPERTIES_BOOTSTRAP_PLUGIN_FOLDER, STANDARD_PLUGIN_LIBRARY_FOLDER);
final File pluginPath = new File(pluginsFolder);
if (!pluginPath.exists() && !pluginPath.isDirectory()) {
throw new IllegalArgumentException(StringUtils.join(" ", PROPERTIES_BOOTSTRAP_PLUGIN_FOLDER, " must exists and be a directory"));
}
return pluginPath;
}
private List<ResolvablePluginDependency> buildPluginDescriptors(final ClassLoader classLoader, final File pluginPath, final File workPath) {
return AccessController.doPrivileged(new PrivilegedAction<List<ResolvablePluginDependency>>() {
@Override
public List<ResolvablePluginDependency> run() {
LOG.info(StringUtils.join(" ", "Searching framework plugins in ", pluginPath.getAbsolutePath(), "..."));
final List<ResolvablePluginDependency> pluginDescriptors = new ArrayList<ResolvablePluginDependency>();
for (final File child : pluginPath.listFiles()) {
PluginDescriptor pluginDescriptor = PluginContextHelper.buildPluginDescriptor(child, workPath, classLoader);
if (pluginDescriptor != null) {
LOG.info(StringUtils.join(" ", "Found plugin '", pluginDescriptor.getName(), "' in path ", child.getAbsolutePath()));
pluginDescriptors.add(pluginDescriptor);
}
}
return pluginDescriptors;
}
});
}
private void addBuddyClassLoaders(final Collection<ResolvablePluginDependency> pluginDescriptors) {
for (final ResolvablePluginDependency pluginDescriptor : pluginDescriptors) {
final PluginClassLoader pluginClassLoader = pluginDescriptor.getPluginClassLoader();
// Find buddy in all resolved dependencies
for (final PluginDependency<PluginClassLoader> dependency : pluginDescriptor.getDependencies()) {
pluginClassLoader.addBuddyClassLoader(dependency.getPluginClassLoader());
}
}
}
@SuppressWarnings("unchecked")
private Collection<Module> loadAndBuildFrameworkModules(final Properties properties, final ClassLoader classLoader) throws ReflectiveOperationException {
final String[] values = loadMultiStringProperty(properties, PROPERTIES_BOOTSTRAP_FRAMEWORK_MODULES);
final Collection<Module> modules = new ArrayList<Module>();
for (final String moduleName : values) {
final Class<? extends Module> moduleClass;
moduleClass = (Class<? extends Module>) classLoader.loadClass(moduleName);
modules.add(moduleClass.newInstance());
}
return modules;
}
private File findWorkPath(final Properties properties) throws IOException {
final String workFolder = getProperty(properties, PROPERTIES_BOOTSTRAP_WORK_FOLDER, STANDARD_WORK_FOLDER);
final File workPath = new File(workFolder);
if (workPath.exists()) {
if (!workPath.isDirectory()) {
throw new IllegalArgumentException(StringUtils.join(" ", PROPERTIES_BOOTSTRAP_WORK_FOLDER, " must be a directory"));
}
// Delete old work directory
deleteFullPath(workPath);
}
// (Re-) Create the work directory
workPath.mkdirs();
return workPath;
}
private void deleteFullPath(final File workPath) {
try {
Files.walkFileTree(workPath.toPath(), new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult visitFile(final Path file, final BasicFileAttributes attrs) throws IOException {
Files.delete(file);
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult postVisitDirectory(final Path dir, final IOException exc) throws IOException {
if (exc == null) {
Files.delete(dir);
}
return FileVisitResult.CONTINUE;
}
});
}
catch (final IOException e) {
throw new RuntimeException("Work directory could not be deleted", e);
}
}
private String[] loadMultiStringProperty(final Properties properties, final String propertyName) {
final String value = properties.getProperty(propertyName);
if (value == null || value.isEmpty()) {
throw new IllegalArgumentException(StringUtils.join(" ", propertyName, " cannot be null"));
}
final String[] values = trimStrings(value.split(","));
return values;
}
private static String[] trimStrings(final String[] packages) {
final String[] temp = new String[packages.length];
for (int i = 0; i < packages.length; i++) {
temp[i] = packages[i].trim();
}
return temp;
}
private String getProperty(final Properties properties, final String propertyName, final String defaultValue) {
final String value = System.getProperty(propertyName);
if (value != null && !value.isEmpty()) {
return value;
}
return properties.getProperty(propertyName, defaultValue);
}
}