/******************************************************************************** * 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.sourcecontrols; import java.io.File; import java.io.FilenameFilter; import java.io.IOException; import java.util.ArrayList; import java.util.Date; import java.util.List; import java.util.Map; import net.sourceforge.cruisecontrol.CruiseControlException; import net.sourceforge.cruisecontrol.Log; import net.sourceforge.cruisecontrol.Modification; import net.sourceforge.cruisecontrol.SourceControl; import net.sourceforge.cruisecontrol.gendoc.annotations.Default; import net.sourceforge.cruisecontrol.gendoc.annotations.Description; import net.sourceforge.cruisecontrol.gendoc.annotations.DescriptionFile; import net.sourceforge.cruisecontrol.gendoc.annotations.Optional; import net.sourceforge.cruisecontrol.gendoc.annotations.Required; import net.sourceforge.cruisecontrol.util.DateUtil; import net.sourceforge.cruisecontrol.util.ValidationHelper; import net.sourceforge.cruisecontrol.util.XMLLogHelper; import org.apache.log4j.Logger; import org.jdom.Document; import org.jdom.Element; import org.jdom.JDOMException; import org.jdom.input.SAXBuilder; /** * This class allows for starting builds based on the results of another * build. It does this by examining the build's log files. Only * successful builds count as modifications. * * @author Garrick Olson */ @DescriptionFile public class BuildStatus implements SourceControl { private static final Logger LOG = Logger.getLogger(BuildStatus.class); /** The location being checked for new log files. */ public static final String MOST_RECENT_LOGDIR_KEY = "most.recent.logdir"; /** The name of the newest logfile included in the modification set * (e.g. "log20040120120000L0.1.xml"). */ public static final String MOST_RECENT_LOGFILE_KEY = "most.recent.logfile"; /** The timestamp of the newest build included in the modification set * (e.g. "20040120120000"). */ public static final String MOST_RECENT_LOGTIME_KEY = "most.recent.logtime"; /** * The label of the newest build included in the modification set * (e.g. "0.1"). */ public static final String MOST_RECENT_LOGLABEL_KEY = "most.recent.loglabel"; private final SourceControlProperties properties = new SourceControlProperties(); private String logDir; private boolean vetoIfFailing = false; /** * This method is used to make certain attributes of the most * recent modification available to Ant tasks. * @return A Hashtable object containing no properties if there * were no modifications, four properties if there were one * or more modifications (keys are provided as constants on this * class), or five is the property attribute was set. * Never returns null. */ public Map<String, String> getProperties() { return properties.getPropertiesAndReset(); } @Description("Will set this property if a modification has occurred. For use in " + "conditionally controlling the build later.") @Optional public void setProperty(String propertyName) { properties.assignPropertyName(propertyName); } /** * Indicate where the build to be monitored places its output (log files). * * @param logDir Absolute path to the log directory. */ @Description("Path to CruiseControl log directory for the project to monitor.") @Required public void setLogDir(String logDir) { this.logDir = logDir; } /** * Make sure any attributes provided by the user are correctly specified. */ public void validate() throws CruiseControlException { ValidationHelper.assertIsSet(logDir, "logdir", this.getClass()); File logDirectory = new File(logDir); ValidationHelper.assertTrue(logDirectory.exists(), "Log directory does not exist: " + logDirectory.getAbsolutePath()); ValidationHelper.assertTrue(logDirectory.isDirectory(), "Log directory is not a directory: " + logDirectory.getAbsolutePath()); } /** * The modifications reported by this method will be the list of new * log files created as the result of successful builds (the log files * will include the build label). * * @param lastBuild Look for successful builds newer than this date * (may not be null). * @param unused The timestamp of the current build is passed here * (as per SourceControl interface) but we don't use it. */ public List<Modification> getModifications(final Date lastBuild, final Date unused) { properties.put(MOST_RECENT_LOGDIR_KEY, logDir); final List<Modification> modifications = new ArrayList<Modification>(); final File logDirectory = new File(logDir); final String filename = Log.formatLogFileName(lastBuild); if (!logDirectory.exists()) { LOG.error("log directory doesn't exist: " + logDir); return modifications; } if (!logDirectory.isDirectory()) { LOG.error("path for log directory exists but isn't a directory: " + logDir); return modifications; } if (vetoIfFailing) { vetoIfFailing(logDirectory); } try { File[] newLogs = logDirectory.listFiles(new FilenameFilter() { public boolean accept(File dir, String name) { return name.compareTo(filename) > 0 && Log.wasSuccessfulBuild(name); } }); Modification mostRecent = null; for (final File newLog : newLogs) { final Modification modification = new Modification("buildstatus"); final String name = newLog.getName(); modification.modifiedTime = Log.parseDateFromLogFileName(name); modification.userName = "cc-" + getProjectFromLog(newLog); modification.comment = logDir.substring(logDir.lastIndexOf('/') + 1); modification.revision = Log.parseLabelFromLogFileName(name); final Modification.ModifiedFile modfile = modification.createModifiedFile(name, null); modfile.revision = modification.revision; modfile.action = "add"; if (mostRecent == null || modification.modifiedTime.after(mostRecent.modifiedTime)) { mostRecent = modification; } modifications.add(modification); } // This makes information about the most recent modification // available to Ant tasks if (mostRecent != null) { properties.put(MOST_RECENT_LOGFILE_KEY, mostRecent.files.get(0).fileName); properties.put(MOST_RECENT_LOGTIME_KEY, DateUtil.getFormattedTime(mostRecent.modifiedTime)); properties.put(MOST_RECENT_LOGLABEL_KEY, mostRecent.revision); } } catch (Exception e) { LOG.error("Error checking for modifications", e); } if (!modifications.isEmpty()) { properties.modificationFound(); } return modifications; } private void vetoIfFailing(File logDirectory) { NewestLogfileFilter filter = new NewestLogfileFilter(); logDirectory.listFiles(filter); String mostRecentLogfileName = filter.mostRecent; if (mostRecentLogfileName != null && !Log.wasSuccessfulBuild(mostRecentLogfileName)) { throw new VetoException("most recent build failed: " + mostRecentLogfileName); } } private static class VetoException extends RuntimeException { public VetoException(String message) { super(message); } } private static class NewestLogfileFilter implements FilenameFilter { private String mostRecent; public boolean accept(File dir, String name) { boolean accept = name.startsWith("log") && name.endsWith(".xml") && name.length() >= minimumFilenameLength(); if (accept) { cacheNewestFilename(name); } return accept; } private void cacheNewestFilename(String name) { try { Date logDate = Log.parseDateFromLogFileName(name); if (mostRecent == null || logDate.after(Log.parseDateFromLogFileName(mostRecent))) { mostRecent = name; } } catch (CruiseControlException e) { LOG.warn("exception getting date from filename: " + name, e); } } private int minimumFilenameLength() { return "log".length() + DateUtil.SIMPLE_DATE_FORMAT.length() + ".xml".length(); } } private String getProjectFromLog(File f) { LOG.info("Getting project from file: " + f.getName()); try { Document doc = readDocFromFile(f); LOG.info("Loaded xml document for BuildStatus"); Element root = doc.getRootElement(); XMLLogHelper log = new XMLLogHelper(root); return log.getProjectName(); } catch (JDOMException ex) { LOG.info("Failed to load BuildStatus xml document" + ex); } catch (IOException ex) { LOG.info("Failed to load BuildStatus xml document" + ex); } catch (CruiseControlException ex) { LOG.info("Could load BuildStatus xml log document, but generated exception anyway" + ex); } return "Unknown"; } private Document readDocFromFile(File f) throws JDOMException, IOException { SAXBuilder sxb = new SAXBuilder(); return sxb.build(f); } @Description("Will abort the build attempt if the last build of the monitored " + "project is failing. Defaults to false.") @Optional @Default("false") public void setVetoIfFailing(boolean b) { vetoIfFailing = b; } }