/******************************************************************************** * 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.dashboard.testhelpers; 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.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; import java.util.TreeMap; import net.sourceforge.cruisecontrol.CruiseControlException; import net.sourceforge.cruisecontrol.LabelIncrementer; import net.sourceforge.cruisecontrol.PluginRegistry; import net.sourceforge.cruisecontrol.ProjectConfig; import net.sourceforge.cruisecontrol.ProjectHelper; import net.sourceforge.cruisecontrol.ProjectInterface; import net.sourceforge.cruisecontrol.ProjectXMLHelper; import net.sourceforge.cruisecontrol.ResolverHolder; import net.sourceforge.cruisecontrol.labelincrementers.DefaultLabelIncrementer; import net.sourceforge.cruisecontrol.util.Util; import org.apache.log4j.Logger; import org.jdom.Element; /** * A plugin that represents the whole XML config file. * * @author <a href="mailto:jerome@coffeebreaks.org">Jerome Lacoste</a> */ public class DashboardConfig { private static final Logger LOG = Logger.getLogger(DashboardConfig.class); public static final String LABEL_INCREMENTER = "labelincrementer"; public static final boolean FAIL_UPON_MISSING_PROPERTY = false; private static final Set KNOWN_ROOT_CHILD_NAMES = new HashSet(); 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"); } private Map rootProperties = new HashMap(); /** * Properties of a particular node. Mapped by the node name. Doesn't handle * rootProperties yet */ private Map templatePluginProperties = new HashMap(); private PluginRegistry rootPlugins = PluginRegistry.createRegistry(); private Map projects = new LinkedHashMap(); // for test purposes only private Map projectPluginRegistries = new TreeMap(); private final ResolverHolder resolvers; public DashboardConfig(final Element ccElement) throws CruiseControlException { this(ccElement, new ResolverHolder.DummeResolvers()); } public DashboardConfig(final Element ccElement, final ResolverHolder resolvers) throws CruiseControlException { this.resolvers = resolvers; parse(ccElement); } private void parse(Element ccElement) throws CruiseControlException { // parse properties and plugins first, so their order in the config file // doesn't matter for (Iterator i = ccElement.getChildren("property").iterator(); i.hasNext();) { handleRootProperty((Element) i.next()); } for (Iterator i = ccElement.getChildren("plugin").iterator(); i.hasNext();) { handleRootPlugin((Element) i.next()); } for (Iterator i = ccElement.getChildren("include.projects").iterator(); i.hasNext();) { handleIncludedProjects((Element) i.next()); } // other childNodes must be projects or the <system> node for (Iterator i = ccElement.getChildren().iterator(); i.hasNext();) { Element childElement = (Element) i.next(); final String nodeName = childElement.getName(); if (isProject(nodeName)) { handleProject(childElement); } else if (!KNOWN_ROOT_CHILD_NAMES.contains(nodeName)) { throw new CruiseControlException("cannot handle child of <" + nodeName + ">"); } } } private DashboardConfig(final Element includedElement, final DashboardConfig parent) throws CruiseControlException { resolvers = parent.resolvers; rootPlugins = PluginRegistry.createRegistry(parent.rootPlugins); rootProperties = new HashMap(parent.rootProperties); templatePluginProperties = new HashMap(parent.templatePluginProperties); parse(includedElement); } private void handleIncludedProjects(final Element includeElement) { 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; } try { path = Util.parsePropertiesInString(rootProperties, path, FAIL_UPON_MISSING_PROPERTY); LOG.debug("getting included projects from " + path); final Element includedElement = resolvers.getXmlResolver().getElement(path); final DashboardConfig includedConfig = new DashboardConfig(includedElement, this); final Set includedProjectNames = includedConfig.getProjectNames(); for (final Iterator iter = includedProjectNames.iterator(); iter.hasNext();) { final String name = (String) iter.next(); if (projects.containsKey(name)) { String message = "Project " + name + " included from " + path + " is a duplicate name. Omitting."; LOG.error(message); } projects.put(name, includedConfig.getProject(name)); } } catch (CruiseControlException e) { LOG.error("Exception including file " + path, e); } } private boolean isProject(String nodeName) throws CruiseControlException { return rootPlugins.isPluginRegistered(nodeName) && ProjectInterface.class.isAssignableFrom(rootPlugins.getPluginClass(nodeName)); } private boolean isProjectTemplate(Element pluginElement) { String pluginName = pluginElement.getAttributeValue("name"); String pluginClassName = pluginElement.getAttributeValue("classname"); 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(Element pluginElement, String pluginName) { List properties = new ArrayList(); for (Iterator i = pluginElement.getChildren("property").iterator(); i.hasNext();) { properties.add(i.next()); } if (properties.size() > 0) { templatePluginProperties.put(pluginName, properties); } pluginElement.removeChildren("property"); } private void handleRootProperty(final Element childElement) throws CruiseControlException { ProjectXMLHelper.registerProperty(rootProperties, childElement, resolvers, FAIL_UPON_MISSING_PROPERTY); } private void handleProject(final Element projectElement) throws CruiseControlException { final String projectName = getProjectName(projectElement); 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); // handle project templates properties final List projectTemplateProperties = (List) templatePluginProperties.get(projectElement.getName()); if (projectTemplateProperties != null) { for (int i = 0; i < projectTemplateProperties.size(); i++) { final Element element = (Element) projectTemplateProperties.get(i); ProjectXMLHelper.registerProperty(nonFullyResolvedProjectProperties, element, resolvers, FAIL_UPON_MISSING_PROPERTY); } } // Register any project specific properties for (final Iterator projProps = projectElement.getChildren("property").iterator(); projProps .hasNext();) { final Element propertyElement = (Element) projProps.next(); ProjectXMLHelper.registerProperty(nonFullyResolvedProjectProperties, propertyElement, resolvers, FAIL_UPON_MISSING_PROPERTY); } // add the resolved rootProperties to the project's properties final Map thisProperties = nonFullyResolvedProjectProperties.thisMap; for (final Iterator iterator = rootProperties.keySet().iterator(); iterator.hasNext();) { final String key = (String) iterator.next(); if (!thisProperties.containsKey(key)) { final String value = (String) 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 PluginRegistry projectPlugins = PluginRegistry.createRegistry(rootPlugins); for (final Iterator pluginIter = projectElement.getChildren("plugin").iterator(); pluginIter .hasNext();) { projectPlugins.register((Element) pluginIter.next()); } projectElement.removeChildren("property"); projectElement.removeChildren("plugin"); LOG.debug("**************** configuring project " + projectName + " *******************"); final ProjectHelper projectHelper = new ProjectXMLHelper(thisProperties, projectPlugins, resolvers); final ProjectInterface project; try { project = (ProjectInterface) projectHelper.configurePlugin(projectElement, false); } catch (CruiseControlException e) { throw new CruiseControlException("error configuring project " + projectName, e); } // TODO: get rid of this ProjectConfig special case if (project instanceof ProjectConfig) { final ProjectConfig projectConfig = (ProjectConfig) project; if (projectConfig.getLabelIncrementer() == null) { final Class labelIncrClass = projectPlugins.getPluginClass(LABEL_INCREMENTER); LabelIncrementer labelIncrementer; try { labelIncrementer = (LabelIncrementer) labelIncrClass.newInstance(); } catch (Exception e) { LOG.error("Error instantiating label incrementer named " + labelIncrClass.getName() + "in project " + projectName + ". Using DefaultLabelIncrementer instead.", e); labelIncrementer = new DefaultLabelIncrementer(); } projectConfig.add(labelIncrementer); } } LOG.debug("**************** end configuring project " + projectName + " *******************"); this.projects.put(projectName, project); this.projectPluginRegistries.put(projectName, projectPlugins); } private String getProjectName(Element childElement) throws CruiseControlException { if (!isProject(childElement.getName())) { throw new IllegalStateException("Invalid Node <" + childElement.getName() + "> (not a project)"); } String rawName = childElement.getAttribute("name").getValue(); return Util.parsePropertiesInString(rootProperties, rawName, false); } public ProjectInterface getProject(String name) { return (ProjectInterface) this.projects.get(name); } public Set getProjectNames() { return Collections.unmodifiableSet(this.projects.keySet()); } PluginRegistry getRootPlugins() { return rootPlugins; } PluginRegistry getProjectPlugins(String name) { return (PluginRegistry) 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 { private Map parent; private Map thisMap; MapWithParent(Map parent) { this.parent = parent; this.thisMap = new HashMap(); } public int size() { int size = thisMap.size(); if (parent != null) { Set keys = parent.keySet(); for (Iterator iterator = keys.iterator(); iterator.hasNext();) { String key = (String) iterator.next(); 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 Object get(Object key) { Object value = thisMap.get(key); if (value == null && parent != null) { value = parent.get(key); } return value; } public Object put(Object o, Object o1) { return thisMap.put(o, o1); } public Object remove(Object key) { throw new UnsupportedOperationException("'remove' not supported on MapWithParent"); } public void putAll(Map map) { thisMap.putAll(map); } public void clear() { throw new UnsupportedOperationException("'clear' not supported on MapWithParent"); } public Set keySet() { Set keys = new HashSet(thisMap.keySet()); if (parent != null) { keys.addAll(parent.keySet()); } return keys; } public Collection 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 entrySet() { Set entries = new HashSet(thisMap.entrySet()); if (parent != null) { entries.addAll(parent.entrySet()); } return entries; } } }