package net.sourceforge.cruisecontrol.builders; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.Map; import net.sourceforge.cruisecontrol.Builder; import net.sourceforge.cruisecontrol.CruiseControlException; import net.sourceforge.cruisecontrol.Progress; import net.sourceforge.cruisecontrol.gendoc.annotations.Cardinality; import net.sourceforge.cruisecontrol.gendoc.annotations.Default; import net.sourceforge.cruisecontrol.gendoc.annotations.Description; import net.sourceforge.cruisecontrol.gendoc.annotations.Optional; import net.sourceforge.cruisecontrol.util.DateUtil; import net.sourceforge.cruisecontrol.util.ValidationHelper; import org.apache.log4j.Logger; import org.jdom.Attribute; import org.jdom.Element; import org.jdom.CDATA; @Description( "<p>The CompositeBuilder executes a list of builders (any builder, except <a " + "href=\"#pause\"><pause></a>). This is necessary for builds in an " + "empty directory (see keyword CRISP-builds in Pragmatic Project Automation " + "from Mike Clark) </p>") public class CompositeBuilder extends Builder { private static final Logger LOG = Logger.getLogger(CompositeBuilder.class); private static final long serialVersionUID = -3819555247003945186L; private final List<Builder> builders = new ArrayList<Builder>(); private long startTime = 0; private long timeoutSeconds = ScriptRunner.NO_TIMEOUT; private boolean isTimedOut; private long childStartTime = 0; @Description("Adds a builder to this composite builder.") @Cardinality(min = 0, max = -1) public void add(Builder builder) { builders.add(builder); } private void startBuild() { startTime = System.currentTimeMillis(); } private void endBuild(Element buildResult) { if (isTimedOut) { LOG.warn("Composite Build timeout timer of " + timeoutSeconds + " seconds has expired"); buildResult.setAttribute("error", "build timeout"); } long endTime = System.currentTimeMillis(); buildResult.setAttribute("time", DateUtil.getDurationAsString((endTime - startTime))); } private void checkTimedOut() { if (timeoutSeconds != ScriptRunner.NO_TIMEOUT && (System.currentTimeMillis() - startTime) > (timeoutSeconds * 1000L)) { isTimedOut = true; } } private void startChild() { childStartTime = System.currentTimeMillis(); } private static boolean processBuildResult(final Element buildResult, final Element compositeBuildResult, final String buildlogMsgPrefix, final Builder builder, final long childStartTime) { // add child builder info to build log insertBuildLogHeader(buildResult, buildlogMsgPrefix + " - " + builder.getClass().getName() + "; child", childStartTime, "composite", "composite-childbuilder"); compositeBuildResult.addContent(buildResult.removeContent()); // check for error (if we found one, we will stop) if (!isBuildSuccessful(buildResult)) { LOG.debug("CompositeBuilder: error element found, stopping)"); final Attribute attribute = buildResult.getAttribute("error"); attribute.detach(); compositeBuildResult.setAttribute(attribute); return true; // stop, since we found an error in the last build } return false; // if we made it this far, no errors were found } /** * set the "header" for this part of the build log. turns it into an Ant target/task style element for reporting * purposes * * @param buildResult the element of the build log for the current child builder * @param buildLogMsg child builder info to add to the build log * @param childStartTime the time this child builder started building * @param attribNameTarget value of name attribute of 'target' element. * @param attribNameTask value of name attribute of 'task' element. */ @SuppressWarnings("unchecked") public static void insertBuildLogHeader(final Element buildResult, final String buildLogMsg, final long childStartTime, final String attribNameTarget, final String attribNameTask) { // add info from attributes of "build" tag from child build String buildMsgWithAttibs = buildLogMsg + " build attributes: "; final Iterator<Attribute> attributes = (Iterator<Attribute>) buildResult.getAttributes().iterator(); while (attributes.hasNext()) { final Attribute attribute = attributes.next(); buildMsgWithAttibs += attribute.getName() + "=" + attribute.getValue() + "; "; } // @todo Rearrange these elements (even nesting childLog elements?), might display this info in reporting apps final Element target = new Element("target"); target.setAttribute("name", attribNameTarget); target.setAttribute("time", DateUtil.getDurationAsString((System.currentTimeMillis() - childStartTime))); final Element task = new Element("task"); task.setAttribute("name", attribNameTask); final Element msg = new Element("message"); msg.addContent(new CDATA(buildMsgWithAttibs)); msg.setAttribute("priority", "warn"); task.addContent(msg); target.addContent(task); buildResult.addContent(0, target); final Element msgBuild = new Element("message"); msgBuild.addContent(new CDATA(buildMsgWithAttibs)); msgBuild.setAttribute("priority", "warn"); buildResult.addContent(1, msgBuild); } // insertBuildLogHeader private static boolean isBuildSuccessful(final Element buildResult) { return (buildResult.getAttribute("error") == null); } public Element build(final Map<String, String> properties, final Progress progressIn) throws CruiseControlException { final Progress progress = getShowProgress() ? progressIn : null; boolean errorOcurred = false; final Element compositeBuildResult = new Element("build"); final Iterator<Builder> iter = builders.iterator(); int i = 0; final int totalBuilders = builders.size(); startBuild(); while (iter.hasNext() & !errorOcurred & !isTimedOut) { i++; final String buildlogMsgPrefix = "composite build " + i + " of " + totalBuilders; if (progress != null) { progress.setValue(buildlogMsgPrefix); } final Builder builder = iter.next(); startChild(); final Element buildResult = builder.build(properties, progress); errorOcurred = processBuildResult(buildResult, compositeBuildResult, buildlogMsgPrefix, builder, childStartTime); checkTimedOut(); } endBuild(compositeBuildResult); return compositeBuildResult; } public Element buildWithTarget(final Map<String, String> properties, final String target, final Progress progressIn) throws CruiseControlException { final Progress progress = getShowProgress() ? progressIn : null; boolean errorOcurred = false; final Element compositeBuildResult = new Element("build"); final Iterator<Builder> iter = builders.iterator(); int i = 0; final int totalBuilders = builders.size(); startBuild(); while (iter.hasNext() & !errorOcurred & !isTimedOut) { i++; final String buildlogMsgPrefix = "composite build " + i + " of " + totalBuilders; if (progress != null) { progress.setValue(buildlogMsgPrefix); } final Builder builder = iter.next(); startChild(); final Element buildResult = builder.buildWithTarget(properties, target, progress); errorOcurred = processBuildResult(buildResult, compositeBuildResult, buildlogMsgPrefix, builder, childStartTime); checkTimedOut(); } endBuild(compositeBuildResult); return compositeBuildResult; } public void validate() throws CruiseControlException { ValidationHelper.assertFalse(builders.isEmpty(), "no builders added"); super.validate(); // validate all child builders for (final Builder builder : builders) { builder.validate(); } } /** @return array of the builders in this composite. */ public Builder[] getBuilders() { return builders.toArray(new Builder[builders.size()]); } /** * @param timeout The timeout (in seconds) to set. */ @Description( "Composite Build will be halted if it continues longer than the specified " + "timeout. Value in seconds.") @Optional public void setTimeout(long timeout) { this.timeoutSeconds = timeout; } /** Method override to allow different @Description annotations. */ @Description( "If true or omitted, the composite builder will provide progress messages, " + "as will any child builders that support this feature (assuming the child " + "builder's own showProgress setting is true). If false, no progress " + "messages will be shown by the composite builder or child builders - " + "regardless of child builder showProgress settings. If any parent " + "showProgress is false, then no progress will be shown, regardless of the " + "composite or child builder settings.") @Optional @Default("true") @Override public void setShowProgress(boolean show) { super.setShowProgress(show); } /** Method override to allow different @Description annotations. */ @Description("Currently, the liveOutput setting has no effect on composite builders.") @Optional @Default("true") @Override public void setLiveOutput(boolean live) { super.setLiveOutput(live); } }