package org.oddjob.schedules.schedules; import java.io.Serializable; import java.util.Date; import org.apache.log4j.Logger; import org.oddjob.schedules.Interval; import org.oddjob.schedules.IntervalHelper; import org.oddjob.schedules.IntervalTo; import org.oddjob.schedules.Schedule; import org.oddjob.schedules.ScheduleContext; import org.oddjob.schedules.ScheduleResult; import org.oddjob.schedules.ScheduleType; import org.oddjob.schedules.SimpleScheduleResult; /** * @oddjob.description This schedule allows a normal schedule * to be broken by the results of another * schedule. This might be a list of bank holidays, or time of day, or any other * schedule. * <p> * This schedule works by moving the schedule forward if the start time of the * next interval falls within the next interval defined by the break. In the * example below for a time of Midday on 24th of December the logic is as follows: * <ul> * <li>The schedule is next due at 10:00 on the 25th of December.</li> * <li>This is within the break, move the schedule on.</li> * <li>The schedule is next due at 10:00 on the 26th of December.</li> * <li>This is within the break, move the schedule on.</li> * <li>The schedule is next due at 10:00 on the 27th of December.</li> * <li>This schedule is outside the break, use this result.</li> * </ul> * <p> * The optional alternative property defines a schedule to be used during the * breaks, instead of simply moving the interval forward. * * @oddjob.example * * A schedule that breaks for Christmas. * * {@oddjob.xml.resource org/oddjob/schedules/schedules/BrokenScheduleExample.xml} * * The logic is explained above. * * @oddjob.example * * A schedule with an alternative. The schedule breaks at weekends and for * Christmas. During the break the schedule will be due once at 11am the * first day of the break, instead of the usual 10am. * * {@oddjob.xml.resource org/oddjob/schedules/schedules/BrokenScheduleAlternative.xml} * * * @oddjob.example * * Examples elsewhere. * <ul> * <li>The {@link AfterSchedule} documentation has an example that uses the * <code>broken</code> schedule to calculate the day after the next * working day.</li> * <li>The {@link ScheduleType} documentation shows a <code>broken</code> * schedule being used to calculate the next working day.</li> * <li>The {@link DayAfterSchedule} and {@link DayBeforeSchedule} documentation * shows a <code>broken</code> schedule being used to move the last day of the month.</li> * </ul> * * * @author Rob Gordon */ public class BrokenSchedule implements Serializable, Schedule{ private static final long serialVersionUID = 20050226; private static final Logger logger = Logger.getLogger(BrokenSchedule.class); /** * @oddjob.property * @oddjob.description The schedule. * @oddjob.required Yes. */ private Schedule schedule; /** * @oddjob.property * @oddjob.description The breaks. * @oddjob.required No, but this schedule is pointless if none are provided. */ private Schedule breaks; /** * @oddjob.property * @oddjob.description An alternative schedule to apply during a break. * The alternative schedule will be passed the interval that is the break. * @oddjob.required No. */ private Schedule alternative; /** * Set the schedule to break up. * * @param schedule The schedule to break up. */ public void setSchedule(Schedule schedule) { this.schedule = schedule; } /** * Get the schedule to break up. * * @return The schedule to break up. */ public Schedule getSchedule() { return this.schedule; } /** * Set the breaks which will break up the schedule. * * @param breaks The breaks schedule. */ public void setBreaks(Schedule breaks) { this.breaks = breaks; } /** * Get the breaks which will break up the schedule. * * @return The break Schedule. */ public Schedule getBreaks() { return this.breaks; } public Schedule getAlternative() { return alternative; } public void setAlternative(Schedule alternative) { this.alternative = alternative; } /** * Implement the schedule. */ public ScheduleResult nextDue(ScheduleContext context) { Date now = context.getDate(); logger.debug(this + ": in interval is " + now); // sanity checks if (schedule == null) { return null; } if (breaks == null) { return schedule.nextDue(context); } Date use = now; // loop until we get a valid interval while (true) { if (use == null) { return null; } ScheduleResult next = schedule.nextDue(context.move(use)); // if the next schedule is never due return. if (next == null) { return null; } // find the first exclusion interval Interval exclude = mergeBreaks(context.move(next.getFromDate())); if (exclude == null) { return next; } // if this interval is before the break if (new IntervalHelper(next).isBefore(exclude)) { return next; } // if we got here the last interval is blocked by an exclude. Date lastUse = use; // move the interval on. if (next.getUseNext() == null) { use = null; } else { if (exclude.getToDate().after(next.getUseNext())) { use = exclude.getToDate(); } else { use = next.getUseNext(); } } // see if there is an alternative. if (alternative != null) { // If the interval is ahead of now move now. if (lastUse.before(exclude.getFromDate())) { lastUse = exclude.getFromDate(); } ScheduleResult alternativeResult = alternative.nextDue( context.spawn(lastUse, exclude)); if (alternativeResult != null) { return new SimpleScheduleResult(alternativeResult, use); } // If the alternative is null, move on. } } } private Interval mergeBreaks(ScheduleContext context) { ScheduleContext useContext = context; Interval merged = null; while (true) { Interval exclude = breaks.nextDue(useContext); if (exclude == null) { return merged; } if (merged == null) { merged = exclude; } else { if (exclude.getFromDate().after(merged.getToDate())) { return merged; } merged = new IntervalTo(merged.getFromDate(), exclude.getToDate()); } useContext = useContext.move(merged.getToDate()); } } /** * Provide a simple string description. */ public String toString() { return "Broken Schedule " + schedule + " with breaks " + breaks; } }