/******************************************************************************** * 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.File; import java.io.IOException; import java.util.Calendar; import java.util.Date; import java.util.LinkedList; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; 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.gendoc.annotations.Required; import net.sourceforge.cruisecontrol.util.BuildOutputLogger; import net.sourceforge.cruisecontrol.util.OSEnvironment; import net.sourceforge.cruisecontrol.util.PerDayScheduleItem; import net.sourceforge.cruisecontrol.util.ValidationHelper; import org.jdom.Element; public abstract class Builder extends PerDayScheduleItem implements Comparable { private int time = NOT_SET; private int multiple = 1; private boolean multipleSet = false; private boolean showProgress = true; private boolean isLiveOutput = true; private BuildOutputLogger buildOutputLogger; private final LinkedList<EnvConf> env = new LinkedList<EnvConf>(); /** Build property name of property that is pass to all builders. */ public static final String BUILD_PROP_PROJECTNAME = "projectname"; /** * Execute a build. * @param properties build properties * @param progress callback to provide progress updates * @return the log resulting from executing the build * @throws CruiseControlException if something breaks */ public abstract Element build(Map<String, String> properties, Progress progress) throws CruiseControlException; /** * Execute a build with the given target. * @param properties build properties * @param target the build target to call, overrides target defined in config * @param progress callback to provide progress updates * @return the log resulting from executing the build * @throws CruiseControlException if something breaks */ public abstract Element buildWithTarget(Map<String, String> properties, String target, Progress progress) throws CruiseControlException; public void validate() throws CruiseControlException { boolean timeSet = time != NOT_SET; if (timeSet) { ValidationHelper.assertFalse(time < 0, "negative values for time are not allowed"); } ValidationHelper.assertFalse(timeSet && multipleSet, "Only one of 'time' or 'multiple' are allowed on builders."); } public int getTime() { return time; } /** * can use ScheduleItem.NOT_SET to reset. * @param timeString new time integer */ @Description("Time in the form HHmm. Can't be set if multiple is set.") @Optional public void setTime(String timeString) { time = Integer.parseInt(timeString); } /** * can use Builder.NOT_SET to reset. * @param multiple new multiple */ @Description("Build index used to run different builders. For example, if this is set to 3, " + "the builder will be run every 3 builds. Default value is 1. Can't be set if time " + "is set.") @Optional public void setMultiple(int multiple) { multipleSet = multiple != NOT_SET; this.multiple = multiple; } public int getMultiple() { boolean timeSet = time != NOT_SET; if (timeSet && !multipleSet) { return NOT_SET; } return multiple; } @Description("If true, the builder will provide short progress messages, visible in the JSP " + "reporting application. If parent builders exist (eg: composite), and any parent's " + "showProgress=false, then no progress messages will be shown, regardless of this " + "builder's showProgress setting.") @Optional @Default("true") public void setShowProgress(final boolean showProgress) { this.showProgress = showProgress; } public boolean getShowProgress() { return showProgress; } @Description("If true, the builder will write all output to a file that can be read by the " + "Dashboard reporting application while the builder is executing.") @Optional @Default("true") public void setLiveOutput(final boolean isLiveOutputEnabled) { isLiveOutput = isLiveOutputEnabled; } public boolean isLiveOutput() { return isLiveOutput; } protected BuildOutputLogger getBuildOutputConsumer(final String projectName, final File workingDir, final String logFilename) { if (isLiveOutput && buildOutputLogger == null) { final File outputFile; if (logFilename != null) { outputFile = new File(workingDir, logFilename); } else { final String outputSuff = ".tmp"; final String outputPref = "ccLiveOutput-" + getFileSystemSafeProjectName(projectName) + "-" + getClass().getSimpleName() + "-"; try { outputFile = File.createTempFile(outputPref, outputSuff, workingDir); } catch (IOException e) { throw new RuntimeException("Unable to create temporary file in workingdir=" + workingDir == null ? "<null>" : workingDir.getAbsolutePath(), e); } } outputFile.deleteOnExit(); final BuildOutputLogger buildOutputConsumer = BuildOutputLoggerManager.INSTANCE.lookupOrCreate(projectName, outputFile); buildOutputConsumer.clear(); buildOutputLogger = buildOutputConsumer; } return buildOutputLogger; } /** * @param projectName the actual project name from the config file. * @return a string that is safe to using in a file system path (eg: with slashes replaced by underscores). */ public static String getFileSystemSafeProjectName(final String projectName) { final String safeProjectName; if (projectName != null) { safeProjectName = projectName.replaceAll("/", "_"); // replace prevents error if name has slash } else { safeProjectName = null; } return safeProjectName; } /** * Is this the correct day to be running this builder? * @param now the current date * @return true if this this the correct day to be running this builder */ public boolean isValidDay(final Date now) { if (getDay() < 0) { return true; } final Calendar cal = Calendar.getInstance(); cal.setTime(now); return cal.get(Calendar.DAY_OF_WEEK) == getDay(); } /** * used to sort builders. we're only going to care about sorting builders based on build number, * so we'll sort based on the multiple attribute. */ public int compareTo(final Object o) { final Builder builder = (Builder) o; final Integer integer = multiple; final Integer integer2 = builder.getMultiple(); return integer2.compareTo(integer); //descending order } public boolean isTimeBuilder() { return time != NOT_SET; } /** * @return new {@link EnvConf} object to configure. */ @Description("Used to define environment variables for the builder. The element has two " + "required attributes: \"name\" and either \"value\" or \"delete\".") @Cardinality(min = 0, max = -1) public EnvConf createEnv() { env.add(new EnvConf()); return env.getLast(); } // createEnv /** * Merges the environment settings configures through {@link EnvConf} classes with the * given environment values. * * Call this method in {@link #build(Map, Progress)} implementation, if the builder * supports the environment configuration. * * @param env the environment holder */ protected void mergeEnv(final OSEnvironment env) { for (final EnvConf e : this.env) { e.merge(env); } } // merge /** * Class for the environment variables configuration. They are configured from XML * configuration in form: * <pre> * {@code * <a_builder ...> * <env name="ENV1" value="" /> * <env name="ENV2" delete="true" /> * </a_builder> * } * </pre> * * The configured class merges the environment changes with the actual environment * configuration using method {@link #merge(OSEnvironment)}. */ @Description("Provides environment variable configuration.") public static final class EnvConf { private String name; private String value; // pattern used to find the ${*} strings to replace by ENV values private final Pattern prop = Pattern.compile("\\$\\{([^}]+)\\}"); /** Constructor */ public EnvConf() { name = ""; value = null; } /** * Sets the name of the environment variable. Avoid explicit calls of the method * as it is supposed to be set when configuring the builder from CC XML configuration * only. * @param name the name of the variable */ @Description("The name of the environment variable.") @Required public void setName(final String name) { this.name = name; } // setName /** * @return the name of the environment variable set by {@link #setName(String)}. */ public String getName() { return this.name; } // setName /** * Sets the the environment variable to the new value. Avoid explicit calls of the * method as it is supposed to be set when configuring the builder from CC XML * configuration only. * @param val the new value to set */ @Description("The (new) value of the environment variable.") @Required("Either a 'value' or 'delete' attribute must be set.") public void setValue(final String val) { this.value = val; } // setValue /** * @return the value of the environment variable set by {@link #setValue(String)}, * or <code>null</code> if no environment variable was defined yet. */ public String getValue() { return this.value; } // setName /** * Mark the environment variable to delete. Avoid explicit calls of the method * as it is supposed to be set when configuring the builder from CC XML configuration * only. * @param thisParameterIsIgnored typically should be "true". */ @Description("Marks the environment variable for removal.") @Required("Either a 'value' or 'delete' attribute must be set.") @SuppressWarnings({"UnusedParameters" }) public void setDelete(final boolean thisParameterIsIgnored) { this.value = null; } // setDelete /** * @return the <code>true</code> if the given environment variable (named as get by * {@link #getValue()} is supposed to be removed from the environment. */ public boolean toDelete() { return this.value == null; } // setName /** * Merges the current configuration to the given environment variables. * * Although properties defined in CC project should already be resolved when passed * to {@link #setValue(String)}, it tries to replace remaining ${NAME} strings in * the variable value by the value of NAME environment variable (if defined). * * @param env the environment holder */ void merge(final OSEnvironment env) { if (this.value == null) { env.del(this.name); } else { StringBuffer sb = new StringBuffer(); Matcher m = prop.matcher(this.value); // resolve the ${*} properties remaining in the config using the environment // variables. while (m.find()) { m.appendReplacement(sb, Matcher.quoteReplacement(env.getVariable(m.group(1), m.group(0)))); } m.appendTail(sb); // Set the new value env.add(name, sb.toString()); } } /** Copy the content of EnvConf to this class */ public void copy(final EnvConf env) { this.name = env.name; this.value = env.value; // NULL is toDelete() was set } } // EnvConf }