001: /********************************************************************************
002: * CruiseControl, a Continuous Integration Toolkit
003: * Copyright (c) 2001-2003, 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.io.File;
039: import java.io.Serializable;
040: import java.text.DateFormat;
041: import java.util.ArrayList;
042: import java.util.Date;
043: import java.util.Hashtable;
044: import java.util.Iterator;
045: import java.util.List;
046: import java.util.StringTokenizer;
047:
048: import net.sourceforge.cruisecontrol.util.ValidationHelper;
049:
050: import org.apache.log4j.Logger;
051: import org.apache.oro.io.GlobFilenameFilter;
052: import org.apache.oro.text.MalformedCachePatternException;
053: import org.jdom.Element;
054:
055: /**
056: * Set of modifications collected from included SourceControls
057: *
058: * @see SourceControl
059: */
060: public class ModificationSet implements Serializable {
061:
062: private static final long serialVersionUID = 7834545928469764690L;
063:
064: private boolean lieOnIsModified = false;
065: private static final Logger LOG = Logger
066: .getLogger(ModificationSet.class);
067: private static final int ONE_SECOND = 1000;
068:
069: private List modifications = new ArrayList();
070: private final List sourceControls = new ArrayList();
071: private int quietPeriod = 60 * ONE_SECOND;
072: private Date timeOfCheck;
073: private final DateFormat formatter = DateFormatFactory
074: .getDateFormat();
075:
076: /**
077: * File-Patterns (as org.apache.oro.io.GlobFilenameFilter) to be ignored
078: */
079: private List ignoreFiles;
080:
081: static final String MSG_PROGRESS_PREFIX_QUIETPERIOD_MODIFICATION_SLEEP = "quiet period modification, sleep ";
082:
083: /**
084: * Set the amount of time in which there is no source control activity after which it is assumed that it is safe to
085: * update from the source control system and initiate a build.
086: * @param seconds quite period in seconds
087: */
088: public void setQuietPeriod(int seconds) {
089: quietPeriod = seconds * ONE_SECOND;
090: }
091:
092: /**
093: * Set the list of Glob-File-Patterns to be ignored
094: *
095: * @param filePatterns
096: * a comma separated list of glob patterns. "*" and "?" are valid wildcards example: "?razy-*-.txt,*.jsp"
097: * @throws CruiseControlException
098: * if at least one of the patterns is malformed
099: */
100: public void setIgnoreFiles(String filePatterns)
101: throws CruiseControlException {
102: if (filePatterns != null) {
103: StringTokenizer st = new StringTokenizer(filePatterns, ",");
104: ignoreFiles = new ArrayList();
105: while (st.hasMoreTokens()) {
106: String pattern = st.nextToken();
107: // Compile the pattern
108: try {
109: ignoreFiles.add(new GlobFilenameFilter(pattern));
110: } catch (MalformedCachePatternException e) {
111: throw new CruiseControlException(
112: "Invalid filename pattern '" + pattern
113: + "'", e);
114: }
115: }
116: }
117: }
118:
119: protected List getIgnoreFiles() {
120: return this .ignoreFiles;
121: }
122:
123: /** @deprecated * */
124: public void addSourceControl(SourceControl sourceControl) {
125: add(sourceControl);
126: }
127:
128: public void add(SourceControl sourceControl) {
129: sourceControls.add(sourceControl);
130: }
131:
132: public List getSourceControls() {
133: return sourceControls;
134: }
135:
136: protected boolean isLastModificationInQuietPeriod(Date timeOfCheck,
137: List modificationList) {
138: long lastModificationTime = getLastModificationMillis(modificationList);
139: final long quietPeriodStart = timeOfCheck.getTime()
140: - quietPeriod;
141: final boolean modificationInFuture = new Date().getTime() < lastModificationTime;
142: if (modificationInFuture) {
143: LOG
144: .warn("A modification has been detected in the future. Building anyway.");
145: }
146: return (quietPeriodStart <= lastModificationTime)
147: && !modificationInFuture;
148: }
149:
150: protected long getLastModificationMillis(List modificationList) {
151: Date timeOfLastModification = new Date(0);
152: Iterator iterator = modificationList.iterator();
153: while (iterator.hasNext()) {
154: Object object = iterator.next();
155: Modification modification = null;
156: if (object instanceof Modification) {
157: modification = (Modification) object;
158: }
159: if (object instanceof Element) {
160: Element element = (Element) object;
161: modification = new Modification("unknown");
162: modification.fromElement(element, formatter);
163: }
164: if (modification != null) {
165: Date modificationDate = modification.modifiedTime;
166: if (modificationDate.after(timeOfLastModification)) {
167: timeOfLastModification = modificationDate;
168: }
169: }
170: }
171: if (modificationList.size() > 0) {
172: LOG.debug("Last modification: "
173: + formatter.format(timeOfLastModification));
174: } else {
175: LOG
176: .debug("list has no modifications; returning new Date(0).getTime()");
177: }
178: return timeOfLastModification.getTime();
179: }
180:
181: protected long getQuietPeriodDifference(Date now,
182: List modificationList) {
183: long diff = quietPeriod
184: - (now.getTime() - getLastModificationMillis(modificationList));
185: return Math.max(0, diff);
186: }
187:
188: /**
189: * Returns a Hashtable of name-value pairs representing any properties set by the SourceControl.
190: *
191: * @return Hashtable of properties.
192: */
193: public Hashtable getProperties() {
194: Hashtable table = new Hashtable();
195: for (Iterator iter = sourceControls.iterator(); iter.hasNext();) {
196: SourceControl control = (SourceControl) iter.next();
197: table.putAll(control.getProperties());
198: }
199: return table;
200: }
201:
202: public List getCurrentModifications() {
203: return this .modifications;
204: }
205:
206: /**
207: * @deprecated As of 10-Oct-2007, replaced by {@link #retrieveModificationsAsElement(java.util.Date, Progress)}
208: */
209: public Element getModifications(final Date lastBuild) {
210: return getModifications(lastBuild, null);
211: }
212:
213: /**
214: * @param lastBuild date of last build
215: * @param progress ModificationSet progress message callback object
216: * @return modifications element
217: */
218: public Element getModifications(final Date lastBuild,
219: final Progress progress) {
220: return retrieveModificationsAsElement(lastBuild, null);
221: }
222:
223: /**
224: * Returns the modifications as of lastBuild as an XML element.
225: */
226: public Element retrieveModificationsAsElement(final Date lastBuild,
227: final Progress progress) {
228: Element modificationsElement;
229: do {
230: timeOfCheck = new Date();
231: modifications = new ArrayList();
232: Iterator sourceControlIterator = sourceControls.iterator();
233: while (sourceControlIterator.hasNext()) {
234: SourceControl sourceControl = (SourceControl) sourceControlIterator
235: .next();
236: modifications.addAll(sourceControl.getModifications(
237: lastBuild, timeOfCheck));
238: }
239:
240: // Postfilter all modifications of ignored files
241: filterIgnoredModifications(modifications);
242:
243: if (modifications.size() > 0) {
244: LOG
245: .info(modifications.size()
246: + ((modifications.size() > 1) ? " modifications have been detected."
247: : " modification has been detected."));
248: }
249: modificationsElement = new Element("modifications");
250: Iterator modificationIterator = modifications.iterator();
251: while (modificationIterator.hasNext()) {
252: Object object = modificationIterator.next();
253: if (object instanceof Element) {
254: modificationsElement.addContent(((Element) object)
255: .detach());
256: } else {
257: Modification modification = (Modification) object;
258: Element modificationElement = (modification)
259: .toElement(formatter);
260: modification.log(formatter);
261: modificationsElement
262: .addContent(modificationElement);
263: }
264: }
265:
266: if (isLastModificationInQuietPeriod(timeOfCheck,
267: modifications)) {
268: LOG
269: .info("A modification has been detected in the quiet period. ");
270: if (LOG.isDebugEnabled()) {
271: final Date quietPeriodStart = new Date(timeOfCheck
272: .getTime()
273: - quietPeriod);
274: LOG.debug(formatter.format(quietPeriodStart)
275: + " <= Quiet Period <= "
276: + formatter.format(timeOfCheck));
277: }
278: Date now = new Date();
279: long timeToSleep = getQuietPeriodDifference(now,
280: modifications);
281: LOG.info("Sleeping for " + (timeToSleep / 1000)
282: + " seconds before retrying.");
283:
284: // @todo Remove "if (progress != null)" when deprecated getModifications(Date lastBuild) is removed
285: if (progress != null) {
286: progress
287: .setValue(MSG_PROGRESS_PREFIX_QUIETPERIOD_MODIFICATION_SLEEP
288: + (timeToSleep / 1000) + " secs");
289: }
290:
291: try {
292: Thread.sleep(timeToSleep);
293: } catch (InterruptedException e) {
294: LOG.error(e);
295: }
296: }
297: } while (isLastModificationInQuietPeriod(timeOfCheck,
298: modifications));
299:
300: return modificationsElement;
301: }
302:
303: /**
304: * Remove all Modifications that match any of the ignoreFiles-patterns
305: */
306: protected void filterIgnoredModifications(List modifications) {
307: if (this .ignoreFiles != null) {
308: for (Iterator iterator = modifications.iterator(); iterator
309: .hasNext();) {
310: Object object = iterator.next();
311: Modification modification = null;
312: if (object instanceof Modification) {
313: modification = (Modification) object;
314: } else if (object instanceof Element) {
315: Element element = (Element) object;
316: modification = new Modification();
317: modification.fromElement(element, formatter);
318: }
319:
320: if (isIgnoredModification(modification)) {
321: iterator.remove();
322: }
323: }
324: }
325: }
326:
327: private boolean isIgnoredModification(Modification modification) {
328: boolean foundAny = false;
329:
330: // Go through all the files in the modification. If all are ignored, ignore this modification.
331: for (final Iterator modFileIter = modification
332: .getModifiedFiles().iterator(); modFileIter.hasNext();) {
333: final Modification.ModifiedFile modFile = (Modification.ModifiedFile) modFileIter
334: .next();
335:
336: File file;
337: if (modFile.folderName == null) {
338: if (modification.getFileName() == null) {
339: continue;
340: } else {
341: file = new File(modFile.fileName);
342: }
343: } else {
344: file = new File(modFile.folderName, modFile.fileName);
345: }
346: String path = file.toString();
347: foundAny = true;
348:
349: // On systems with a '\' as pathseparator convert it to a forward slash '/'
350: // That makes patterns platform independent
351: if (File.separatorChar == '\\') {
352: path = path.replace('\\', '/');
353: }
354:
355: boolean useThisFile = true;
356: for (Iterator iterator = ignoreFiles.iterator(); iterator
357: .hasNext()
358: && useThisFile;) {
359: GlobFilenameFilter pattern = (GlobFilenameFilter) iterator
360: .next();
361:
362: // We have to use a little tweak here, since GlobFilenameFilter only matches the filename, but not
363: // the path, so we use the complete path as the 'filename'-argument.
364: if (pattern.accept(file, path)) {
365: useThisFile = false;
366: }
367: }
368: if (useThisFile) {
369: return false;
370: }
371: }
372: return foundAny;
373: }
374:
375: public Date getTimeOfCheck() {
376: return timeOfCheck;
377: }
378:
379: public boolean isModified() {
380: return (!modifications.isEmpty()) || lieOnIsModified;
381: }
382:
383: public void validate() throws CruiseControlException {
384: ValidationHelper
385: .assertFalse(sourceControls.isEmpty(),
386: "modificationset element requires at least one nested source control element");
387:
388: for (Iterator i = sourceControls.iterator(); i.hasNext();) {
389: SourceControl sc = (SourceControl) i.next();
390: sc.validate();
391: }
392: }
393:
394: int getQuietPeriod() {
395: return quietPeriod;
396: }
397:
398: /**
399: * @param isModifiedAccurate
400: * @deprecated
401: */
402: public void setRequireModification(boolean isModifiedAccurate) {
403: LOG
404: .warn("<modificationset requiremodification=\"true|false\" is deprecated. "
405: + "Use <project requiremodification=\"true|false\".");
406: lieOnIsModified = !isModifiedAccurate;
407: }
408: }
|