/******************************************************************************** * 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.Serializable; import java.text.DateFormat; import java.text.DateFormatSymbols; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Calendar; import java.util.Collections; import java.util.Comparator; import java.util.Date; import java.util.List; import java.util.Locale; import java.util.Map; import net.sourceforge.cruisecontrol.util.DateUtil; import net.sourceforge.cruisecontrol.util.ValidationHelper; import org.apache.log4j.Logger; import org.jdom.Element; /** * Handles scheduling different builds. * * @author alden almagro, ThoughtWorks, Inc. 2001-2 */ public class Schedule implements Serializable { private static final long serialVersionUID = -33682332427948426L; private static final Logger LOG = Logger.getLogger(Schedule.class); static final long ONE_SECOND = 1000; static final long ONE_MINUTE = 60 * ONE_SECOND; static final long ONE_DAY = 24 * 60 * ONE_MINUTE; static final long ONE_YEAR = ONE_DAY * 365; static final long MAX_INTERVAL_SECONDS = 60 * 60 * 24 * 365; static final long MAX_INTERVAL_MILLISECONDS = MAX_INTERVAL_SECONDS * 1000; private final List<Builder> builders = new ArrayList<Builder>(); private final List<PauseBuilder> pauseBuilders = new ArrayList<PauseBuilder>(); private long interval = 300 * ONE_SECOND; private boolean showProgress = true; /** date formatting for time statements */ private final DateFormat timeFormatter = new SimpleDateFormat("HH:mm"); private final Comparator<Builder> builderComparator = new BuilderComparitor(); public void add(final Builder builder) { checkParamNotNull("builder", builder); builders.add(builder); Collections.sort(builders, builderComparator); } public void add(final PauseBuilder pause) { checkParamNotNull("pauseBuilder", pause); pauseBuilders.add(pause); } /** * Determine if CruiseControl should run a build, given the current time. * * @param now * The current date * @return true if CruiseControl is currently paused (no build should run). */ public boolean isPaused(Date now) { checkParamNotNull("date", now); PauseBuilder pause = findPause(now); if (pause != null) { LOG.info("CruiseControl is paused until: " + getEndTimeString(pause)); return true; } return false; } /** * Returns a String representing the time following the end time of the given {@link PauseBuilder}. * * @param builder * the <code>PauseBuilder</code> to be considered. * @return a String representing the time following the end time of the <code>PauseBuilder</code>. */ private String getEndTimeString(final PauseBuilder builder) { final Calendar cal = Calendar.getInstance(); cal.set(Calendar.HOUR_OF_DAY, builder.getEndTime() / 100); cal.set(Calendar.MINUTE, builder.getEndTime() % 100); cal.add(Calendar.MINUTE, 1); return timeFormatter.format(cal.getTime()); } PauseBuilder findPause(final Date date) { checkParamNotNull("date", date); for (final PauseBuilder pause : pauseBuilders) { if (pause.isPaused(date)) { return pause; } } return null; } /** * Select the correct <code>Builder</code> and start a build. * * @param buildNumber * The sequential build number. * @param lastBuild * The date of the last build. * @param now * The current time. * @param properties * Properties that would need to be passed in to the actual build tool. * @param buildTarget * the build target to use instead of the configured one (pass in null if no override is needed) * @param progress * the progress callback object. * @return JDOM Element representation of build log. * @throws CruiseControlException if something fails */ public Element build(final int buildNumber, final Date lastBuild, final Date now, final Map<String, String> properties, final String buildTarget, final Progress progress) throws CruiseControlException { final Builder builder = selectBuilder(buildNumber, lastBuild, now); if (buildTarget != null) { LOG.info("Overriding build target with \"" + buildTarget + "\""); return builder.buildWithTarget(properties, buildTarget, (getShowProgress() ? progress : null)); } return builder.build(properties, (getShowProgress() ? progress : null)); } /** * Select the correct build based on the current buildNumber and time. * * @param buildNumber * The sequential build number * @param lastBuild * The date of the last build. * @param now * The current time. * @return The <code>Builder</code> that should be run. * @throws CruiseControlException if something fails */ protected Builder selectBuilder(final int buildNumber, final Date lastBuild, final Date now) throws CruiseControlException { Builder builder = findBuilder(buildNumber, lastBuild, now); if (builder == null) { final long timeToNextBuild = getTimeToNextBuild(now, ONE_MINUTE); final Date futureDate = getFutureDate(now, timeToNextBuild); builder = findBuilder(buildNumber, now, futureDate); } if (builder == null) { validate(); throw new CruiseControlException("configuration error not caught by validate? no builder selected"); } return builder; } private Builder findBuilder(final int buildNumber, final Date lastBuild, final Date now) throws CruiseControlException { for (final Builder builder : builders) { if (builder.isTimeBuilder()) { final int buildTime = builder.getTime(); final boolean didntBuildToday = builderDidntBuildToday(lastBuild, now, buildTime); final int nowTime = DateUtil.getTimeFromDate(now); final boolean isAfterBuildTime = buildTime <= nowTime; final boolean isValidDay = builder.isValidDay(now); if (didntBuildToday && isAfterBuildTime && isValidDay) { return builder; } } else if (builder.getMultiple() > 0) { if (builder.isValidDay(now)) { if ((buildNumber % builder.getMultiple()) == 0) { return builder; } } } else { throw new CruiseControlException("The selected Builder is not properly configured"); } } return null; } boolean builderDidntBuildToday(Date lastBuild, Date now, int buildTime) { int time = DateUtil.getTimeFromDate(now); long timeMillis = DateUtil.convertToMillis(time); long startOfToday = now.getTime() - timeMillis; boolean lastBuildYesterday = lastBuild.getTime() < startOfToday; boolean lastBuildTimeBeforeBuildTime = DateUtil.getTimeFromDate(lastBuild) < buildTime; return lastBuildYesterday || lastBuildTimeBeforeBuildTime; } long getTimeToNextBuild(Date now, long sleepInterval) { return getTimeToNextBuild(now, sleepInterval, 0); } private long getTimeToNextBuild(Date now, long sleepInterval, long priorPauseAdjustment) { long timeToNextBuild = sleepInterval; LOG.debug("getTimeToNextBuild: initial timeToNextBuild = " + timeToNextBuild); timeToNextBuild = checkMultipleBuilders(now, timeToNextBuild); LOG.debug("getTimeToNextBuild: after checkMultipleBuilders = " + timeToNextBuild); timeToNextBuild = checkTimeBuilders(now, timeToNextBuild); LOG.debug("getTimeToNextBuild: after checkTimeBuilders = " + timeToNextBuild); long timeTillNotPaused = checkPauseBuilders(now, timeToNextBuild); LOG.debug("getTimeToNextBuild: after checkPauseBuilders = " + timeToNextBuild); if (timeToNextBuild != timeTillNotPaused) { boolean atMaxTime = timeTillNotPaused >= MAX_INTERVAL_MILLISECONDS || priorPauseAdjustment >= MAX_INTERVAL_MILLISECONDS; if (hasOnlyTimeBuilders() && !atMaxTime) { Date dateAfterPause = getFutureDate(now, timeTillNotPaused); long adjustmentFromEndOfPause = getTimeToNextBuild(dateAfterPause, 0, priorPauseAdjustment + timeTillNotPaused); timeToNextBuild = timeTillNotPaused + adjustmentFromEndOfPause; timeToNextBuild = checkMaximumInterval(timeToNextBuild); } else { timeToNextBuild = timeTillNotPaused; } } return timeToNextBuild; } private long checkMultipleBuilders(final Date now, final long interval) { if (hasOnlyTimeBuilders()) { LOG.debug("has only time builders, so no correction for multiple builders."); return interval; } Date then = getFutureDate(now, interval); final List<Builder> buildersForOtherDays = new ArrayList<Builder>(); for (final Builder builder : builders) { if (!builder.isTimeBuilder()) { if (builder.getMultiple() == 1) { if (builder.isValidDay(then)) { LOG.debug("multiple=1 builder found that could run on " + then); return interval; } else { buildersForOtherDays.add(builder); } } } } if (buildersForOtherDays.size() == 0) { LOG.error("configuration error: has some multiple builders but no multiple=1 builders found!"); return interval; } else { LOG.debug("no multiple=1 builders found for " + then + ". checking other days"); } for (int i = 1; i < 7; i++) { long daysPastInitialInterval = i * ONE_DAY; then = getFutureDate(now, interval + daysPastInitialInterval); for (final Builder builder : builders) { if (builder.isValidDay(then)) { LOG.debug("multiple=1 builder found that could run on " + then); long correctionToMidnight = getTimePastMidnight(then); return interval + daysPastInitialInterval - correctionToMidnight; } } } LOG.error("configuration error? could not find appropriate multiple=1 builder."); return interval; } private long getTimePastMidnight(Date date) { Calendar cal = Calendar.getInstance(); cal.setTime(date); long time = 60 * ONE_MINUTE * cal.get(Calendar.HOUR_OF_DAY); time += ONE_MINUTE * cal.get(Calendar.MINUTE); return time; } private boolean hasOnlyTimeBuilders() { boolean onlyTimeBuilders = true; for (final Builder builder : builders) { if (!builder.isTimeBuilder()) { onlyTimeBuilders = false; break; } } return onlyTimeBuilders; } long checkTimeBuilders(final Date now, final long proposedTime) { long timeToNextBuild = proposedTime; if (hasOnlyTimeBuilders()) { timeToNextBuild = Long.MAX_VALUE; } final int nowTime = DateUtil.getTimeFromDate(now); for (final Builder builder : builders) { if (builder.isTimeBuilder()) { long timeToThisBuild = Long.MAX_VALUE; final Calendar cal = Calendar.getInstance(); final long oneYear = 365; for (int daysInTheFuture = 0; daysInTheFuture < oneYear; daysInTheFuture++) { cal.setTime(now); cal.add(Calendar.DATE, daysInTheFuture); Date future = cal.getTime(); final boolean dayIsValid = builder.isValidDay(future); if (dayIsValid) { int thisBuildTime = builder.getTime(); boolean timePassedToday = (daysInTheFuture == 0) && (nowTime > thisBuildTime); if (!timePassedToday) { int buildHour = thisBuildTime / 100; int buildMinute = thisBuildTime % 100; cal.set(Calendar.HOUR_OF_DAY, buildHour); cal.set(Calendar.MINUTE, buildMinute); future = cal.getTime(); timeToThisBuild = future.getTime() - now.getTime(); break; } } } if (timeToThisBuild < timeToNextBuild) { timeToNextBuild = timeToThisBuild; } } } if (timeToNextBuild > MAX_INTERVAL_MILLISECONDS) { LOG.error("checkTimeBuilders exceeding maximum interval. using proposed value [" + proposedTime + "] instead"); timeToNextBuild = proposedTime; } return timeToNextBuild; } long checkPauseBuilders(Date now, long proposedTime) { long oldTime = proposedTime; long newTime = checkForPauseAtProposedTime(now, oldTime); while (oldTime != newTime) { oldTime = newTime; newTime = checkForPauseAtProposedTime(now, oldTime); } return newTime; } private long checkForPauseAtProposedTime(Date now, long proposedTime) { Date futureDate = getFutureDate(now, proposedTime); PauseBuilder pause = findPause(futureDate); if (pause == null) { return proposedTime; } int endPause = pause.getEndTime(); int futureTime = DateUtil.getTimeFromDate(futureDate); long timeToEndOfPause = proposedTime + DateUtil.milliTimeDifference(futureTime, endPause); timeToEndOfPause = checkMaximumInterval(timeToEndOfPause); return timeToEndOfPause == MAX_INTERVAL_MILLISECONDS ? timeToEndOfPause : timeToEndOfPause + ONE_MINUTE; } private long checkMaximumInterval(long timeToEndOfPause) { if (timeToEndOfPause > MAX_INTERVAL_MILLISECONDS) { LOG.error("maximum interval exceeded! project perpetually paused?"); return MAX_INTERVAL_MILLISECONDS; } return timeToEndOfPause; } private Date getFutureDate(Date now, long delay) { long futureMillis = now.getTime() + delay; return new Date(futureMillis); } public void setInterval(long intervalBetweenModificationChecks) { if (intervalBetweenModificationChecks <= 0) { throw new IllegalArgumentException("interval must be greater than zero"); } interval = intervalBetweenModificationChecks * ONE_SECOND; } public long getInterval() { return interval; } public void setShowProgress(final boolean showProgress) { this.showProgress = showProgress; } public boolean getShowProgress() { return showProgress; } public void validate() throws CruiseControlException { ValidationHelper.assertTrue(builders.size() > 0, "schedule element requires at least one nested builder element"); ValidationHelper.assertFalse(interval > ONE_YEAR, "maximum interval value is " + MAX_INTERVAL_SECONDS + " (one year)"); if (hasOnlyTimeBuilders()) { LOG.warn("schedule has all time based builders: interval value will be ignored."); ValidationHelper.assertFalse(checkWithinPause(new ArrayList<Builder>(builders)), "all build times during pauses."); } // Validate the child builders, since no one else seems to be doing it. for (final Builder next : builders) { next.validate(); } } private boolean checkWithinPause(List timeBuilders) { for (int i = 0; i < timeBuilders.size(); i++) { Builder builder = (Builder) timeBuilders.get(i); for (final PauseBuilder pauseBuilder : pauseBuilders) { if (buildDaySameAsPauseDay(builder, pauseBuilder) && buildTimeWithinPauseTime(builder, pauseBuilder)) { timeBuilders.remove(builder); StringBuffer message = new StringBuffer(); message.append("time Builder for time "); message.append(Integer.toString(builder.getTime())); if (builder.getDay() != Builder.NOT_SET) { message.append(" and day of "); message.append(getDayString(builder.getDay())); } message.append(" is always within a pause and will never build"); LOG.error(message.toString()); } } } return timeBuilders.isEmpty(); } /** * @param day * int value * @return english string value */ String getDayString(int day) { if (day < 1 || day > 7) { throw new IllegalArgumentException("valid values of days are between 1 and 7, was " + day); } DateFormatSymbols symbols = new DateFormatSymbols(Locale.ENGLISH); String[] weekdays = symbols.getWeekdays(); return weekdays[day]; } private boolean buildDaySameAsPauseDay(Builder builder, PauseBuilder pauseBuilder) { return pauseBuilder.getDay() == PauseBuilder.NOT_SET || pauseBuilder.getDay() == builder.getDay(); } private boolean buildTimeWithinPauseTime(Builder builder, PauseBuilder pauseBuilder) { return pauseBuilder.getStartTime() < builder.getTime() && builder.getTime() < pauseBuilder.getEndTime(); } /** * utility method to check method parameters and ensure they're not null * * @param paramName * name of the parameter to check * @param param * parameter to check */ private void checkParamNotNull(String paramName, Object param) { if (param == null) { throw new IllegalArgumentException(paramName + " can't be null"); } } public List getBuilders() { return builders; } /** * sort time builders before non-time builders, then by multiple (higher first) * then ones with days before non-days */ private static class BuilderComparitor implements Comparator<Builder> { public int compare(Builder b1, Builder b2) { if (b1.isTimeBuilder() && !b2.isTimeBuilder()) { return -1; } if (!b1.isTimeBuilder() && b2.isTimeBuilder()) { return 1; } if (b1.isTimeBuilder() && b2.isTimeBuilder()) { return checkDays(b1, b2); } if (b1.getMultiple() > b2.getMultiple()) { return -1; } if (b1.getMultiple() < b2.getMultiple()) { return 1; } return checkDays(b1, b2); } private int checkDays(Builder b1, Builder b2) { boolean b1HasDaySet = b1.getDay() >= 0; boolean b2HasDaySet = b2.getDay() >= 0; if (b1HasDaySet && !b2HasDaySet) { return -1; } if (!b1HasDaySet && b2HasDaySet) { return 1; } return 0; } } }