package org.oddjob.schedules.schedules; import java.io.Serializable; import java.text.ParseException; import java.util.Calendar; import java.util.Date; import java.util.TimeZone; import org.apache.log4j.Logger; import org.oddjob.OddjobException; import org.oddjob.arooa.utils.SpringSafeCalendar; import org.oddjob.arooa.utils.TimeParser; import org.oddjob.schedules.AbstractSchedule; import org.oddjob.schedules.CalendarUnit; import org.oddjob.schedules.ConstrainedSchedule; import org.oddjob.schedules.DateUtils; import org.oddjob.schedules.Interval; import org.oddjob.schedules.Schedule; import org.oddjob.schedules.ScheduleContext; import org.oddjob.schedules.ScheduleResult; import org.oddjob.schedules.SimpleInterval; import org.oddjob.schedules.SimpleScheduleResult; import org.oddjob.scheduling.Timer; /** * @oddjob.description Provide a schedule for an interval of time. When used as a * refinement this schedule will narrow the parent interval down to an interval of * time on the first day of the parent interval, or if the <code>toLast</code> * property is specified, from the first day to the last day of the parent interval. When used as the * topmost definition for a schedule then this schedule specifies a single interval * of time starting on the current day. * <p> * To provide a schedule for each day at a certain time see the {@link DailySchedule} * schedules. * * @oddjob.example * * A simple time example. * * {@oddjob.xml.resource org/oddjob/schedules/schedules/TimeScheduleSimpleExample.xml} * * When used with a {@link Timer} this would run a job just once at 10am, and * never again. If the * timer was started after 10am, then the job would run the following day at 10am. * If it was required that the job would run any time the timer was started * after 10am then the <code> * from</code> property should be used instead of the <code>at</code> property. * * @oddjob.example * * Using an interval with time to schedule something every 15 minutes between * 10pm and 4am the next day. The end time is 03:50 yet the last interval is * 03:45 to 04:00 because the interval starts before the end time. * * {@oddjob.xml.resource org/oddjob/schedules/schedules/TimeAndIntervalExample.xml} * * @oddjob.example * * Schedule something over a whole week between two times. This demonstrates * how the <code>toLast</code> property works. * * {@oddjob.xml.resource org/oddjob/schedules/schedules/TimeOverWeekExample.xml} * * The schedule would be due every two hours all day and all night from 8am * Monday morning until 6pm Friday afternoon. * * @author Rob Gordon */ final public class TimeSchedule extends AbstractSchedule implements Serializable { private static Logger logger = Logger.getLogger(ConstrainedSchedule.class); private static final long serialVersionUID = 200502262011092000L; private String from; private String to; private String toLast; /** * @oddjob.property from * @oddjob.description The from time. * @oddjob.required No. Defaults to the start of any parent interval * or the beginning of time. * * @param from The from date. */ public void setFrom(String from) { this.from = from; } /* * (non-Javadoc) * @see org.treesched.ConstrainedSchedule#getFrom() */ public String getFrom() { return from; } /** * @oddjob.property to * @oddjob.description The to time. If specified, this is the * time on the first day of the parent interval. * @oddjob.required No. Defaults to the end of the last day of the * parent interval, or the end of time. * * @param to The to date. * */ public void setTo(String to) { this.to = to; } /* * (non-Javadoc) * @see org.treesched.ConstrainedSchedule#getTo() */ public String getTo() { return to; } /** * @oddjob.property at * @oddjob.description The time at which this schedule is for. * This has the same effect as setting from and to to the same thing. * @oddjob.required No. * * @param at The at time. */ public void setAt(String at) { this.setFrom(at); this.setTo(at); } public String getToLast() { return toLast; } /** * @oddjob.property toLast * @oddjob.description The to time for the end of the parent interval. * This differs from the to property in that the to property is for the first * day of the parent interval. * @oddjob.required No. The to property, or it's default value, * will be used instead. * * @param toLast The to last time of the interval. */ public void setToLast(String toLast) { this.toLast = toLast; } protected CalendarUnit intervalBetween() { return new CalendarUnit(Calendar.DATE, 1); } static Date parseTime(String textField, Date referenceDate, TimeZone timeZone, String fieldName) { TimeParser timeFormatter = new TimeParser( new SpringSafeCalendar(referenceDate, timeZone)); try { Date now = timeFormatter.parse(textField); return now; } catch (ParseException e) { throw new OddjobException("Failed to parse " + fieldName + "[" + textField + "]"); } } protected Calendar fromCalendar(ScheduleContext context) { TimeZone timeZone = context.getTimeZone(); Calendar fromCal = Calendar.getInstance(timeZone); Interval parentInterval = context.getParentInterval(); if (from == null) { if (parentInterval == null) { fromCal.setTime(Interval.START_OF_TIME); } else { fromCal.setTime(DateUtils.startOfDay(parentInterval.getFromDate(), timeZone)); } } else { if (parentInterval == null) { fromCal.setTime(parseTime(from, context.getDate(), timeZone, "from")); } else { fromCal.setTime(parseTime(from, parentInterval.getFromDate(), timeZone, "from")); } } return fromCal; } protected Calendar toCalendar(ScheduleContext context) { TimeZone timeZone = context.getTimeZone(); Calendar toCal = Calendar.getInstance(timeZone); Interval parentInterval = context.getParentInterval(); if (toLast != null) { if (parentInterval == null) { toCal.setTime(parseTime(to, context.getDate(), timeZone, "to")); } else { toCal.setTime(parseTime(to, DateUtils.oneMillisBefore(parentInterval.getToDate()), timeZone, "to")); } } if (to != null) { if (parentInterval == null) { toCal.setTime(parseTime(to, context.getDate(), timeZone, "to")); } else { toCal.setTime(parseTime(to, parentInterval.getFromDate(), timeZone, "to")); } } else { if (parentInterval == null) { toCal.setTime(Interval.END_OF_TIME); } else { toCal.setTime(DateUtils.endOfDay( DateUtils.oneMillisBefore(parentInterval.getToDate()), timeZone)); }; } return toCal; } /** * @param context * @return */ protected Calendar nowCalendar(ScheduleContext context) { Calendar nowCal = Calendar.getInstance(context.getTimeZone()); nowCal.setTime(context.getDate()); Interval parentInterval = context.getParentInterval(); if (parentInterval != null) { if (parentInterval.getToDate().compareTo(context.getDate()) <= 0) { nowCal.setTime(DateUtils.oneMillisBefore(parentInterval.getToDate())); } else if (parentInterval.getFromDate().compareTo(context.getDate()) > 0) { nowCal.setTime(parentInterval.getFromDate()); } } return nowCal; } /** /** * Calculate the next interval, without children. * * @param context * @return */ protected final Interval nextInterval(ScheduleContext context) { Calendar fromCal = fromCalendar(context); Calendar toCal = toCalendar(context); if (fromCal.getTime().equals(toCal.getTime())) { toCal.add(Calendar.MILLISECOND, 1); } Calendar nowCal = nowCalendar(context); if (fromCal.after(toCal)) { toCal = shiftFromCalendar(toCal, 1); } if (nowCal.compareTo(toCal) >= 0) { return null; } return new SimpleInterval(fromCal.getTime(), toCal.getTime()); } /** * Calculate the last interval. * * @param context * @return */ protected final Interval lastInterval(ScheduleContext context) { Calendar fromCal = fromCalendar(context); Calendar toCal = toCalendar(context); if (fromCal.getTime().equals(toCal.getTime())) { toCal.add(Calendar.MILLISECOND, 1); } Calendar nowCal = nowCalendar(context); if (fromCal.after(toCal)) { fromCal = shiftFromCalendar(fromCal, -1); } if (nowCal.compareTo(toCal) < 0) { return null; } return new SimpleInterval(fromCal.getTime(), toCal.getTime()); } protected Calendar shiftFromCalendar(Calendar calendar, int intervals) { if (calendar.getTime().equals(Interval.START_OF_TIME)) { return calendar; } else { return shiftCalendar(calendar, intervals); } } protected Calendar shiftToCalendar(Calendar calendar, int intervals) { if (calendar.getTime().equals(Interval.END_OF_TIME)) { return calendar; } else { return shiftCalendar(calendar, intervals); } } private Calendar shiftCalendar(Calendar calendar, int intervals) { CalendarUnit unit = intervalBetween(); calendar.add(unit.getField(), intervals * unit.getValue()); return calendar; } /* * (non-Javadoc) * @see org.oddjob.schedules.Schedule#nextDue(org.oddjob.schedules.ScheduleContext) */ public ScheduleResult nextDue(ScheduleContext context) { Date now = context.getDate(); if (now == null) { return null; } final Interval thisNextInterval = nextInterval(context); Interval thisInterval = thisNextInterval; ScheduleResult nextResult = null; if (thisNextInterval != null) { ParentChildSchedule parentChild = new ParentChildSchedule(new Schedule() { public ScheduleResult nextDue(ScheduleContext context) { return new SimpleScheduleResult(thisNextInterval); } }, getRefinement()); nextResult = parentChild.nextDue(context); } // Maybe we are beyond the interval but still in the interval of // a child (because a child interval could extend beyond the limit // of it's parent). if ((nextResult == null || now.before(nextResult.getFromDate())) && // We need this extra check because time intervals can extend forever. (to != null && getRefinement() != null)) { final Interval thisPreviousInterval = lastInterval(context); if (thisPreviousInterval != null) { ParentChildSchedule parentChild = new ParentChildSchedule(new Schedule() { public ScheduleResult nextDue(ScheduleContext context) { return new SimpleScheduleResult(thisPreviousInterval); } }, getRefinement()); ScheduleResult previous = parentChild.nextDue(context); if (previous != null && now.before(previous.getToDate())) { nextResult = previous; thisInterval = thisPreviousInterval; } } } if (nextResult == null) { return null; } if (!thisInterval.getToDate().after(nextResult.getToDate())) { // time is a once only schedule. nextResult = new SimpleScheduleResult(nextResult, null); } logger.debug(this + ": in date is " + now + ", next interval is " + nextResult); return nextResult; } /** * Override toString. * * @return A description of the schedule. */ public String toString() { String from; if (this.from == null) { from = null; } else { from = this.from; } String to; if (this.toLast != null) { to = "last " + this.toLast; } if (this.to != null) { to = this.to; } else { to = null; } StringBuilder description = new StringBuilder(); if (from != null && from.equals(to)) { description.append(" at "); description.append(from); } else { if (from != null) { description.append(" from "); description.append(from); } if (to != null) { description.append(" to "); description.append(to); } } if (getRefinement() != null) { description.append(" with refinement "); description.append(getRefinement().toString()); } return "Time" + description; } }