/********************************************************************************
* 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.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import net.sourceforge.cruisecontrol.config.DashboardConfigurationPlugin;
import net.sourceforge.cruisecontrol.config.DefaultPropertiesPlugin;
import net.sourceforge.cruisecontrol.config.IncludeProjectsPlugin;
import net.sourceforge.cruisecontrol.config.PluginPlugin;
import net.sourceforge.cruisecontrol.config.PropertiesPlugin;
import net.sourceforge.cruisecontrol.config.SystemPlugin;
import net.sourceforge.cruisecontrol.config.XmlResolver;
import net.sourceforge.cruisecontrol.gendoc.annotations.Cardinality;
import net.sourceforge.cruisecontrol.gendoc.annotations.Description;
import net.sourceforge.cruisecontrol.util.Util;
import org.apache.log4j.Logger;
import org.jdom.Element;
/**
* @author <a href="mailto:jerome@coffeebreaks.org">Jerome Lacoste</a>
*/
@Description(
"The root element of the configuration, acting as a container to the rest of "
+ "the configuration elements.")
public class CruiseControlConfig {
private static final Logger LOG = Logger.getLogger(CruiseControlConfig.class);
public static final String LABEL_INCREMENTER = "labelincrementer";
public static final boolean FAIL_UPON_MISSING_PROPERTY = false;
private static final Map<String, ProjectQuery> PROJECTS_REGISTRY = new HashMap<String, ProjectQuery>();
private static final Set<String> KNOWN_ROOT_CHILD_NAMES = new HashSet<String>();
static {
KNOWN_ROOT_CHILD_NAMES.add("include.projects");
KNOWN_ROOT_CHILD_NAMES.add("property");
KNOWN_ROOT_CHILD_NAMES.add("plugin");
KNOWN_ROOT_CHILD_NAMES.add("system");
KNOWN_ROOT_CHILD_NAMES.add("dashboard");
}
private Map<String, String> rootProperties = new HashMap<String, String>();
/**
* Properties of a particular node. Mapped by the node name. Doesn't handle
* rootProperties yet
*/
private Map<String, List> templatePluginProperties = new HashMap<String, List>();
private PluginRegistry rootPlugins = PluginRegistry.createRegistry();
private final Map<String, ProjectInterface> projects = new LinkedHashMap<String, ProjectInterface>();
// for test purposes only
private final Map<String, PluginRegistry> projectPluginRegistries = new TreeMap<String, PluginRegistry>();
private final ResolverHolder resolvers;
private SystemPlugin system;
/**
* Returns the interface through which the state of a project of the given name can be queries.
* <p>
* Note: the method must be static in order to be accessible from objects not holding reference to the
* {@link CruiseControlConfig}. It is partially duplication of {@link #getProject(String)} method.
* </p>
* @param name the name of project to be found
* @return the instance of ProjectChecker for the given name
* @throws CruiseControlException if no such project is registered
*/
public static ProjectQuery findProject(final String name) throws CruiseControlException {
if (PROJECTS_REGISTRY.containsKey(name)) {
return PROJECTS_REGISTRY.get(name);
}
// No such project
throw new CruiseControlException("No project named '" + name + "'");
}
public int getMaxNbThreads() {
if (system != null) {
if (system.getConfig() != null) {
if (system.getConfig().getThreads() != null) {
return system.getConfig().getThreads().getCount();
}
}
}
return 1;
}
private final CruiseControlController controller;
private final Set<String> customPropertiesPlugins = new HashSet<String>();
public CruiseControlConfig(final Element ccElement) throws CruiseControlException {
this(ccElement, new ResolverHolder.DummeResolvers(), null);
}
public CruiseControlConfig(final Element ccElement, final CruiseControlController controller)
throws CruiseControlException {
this(ccElement, new ResolverHolder.DummeResolvers(), controller);
}
public CruiseControlConfig(final Element ccElement, final ResolverHolder resolvers)
throws CruiseControlException {
this(ccElement, resolvers, null);
}
public CruiseControlConfig(final Element ccElement, final ResolverHolder resolvers,
final CruiseControlController controller) throws CruiseControlException {
this.resolvers = resolvers;
this.controller = controller;
parse(ccElement);
}
private void parse(final Element ccElement) throws CruiseControlException {
// parse properties and plugins first, so their order in the config file
// doesn't matter
for (final Object o : ccElement.getChildren("property")) {
handleRootProperty((Element) o);
}
for (final Object o : ccElement.getChildren("plugin")) {
handleRootPlugin((Element) o);
}
// handle custom properties after plugin registration and before projects
for (final Object o : ccElement.getChildren()) {
final Element childElement = (Element) o;
final String nodeName = childElement.getName();
if (KNOWN_ROOT_CHILD_NAMES.contains(nodeName)
|| "system".equals(nodeName)
|| isProject(nodeName)) {
continue;
}
if (isCustomPropertiesPlugin(nodeName)) {
handleCustomRootProperty(childElement);
}
}
for (final Object o : ccElement.getChildren("include.projects")) {
handleIncludedProjects((Element) o);
}
for (final Object o : ccElement.getChildren("dashboard")) {
handleDashboard((Element) o);
}
// other childNodes must be projects or the <system> node
for (final Object o : ccElement.getChildren()) {
final Element childElement = (Element) o;
final String nodeName = childElement.getName();
if (isProject(nodeName)) {
handleProject(childElement);
} else if ("system".equals(nodeName)) {
add((SystemPlugin) new ProjectXMLHelper(resolvers).configurePlugin(childElement, false));
} else if (!KNOWN_ROOT_CHILD_NAMES.contains(nodeName) && !customPropertiesPlugins.contains(nodeName)) {
throw new CruiseControlException("cannot handle child of <" + nodeName + ">");
}
}
}
private CruiseControlConfig(final Element includedElement, final CruiseControlConfig parent)
throws CruiseControlException {
this.controller = parent.controller;
resolvers = parent.resolvers;
rootPlugins = PluginRegistry.createRegistry(parent.rootPlugins);
rootProperties = new HashMap<String, String>(parent.rootProperties);
templatePluginProperties = new HashMap<String, List>(parent.templatePluginProperties);
parse(includedElement);
}
private void handleIncludedProjects(final Element includeElement) {
final String path = includeElement.getAttributeValue("file");
if (path == null) {
LOG.warn("include.projects element missing file attribute. Skipping.");
}
if (resolvers == null || resolvers.getXmlResolver() == null) {
LOG.debug("xmlResolver not available; skipping include.projects element. ok if validating config.");
return;
}
if (resolvers.getXmlResolver() instanceof XmlResolver.DummyResolver) {
LOG.debug("dummy xmlResolver available only; changes in project config will not be reflected.");
}
try {
final IncludeProjectsPlugin includeProjects
= (IncludeProjectsPlugin) new ProjectXMLHelper(rootProperties, this
.getRootPlugins(), resolvers).configurePlugin(includeElement, FAIL_UPON_MISSING_PROPERTY);
add(includeProjects);
} catch (CruiseControlException e) {
LOG.error("Exception including file " + path, e);
}
}
private void handleDashboard(final Element dashboardElement) throws CruiseControlException {
final DashboardConfigurationPlugin dashboard =
(DashboardConfigurationPlugin) new ProjectXMLHelper(rootProperties, getRootPlugins(), resolvers)
.configurePlugin(dashboardElement, FAIL_UPON_MISSING_PROPERTY);
dashboard.setController(controller);
dashboard.validate();
dashboard.startPostingToDashboard();
}
private boolean isCustomPropertiesPlugin(String nodeName) throws CruiseControlException {
if (customPropertiesPlugins.contains(nodeName)) {
return true;
}
boolean isPropetiesPlugin = rootPlugins.isPluginRegistered(nodeName)
&& PropertiesPlugin.class.isAssignableFrom(rootPlugins.getPluginClass(nodeName));
if (isPropetiesPlugin) {
customPropertiesPlugins.add(nodeName);
}
return isPropetiesPlugin;
}
private boolean isProject(String nodeName) throws CruiseControlException {
return rootPlugins.isPluginRegistered(nodeName)
&& ProjectInterface.class.isAssignableFrom(rootPlugins.getPluginClass(nodeName));
}
private boolean isProjectTemplate(Element pluginElement) {
rootPlugins.from2classname(pluginElement);
String pluginName = pluginElement.getAttributeValue("name");
String pluginClassName = pluginElement.getAttributeValue("classname");
// String pluginFrom = pluginElement.getAttributeValue("from");
// if (pluginClassName == null && pluginFrom != null) {
// pluginClassName = rootPlugins.getPluginClassname(pluginFrom);
// // No standard plugin
// if (pluginClassName == null) {
// LOG.warn("<plugin name = '" + pluginName + "' from = '" + pluginFrom
// + "'> does not contain in-built element name");
// return false;
// }
// // Create "standard" plugin element
// pluginElement.setAttribute("classname", pluginClassName);
// pluginElement.removeAttribute("from");
// }
if (pluginClassName == null) {
pluginClassName = rootPlugins.getPluginClassname(pluginName);
}
try {
Class pluginClass = rootPlugins.instanciatePluginClass(pluginClassName, pluginName);
return ProjectInterface.class.isAssignableFrom(pluginClass);
} catch (CruiseControlException e) {
// this is only triggered by tests today, when a class is not
// loadable.
// I didn't want to propagate the exception
// in case something like Distributed CC requires a class to not be
// loadable locally at this point...
LOG.warn("Couldn't check if the plugin " + pluginName + " is an instance of ProjectInterface", e);
return false;
}
}
private void handleRootPlugin(Element pluginElement) throws CruiseControlException {
String pluginName = pluginElement.getAttributeValue("name");
if (pluginName == null) {
LOG.warn("Config contains plugin without a name-attribute, ignoring it");
return;
}
if (isProjectTemplate(pluginElement)) {
handleNodeProperties(pluginElement, pluginName);
}
rootPlugins.register(pluginElement);
}
private void handleNodeProperties(final Element pluginElement, final String pluginName) {
final List<Object> properties = new ArrayList<Object>();
final Set<String> propnodes = new HashSet<String>();
for (final Object o : pluginElement.getChildren()) {
final Element childElement = (Element) o;
final String childName = childElement.getName();
try {
if ("property".equals(childName) || isCustomPropertiesPlugin(childName)) {
properties.add(o);
propnodes.add(childName);
}
} catch (CruiseControlException e) {
LOG.error("Unable to register property " + childElement.getName(), e);
}
}
if (properties.size() > 0) {
templatePluginProperties.put(pluginName, properties);
}
// Remove the nodes from the element
for (String name : propnodes) {
pluginElement.removeChildren(name);
}
}
private void handleRootProperty(final Element childElement) throws CruiseControlException {
ProjectXMLHelper.registerProperty(rootProperties, childElement, resolvers,
FAIL_UPON_MISSING_PROPERTY);
}
private void handleCustomRootProperty(final Element childElement) throws CruiseControlException {
ProjectXMLHelper.registerCustomProperty(rootProperties, childElement, resolvers,
FAIL_UPON_MISSING_PROPERTY, PluginRegistry.createRegistry(rootPlugins));
}
/**
* @param project other project to add
* @throws CruiseControlException when something breaks
*/
@Description("Add projects defined in other configuration files.")
public void add(final IncludeProjectsPlugin project) throws CruiseControlException {
final String file = project.getFile();
final String path = Util.parsePropertiesInString(rootProperties, file, FAIL_UPON_MISSING_PROPERTY);
LOG.debug("getting included projects from " + path);
final Element includedElement = resolvers.getXmlResolver().getElement(path);
final CruiseControlConfig includedConfig = new CruiseControlConfig(includedElement, this);
final Set<String> includedProjectNames = includedConfig.getProjectNames();
for (final String name : includedProjectNames) {
if (projects.containsKey(name)) {
final String message = "Project " + name + " included from " + path + " is a duplicate name. Omitting.";
LOG.error(message);
}
final ProjectInterface projobj = includedConfig.getProject(name);
projects.put(name, projobj);
PROJECTS_REGISTRY.put(name, projobj);
}
}
/**
* @param system system place holder plugin
*/
@Description(
"Currently just a placeholder for the <code><configuration></code> element, which in "
+ "its turn is just a placeholder for the <code><threads></code> element. We expect that "
+ "in the future, more system-level features can be configured under this "
+ "element.")
@Cardinality(min = 0, max = 1)
public void add(final SystemPlugin system) {
this.system = system;
}
/**
* @param plugin only for gendoc
* @deprecated exists only for gendoc, should not be called.
*/
@Description("Registers a classname with an alias.")
public void add(final PluginPlugin plugin) {
// FIXME this is empty today for the documentation to be generated properly
throw new IllegalStateException("GenDoc-only method should not be invoked.");
}
/**
* @param project only for gendoc
* @deprecated exists only for gendoc, should not be called.
*/
@Description("Defines a basic unit of work.")
@Cardinality(min = 1, max = -1)
public void add(final ProjectInterface project) {
// FIXME this is empty today for the documentation to be generated properly
throw new IllegalStateException("GenDoc-only method should not be invoked.");
}
/**
* @param plugin only for gendoc
* @deprecated exists only for gendoc, should not be called.
*/
@Description("Defines a name/value pair used in configuration.")
public void add(final DefaultPropertiesPlugin plugin) {
// FIXME currently only declared for documentation generation purposes
throw new IllegalStateException("GenDoc-only method should not be invoked.");
}
/**
* @param dashboard only for gendoc
* @deprecated exists only for gendoc, should not be called.
*/
@Description("Configures dashboard-related settings.")
@Cardinality(min = 0, max = 1)
public void add(final DashboardConfigurationPlugin dashboard) {
// FIXME this is empty today for the documentation to be generated properly
throw new IllegalStateException("GenDoc-only method should not be invoked.");
}
private void handleProject(final Element projectElement) throws CruiseControlException {
final String projectName = getProjectName(projectElement);
final Set<String> projectProps = new HashSet<String>();
if (projects.containsKey(projectName)) {
final String duplicateEntriesMessage = "Duplicate entries in config file for project name " + projectName;
throw new CruiseControlException(duplicateEntriesMessage);
}
// property handling is a little bit dirty here.
// we have a set of properties mostly resolved in the rootProperties
// and a child set of properties
// it is possible that the rootProperties contain references to child
// properties
// in particular the project.name one
final MapWithParent nonFullyResolvedProjectProperties = new MapWithParent(rootProperties);
// Register the project's name as a built-in property
LOG.debug("Setting property \"project.name\" to \"" + projectName + "\".");
nonFullyResolvedProjectProperties.put("project.name", projectName);
// Register any project specific properties
for (final Object o : projectElement.getChildren("property")) {
final Element propertyElement = (Element) o;
final String propertyName = propertyElement.getAttributeValue("name");
ProjectXMLHelper.registerProperty(nonFullyResolvedProjectProperties, propertyElement,
resolvers, FAIL_UPON_MISSING_PROPERTY);
if (propertyName != null) {
projectProps.add(propertyElement.getAttributeValue("name"));
}
}
// And custom properties plugins
for (final Object o : projectElement.getChildren().toArray()) {
final Element childElement = (Element) o;
final String nodeName = childElement.getName();
if (KNOWN_ROOT_CHILD_NAMES.contains(nodeName)) {
continue;
}
if (isCustomPropertiesPlugin(nodeName)) {
ProjectXMLHelper.registerCustomProperty(nonFullyResolvedProjectProperties, childElement,
resolvers, FAIL_UPON_MISSING_PROPERTY, PluginRegistry.createRegistry(rootPlugins));
projectElement.removeContent(childElement);
}
}
// handle project templates properties
final List projectTemplateProperties = templatePluginProperties.get(projectElement.getName());
if (projectTemplateProperties != null) {
for (final Object projectTemplateProperty : projectTemplateProperties) {
final Element element = (Element) projectTemplateProperty;
final String propertyName = element.getAttributeValue("name");
// Here it ignores properties defined in <plugin /> with the same name as those
// defined in <project />. In this way, the project-defined properties override
// the plugin-defined ones.
if (propertyName != null && projectProps.contains(element.getAttributeValue("name"))) {
continue;
}
if (isCustomPropertiesPlugin(element.getName())) {
ProjectXMLHelper.registerCustomProperty(nonFullyResolvedProjectProperties,
(Element) element.clone(), resolvers, FAIL_UPON_MISSING_PROPERTY,
PluginRegistry.createRegistry(rootPlugins));
} else {
ProjectXMLHelper.registerProperty(nonFullyResolvedProjectProperties, (Element) element.clone(),
resolvers, FAIL_UPON_MISSING_PROPERTY);
}
}
}
// add the resolved rootProperties to the project's properties
final Map<String, String> thisProperties = nonFullyResolvedProjectProperties.thisMap;
for (final String key : rootProperties.keySet()) {
if (!thisProperties.containsKey(key)) {
final String value = rootProperties.get(key);
thisProperties.put(key, Util.parsePropertiesInString(thisProperties, value, false));
}
}
// Parse the entire element tree, expanding all property macros
ProjectXMLHelper.parsePropertiesInElement(projectElement, thisProperties, FAIL_UPON_MISSING_PROPERTY);
// Register any custom plugins
final ProjectXMLHelper helper = new ProjectXMLHelper(resolvers);
final PluginRegistry projectPlugins = PluginRegistry.createRegistry(rootPlugins);
// First register plugins embedded in the project template, if there are such
final Element pparentPlugin = rootPlugins.getPluginConfig(projectElement.getName());
if (pparentPlugin != null) {
for (final Object o : pparentPlugin.getChildren("plugin")) {
final Element element = (Element) o;
projectPlugins.from2classname(element);
final PluginPlugin plugin = (PluginPlugin) helper.configurePlugin(element, false);
projectPlugins.register(plugin);
}
}
// Register project plugins
for (final Object o : projectElement.getChildren("plugin")) {
final Element element = (Element) o;
projectPlugins.from2classname(element);
final PluginPlugin plugin = (PluginPlugin) helper.configurePlugin(element, false);
projectPlugins.register(plugin);
}
projectElement.removeChildren("property");
projectElement.removeChildren("plugin");
LOG.debug("**************** configuring project " + projectName + " *******************");
ProjectHelper projectHelper = new ProjectXMLHelper(thisProperties, projectPlugins, resolvers, controller);
final ProjectInterface project;
try {
project = (ProjectInterface) projectHelper.configurePlugin(projectElement, false);
} catch (CruiseControlException e) {
throw new CruiseControlException("error configuring project " + projectName, e);
}
// Why call method that is a no-op, and exists only for gendoc purposes?
//add(project);
project.validate();
LOG.debug("**************** end configuring project " + projectName + " *******************");
this.projects.put(projectName, project);
this.PROJECTS_REGISTRY.put(projectName, project);
this.projectPluginRegistries.put(projectName, projectPlugins);
}
private String getProjectName(final Element childElement) throws CruiseControlException {
if (!isProject(childElement.getName())) {
throw new IllegalStateException("Invalid Node <" + childElement.getName() + "> (not a project)");
}
final String rawName = childElement.getAttribute("name").getValue();
return Util.parsePropertiesInString(rootProperties, rawName, false);
}
public ProjectInterface getProject(String name) {
return this.projects.get(name);
}
public Set<String> getProjectNames() {
return Collections.unmodifiableSet(this.projects.keySet());
}
public PluginRegistry getRootPlugins() {
return rootPlugins;
}
public PluginRegistry getProjectPlugins(String name) {
return this.projectPluginRegistries.get(name);
}
// Unfortunately it seems like the commons-collection CompositeMap doesn't
// fit that role
// at least size is not implemented the way I want it.
// TODO is there a clean way to do without this?
private static class MapWithParent implements Map<String, String> {
private final Map<String, String> parent;
private final Map<String, String> thisMap;
MapWithParent(Map<String, String> parent) {
this.parent = parent;
this.thisMap = new HashMap<String, String>();
}
public int size() {
int size = thisMap.size();
if (parent != null) {
final Set<String> keys = parent.keySet();
for (final String key : keys) {
if (!thisMap.containsKey(key)) {
size++;
}
}
}
return size;
}
public boolean isEmpty() {
boolean parentIsEmpty = parent == null || parent.isEmpty();
return parentIsEmpty && thisMap.isEmpty();
}
public boolean containsKey(Object key) {
return thisMap.containsKey(key) || (parent != null && parent.containsKey(key));
}
public boolean containsValue(Object value) {
return thisMap.containsValue(value) || (parent != null && parent.containsValue(value));
}
public String get(Object key) {
String value = thisMap.get(key);
if (value == null && parent != null) {
value = parent.get(key);
}
return value;
}
public String put(String o, String o1) {
return thisMap.put(o, o1);
}
public String remove(Object key) {
throw new UnsupportedOperationException("'remove' not supported on MapWithParent");
}
public void putAll(Map< ? extends String, ? extends String> map) {
thisMap.putAll(map);
}
public void clear() {
throw new UnsupportedOperationException("'clear' not supported on MapWithParent");
}
public Set<String> keySet() {
Set<String> keys = new HashSet<String>(thisMap.keySet());
if (parent != null) {
keys.addAll(parent.keySet());
}
return keys;
}
public Collection<String> values() {
throw new UnsupportedOperationException("not implemented");
/*
* we have to support the Map contract. Back the returned values.
* Mmmmm
*/
/*
* Collection values = thisMap.values(); if (parent != null) { Set
* keys = parent.keySet(); List parentValues = new ArrayList(); for
* (Iterator iterator = keys.iterator(); iterator.hasNext();) {
* String key = (String) iterator.next(); if (!
* thisMap.containsKey(key)) { parentValues.add(parent.get(key)); } } }
* return values;
*/
}
public Set<Map.Entry<String, String>> entrySet() {
final Set<Map.Entry<String, String>> entries = new HashSet<Map.Entry<String, String>>(thisMap.entrySet());
if (parent != null) {
entries.addAll(parent.entrySet());
}
return entries;
}
}
}