/********************************************************************************
* CruiseControl, a Continuous Integration Toolkit
* Copyright (c) 2001, ThoughtWorks, Inc.
* 200 E. Randolph, 25th Floor
* Chicago, IL 60601 USA
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* + Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* + Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the following
* disclaimer in the documentation and/or other materials provided
* with the distribution.
*
* + Neither the name of ThoughtWorks, Inc., CruiseControl, nor the
* names of its contributors may be used to endorse or promote
* products derived from this software without specific prior
* written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
********************************************************************************/
package net.sourceforge.cruisecontrol;
import java.io.IOException;
import java.io.Serializable;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Properties;
import org.apache.log4j.Logger;
import org.jdom.Attribute;
import org.jdom.Element;
import net.sourceforge.cruisecontrol.config.PluginPlugin;
/**
* Handles "registering" plugins that will be used by the CruiseControl
* configuration file.
* A PluginRegistry can have a parent registry, which it will query for
* a plugin if it's not defined in the registry itself. This is used to
* enable projects to have their own plugins and override the classname
* for a specific plugin, like the labelincrementer.
*
* The root-registry contains the default list of plugins, i.e. those
* that are already registered like AntBuilder that don't have to be registered
* separately in the configuration file.
*
* The registry keeps track of the {@link #getPluginConfig(String) plugin configurations}
* in order to allow full plugin preconfigurations (default properties + nested elements).
*
* @see PluginXMLHelper
*/
public final class PluginRegistry implements Serializable, Iterable<String> {
private static final long serialVersionUID = 5941716771646177086L;
/** Root plugin name. */
public static final String ROOT_PLUGIN = "cruisecontrol";
private static final Logger LOG = Logger.getLogger(PluginRegistry.class);
/**
* The only instance of the root plugin registry.
* This contains the default plugins and the plugins that are defined
* external to projects.
*/
private static final PluginRegistry ROOTREGISTRY = loadDefaultPluginRegistry();
/**
* @return PluginRegistry with the ROOTREGISTRY as its parent.
*/
public static PluginRegistry createRegistry() {
return createRegistry(ROOTREGISTRY);
}
/**
* @param parent parent registry of the new plugin registry
* @return PluginRegistry with the specified registry as its parent.
*/
public static PluginRegistry createRegistry(final PluginRegistry parent) {
return new PluginRegistry(parent);
}
/**
* The parent registry that will be searched for plugin definitions
* if they're not defined in the registry itself. May be null.
*/
private final PluginRegistry parentRegistry;
/**
* Map of plugins where the key is the plugin name (e.g. ant) and the value is
* the fully qualified classname
* (e.g. net.sourceforge.cruisecontrol.builders.AntBuilder).
*/
private final Map<String, String> plugins = new HashMap<String, String>();
/**
* Map that holds the DOM element representing the plugin declaration.
* Key is the plugin name (as taken from the DOM element),
* value is the Element representing the plugin configuration.
*/
private final Map<String, Element> pluginConfigs = new HashMap<String, Element>();
/**
* Creates a new PluginRegistry with no plugins registered, with the given parent registry.
* Only used internally for now, Projects should call createRegistry instead.
* @param parentRegistry parent registry of the new plugin registry
*/
private PluginRegistry(final PluginRegistry parentRegistry) {
this.parentRegistry = parentRegistry;
}
/**
* @param pluginName The name for the plugin, e.g. ant. Note that plugin
* names are always treated as case insensitive, so Ant, ant, and AnT are
* all treated as the same plugin.
*
* @param pluginClassname The fully qualified classname for the
* plugin class, e.g. net.sourceforge.cruisecontrol.builders.AntBuilder.
*/
void register(final String pluginName, final String pluginClassname) {
plugins.put(pluginName.toLowerCase(), pluginClassname);
}
public void register(final PluginPlugin plugin) throws CruiseControlException {
final String pluginName = plugin.getName();
final String pluginClassName = plugin.getClassname();
Element transformedElement = plugin.getTransformedElement();
// Resolve inheritance
final Attribute parentPlugin = transformedElement.getAttribute("inherits");
if (parentPlugin != null) {
// find the plugin to inherrit from
final Element parentConfig = this.getPluginConfig(parentPlugin.getValue());
if (parentConfig == null) {
throw new CruiseControlException("Unknown plugin '"
+ parentPlugin.getValue() + "' to inherit from.");
}
transformedElement = overridePluginConfig(parentPlugin.getValue(), pluginClassName, transformedElement);
transformedElement.removeAttribute("inherits");
}
if (pluginClassName != null) {
register(pluginName, pluginClassName);
} else {
// should be known plugin, then
if (!isPluginRegistered(pluginName)) {
throw new CruiseControlException("Unknown plugin '"
+ pluginName + "'; maybe you forgot to specify a classname?");
}
register(pluginName, getPluginClassname(pluginName));
}
if (LOG.isDebugEnabled()) {
LOG.debug("storing plugin configuration " + pluginName);
}
pluginConfigs.put(pluginName, transformedElement);
}
/**
* Registers the given plugin, including plugin configuration.
*
* @param pluginElement the JDom element that contains the plugin definition.
* @throws CruiseControlException if operation fails
* @deprecated use {@link #register(PluginPlugin)}
*/
public void register(Element pluginElement) throws CruiseControlException {
// wants to inherit the plugin
final Attribute parentPlugin = pluginElement.getAttribute("inherits");
if (parentPlugin != null) {
/* Check the name of plugin to inherit from */
if (getPluginClassname(parentPlugin.getValue()) == null) {
throw new CruiseControlException("Unknown plugin '"
+ parentPlugin.getValue() + "' to inherit from.");
}
Attribute pluginName = pluginElement.getAttribute("name").detach();
Attribute pluginClass = pluginElement.getAttribute("classname");
if (pluginClass == null) {
throw new CruiseControlException("Unknown plugin '"
+ pluginName.getValue() + "'; maybe you forgot to specify a classname?");
}
pluginElement = overridePluginConfig(parentPlugin.getValue(), pluginClass.getValue(), pluginElement);
pluginElement.setAttribute(pluginName);
pluginElement.removeAttribute(parentPlugin.getName());
}
final PluginPlugin plugin = (PluginPlugin) new ProjectXMLHelper(
new ResolverHolder.DummeResolvers()).configurePlugin(pluginElement, false);
register(plugin);
}
/**
* For plugins defining <code>from="type"</code> element, it finds the class for the
* given type, sets it to the <code>classname=""</code> attribute and removes the
* <code>from</code> attribute.
*
* @param pluginElement the XML element with plugin configuration.
*/
public void from2classname(Element pluginElement) {
if (!"plugin".equals(pluginElement.getName())) {
LOG.warn("Node <" + pluginElement.getName() + "> is not plugin");
return;
}
String pluginName = pluginElement.getAttributeValue("name");
String pluginFrom = pluginElement.getAttributeValue("from");
String pluginClassName = pluginElement.getAttributeValue("classname");
if (pluginClassName == null && pluginFrom != null) {
pluginClassName = getPluginClassname(pluginFrom);
// No standard plugin
if (pluginClassName == null) {
LOG.warn("<plugin name = '" + pluginName + "' from = '" + pluginFrom
+ "'> does not contain in-built element name");
return;
}
// Create "standard" plugin element
pluginElement.setAttribute("classname", pluginClassName);
pluginElement.removeAttribute("from");
}
}
/**
* Registers the given plugin in the root registry, so it will be
* available to all projects.
* @param pluginElement the plugin to register
* @throws CruiseControlException if operation fails
*/
static void registerToRoot(final Element pluginElement) throws CruiseControlException {
ROOTREGISTRY.register(pluginElement);
}
/**
* Clears all plugin registrations and defaults in the root registry, so they can be re-registered
* when reloading the config file. The default-properties are re-read.
*/
static void resetRootRegistry() {
ROOTREGISTRY.pluginConfigs.clear();
ROOTREGISTRY.plugins.clear();
ROOTREGISTRY.plugins.putAll(loadDefaultPluginRegistry().plugins);
}
/**
* @param pluginName the case insensitive plugin name.
* @return Returns null if no plugin has been registered with the specified
* name, otherwise a String representing the fully qualified classname
* for the plugin class. Note that plugin
* names are always treated as case insensitive, so Ant, ant, and AnT are
* all treated as the same plugin.
* Note: a parent name-to-class mapping can be overridden by children registries.
*/
public String getPluginClassname(String pluginName) {
pluginName = pluginName.toLowerCase();
String className = internalGetPluginClassname(pluginName);
if (className == null && parentRegistry != null) {
className = parentRegistry.getPluginClassname(pluginName);
}
return className;
}
/**
* @param pluginName lower case plugin name.
* @return the class name for this plugin on this registry. May be <code>null</code>
* Assumes the pluginName is lower case
*/
private String internalGetPluginClassname(final String pluginName) {
return plugins.get(pluginName);
}
/**
* @param pluginName the plugin name
* @return Returns null if no plugin has been registered with the specified
* name, otherwise the Class representing the plugin class. Note that
* plugin names are always treated as case insensitive, so Ant, ant,
* and AnT are all treated as the same plugin.
*
* @throws CruiseControlException If the class provided cannot be loaded.
*/
public Class getPluginClass(final String pluginName) throws CruiseControlException {
if (!isPluginRegistered(pluginName)) {
return null;
}
final String pluginClassname = getPluginClassname(pluginName);
return instanciatePluginClass(pluginClassname, pluginName);
}
/**
* @param pluginClassname fully qualified plugin class name
* @param pluginName plugin name
* @return instantiate the Class representing the plugin class name.
* @throws CruiseControlException If the class provided cannot be loaded.
*/
public Class instanciatePluginClass(final String pluginClassname, final String pluginName)
throws CruiseControlException {
try {
return Class.forName(pluginClassname);
} catch (ClassNotFoundException e) {
String msg = "Attemping to load plugin named [" + pluginName
+ "], but couldn't load corresponding class ["
+ pluginClassname + "].";
throw new CruiseControlException(msg);
}
}
public String getPluginName(final Class pluginClass) {
String pluginName = null;
if (parentRegistry != null) {
pluginName = parentRegistry.getPluginName(pluginClass);
}
if (pluginName == null) {
for (final Map.Entry<String, String> entry : plugins.entrySet()) {
final String value = entry.getValue();
if (value.equals(pluginClass.getName())) {
pluginName = entry.getKey();
break;
}
}
}
return pluginName;
}
public PluginDetail[] getPluginDetails() throws CruiseControlException {
final List<PluginDetail> availablePlugins = new LinkedList<PluginDetail>();
if (parentRegistry != null) {
availablePlugins.addAll(Arrays.asList(parentRegistry.getPluginDetails()));
}
for (final String pluginName : plugins.keySet()) {
try {
Class pluginClass = getPluginClass(pluginName);
availablePlugins.add(new GenericPluginDetail(pluginName, pluginClass));
} catch (CruiseControlException e) {
String message = e.getMessage();
// TODO: handle these potential unloadable plugins in a better way
if (message.indexOf("starteam") == -1 && message.indexOf("harvest") == -1) {
throw e;
}
}
}
return availablePlugins.toArray(new PluginDetail[availablePlugins.size()]);
}
public PluginType[] getPluginTypes() {
return PluginType.getTypes();
}
/**
* @param pluginName the short name for the plugin.
* @return True if this registry or its parent contains
* an entry for the plugin specified by the name.
* The name is the short name for the plugin, not
* the classname, e.g. ant. Note that plugin
* names are always treated as case insensitive, so Ant, ant, and AnT are
* all treated as the same plugin.
*
* @throws NullPointerException If a null pluginName is passed, then
* a NullPointerException will occur. It's recommended to not pass a
* null pluginName.
*/
public boolean isPluginRegistered(final String pluginName) {
boolean isRegistered = plugins.containsKey(pluginName.toLowerCase());
if (!isRegistered && parentRegistry != null) {
isRegistered = parentRegistry.isPluginRegistered(pluginName);
}
return isRegistered;
}
/**
* Returns a PluginRegistry containing all the default plugins.
* The key is the plugin name (e.g. ant) and the value is
* the fully qualified classname
* (e.g. net.sourceforge.cruisecontrol.builders.AntBuilder).
* @return a PluginRegistry containing all the default plugins.
* @throws RuntimeException in case of IOException during the reading of the properties-file
*/
static PluginRegistry loadDefaultPluginRegistry() {
final PluginRegistry rootRegistry = new PluginRegistry(null);
final Properties pluginDefinitions = new Properties();
try {
pluginDefinitions.load(PluginRegistry.class.getResourceAsStream("default-plugins.properties"));
} catch (IOException e) {
throw new RuntimeException("Failed to load plugin-definitions from default-plugins.properties: " + e);
}
for (final Map.Entry entry : pluginDefinitions.entrySet()) {
rootRegistry.register((String) entry.getKey(), (String) entry.getValue());
}
return rootRegistry;
}
/**
* @param pluginName the plugin name
* @return the plugin configuration particular to this plugin, merged with the parents
* @throws NullPointerException if pluginName is null
*/
public Element getPluginConfig(String pluginName) {
pluginName = pluginName.toLowerCase();
final String className = getPluginClassname(pluginName);
return overridePluginConfig(pluginName, className, null);
}
/**
* Return a merged plugin configuration, taking into account parent classes where appropriate.
*
* This method is used recursively to fill up the specified pluginConfig Element.
*
* Properties are taken from parent plugins if they have not been defined in the child.
* Nested elements of the parent are always added to the beginning of child's config (in order to
* the later child's config values being able to overwrite the parent's values).
*
* Note: as we have no way to enforce the cardinality of the nested elements, the parent/default nested
* elements are always added to the config of the child. The validity of the resulting config then
* depends on the config to be correctly specified.
*
* @param pluginName the name of the plugin to create a config for (must be lower case)
* @param pluginClass the mapped class name for the plugin
* @param pluginConfig the current config, passed up for completion
* @return an Element representing the combination of the various plugin configurations for
* the same plugin, following the hierarchy.
*/
@SuppressWarnings("unchecked")
private Element overridePluginConfig(final String pluginName, final String pluginClass, Element pluginConfig) {
Element pluginElement = this.pluginConfigs.get(pluginName);
// clone the first found plugin config
if (pluginElement != null && pluginConfig == null) {
pluginElement = (Element) pluginElement.clone();
}
if (pluginConfig == null) {
pluginConfig = pluginElement;
} else {
// do not override if class names do not match
if (pluginElement != null && pluginClass.equals(this.internalGetPluginClassname(pluginName))) {
// override properties
final List<Attribute> attributes = (List<Attribute>) pluginElement.getAttributes();
for (final Attribute attribute : attributes) {
final String name = attribute.getName();
if (pluginConfig.getAttribute(name) == null) {
pluginConfig.setAttribute(name, attribute.getValue());
}
}
// combine child elements
final List<Element> children = (List<Element>) pluginElement.getChildren();
int index = 0;
for (final Element child : children) {
pluginConfig.addContent(index, (Element) child.clone());
index++;
}
}
}
if (this.parentRegistry != null) {
pluginConfig = this.parentRegistry.overridePluginConfig(pluginName, pluginClass, pluginConfig);
}
return pluginConfig;
}
/**
* Gets an iterator for iterating over all the plugin class names in this registry and
* its parents.
* @return The iterator object. Iteration over the class names is not guaranteed to take
* place in any particular order.
*/
public Iterator<String> iterator() {
return new PluginIterator();
}
/**
* Iterator for compositing this registry's class names with its parent's class names.
* @author pollens
*/
private class PluginIterator implements Iterator<String> {
/** Iterator over this registry's class names. */
private final Iterator<String> myClassNames;
/** Iterator over parent registry's classes. */
private final Iterator<String> parentClassNames;
/**
* Instantiates a PluginIterator for iterating over the classes in this PluginRegistry.
*/
public PluginIterator() {
myClassNames = PluginRegistry.this.plugins.values().iterator();
if (PluginRegistry.this.parentRegistry != null) {
parentClassNames = PluginRegistry.this.parentRegistry.iterator();
} else {
parentClassNames = null;
}
}
public boolean hasNext() {
return myClassNames.hasNext()
|| (parentClassNames != null && parentClassNames.hasNext());
}
public String next() {
// First, use up this registry's classes.
if (myClassNames.hasNext()) {
return myClassNames.next();
} else if (parentClassNames != null) {
// This registry has run out of classes, so fall back on the parent registry.
return parentClassNames.next();
} else {
// We have run out of plugins.
throw new NoSuchElementException();
}
}
public void remove() {
throw new UnsupportedOperationException("Removal not supported by this iterator.");
}
}
}