/*****************************************************************************
*
* Copyright (C) Zenoss, Inc. 2010-2011, all rights reserved.
*
* This content is made available according to terms specified in
* License.zenoss under the directory where your Zenoss product is installed.
*
****************************************************************************/
package org.zenoss.zep.impl;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactoryUtils;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.zenoss.utils.ZenPack;
import org.zenoss.utils.ZenPacks;
import org.zenoss.utils.ZenossException;
import org.zenoss.zep.PluginService;
import org.zenoss.zep.plugins.EventPlugin;
import org.zenoss.zep.plugins.EventPostCreatePlugin;
import org.zenoss.zep.plugins.EventPostIndexPlugin;
import org.zenoss.zep.plugins.EventPreCreatePlugin;
import org.zenoss.zep.plugins.EventUpdatePlugin;
import org.zenoss.zep.plugins.exceptions.DependencyCycleException;
import org.zenoss.zep.plugins.exceptions.MissingDependencyException;
import java.io.File;
import java.io.FileFilter;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* {@link PluginService} implementation which supports loading plug-ins from a
* Spring {@link ApplicationContext}.
*/
public class PluginServiceImpl implements PluginService, ApplicationContextAware {
private static final Logger logger = LoggerFactory.getLogger(PluginServiceImpl.class);
private final PluginRepository pluginRepository;
private URLClassLoader pluginClassLoader = null;
private ApplicationContext applicationContext;
public PluginServiceImpl(Properties pluginProperties) throws ZenossException {
this(pluginProperties, false);
}
public PluginServiceImpl(Properties pluginProperties, boolean disableExternalPlugins) throws ZenossException {
this.pluginRepository = new PluginRepository(pluginProperties);
if (!disableExternalPlugins) {
this.pluginClassLoader = createPluginClassLoader();
}
else {
logger.info("Loading of external plug-ins disabled.");
}
}
private URLClassLoader createPluginClassLoader() throws ZenossException {
final List<URL> urls = new ArrayList<URL>();
List<ZenPack> zenPacks;
try {
zenPacks = ZenPacks.getAllZenPacks();
} catch (ZenossException e) {
logger.warn("Unable to find ZenPacks", e);
return null;
}
for (ZenPack zenPack : zenPacks) {
final File pluginDir = new File(zenPack.packPath("zep", "plugins"));
if (!pluginDir.isDirectory()) {
continue;
}
final File[] pluginJars = pluginDir.listFiles(new FileFilter() {
@Override
public boolean accept(File pathname) {
return pathname.isFile() && pathname.getName().endsWith(".jar");
}
});
if (pluginJars != null) {
for (File pluginJar : pluginJars) {
try {
urls.add(pluginJar.toURI().toURL());
logger.info("Loading plugin: {}", pluginJar.getAbsolutePath());
} catch (MalformedURLException e) {
logger.warn("Failed to get URL from file: {}", pluginJar.getAbsolutePath());
}
}
}
}
URLClassLoader classLoader = null;
if (!urls.isEmpty()) {
logger.info("Discovered plug-ins: {}", urls);
classLoader = new URLClassLoader(urls.toArray(new URL[urls.size()]), getClass().getClassLoader());
}
else {
logger.info("No external plug-ins found.");
}
return classLoader;
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
/**
* Recursive method used to find plug-in cycles.
*
* @param plugins
* The plug-ins to analyze.
* @param existingDeps
* Any existing dependencies which will be scanned to avoid
* {@link MissingDependencyException} from being thrown.
* @param plugin
* The plug-in which is currently being analyzed.
* @param analyzed
* The previously analyzed plug-ins which are not analyzed again.
* @param dependencies
* The current dependency chain which is being analyzed.
* @throws MissingDependencyException
* If a missing dependency is discovered.
* @throws DependencyCycleException
* If a cycle in dependencies is detected.
*/
private static void detectPluginCycles(
Map<String, ? extends EventPlugin> plugins,
Set<String> existingDeps, EventPlugin plugin, Set<String> analyzed,
Set<String> dependencies) throws MissingDependencyException,
DependencyCycleException {
// Don't detect cycles again on the same plug-in.
if (!analyzed.add(plugin.getId())) {
return;
}
dependencies.add(plugin.getId());
Set<String> pluginDependencies = plugin.getDependencies();
if (pluginDependencies != null) {
for (String dependencyId : pluginDependencies) {
EventPlugin dependentPlugin = plugins.get(dependencyId);
if (dependentPlugin == null) {
if (!existingDeps.contains(dependencyId)) {
throw new MissingDependencyException(plugin.getId(), dependencyId);
}
} else {
if (dependencies.contains(dependencyId)) {
throw new DependencyCycleException(dependencyId);
}
detectPluginCycles(plugins, existingDeps, dependentPlugin, analyzed, dependencies);
}
}
}
dependencies.remove(plugin.getId());
}
/**
* Detects any cycles in the specified plug-ins and their dependencies.
*
* @param plugins
* The plug-ins to analyze.
* @param existingDeps
* Any existing dependency ids which don't trigger a
* {@link MissingDependencyException}.
* @throws MissingDependencyException
* If a dependency is not found.
* @throws DependencyCycleException
* If a cycle in the dependencies is detected.
*/
static void detectCycles(Map<String, ? extends EventPlugin> plugins,
Set<String> existingDeps) throws MissingDependencyException,
DependencyCycleException {
Set<String> analyzed = new HashSet<String>();
for (EventPlugin plugin : plugins.values()) {
detectPluginCycles(plugins, existingDeps, plugin, analyzed, new HashSet<String>());
}
}
/**
* Sorts plug-ins in the order of their dependencies, so all dependencies
* come before the plug-in which depends on them.
*
* @param <T>
* The type of {@link EventPlugin}.
* @param plugins
* Map of plug-ins keyed by the plug-in ID.
* @return A sorted list of plug-ins in order based on their dependencies.
*/
static <T extends EventPlugin> List<T> sortPluginsByDependencies(Map<String, T> plugins) {
List<T> sorted = new ArrayList<T>(plugins.size());
Map<String, T> mutablePlugins = new HashMap<String, T>(plugins);
while (!mutablePlugins.isEmpty()) {
for (Iterator<T> it = mutablePlugins.values().iterator(); it.hasNext();) {
T plugin = it.next();
boolean allDependenciesResolved = true;
final Set<String> pluginDependencies = plugin.getDependencies();
if (pluginDependencies != null) {
for (String dep : pluginDependencies) {
T depPlugin = mutablePlugins.get(dep);
if (depPlugin != null) {
allDependenciesResolved = false;
break;
}
}
}
if (allDependenciesResolved) {
sorted.add(plugin);
it.remove();
}
}
}
return sorted;
}
private static class PluginConfig {
private final Map<String, Map<String, String>> allPluginProperties = new HashMap<String, Map<String, String>>();
public Map<String, String> getPluginProperties(String pluginId) {
Map<String, String> pluginProps = allPluginProperties.get(pluginId);
if (pluginProps == null) {
pluginProps = Collections.emptyMap();
}
return pluginProps;
}
public void setPluginProperty(String pluginId, String name, String value) {
Map<String, String> pluginProps = allPluginProperties.get(pluginId);
if (pluginProps == null) {
pluginProps = new HashMap<String, String>();
allPluginProperties.put(pluginId, pluginProps);
}
pluginProps.put(name, value);
}
}
private static class PluginRepository {
private Map<Class<? extends EventPlugin>, List<? extends EventPlugin>> plugins =
new HashMap<Class<? extends EventPlugin>, List<? extends EventPlugin>>();
private Set<String> allPluginIds = new HashSet<String>();
private final PluginConfig pluginConfig;
private final Set<String> disabledPlugins;
public PluginRepository(Properties properties) {
this.pluginConfig = loadPluginConfig(properties);
this.disabledPlugins = getDisabledPlugins(properties);
}
private static PluginConfig loadPluginConfig(Properties properties) {
final PluginConfig cfg = new PluginConfig();
final Pattern pattern = Pattern.compile("plugin\\.([^\\.]+)\\.(.+)");
for (Map.Entry<Object, Object> entry : properties.entrySet()) {
final String key = (String) entry.getKey();
final String val = (String) entry.getValue();
final Matcher matcher = pattern.matcher(key);
if (matcher.matches()) {
cfg.setPluginProperty(matcher.group(1), matcher.group(2), val);
}
}
return cfg;
}
private static Set<String> getDisabledPlugins(Properties properties) {
final Set<String> disabledPlugins = new HashSet<String>();
final String disabledPluginsProp = properties.getProperty("zep.plugins.disabled");
if (disabledPluginsProp != null) {
String[] userPluginsArray = disabledPluginsProp.split(",");
for (String userPlugin : userPluginsArray) {
String userPluginId = userPlugin.trim();
if (userPluginId.length() > 0) {
disabledPlugins.add(userPluginId);
}
}
}
return disabledPlugins;
}
public <T extends EventPlugin> void loadPluginsOfType(Class<T> type, ApplicationContext context) {
final Map<String, T> pluginsById = new HashMap<String, T>();
// Load the plug-ins of the specified type from Spring
final Collection<T> pluginsFromSpring = BeanFactoryUtils.beansOfTypeIncludingAncestors(context, type, false,
true).values();
for (T plugin : pluginsFromSpring) {
if (disabledPlugins.contains(plugin.getId())) {
logger.info("Plugin {} is disabled", plugin.getId());
}
else if (allPluginIds.contains(plugin.getId()) || pluginsById.containsKey(plugin.getId())) {
logger.warn("Multiple plugins with id {} found", plugin.getId());
}
else {
pluginsById.put(plugin.getId(), plugin);
}
}
// Attempt to resolve dependencies and detect dependency cycles
boolean resolved = false;
while (!resolved) {
try {
detectCycles(pluginsById, allPluginIds);
resolved = true;
} catch (MissingDependencyException e) {
logger.error("Failed to resolve dependency {} of {}, disabling", e.getDependencyId(),
e.getPluginId());
pluginsById.remove(e.getPluginId());
} catch (DependencyCycleException e) {
logger.error("Cycle detected in dependencies for {}, disabling", e.getPluginId());
pluginsById.remove(e.getPluginId());
}
}
// Sort the resulting plug-ins in order of their dependencies
final List<T> sorted = sortPluginsByDependencies(pluginsById);
for (T plugin : sorted) {
try {
logger.info("Starting plug-in: {}", plugin.getId());
plugin.start(this.pluginConfig.getPluginProperties(plugin.getId()));
this.allPluginIds.add(plugin.getId());
} catch (Exception e) {
logger.warn("Failed to start plug-in: " + plugin.getId(), e);
}
}
this.plugins.put(type, sorted);
}
public void shutdown() {
for (List<? extends EventPlugin> plugins : this.plugins.values()) {
for (EventPlugin plugin : plugins) {
try {
logger.info("Stopping plug-in: {}", plugin.getId());
plugin.stop();
} catch (Exception e) {
logger.warn("Failed to stop plug-in: " + plugin.getId(), e);
}
}
}
this.plugins.clear();
this.allPluginIds.clear();
}
public <T extends EventPlugin> List<T> getPluginsByType(Class<T> type) {
List<T> existing = (List<T>) this.plugins.get(type);
if (existing == null) {
existing = Collections.emptyList();
}
else {
existing = Collections.unmodifiableList(existing);
}
return existing;
}
}
private void loadPlugins(ApplicationContext context) {
// Load the plug-ins in order of execution. Post-create can depend on pre-create,
// and post-index can depend on post-create and pre-create.
this.pluginRepository.loadPluginsOfType(EventPreCreatePlugin.class, context);
this.pluginRepository.loadPluginsOfType(EventPostCreatePlugin.class, context);
this.pluginRepository.loadPluginsOfType(EventPostIndexPlugin.class, context);
this.pluginRepository.loadPluginsOfType(EventUpdatePlugin.class, context);
logger.info("Initialized plug-ins");
}
private AtomicBoolean initializedPlugins = new AtomicBoolean();
public void initializePlugins() {
if (!initializedPlugins.compareAndSet(false, true)) {
return;
}
if (this.pluginClassLoader != null) {
// Create a child ApplicationContext to use to load plug-ins with the plug-in class loader.
final ClassLoader current = Thread.currentThread().getContextClassLoader();
Thread.currentThread().setContextClassLoader(this.pluginClassLoader);
try {
AnnotationConfigApplicationContext pluginApplicationContext = new AnnotationConfigApplicationContext();
pluginApplicationContext.setId("Plug-in Application Context");
pluginApplicationContext.setClassLoader(this.pluginClassLoader);
pluginApplicationContext.setParent(this.applicationContext);
pluginApplicationContext.scan("org.zenoss", "com.zenoss", "zenpacks");
pluginApplicationContext.refresh();
loadPlugins(pluginApplicationContext);
} catch (RuntimeException e) {
logger.warn("Failed to configure plug-in application context", e);
throw e;
} finally {
Thread.currentThread().setContextClassLoader(current);
}
}
else {
// Load plug-ins using the primary application context - no plug-ins were found on the classpath.
loadPlugins(this.applicationContext);
}
}
@Override
public <T extends EventPlugin> List<T> getPluginsByType(Class<T> clazz) {
return this.pluginRepository.getPluginsByType(clazz);
}
public void shutdown() {
pluginRepository.shutdown();
logger.info("Shutdown plug-ins");
}
}