/* * RHQ Management Platform * Copyright (C) 2005-2008 Red Hat, Inc. * All rights reserved. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License, version 2, as * published by the Free Software Foundation, and/or the GNU Lesser * General Public License, version 2.1, also as published by the Free * Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License and the GNU Lesser General Public License * for more details. * * You should have received a copy of the GNU General Public License * and the GNU Lesser General Public License along with this program; * if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.rhq.core.clientapi.agent.metadata; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.TreeSet; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; /** * This class determines the deployment order for plugins by building the dependency graph of the plugins. You use this * class by first {@link #addPlugin(String, List) adding} plugins to the graph, and then when all plugins have been * added you can get the {@link #getDeploymentOrder() deployment order} that lists all the plugins in the order in which * they should be deployed. * * <p>Note that circular dependencies are NOT allowed nor supported.</p> * * @author John Mazzitelli */ public class PluginDependencyGraph { private static final Log log = LogFactory.getLog(PluginDependencyGraph.class); /** * Keyed on plugin name with the values of their dependencies (which are other plugin names). The values must never * be null - if there are no dependencies, an empty list will exist. */ private Map<String, List<PluginDependency>> dependencyMap = new HashMap<String, List<PluginDependency>>(); /** * Adds a plugin to the graph. The plugin names in the dependencies must match names of other plugins that were, * or will be, added to this graph. * * <p>If the plugin already exists, it will be overridden such that the given dependencies replace its old ones.</p> * * @param pluginName the name of the plugin getting added to the graph * @param dependencies list of plugin names that are dependencies of this plugin */ public void addPlugin(String pluginName, List<PluginDependency> dependencies) { // it doesn't make sense that a plugin depends on itself. // remove duplicates to avoid erroneous circular dependencies. dependencies.remove(new PluginDependency(pluginName, false, false)); dependencyMap.put(pluginName, dependencies); } /** * Returns the name of the plugin who's classloader will be used as the parent of this plugin. If none is explicitly * declared, the last one in the dependency list will be used. * * @param the name of the plugin whose required dependency contains classes that are to be accessible by the plugin * @return the required dependency for the given plugin, or <code>null</code> if there is no required dependency */ public String getUseClassesDependency(String pluginName) { PluginDependency last = null; if (this.dependencyMap.containsKey(pluginName)) { for (PluginDependency dependency : this.dependencyMap.get(pluginName)) { // only required deps can have their classes used - optional deps cannot (since classes may not exist) if (dependency.required) { if (dependency.useClasses) { return dependency.name; // classes from only one dep can be used, so we only return one } last = dependency; } } } return (last == null) ? null : last.name; } /** * Returns the set of plugin names that have been added to the dependency graph. * * @return set of all plugin names that were added to this graph */ public Set<String> getPlugins() { return new HashSet<String>(dependencyMap.keySet()); } /** * Given a plugin that is in this dependency graph, this will return the list of its direct dependencies (the * plugins this plugin explicitly depends on). The list will be empty if there are no dependencies or the * plugin does not exist in this graph. * * Note that if a dependency is not required to exist, and it does not exist, it will not be returned * in the list. This is to say that if a plugin was configured to depend on another plugin, but that * dependency was not required, that other plugin will not be in the returned list if it hasn't been added * to this graph yet. * * @param pluginName the plugin name * * @return list of plugin dependencies */ public List<String> getPluginDependencies(String pluginName) { List<String> dependencies = new ArrayList<String>(); for (PluginDependency dependency : this.dependencyMap.get(pluginName)) { if (dependency.required || this.dependencyMap.containsKey(dependency.name)) { dependencies.add(dependency.name); } } return dependencies; } /** * Given a plugin that is in this dependency graph, this will return all those plugins * that <i>optionally</i> depend on it. If a plugin has a required dependency on * the given plugin, or a plugin does not depend on the given plugin at all, that plugin * will not be in the returned list. * * @param pluginName the plugin whose dependents are to be returned * * @return list of all plugins that optionally depend on the given plugin */ public List<String> getOptionalDependents(String pluginName) { List<String> dependents = new ArrayList<String>(); for (Map.Entry<String, List<PluginDependency>> entry : this.dependencyMap.entrySet()) { if (entry.getKey().equals(pluginName)) { continue; // don't bother examining the plugin itself } // see if current plugin optionally depends on the given pluginName, if so, add it to the list for (PluginDependency dependency : entry.getValue()) { if (!dependency.required && dependency.name.equals(pluginName)) { dependents.add(entry.getKey()); break; } } } return dependents; } /** * Given a plugin that is in this dependency graph, this will return all those plugins * the plugin either directly or indirectly depends on it. This is different * than {@link #getPluginDependencies(String)} because this method does a deep * dive and returns all direct dependencies and dependencies of those dependencies. * * @param pluginName the plugin whose dependencies are to be returned * * @return list of all plugins that the given plugin depends on */ public Collection<String> getAllDependencies(String pluginName) { if (this.dependencyMap.containsKey(pluginName)) { return getDeepDependencies(pluginName, new ArrayList<String>(), true); } else { return new HashSet<String>(); } } /** * Given a plugin that is in this dependency graph, this will return all those plugins * that either directly or indirectly depend on it (both optional and required dependencies). * * @param pluginName the plugin whose dependents are to be returned * * @return list of all plugins that depend on the given plugin */ public Collection<String> getAllDependents(String pluginName) { Set<String> dependents = new HashSet<String>(); for (Map.Entry<String, List<PluginDependency>> entry : this.dependencyMap.entrySet()) { if (entry.getKey().equals(pluginName)) { continue; // don't bother examining the plugin itself } // see if current plugin depends on the given pluginName, if so, add it to the list for (PluginDependency dependency : entry.getValue()) { if (dependency.name.equals(pluginName)) { dependents.addAll(getAllDependents(entry.getKey())); dependents.add(entry.getKey()); break; } } } return dependents; } /** * Returns <code>true</code> if the dependency graph has no missing required plugins. * That is to say, all required dependencies of all plugins can be found in this graph. If this returns <code>true</code>, * you can safely call {@link #getDeploymentOrder()} and expect it to return an ordered list of plugins. * This will return <code>false</code> if one or more required dependencies are missing and still need to be * {@link #addPlugin(String, List) added}. This will throw an exception if a circular dependency has been * detected. * * @param errorBuffer if not <code>null</code> and this method returns <code>false</code>, this will be appended * with the error message that will contain information on the first plugin found to be missing * * @return <code>true</code> if there are no missing dependencies and {@link #getDeploymentOrder()} can be called * * @throws IllegalStateException if a circular dependency has been detected */ public boolean isComplete(StringBuilder errorBuffer) throws IllegalStateException { try { getDeploymentOrder(); return true; } catch (IllegalArgumentException e) { if (errorBuffer != null) { errorBuffer.append(e.getMessage()); } return false; } } /** * Returns the deployment order for all added plugins. If a required dependency is missing and thus one or * more plugins cannot be deployed, an exception is thrown. If an optional dependency is missing, that * optional dependency plugin will be ignored and not returned in the list. * * @return the list of plugin names, in the order in which they can be deployed. * * @throws IllegalStateException if a circular dependency has been detected * @throws IllegalArgumentException if one or more plugins depend on other plugins that are missing from the graph */ public List<String> getDeploymentOrder() throws IllegalStateException, IllegalArgumentException { List<PluginItem> pluginItems = new ArrayList<PluginItem>(); // Compute the deep dependencies so we know all the plugins that must be deployed before each plugin. // We use TreeSet so we can be able to predict the resulting order based on alphabetic ordering of plugins (mainly for tests) for (String pluginName : new TreeSet<String>(dependencyMap.keySet())) { pluginItems.add(new PluginItem(pluginName, getDeepDependencies(pluginName, new ArrayList<String>(), true))); } // got through each plugin and put it in the returned list such that it appears // as far in the front of the list as it can, but not before any of its dependencies List<String> retList = new ArrayList<String>(pluginItems.size()); for (PluginItem pluginItem : pluginItems) { int insertIndex = 0; for (String dependency : pluginItem.deepDependencies) { int dependencyIndex = retList.indexOf(dependency); if ((dependencyIndex > -1) && (insertIndex < (dependencyIndex + 1))) { insertIndex = dependencyIndex + 1; } } retList.add(insertIndex, pluginItem.name); } return retList; } /** * If the current dependency graph is not yet {@link #isComplete(StringBuilder) complete}, you can call * this method to reduce the graph such that plugins with missing required dependencies are removed and * only those plugins whose dependencies exist are in the returned graph. In other words, this method will * return a dependency graph that is guaranteed to be complete and return a * {@link #getDeploymentOrder()} - albeit with only those plugins that currently have all dependencies. * * @return a reduced graph that contains only those plugins that have all their dependencies */ public PluginDependencyGraph reduceGraph() { PluginDependencyGraph reducedGraph = new PluginDependencyGraph(); // Compute the deep dependencies so we know all the plugins that must be deployed before each plugin. for (String pluginName : new TreeSet<String>(dependencyMap.keySet())) { try { getDeepDependencies(pluginName, new ArrayList<String>(), true); // throws exception if not complete reducedGraph.addPlugin(pluginName, this.dependencyMap.get(pluginName)); } catch (Exception e) { log.info("Reducing dependency graph by not including plugin [" + pluginName + "]. Cause: " + e); } } return reducedGraph; } public String toString() { StringBuffer str = new StringBuffer("Plugin dependency graph:"); for (Map.Entry<String, List<PluginDependency>> entry : dependencyMap.entrySet()) { str.append("\n"); str.append(entry.getKey()); str.append(":"); str.append(entry.getValue()); } return str.toString(); } /** * Given a known plugin name, this returns all dependencies of that plugin (including those dependencies of its * dependencies, down N levels). If a dependency is missing but is required, an exception is thrown - missing * optional plugins are simply ignored and not returned in the set but otherwise no errors occur. * * @param pluginName * @param dependingPlugins set of plugins that are known to be depending on pluginName (must not be <code> * null</code>) * @param required if <code>true</code>, then <code>pluginName</code> must exist in the graph. If it does not, an * exception will be thrown. Otherwise, it is considered an optional plugin and if it is missing, * it will be ignored. * * @return the dependencies * * @throws IllegalStateException if the given plugin has a circular dependency * @throws IllegalArgumentException if the plugin hasn't been added to the graph yet */ private Set<String> getDeepDependencies(String pluginName, Collection<String> dependingPlugins, boolean required) throws IllegalStateException, IllegalArgumentException { HashSet<String> results = new HashSet<String>(); List<PluginDependency> childDependencies = dependencyMap.get(pluginName); if (childDependencies == null) { if (required) { throw new IllegalArgumentException("Plugin [" + pluginName + "] is required by plugins [" + dependingPlugins + "] but it does not exist in the dependency graph yet"); } log.info("Optional plugin [" + pluginName + "] was requested by plugins [" + dependingPlugins + "] but it does not exist in the dependency graph yet and will be ignored"); } else { for (PluginDependency childDependency : childDependencies) { if (dependingPlugins.contains(childDependency.name)) { throw createCircularDependencyException(childDependency.name); } dependingPlugins.add(pluginName); Set<String> childDeepDependencies = getDeepDependencies(childDependency.name, dependingPlugins, childDependency.required); dependingPlugins.remove(pluginName); results.add(childDependency.name); results.addAll(childDeepDependencies); } } return results; } private IllegalStateException createCircularDependencyException(String badPlugin) { StringBuffer err = new StringBuffer("Circular dependency detected in plugins!\n"); err.append("Plugin with the circular dependency is [" + badPlugin + "]\n"); err.append("Circular dependency path is ["); err.append(getCircularDependencyString(badPlugin, new ArrayList<String>())); err.append("]\n"); err.append(toString()); return new IllegalStateException(err.toString()); } private String getCircularDependencyString(String startPlugin, List<String> path) { boolean gotIt = path.contains(startPlugin); if (!gotIt) { path.add(startPlugin); List<PluginDependency> deps = dependencyMap.get(startPlugin); for (PluginDependency dep : deps) { List<String> tmpPath = new ArrayList<String>(path); String str = getCircularDependencyString(dep.name, tmpPath); if (str != null) { return str; } } return null; } StringBuffer retPath = new StringBuffer(); for (String pathElement : path) { retPath.append(pathElement); retPath.append("->"); } retPath.append(startPlugin); path.add(startPlugin); return retPath.toString(); } /** * Used to properly sort our dependencies in a tree map. */ private class PluginItem { final String name; final Set<String> deepDependencies; PluginItem(String name, Set<String> deepDependencies) { this.name = name; this.deepDependencies = deepDependencies; } public String toString() { return this.name + ':' + this.deepDependencies; } } public static class PluginDependency { final String name; final boolean useClasses; final boolean required; public PluginDependency(String name) { this(name, false, false); } public PluginDependency(String name, boolean useClasses, boolean required) { this.name = name; this.useClasses = useClasses; this.required = required; } public boolean equals(Object o) { if (this == o) { return true; } if ((o == null) || (getClass() != o.getClass())) { return false; } PluginDependency that = (PluginDependency) o; if (!name.equals(that.name)) { return false; } return true; } public int hashCode() { return name.hashCode(); } public String toString() { return "name=[" + this.name + "], required=[" + this.required + "], useClasses=[" + this.useClasses + "]"; } } }