001: /********************************************************************************
002: * CruiseControl, a Continuous Integration Toolkit
003: * Copyright (c) 2001, ThoughtWorks, Inc.
004: * 200 E. Randolph, 25th Floor
005: * Chicago, IL 60601 USA
006: * All rights reserved.
007: *
008: * Redistribution and use in source and binary forms, with or without
009: * modification, are permitted provided that the following conditions
010: * are met:
011: *
012: * + Redistributions of source code must retain the above copyright
013: * notice, this list of conditions and the following disclaimer.
014: *
015: * + Redistributions in binary form must reproduce the above
016: * copyright notice, this list of conditions and the following
017: * disclaimer in the documentation and/or other materials provided
018: * with the distribution.
019: *
020: * + Neither the name of ThoughtWorks, Inc., CruiseControl, nor the
021: * names of its contributors may be used to endorse or promote
022: * products derived from this software without specific prior
023: * written permission.
024: *
025: * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
026: * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
027: * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
028: * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR
029: * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
030: * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
031: * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
032: * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
033: * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
034: * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
035: * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
036: ********************************************************************************/package net.sourceforge.cruisecontrol;
037:
038: import java.util.ArrayList;
039: import java.util.Collection;
040: import java.util.Collections;
041: import java.util.HashMap;
042: import java.util.HashSet;
043: import java.util.Iterator;
044: import java.util.LinkedHashMap;
045: import java.util.List;
046: import java.util.Map;
047: import java.util.Set;
048: import java.util.TreeMap;
049:
050: import net.sourceforge.cruisecontrol.labelincrementers.DefaultLabelIncrementer;
051: import net.sourceforge.cruisecontrol.config.DashboardConfigurationPlugin;
052: import net.sourceforge.cruisecontrol.config.PluginPlugin;
053: import net.sourceforge.cruisecontrol.config.XmlResolver;
054: import net.sourceforge.cruisecontrol.config.SystemPlugin;
055: import net.sourceforge.cruisecontrol.config.IncludeProjectsPlugin;
056: import net.sourceforge.cruisecontrol.config.DefaultPropertiesPlugin;
057:
058: import org.apache.log4j.Logger;
059: import org.jdom.Element;
060:
061: /**
062: * <p>
063: * The <code><cruisecontrol></code> element is the root element of the
064: * configuration, and acts as a container to the rest of the configuration
065: * elements.
066: * </p>
067: *
068: * @author <a href="mailto:jerome@coffeebreaks.org">Jerome Lacoste</a>
069: */
070: public class CruiseControlConfig {
071: private static final Logger LOG = Logger
072: .getLogger(CruiseControlConfig.class);
073:
074: public static final String LABEL_INCREMENTER = "labelincrementer";
075:
076: public static final boolean FAIL_UPON_MISSING_PROPERTY = false;
077:
078: private static final Set KNOWN_ROOT_CHILD_NAMES = new HashSet();
079: static {
080: KNOWN_ROOT_CHILD_NAMES.add("include.projects");
081: KNOWN_ROOT_CHILD_NAMES.add("property");
082: KNOWN_ROOT_CHILD_NAMES.add("plugin");
083: KNOWN_ROOT_CHILD_NAMES.add("system");
084: KNOWN_ROOT_CHILD_NAMES.add("dashboard");
085: }
086:
087: private Map rootProperties = new HashMap();
088: /**
089: * Properties of a particular node. Mapped by the node name. Doesn't handle
090: * rootProperties yet
091: */
092: private Map templatePluginProperties = new HashMap();
093: private PluginRegistry rootPlugins = PluginRegistry
094: .createRegistry();
095: private Map projects = new LinkedHashMap();
096: // for test purposes only
097: private Map projectPluginRegistries = new TreeMap();
098:
099: private XmlResolver xmlResolver;
100:
101: private SystemPlugin system;
102:
103: public int getMaxNbThreads() {
104: if (system != null) {
105: if (system.getConfig() != null) {
106: if (system.getConfig().getThreads() != null) {
107: return system.getConfig().getThreads().getCount();
108: }
109: }
110: }
111: return 1;
112: }
113:
114: private final CruiseControlController controller;
115:
116: public CruiseControlConfig(Element ccElement)
117: throws CruiseControlException {
118: this (ccElement, null, null);
119: }
120:
121: public CruiseControlConfig(Element ccElement,
122: CruiseControlController controller)
123: throws CruiseControlException {
124: this (ccElement, (XmlResolver) null, controller);
125: }
126:
127: public CruiseControlConfig(Element ccElement,
128: XmlResolver xmlResolver) throws CruiseControlException {
129: this (ccElement, xmlResolver, null);
130: }
131:
132: public CruiseControlConfig(Element ccElement,
133: XmlResolver xmlResolver, CruiseControlController controller)
134: throws CruiseControlException {
135: this .xmlResolver = xmlResolver;
136: this .controller = controller;
137: parse(ccElement);
138: }
139:
140: private void parse(Element ccElement) throws CruiseControlException {
141: // parse properties and plugins first, so their order in the config file
142: // doesn't matter
143: for (Iterator i = ccElement.getChildren("property").iterator(); i
144: .hasNext();) {
145: handleRootProperty((Element) i.next());
146: }
147: for (Iterator i = ccElement.getChildren("plugin").iterator(); i
148: .hasNext();) {
149: handleRootPlugin((Element) i.next());
150: }
151: for (Iterator i = ccElement.getChildren("include.projects")
152: .iterator(); i.hasNext();) {
153: handleIncludedProjects((Element) i.next());
154: }
155: for (Iterator i = ccElement.getChildren("dashboard").iterator(); i
156: .hasNext();) {
157: handleDashboard((Element) i.next());
158: }
159:
160: // other childNodes must be projects or the <system> node
161: for (Iterator i = ccElement.getChildren().iterator(); i
162: .hasNext();) {
163: Element childElement = (Element) i.next();
164: final String nodeName = childElement.getName();
165: if (isProject(nodeName)) {
166: handleProject(childElement);
167: } else if ("system".equals(nodeName)) {
168: add((SystemPlugin) new ProjectXMLHelper()
169: .configurePlugin(childElement, false));
170: } else if (!KNOWN_ROOT_CHILD_NAMES.contains(nodeName)) {
171: throw new CruiseControlException(
172: "cannot handle child of <" + nodeName + ">");
173: }
174: }
175: }
176:
177: private CruiseControlConfig(Element includedElement,
178: CruiseControlConfig parent) throws CruiseControlException {
179: this .controller = parent.controller;
180: xmlResolver = parent.xmlResolver;
181: rootPlugins = PluginRegistry.createRegistry(parent.rootPlugins);
182: rootProperties = new HashMap(parent.rootProperties);
183: templatePluginProperties = new HashMap(
184: parent.templatePluginProperties);
185:
186: parse(includedElement);
187: }
188:
189: private void handleIncludedProjects(Element includeElement) {
190: String path = includeElement.getAttributeValue("file");
191: if (path == null) {
192: LOG
193: .warn("include.projects element missing file attribute. Skipping.");
194: }
195: if (xmlResolver == null) {
196: LOG
197: .debug("xmlResolver not available; skipping include.projects element. ok if validating config.");
198: return;
199: }
200: try {
201: IncludeProjectsPlugin includeProjects = (IncludeProjectsPlugin) new ProjectXMLHelper(
202: rootProperties, this .getRootPlugins())
203: .configurePlugin(includeElement,
204: FAIL_UPON_MISSING_PROPERTY);
205: add(includeProjects);
206: } catch (CruiseControlException e) {
207: LOG.error("Exception including file " + path, e);
208: }
209: }
210:
211: private void handleDashboard(Element dashboardElement)
212: throws CruiseControlException {
213: DashboardConfigurationPlugin dashboard = (DashboardConfigurationPlugin) new ProjectXMLHelper(
214: rootProperties, getRootPlugins()).configurePlugin(
215: dashboardElement, FAIL_UPON_MISSING_PROPERTY);
216: dashboard.setController(controller);
217: dashboard.validate();
218: dashboard.startPostingToDashboard();
219: }
220:
221: private boolean isProject(String nodeName)
222: throws CruiseControlException {
223: return rootPlugins.isPluginRegistered(nodeName)
224: && ProjectInterface.class.isAssignableFrom(rootPlugins
225: .getPluginClass(nodeName));
226: }
227:
228: private boolean isProjectTemplate(Element pluginElement) {
229: String pluginName = pluginElement.getAttributeValue("name");
230: String pluginClassName = pluginElement
231: .getAttributeValue("classname");
232: if (pluginClassName == null) {
233: pluginClassName = rootPlugins
234: .getPluginClassname(pluginName);
235: }
236: try {
237: Class pluginClass = rootPlugins.instanciatePluginClass(
238: pluginClassName, pluginName);
239: return ProjectInterface.class.isAssignableFrom(pluginClass);
240: } catch (CruiseControlException e) {
241: // this is only triggered by tests today, when a class is not
242: // loadable.
243: // I didn't want to propagate the exception
244: // in case something like Distributed CC requires a class to not be
245: // loadable locally at this point...
246: LOG.warn("Couldn't check if the plugin " + pluginName
247: + " is an instance of ProjectInterface", e);
248: return false;
249: }
250: }
251:
252: private void handleRootPlugin(Element pluginElement)
253: throws CruiseControlException {
254: String pluginName = pluginElement.getAttributeValue("name");
255: if (pluginName == null) {
256: LOG
257: .warn("Config contains plugin without a name-attribute, ignoring it");
258: return;
259: }
260: if (isProjectTemplate(pluginElement)) {
261: handleNodeProperties(pluginElement, pluginName);
262: }
263: rootPlugins.register(pluginElement);
264: }
265:
266: private void handleNodeProperties(Element pluginElement,
267: String pluginName) {
268: List properties = new ArrayList();
269: for (Iterator i = pluginElement.getChildren("property")
270: .iterator(); i.hasNext();) {
271: properties.add(i.next());
272: }
273: if (properties.size() > 0) {
274: templatePluginProperties.put(pluginName, properties);
275: }
276: pluginElement.removeChildren("property");
277: }
278:
279: private void handleRootProperty(Element childElement)
280: throws CruiseControlException {
281: DefaultPropertiesPlugin props = ProjectXMLHelper
282: .registerProperty(rootProperties, childElement,
283: FAIL_UPON_MISSING_PROPERTY);
284: add(props);
285: }
286:
287: /**
288: * Defines a name/value pair used in configuration.
289: *
290: * @param property
291: * @cardinality 0..*;
292: */
293: public void add(DefaultPropertiesPlugin property) {
294: // FIXME this is empty today for the documentation to be generated
295: // properly
296: }
297:
298: /**
299: * Add projects defined in other configuration files.
300: *
301: * @cardinality 0..*;
302: */
303: public void add(IncludeProjectsPlugin project)
304: throws CruiseControlException {
305: String file = project.getFile();
306: String path = ProjectXMLHelper.parsePropertiesInString(
307: rootProperties, file, FAIL_UPON_MISSING_PROPERTY);
308: // FIXME GENDOC Self configure ??
309: LOG.debug("getting included projects from " + path);
310: Element includedElement = xmlResolver.getElement(path);
311: CruiseControlConfig includedConfig = new CruiseControlConfig(
312: includedElement, this );
313: Set includedProjectNames = includedConfig.getProjectNames();
314: for (Iterator iter = includedProjectNames.iterator(); iter
315: .hasNext();) {
316: String name = (String) iter.next();
317: if (projects.containsKey(name)) {
318: String message = "Project " + name + " included from "
319: + path + " is a duplicate name. Omitting.";
320: LOG.error(message);
321: }
322: projects.put(name, includedConfig.getProject(name));
323: }
324: }
325:
326: /**
327: * Currently just a placeholder for the <configuration> element, which in
328: * its turn is just a placeholder for the <threads> element. We expect that
329: * in the future, more system-level features can be configured under this
330: * element.
331: *
332: * @param system
333: * @cardinality 0..1;
334: */
335: public void add(SystemPlugin system) {
336: this .system = system;
337: }
338:
339: /**
340: * Registers a classname with an alias.
341: *
342: * @param plugin
343: * @cardinality 0..*;
344: */
345: public void add(PluginPlugin plugin) {
346: // FIXME this is empty today for the documentation to be generated
347: // properly
348: }
349:
350: /**
351: * Defines a basic unit of work
352: *
353: * @param project
354: * @cardinality 1..*;
355: */
356: public void add(ProjectInterface project) {
357: // FIXME this is empty today for the documentation to be generated
358: // properly
359: }
360:
361: private void handleProject(Element projectElement)
362: throws CruiseControlException {
363:
364: String projectName = getProjectName(projectElement);
365:
366: if (projects.containsKey(projectName)) {
367: final String duplicateEntriesMessage = "Duplicate entries in config file for project name "
368: + projectName;
369: throw new CruiseControlException(duplicateEntriesMessage);
370: }
371:
372: // property handling is a little bit dirty here.
373: // we have a set of properties mostly resolved in the rootProperties
374: // and a child set of properties
375: // it is possible that the rootProperties contain references to child
376: // properties
377: // in particular the project.name one
378: MapWithParent nonFullyResolvedProjectProperties = new MapWithParent(
379: rootProperties);
380: // Register the project's name as a built-in property
381: LOG.debug("Setting property \"project.name\" to \""
382: + projectName + "\".");
383: nonFullyResolvedProjectProperties.put("project.name",
384: projectName);
385:
386: // handle project templates properties
387: List projectTemplateProperties = (List) templatePluginProperties
388: .get(projectElement.getName());
389: if (projectTemplateProperties != null) {
390: for (int i = 0; i < projectTemplateProperties.size(); i++) {
391: Element element = (Element) projectTemplateProperties
392: .get(i);
393: ProjectXMLHelper.registerProperty(
394: nonFullyResolvedProjectProperties, element,
395: FAIL_UPON_MISSING_PROPERTY);
396: }
397: }
398:
399: // Register any project specific properties
400: for (Iterator projProps = projectElement
401: .getChildren("property").iterator(); projProps
402: .hasNext();) {
403: final Element propertyElement = (Element) projProps.next();
404: ProjectXMLHelper.registerProperty(
405: nonFullyResolvedProjectProperties, propertyElement,
406: FAIL_UPON_MISSING_PROPERTY);
407: }
408:
409: // add the resolved rootProperties to the project's properties
410: Map this Properties = nonFullyResolvedProjectProperties.this Map;
411: for (Iterator iterator = rootProperties.keySet().iterator(); iterator
412: .hasNext();) {
413: String key = (String) iterator.next();
414: if (!this Properties.containsKey(key)) {
415: String value = (String) rootProperties.get(key);
416: this Properties.put(key, ProjectXMLHelper
417: .parsePropertiesInString(this Properties, value,
418: false));
419: }
420: }
421:
422: // Parse the entire element tree, expanding all property macros
423: ProjectXMLHelper.parsePropertiesInElement(projectElement,
424: this Properties, FAIL_UPON_MISSING_PROPERTY);
425:
426: // Register any custom plugins
427: PluginRegistry projectPlugins = PluginRegistry
428: .createRegistry(rootPlugins);
429: for (Iterator pluginIter = projectElement.getChildren("plugin")
430: .iterator(); pluginIter.hasNext();) {
431: Element element = (Element) pluginIter.next();
432: PluginPlugin plugin = (PluginPlugin) new ProjectXMLHelper()
433: .configurePlugin(element, false);
434: // projectPlugins.register(plugin);
435: projectPlugins.register(element);
436: // add(plugin);
437: }
438:
439: projectElement.removeChildren("property");
440: projectElement.removeChildren("plugin");
441:
442: LOG.debug("**************** configuring project " + projectName
443: + " *******************");
444: ProjectHelper projectHelper = new ProjectXMLHelper(
445: this Properties, projectPlugins, controller);
446: ProjectInterface project;
447:
448: try {
449: project = (ProjectInterface) projectHelper.configurePlugin(
450: projectElement, false);
451: } catch (CruiseControlException e) {
452: throw new CruiseControlException(
453: "error configuring project " + projectName, e);
454: }
455:
456: add(project);
457:
458: // TODO: get rid of this ProjectConfig special case
459: if (project instanceof ProjectConfig) {
460: ProjectConfig projectConfig = (ProjectConfig) project;
461:
462: if (projectConfig.getLabelIncrementer() == null) {
463: LabelIncrementer labelIncrementer;
464: Class labelIncrClass = projectPlugins
465: .getPluginClass(LABEL_INCREMENTER);
466: try {
467: labelIncrementer = (LabelIncrementer) labelIncrClass
468: .newInstance();
469: } catch (Exception e) {
470: LOG
471: .error(
472: "Error instantiating label incrementer named "
473: + labelIncrClass.getName()
474: + "in project "
475: + projectName
476: + ". Using DefaultLabelIncrementer instead.",
477: e);
478: labelIncrementer = new DefaultLabelIncrementer();
479: }
480: projectConfig.add(labelIncrementer);
481: }
482: }
483:
484: project.validate();
485: LOG.debug("**************** end configuring project "
486: + projectName + " *******************");
487:
488: this .projects.put(projectName, project);
489: this .projectPluginRegistries.put(projectName, projectPlugins);
490: }
491:
492: private String getProjectName(Element childElement)
493: throws CruiseControlException {
494: if (!isProject(childElement.getName())) {
495: throw new IllegalStateException("Invalid Node <"
496: + childElement.getName() + "> (not a project)");
497: }
498: String rawName = childElement.getAttribute("name").getValue();
499: return ProjectXMLHelper.parsePropertiesInString(rootProperties,
500: rawName, false);
501: }
502:
503: public ProjectInterface getProject(String name) {
504: return (ProjectInterface) this .projects.get(name);
505: }
506:
507: public Set getProjectNames() {
508: return Collections.unmodifiableSet(this .projects.keySet());
509: }
510:
511: PluginRegistry getRootPlugins() {
512: return rootPlugins;
513: }
514:
515: PluginRegistry getProjectPlugins(String name) {
516: return (PluginRegistry) this .projectPluginRegistries.get(name);
517: }
518:
519: // Unfortunately it seems like the commons-collection CompositeMap doesn't
520: // fit that role
521: // at least size is not implemented the way I want it.
522: // TODO is there a clean way to do without this?
523: private static class MapWithParent implements Map {
524: private Map parent;
525: private Map this Map;
526:
527: MapWithParent(Map parent) {
528: this .parent = parent;
529: this .this Map = new HashMap();
530: }
531:
532: public int size() {
533: int size = this Map.size();
534: if (parent != null) {
535: Set keys = parent.keySet();
536: for (Iterator iterator = keys.iterator(); iterator
537: .hasNext();) {
538: String key = (String) iterator.next();
539: if (!this Map.containsKey(key)) {
540: size++;
541: }
542: }
543: }
544: return size;
545: }
546:
547: public boolean isEmpty() {
548: boolean parentIsEmpty = parent == null || parent.isEmpty();
549: return parentIsEmpty && this Map.isEmpty();
550: }
551:
552: public boolean containsKey(Object key) {
553: return this Map.containsKey(key)
554: || (parent != null && parent.containsKey(key));
555: }
556:
557: public boolean containsValue(Object value) {
558: return this Map.containsValue(value)
559: || (parent != null && parent.containsValue(value));
560: }
561:
562: public Object get(Object key) {
563: Object value = this Map.get(key);
564: if (value == null && parent != null) {
565: value = parent.get(key);
566: }
567: return value;
568: }
569:
570: public Object put(Object o, Object o1) {
571: return this Map.put(o, o1);
572: }
573:
574: public Object remove(Object key) {
575: throw new UnsupportedOperationException(
576: "'remove' not supported on MapWithParent");
577: }
578:
579: public void putAll(Map map) {
580: this Map.putAll(map);
581: }
582:
583: public void clear() {
584: throw new UnsupportedOperationException(
585: "'clear' not supported on MapWithParent");
586: }
587:
588: public Set keySet() {
589: Set keys = new HashSet(this Map.keySet());
590: if (parent != null) {
591: keys.addAll(parent.keySet());
592: }
593: return keys;
594: }
595:
596: public Collection values() {
597: throw new UnsupportedOperationException("not implemented");
598: /*
599: * we have to support the Map contract. Back the returned values.
600: * Mmmmm
601: */
602: /*
603: * Collection values = thisMap.values(); if (parent != null) { Set
604: * keys = parent.keySet(); List parentValues = new ArrayList(); for
605: * (Iterator iterator = keys.iterator(); iterator.hasNext();) {
606: * String key = (String) iterator.next(); if (!
607: * thisMap.containsKey(key)) { parentValues.add(parent.get(key)); } } }
608: * return values;
609: */
610: }
611:
612: public Set entrySet() {
613: Set entries = new HashSet(thisMap.entrySet());
614: if (parent != null) {
615: entries.addAll(parent.entrySet());
616: }
617: return entries;
618: }
619: }
620:
621: }
|