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.Serializable;
039: import java.text.DateFormat;
040: import java.text.DateFormatSymbols;
041: import java.text.SimpleDateFormat;
042: import java.util.ArrayList;
043: import java.util.Calendar;
044: import java.util.Date;
045: import java.util.Iterator;
046: import java.util.List;
047: import java.util.Locale;
048: import java.util.Map;
049:
050: import net.sourceforge.cruisecontrol.util.DateUtil;
051: import net.sourceforge.cruisecontrol.util.ValidationHelper;
052:
053: import org.apache.log4j.Logger;
054: import org.jdom.Element;
055:
056: /**
057: * Handles scheduling different builds.
058: *
059: * @author alden almagro, ThoughtWorks, Inc. 2001-2
060: */
061: public class Schedule implements Serializable {
062:
063: private static final long serialVersionUID = -33682332427948426L;
064:
065: private static final Logger LOG = Logger.getLogger(Schedule.class);
066:
067: static final long ONE_SECOND = 1000;
068: static final long ONE_MINUTE = 60 * ONE_SECOND;
069: static final long ONE_DAY = 24 * 60 * ONE_MINUTE;
070: static final long ONE_YEAR = ONE_DAY * 365;
071:
072: static final long MAX_INTERVAL_SECONDS = 60 * 60 * 24 * 365;
073: static final long MAX_INTERVAL_MILLISECONDS = MAX_INTERVAL_SECONDS * 1000;
074:
075: private final List builders = new ArrayList();
076: private final List pauseBuilders = new ArrayList();
077: private long interval = 300 * ONE_SECOND;
078:
079: private boolean showProgress = true;
080:
081: /** date formatting for time statements */
082: private final DateFormat timeFormatter = new SimpleDateFormat(
083: "HH:mm");
084:
085: public void add(Builder builder) {
086: checkParamNotNull("builder", builder);
087: builders.add(builder);
088: }
089:
090: public void add(PauseBuilder pause) {
091: checkParamNotNull("pauseBuilder", pause);
092: pauseBuilders.add(pause);
093: }
094:
095: /**
096: * Determine if CruiseControl should run a build, given the current time.
097: *
098: * @param now
099: * The current date
100: * @return true if CruiseControl is currently paused (no build should run).
101: */
102: public boolean isPaused(Date now) {
103: checkParamNotNull("date", now);
104: PauseBuilder pause = findPause(now);
105: if (pause != null) {
106: LOG.info("CruiseControl is paused until: "
107: + getEndTimeString(pause));
108: return true;
109: }
110: return false;
111: }
112:
113: /**
114: * Returns a String representing the time following the end time of the given {@link PauseBuilder}.
115: *
116: * @param builder
117: * the <code>PauseBuilder</code> to be considered.
118: * @return a String representing the time following the end time of the <code>PauseBuilder</code>.
119: */
120: private String getEndTimeString(PauseBuilder builder) {
121: Calendar cal = Calendar.getInstance();
122: cal.set(Calendar.HOUR_OF_DAY, builder.getEndTime() / 100);
123: cal.set(Calendar.MINUTE, builder.getEndTime() % 100);
124: cal.add(Calendar.MINUTE, 1);
125: return timeFormatter.format(cal.getTime());
126: }
127:
128: PauseBuilder findPause(Date date) {
129: checkParamNotNull("date", date);
130: Iterator pauseBuilderIterator = pauseBuilders.iterator();
131: while (pauseBuilderIterator.hasNext()) {
132: PauseBuilder pause = (PauseBuilder) pauseBuilderIterator
133: .next();
134: if (pause.isPaused(date)) {
135: return pause;
136: }
137: }
138: return null;
139: }
140:
141: /**
142: * Select the correct <code>Builder</code> and start a build.
143: *
144: * @param buildNumber
145: * The sequential build number.
146: * @param lastBuild
147: * The date of the last build.
148: * @param now
149: * The current time.
150: * @param properties
151: * Properties that would need to be passed in to the actual build tool.
152: * @param buildTarget
153: * the build target to use instead of the configured one (pass in null if no override is needed)
154: * @param progress
155: * the progress callback object.
156: * @return JDOM Element representation of build log.
157: * @throws CruiseControlException if something fails
158: */
159: public Element build(int buildNumber, Date lastBuild, Date now,
160: Map properties, String buildTarget, Progress progress)
161: throws CruiseControlException {
162: Builder builder = selectBuilder(buildNumber, lastBuild, now);
163: if (buildTarget != null) {
164: LOG.info("Overriding build target with \"" + buildTarget
165: + "\"");
166: return builder.buildWithTarget(properties, buildTarget,
167: (getShowProgress() ? progress : null));
168: }
169: return builder.build(properties, (getShowProgress() ? progress
170: : null));
171: }
172:
173: /**
174: * Select the correct build based on the current buildNumber and time.
175: *
176: * @param buildNumber
177: * The sequential build number
178: * @param lastBuild
179: * The date of the last build.
180: * @param now
181: * The current time.
182: * @return The <code>Builder</code> that should be run.
183: * @throws CruiseControlException if something fails
184: */
185: protected Builder selectBuilder(int buildNumber, Date lastBuild,
186: Date now) throws CruiseControlException {
187: Builder builder = findBuilder(buildNumber, lastBuild, now);
188:
189: if (builder == null) {
190: long timeToNextBuild = getTimeToNextBuild(now, ONE_MINUTE);
191: Date futureDate = getFutureDate(now, timeToNextBuild);
192: builder = findBuilder(buildNumber, now, futureDate);
193: }
194:
195: if (builder == null) {
196: validate();
197: throw new CruiseControlException(
198: "configuration error not caught by validate? no builder selected");
199: }
200:
201: return builder;
202: }
203:
204: private Builder findBuilder(int buildNumber, Date lastBuild,
205: Date now) throws CruiseControlException {
206: Iterator builderIterator = builders.iterator();
207: while (builderIterator.hasNext()) {
208: Builder builder = (Builder) builderIterator.next();
209: int buildTime = builder.getTime();
210: boolean isTimeBuilder = buildTime >= 0;
211: if (isTimeBuilder) {
212: boolean didntBuildToday = builderDidntBuildToday(
213: lastBuild, now, buildTime);
214: int nowTime = DateUtil.getTimeFromDate(now);
215: boolean isAfterBuildTime = buildTime <= nowTime;
216: boolean isValidDay = builder.isValidDay(now);
217: if (didntBuildToday && isAfterBuildTime && isValidDay) {
218: return builder;
219: }
220: } else if (builder.getMultiple() > 0) {
221: if (builder.isValidDay(now)) {
222: if ((buildNumber % builder.getMultiple()) == 0) {
223: return builder;
224: }
225: }
226: } else {
227: throw new CruiseControlException(
228: "The selected Builder is not properly configured");
229: }
230: }
231:
232: return null;
233: }
234:
235: boolean builderDidntBuildToday(Date lastBuild, Date now,
236: int buildTime) {
237: int time = DateUtil.getTimeFromDate(now);
238: long timeMillis = DateUtil.convertToMillis(time);
239: long startOfToday = now.getTime() - timeMillis;
240: boolean lastBuildYesterday = lastBuild.getTime() < startOfToday;
241: boolean lastBuildTimeBeforeBuildTime = DateUtil
242: .getTimeFromDate(lastBuild) < buildTime;
243: return lastBuildYesterday || lastBuildTimeBeforeBuildTime;
244: }
245:
246: long getTimeToNextBuild(Date now, long sleepInterval) {
247: return getTimeToNextBuild(now, sleepInterval, 0);
248: }
249:
250: private long getTimeToNextBuild(Date now, long sleepInterval,
251: long priorPauseAdjustment) {
252: long timeToNextBuild = sleepInterval;
253: LOG.debug("getTimeToNextBuild: initial timeToNextBuild = "
254: + timeToNextBuild);
255: timeToNextBuild = checkMultipleBuilders(now, timeToNextBuild);
256: LOG.debug("getTimeToNextBuild: after checkMultipleBuilders = "
257: + timeToNextBuild);
258: timeToNextBuild = checkTimeBuilders(now, timeToNextBuild);
259: LOG.debug("getTimeToNextBuild: after checkTimeBuilders = "
260: + timeToNextBuild);
261: long timeTillNotPaused = checkPauseBuilders(now,
262: timeToNextBuild);
263: LOG.debug("getTimeToNextBuild: after checkPauseBuilders = "
264: + timeToNextBuild);
265:
266: if (timeToNextBuild != timeTillNotPaused) {
267: boolean atMaxTime = timeTillNotPaused >= MAX_INTERVAL_MILLISECONDS
268: || priorPauseAdjustment >= MAX_INTERVAL_MILLISECONDS;
269: if (hasOnlyTimeBuilders() && !atMaxTime) {
270: Date dateAfterPause = getFutureDate(now,
271: timeTillNotPaused);
272: long adjustmentFromEndOfPause = getTimeToNextBuild(
273: dateAfterPause, 0, priorPauseAdjustment
274: + timeTillNotPaused);
275: timeToNextBuild = timeTillNotPaused
276: + adjustmentFromEndOfPause;
277: timeToNextBuild = checkMaximumInterval(timeToNextBuild);
278: } else {
279: timeToNextBuild = timeTillNotPaused;
280: }
281: }
282:
283: return timeToNextBuild;
284: }
285:
286: private long checkMultipleBuilders(Date now, long interval) {
287: if (hasOnlyTimeBuilders()) {
288: LOG
289: .debug("has only time builders, so no correction for multiple builders.");
290: return interval;
291: }
292:
293: Date then = getFutureDate(now, interval);
294:
295: List buildersForOtherDays = new ArrayList();
296: Iterator iterator = builders.iterator();
297: while (iterator.hasNext()) {
298: Builder builder = (Builder) iterator.next();
299: boolean isTimeBuilder = builder.getTime() != Builder.NOT_SET;
300: if (!isTimeBuilder) {
301: if (builder.getMultiple() == 1) {
302: if (builder.isValidDay(then)) {
303: LOG
304: .debug("multiple=1 builder found that could run on "
305: + then);
306: return interval;
307: } else {
308: buildersForOtherDays.add(builder);
309: }
310: }
311: }
312: }
313:
314: if (buildersForOtherDays.size() == 0) {
315: LOG
316: .error("configuration error: has some multiple builders but no multiple=1 builders found!");
317: return interval;
318: } else {
319: LOG.debug("no multiple=1 builders found for " + then
320: + ". checking other days");
321: }
322:
323: for (int i = 1; i < 7; i++) {
324: long daysPastInitialInterval = i * ONE_DAY;
325: then = getFutureDate(now, interval
326: + daysPastInitialInterval);
327: iterator = builders.iterator();
328: while (iterator.hasNext()) {
329: Builder builder = (Builder) iterator.next();
330: if (builder.isValidDay(then)) {
331: LOG
332: .debug("multiple=1 builder found that could run on "
333: + then);
334: long correctionToMidnight = getTimePastMidnight(then);
335: return interval + daysPastInitialInterval
336: - correctionToMidnight;
337: }
338: }
339: }
340:
341: LOG
342: .error("configuration error? could not find appropriate multiple=1 builder.");
343: return interval;
344: }
345:
346: private long getTimePastMidnight(Date date) {
347: Calendar cal = Calendar.getInstance();
348: cal.setTime(date);
349: long time = 60 * ONE_MINUTE * cal.get(Calendar.HOUR_OF_DAY);
350: time += ONE_MINUTE * cal.get(Calendar.MINUTE);
351: return time;
352: }
353:
354: private boolean hasOnlyTimeBuilders() {
355: boolean onlyTimeBuilders = true;
356: Iterator iterator = builders.iterator();
357: while (iterator.hasNext()) {
358: Builder builder = (Builder) iterator.next();
359: boolean isTimeBuilder = builder.getTime() != Builder.NOT_SET;
360: if (!isTimeBuilder) {
361: onlyTimeBuilders = false;
362: break;
363: }
364: }
365: return onlyTimeBuilders;
366: }
367:
368: long checkTimeBuilders(Date now, long proposedTime) {
369: long timeToNextBuild = proposedTime;
370: if (hasOnlyTimeBuilders()) {
371: timeToNextBuild = Long.MAX_VALUE;
372: }
373: int nowTime = DateUtil.getTimeFromDate(now);
374: Iterator builderIterator = builders.iterator();
375: while (builderIterator.hasNext()) {
376: Builder builder = (Builder) builderIterator.next();
377: int this BuildTime = builder.getTime();
378: boolean isTimeBuilder = this BuildTime != Builder.NOT_SET;
379: if (isTimeBuilder) {
380: long timeToThisBuild = Long.MAX_VALUE;
381: Calendar cal = Calendar.getInstance();
382: long oneYear = 365;
383: for (int daysInTheFuture = 0; daysInTheFuture < oneYear; daysInTheFuture++) {
384: cal.setTime(now);
385: cal.add(Calendar.DATE, daysInTheFuture);
386: Date future = cal.getTime();
387: boolean dayIsValid = builder.isValidDay(future);
388: if (dayIsValid) {
389: boolean timePassedToday = (daysInTheFuture == 0)
390: && (nowTime > this BuildTime);
391: if (!timePassedToday) {
392: int buildHour = this BuildTime / 100;
393: int buildMinute = this BuildTime % 100;
394: cal.set(Calendar.HOUR_OF_DAY, buildHour);
395: cal.set(Calendar.MINUTE, buildMinute);
396: future = cal.getTime();
397: timeToThisBuild = future.getTime()
398: - now.getTime();
399: break;
400: }
401: }
402: }
403: if (timeToThisBuild < timeToNextBuild) {
404: timeToNextBuild = timeToThisBuild;
405: }
406: }
407: }
408:
409: if (timeToNextBuild > MAX_INTERVAL_MILLISECONDS) {
410: LOG
411: .error("checkTimeBuilders exceeding maximum interval. using proposed value ["
412: + proposedTime + "] instead");
413: timeToNextBuild = proposedTime;
414: }
415: return timeToNextBuild;
416: }
417:
418: long checkPauseBuilders(Date now, long proposedTime) {
419: long oldTime = proposedTime;
420: long newTime = checkForPauseAtProposedTime(now, oldTime);
421: while (oldTime != newTime) {
422: oldTime = newTime;
423: newTime = checkForPauseAtProposedTime(now, oldTime);
424: }
425:
426: return newTime;
427: }
428:
429: private long checkForPauseAtProposedTime(Date now, long proposedTime) {
430: Date futureDate = getFutureDate(now, proposedTime);
431: PauseBuilder pause = findPause(futureDate);
432: if (pause == null) {
433: return proposedTime;
434: }
435:
436: int endPause = pause.getEndTime();
437: int currentTime = DateUtil.getTimeFromDate(now);
438:
439: long timeToEndOfPause = DateUtil.milliTimeDifference(
440: currentTime, endPause);
441:
442: while (timeToEndOfPause < proposedTime) {
443: timeToEndOfPause += ONE_DAY;
444: }
445:
446: timeToEndOfPause = checkMaximumInterval(timeToEndOfPause);
447:
448: return timeToEndOfPause == MAX_INTERVAL_MILLISECONDS ? timeToEndOfPause
449: : timeToEndOfPause + ONE_MINUTE;
450: }
451:
452: private long checkMaximumInterval(long timeToEndOfPause) {
453: if (timeToEndOfPause > MAX_INTERVAL_MILLISECONDS) {
454: LOG
455: .error("maximum interval exceeded! project perpetually paused?");
456: return MAX_INTERVAL_MILLISECONDS;
457: }
458: return timeToEndOfPause;
459: }
460:
461: private Date getFutureDate(Date now, long delay) {
462: long futureMillis = now.getTime() + delay;
463: return new Date(futureMillis);
464: }
465:
466: public void setInterval(long intervalBetweenModificationChecks) {
467: if (intervalBetweenModificationChecks <= 0) {
468: throw new IllegalArgumentException(
469: "interval must be greater than zero");
470: }
471: interval = intervalBetweenModificationChecks * ONE_SECOND;
472: }
473:
474: public long getInterval() {
475: return interval;
476: }
477:
478: public void setShowProgress(final boolean showProgress) {
479: this .showProgress = showProgress;
480: }
481:
482: public boolean getShowProgress() {
483: return showProgress;
484: }
485:
486: public void validate() throws CruiseControlException {
487: ValidationHelper
488: .assertTrue(builders.size() > 0,
489: "schedule element requires at least one nested builder element");
490:
491: ValidationHelper.assertFalse(interval > ONE_YEAR,
492: "maximum interval value is " + MAX_INTERVAL_SECONDS
493: + " (one year)");
494:
495: if (hasOnlyTimeBuilders()) {
496: LOG
497: .warn("schedule has all time based builders: interval value will be ignored.");
498: ValidationHelper.assertFalse(
499: checkWithinPause(new ArrayList(builders)),
500: "all build times during pauses.");
501: }
502:
503: // Validate the child builders, since no one else seems to be doing it.
504: for (Iterator iterator = builders.iterator(); iterator
505: .hasNext();) {
506: Builder next = (Builder) iterator.next();
507: next.validate();
508: }
509: }
510:
511: private boolean checkWithinPause(List timeBuilders) {
512: for (int i = 0; i < timeBuilders.size(); i++) {
513: Builder builder = (Builder) timeBuilders.get(i);
514: for (int j = 0; j < pauseBuilders.size(); j++) {
515: PauseBuilder pauseBuilder = (PauseBuilder) pauseBuilders
516: .get(j);
517: if (buildDaySameAsPauseDay(builder, pauseBuilder)
518: && buildTimeWithinPauseTime(builder,
519: pauseBuilder)) {
520: timeBuilders.remove(builder);
521: StringBuffer message = new StringBuffer();
522: message.append("time Builder for time ");
523: message.append(Integer.toString(builder.getTime()));
524: if (builder.getDay() != Builder.NOT_SET) {
525: message.append(" and day of ");
526: message.append(getDayString(builder.getDay()));
527: }
528: message
529: .append(" is always within a pause and will never build");
530: LOG.error(message.toString());
531: }
532: }
533: }
534: return timeBuilders.isEmpty();
535: }
536:
537: /**
538: * @param day
539: * int value
540: * @return english string value
541: */
542: String getDayString(int day) {
543: if (day < 1 || day > 7) {
544: throw new IllegalArgumentException(
545: "valid values of days are between 1 and 7, was "
546: + day);
547: }
548: DateFormatSymbols symbols = new DateFormatSymbols(
549: Locale.ENGLISH);
550: String[] weekdays = symbols.getWeekdays();
551: return weekdays[day];
552: }
553:
554: private boolean buildDaySameAsPauseDay(Builder builder,
555: PauseBuilder pauseBuilder) {
556: return pauseBuilder.getDay() == PauseBuilder.NOT_SET
557: || pauseBuilder.getDay() == builder.getDay();
558: }
559:
560: private boolean buildTimeWithinPauseTime(Builder builder,
561: PauseBuilder pauseBuilder) {
562: return pauseBuilder.getStartTime() < builder.getTime()
563: && builder.getTime() < pauseBuilder.getEndTime();
564: }
565:
566: /**
567: * utility method to check method parameters and ensure they're not null
568: *
569: * @param paramName
570: * name of the parameter to check
571: * @param param
572: * parameter to check
573: */
574: private void checkParamNotNull(String paramName, Object param) {
575: if (param == null) {
576: throw new IllegalArgumentException(paramName
577: + " can't be null");
578: }
579: }
580:
581: public List getBuilders() {
582: return builders;
583: }
584: }
|