/******************************************************************************** * CruiseControl, a Continuous Integration Toolkit * Copyright (c) 2001-2003, 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.Serializable; import java.text.DateFormat; import java.text.ParseException; import java.util.ArrayList; import java.util.Collections; import java.util.Date; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import java.util.StringTokenizer; import java.util.HashMap; import net.sourceforge.cruisecontrol.util.DateUtil; import net.sourceforge.cruisecontrol.util.ValidationHelper; import org.apache.log4j.Logger; import org.apache.oro.io.GlobFilenameFilter; import org.apache.oro.text.MalformedCachePatternException; import org.jdom.Element; /** * Set of modifications collected from included SourceControls * * @see SourceControl */ public class ModificationSet implements Serializable { private static final long serialVersionUID = 7834545928469764690L; private static final Logger LOG = Logger.getLogger(ModificationSet.class); private static final int ONE_SECOND = 1000; private List<Modification> modifications = new ArrayList<Modification>(); private final List<SourceControl> sourceControls = new ArrayList<SourceControl>(); private int quietPeriod = 60 * ONE_SECOND; private Date timeOfCheck; /** * File-Patterns (as org.apache.oro.io.GlobFilenameFilter) to be ignored */ private List<GlobFilenameFilter> ignoreFiles; static final String MSG_PROGRESS_PREFIX_QUIETPERIOD_MODIFICATION_SLEEP = "quiet period modification, sleep "; /** * Set the amount of time in which there is no source control activity after which it is assumed that it is safe to * update from the source control system and initiate a build. * @param seconds quite period in seconds */ public void setQuietPeriod(int seconds) { quietPeriod = seconds * ONE_SECOND; } /** * Set the list of Glob-File-Patterns to be ignored * * @param filePatterns * a comma separated list of glob patterns. "*" and "?" are valid wildcards example: "?razy-*-.txt,*.jsp" * @throws CruiseControlException * if at least one of the patterns is malformed */ public void setIgnoreFiles(String filePatterns) throws CruiseControlException { if (filePatterns != null) { StringTokenizer st = new StringTokenizer(filePatterns, ","); ignoreFiles = new ArrayList<GlobFilenameFilter>(); while (st.hasMoreTokens()) { String pattern = st.nextToken(); // Compile the pattern try { ignoreFiles.add(new GlobFilenameFilter(pattern)); } catch (MalformedCachePatternException e) { throw new CruiseControlException("Invalid filename pattern '" + pattern + "'", e); } } } } public void add(SourceControl sourceControl) { sourceControls.add(sourceControl); } public List<SourceControl> getSourceControls() { return sourceControls; } protected boolean isLastModificationInQuietPeriod(final Date timeOfCheck, final List<Modification> modificationList) { final long lastModificationTime = getLastModificationMillis(modificationList); final long quietPeriodStart = timeOfCheck.getTime() - quietPeriod; final boolean modificationInFuture = new Date().getTime() < lastModificationTime; if (modificationInFuture) { LOG.warn("A modification has been detected in the future. Building anyway."); } return (quietPeriodStart <= lastModificationTime) && !modificationInFuture; } protected long getLastModificationMillis(final List<Modification> modificationList) { Date timeOfLastModification = new Date(0); for (final Modification modification : modificationList) { final Date modificationDate = modification.modifiedTime; if (modificationDate.after(timeOfLastModification)) { timeOfLastModification = modificationDate; } } if (modificationList.size() > 0) { LOG.debug("Last modification: " + DateUtil.formatIso8601(timeOfLastModification)); } else { LOG.debug("list has no modifications; returning new Date(0).getTime()"); } return timeOfLastModification.getTime(); } protected long getQuietPeriodDifference(final Date now, final List<Modification> modificationList) { long diff = quietPeriod - (now.getTime() - getLastModificationMillis(modificationList)); return Math.max(0, diff); } /** * Returns a Map of name-value pairs representing any properties set by the SourceControl. * * @return Map of properties. */ public Map<String, String> getProperties() { final Map<String, String> table = new HashMap<String, String>(); for (final SourceControl control : sourceControls) { mergeProperties(table, control); } return table; } private void mergeProperties(final Map<String, String> properties, final SourceControl control) { final Map<String, String> newProperties = control.getProperties(); final Set<String> existingKeys = properties.keySet(); final Set<String> newKeys = newProperties.keySet(); if (Collections.disjoint(existingKeys, newKeys)) { properties.putAll(newProperties); return; } final Set<String> disjointKeys = new HashSet<String>(newKeys); final Set<String> unionKeys = new HashSet<String>(newKeys); disjointKeys.removeAll(existingKeys); unionKeys.retainAll(existingKeys); for (final String key : disjointKeys) { properties.put(key, newProperties.get(key)); } for (final String key : unionKeys) { final String oldValue = properties.get(key); final String newValue = newProperties.get(key); final String value = chooseValue(oldValue, newValue); properties.put(key, value); } } private String chooseValue(final String oldValue, final String newValue) { if (oldValue.equals(newValue)) { return newValue; } if (!(newValue != null)) { return newValue; } final Integer oldInt = getInteger(oldValue); final Integer newInt = getInteger(newValue); Date oldDate = getDate(oldValue); Date newDate = getDate(newValue); final boolean oldBigger; if (oldInt != null && newInt != null) { oldBigger = oldInt.compareTo(newInt) > 0; } else if (oldDate != null && newDate != null) { oldBigger = oldDate.compareTo(newDate) > 0; } else { oldBigger = oldValue.compareTo(newValue) > 0; } if (oldBigger) { return oldValue; } return newValue; } private Date getDate(final String string) { try { return DateFormat.getDateInstance().parse(string); } catch (ParseException e) { return null; } } private Integer getInteger(final String string) { try { return Integer.parseInt(string); } catch (NumberFormatException e) { return null; } } public List<Modification> getCurrentModifications() { return this.modifications; } /** * Returns the modifications as of lastBuild as an XML element. * @param lastBuild date of last build * @param progress ModificationSet progress message callback object * @return modifications element */ // @todo Make this non-public? (package visible only) public Element retrieveModificationsAsElement(final Date lastBuild, final Progress progress) { Element modificationsElement; do { timeOfCheck = new Date(); modifications = new ArrayList<Modification>(); for (final SourceControl sourceControl : sourceControls) { modifications.addAll(sourceControl.getModifications(lastBuild, timeOfCheck)); } // Postfilter all modifications of ignored files filterIgnoredModifications(modifications); if (modifications.size() > 0) { LOG.info(modifications.size() + ((modifications.size() > 1) ? " modifications have been detected." : " modification has been detected.")); } modificationsElement = new Element("modifications"); for (final Modification modification : modifications) { final Element modificationElement = modification.toElement(); modification.log(); modificationsElement.addContent(modificationElement); } if (isLastModificationInQuietPeriod(timeOfCheck, modifications)) { LOG.info("A modification has been detected in the quiet period. "); if (LOG.isDebugEnabled()) { final Date quietPeriodStart = new Date(timeOfCheck.getTime() - quietPeriod); LOG.debug(DateUtil.formatIso8601(quietPeriodStart) + " <= Quiet Period <= " + DateUtil.formatIso8601(timeOfCheck)); } final Date now = new Date(); final long timeToSleep = getQuietPeriodDifference(now, modifications); LOG.info("Sleeping for " + (timeToSleep / 1000) + " seconds before retrying."); if (progress == null) { throw new IllegalStateException( "retrieveModificationsAsElement(): 'progress' parameter must not be null."); } progress.setValue(MSG_PROGRESS_PREFIX_QUIETPERIOD_MODIFICATION_SLEEP + (timeToSleep / 1000) + " secs"); try { Thread.sleep(timeToSleep); } catch (InterruptedException e) { LOG.error(e); } } } while (isLastModificationInQuietPeriod(timeOfCheck, modifications)); return modificationsElement; } /** * Remove all Modifications that match any of the ignoreFiles-patterns * @param modifications the list of modifications to be filtered (altered). */ protected void filterIgnoredModifications(final List<Modification> modifications) { if (ignoreFiles != null) { for (Iterator<Modification> iterator = modifications.iterator(); iterator.hasNext();) { final Modification modification = iterator.next(); if (isIgnoredModification(modification)) { iterator.remove(); } } } } private boolean isIgnoredModification(final Modification modification) { boolean foundAny = false; // Go through all the files in the modification. If all are ignored, ignore this modification. for (final Modification.ModifiedFile modFile : modification.getModifiedFiles()) { final File file; if (modFile.folderName == null) { if (modification.getFileName() == null) { continue; } else { file = new File(modFile.fileName); } } else { file = new File(modFile.folderName, modFile.fileName); } String path = file.toString(); foundAny = true; // On systems with a '\' as pathseparator convert it to a forward slash '/' // That makes patterns platform independent if (File.separatorChar == '\\') { path = path.replace('\\', '/'); } boolean useThisFile = true; for (Iterator<GlobFilenameFilter> iterator = ignoreFiles.iterator(); iterator.hasNext() && useThisFile;) { final GlobFilenameFilter pattern = iterator.next(); // We have to use a little tweak here, since GlobFilenameFilter only matches the filename, but not // the path, so we use the complete path as the 'filename'-argument. if (pattern.accept(file, path)) { useThisFile = false; } } if (useThisFile) { return false; } } return foundAny; } public Date getTimeOfCheck() { return timeOfCheck; } public boolean isModified() { return !modifications.isEmpty(); } public void validate() throws CruiseControlException { ValidationHelper.assertFalse(sourceControls.isEmpty(), "modificationset element requires at least one nested source control element"); for (final SourceControl sc : sourceControls) { sc.validate(); } } int getQuietPeriod() { return quietPeriod; } }