package org.oddjob.schedules;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.TimeZone;
import org.apache.log4j.Logger;
import org.oddjob.schedules.schedules.CountSchedule;
import org.oddjob.schedules.schedules.NowSchedule;
import org.oddjob.util.Clock;
/**
* A class capable of calculating next due times for a job by using two
* schedules - a normal schedule for normal completion and a retry schedule
* for when a job hasn't completed.
*
* @author Rob Gordon
*/
public class ScheduleCalculator {
private static final Logger logger = Logger.getLogger(ScheduleCalculator.class);
/**
* The normal schedule.
*/
private final Schedule normalSchedule;
/**
* The Retry schedule.
*/
private final Schedule retrySchedule;
/** The current schedule being used. */
private Schedule currentSchedule;
/** The current/last interval in which schedule is due */
private ScheduleResult currentInterval;
/** last normal interval from the regular schedule */
private ScheduleResult normalInterval;
/** Initialised */
private boolean initialised;
/** Listeners. */
private final List<ScheduleListener> listeners =
new ArrayList<ScheduleListener>();
/** Current schedule context */
private ScheduleContext normalContext;
/** Retry schedule context */
private ScheduleContext retryContext;
/** The time zone. */
private final TimeZone timeZone;
private final Clock clock;
/**
* Constructor for a calculator with no retry for the default time zone.
*
* @param clock The clock to use, may not be null.
* @param schedule The normal schedule, may not be null.
*/
public ScheduleCalculator(Clock clock, Schedule schedule) {
this(clock, schedule, null, null);
}
/**
* Constructor for a calculator with with a timeZone.
*
* @param clock The clock to use, may not be null.
* @param schedule The normal schedule, may not be null.
* @param timeZone The time zone. May be null
*/
public ScheduleCalculator(Clock clock, Schedule schedule, TimeZone timeZone) {
this(clock, schedule, null, timeZone);
}
/**
* Constructor for a calculator with a retry schedule using the default time zone.
*
* @param clock The clock to use, may not be null.
* @param schedule The normal schedule, may not be null.
* @param retry The retrySchedule. May be null.
*/
public ScheduleCalculator(Clock clock, Schedule schedule, Schedule retry) {
this(clock, schedule, retry, null);
}
/**
* Constructor for a calculator with a retry schedule and a timeZone.
*
* @param clock The clock to use, may not be null.
* @param schedule The normal schedule, may not be null.
* @param retry The retrySchedule. May be null.
* @param timeZone The time zone. May be null
*/
public ScheduleCalculator(Clock clock, Schedule schedule, Schedule retry, TimeZone timeZone) {
if (clock == null) {
throw new IllegalStateException("Null clock not allowed.");
}
if (schedule == null) {
schedule = defaultSchedule();
}
this.clock = clock;
this.normalSchedule = schedule;
this.retrySchedule = retry;
this.timeZone = timeZone;
}
/**
* Getter for schedule.
*
* @return The schedule.
*/
public Schedule getSchedule() {
return this.normalSchedule;
}
/**
* Getter for retry.
*
* @return The retry schedule.
*/
public Schedule getRetry() {
return this.retrySchedule;
}
public void initialise() {
initialise(null, new HashMap<Object, Object>());
}
/**
* Initialize the scheduler.
*/
synchronized public void initialise(ScheduleResult lastComplete, Map<Object, Object> contextData) {
if (initialised) {
throw new IllegalStateException("Already initialised.");
}
Date nowTime = clock.getDate();
logger.debug("Initialising, time now [" + nowTime + "], lastComplete [" + lastComplete + "]");
// always start on a normal schedule.
currentSchedule(normalSchedule);
// if the last complete time was persisted.
if (lastComplete != null && lastComplete.getUseNext() != null) {
// work with a time that is one millisecond after the lastComplete
// interval.
Date useTime = lastComplete.getUseNext();
normalContext = new ScheduleContext(
useTime, timeZone, contextData);
currentInterval = currentSchedule.nextDue(normalContext);
normalInterval = currentInterval;
fireInitialised();
}
else {
logger.debug("Starting up with no last complete date.");
normalContext = new ScheduleContext(
nowTime, timeZone, contextData);
currentInterval = currentSchedule.nextDue(normalContext);
normalInterval = currentInterval;
fireInitialised();
}
initialised = true;
}
/**
* Change the current schedule.
*
* @param schedule The current schedule.
*/
private void currentSchedule(Schedule schedule) {
this.currentSchedule = schedule;
}
/**
* Get the name of the current schedule.
*
* @return The name of the current schedule.
*/
synchronized public String getCurrentScheduleType() {
if (currentSchedule == normalSchedule) {
return "Normal";
}
else if (currentSchedule == retrySchedule) {
return "Retry";
}
else {
return "Undefined";
}
}
synchronized public void calculateComplete() {
logger.debug("Calculate Complete");
currentSchedule(normalSchedule);
ScheduleResult lastComplete = normalInterval;
normalContext = normalContext.move(
currentInterval.getToDate());
// calculate the next due time.
currentInterval = currentSchedule.nextDue(
normalContext);
normalInterval = currentInterval;
fireComplete(lastComplete);
}
/**
* Calculate the retry schedule.
*
*/
synchronized public void calculateRetry() {
logger.debug("Calculate Retry");
if (currentSchedule == normalSchedule) {
// job just failed
if (retrySchedule != null) {
logger.debug("Switching to retry schedule.");
normalInterval = currentInterval;
currentSchedule(retrySchedule);
// use the current time
retryContext = new ScheduleContext(clock.getDate(),
timeZone);
// add parent limits to the context. Used by Interval.
if (!new IntervalHelper(normalInterval).isPoint()) {
retryContext = retryContext.spawn(normalInterval);
}
retryAndFail();
}
else {
// no retry
normalContext = normalContext.move(normalInterval.getToDate());
currentInterval = normalSchedule.nextDue(normalContext);
normalInterval = currentInterval;
fireFailed();
}
}
else {
retryContext = retryContext.move(currentInterval.getToDate());
retryAndFail();
}
logger.debug("Next inteval is [" + currentInterval + "]");
}
private void retryAndFail() {
currentInterval = retrySchedule.nextDue(retryContext);
if (currentInterval != null) {
Interval retryInterval = currentInterval;
// if the normal interval isn't a point then use it to
// limit the retry schedule.
IntervalHelper helper = new IntervalHelper(retryInterval);
if (!helper.isPoint()) {
retryInterval = helper.limit(currentInterval);
}
fireRetry(retryInterval);
}
else {
// else fail
currentSchedule(normalSchedule);
normalContext = normalContext.move(normalInterval.getUseNext());
currentInterval = normalSchedule.nextDue(normalContext);
normalInterval = currentInterval;
logger.debug("Switched back to normal schedule and next interval is [" + currentInterval + "]");
fireFailed();
}
}
synchronized public void addScheduleListener(ScheduleListener l) {
listeners.add(l);
}
synchronized public void removeScheduleListener(ScheduleListener l) {
listeners.remove(l);
}
protected void fireInitialised() {
Date scheduleDate;
if (normalInterval == null) {
scheduleDate = null;
}
else {
scheduleDate = normalInterval.getFromDate();
}
for (Iterator<ScheduleListener> it = listeners.iterator(); it.hasNext(); ) {
ScheduleListener l = (ScheduleListener) it.next();
l.initialised(scheduleDate);
}
}
protected void fireComplete(ScheduleResult lastComplete) {
Date scheduleDate;
if (normalInterval == null) {
scheduleDate = null;
}
else {
scheduleDate = normalInterval.getFromDate();
}
for (Iterator<ScheduleListener> it = listeners.iterator(); it.hasNext(); ) {
ScheduleListener l = (ScheduleListener) it.next();
l.complete(scheduleDate, lastComplete);
}
}
protected void fireRetry(Interval limits) {
if (normalInterval == null) {
throw new IllegalStateException("Can't retry without have a normal interval!");
}
if (limits == null) {
throw new IllegalStateException("Can't retry without have an interval!");
}
Date scheduleDate = normalInterval.getFromDate();
Date retryDate = limits.getToDate();
for (Iterator<ScheduleListener> it = listeners.iterator(); it.hasNext(); ) {
ScheduleListener l = (ScheduleListener) it.next();
l.retry(scheduleDate, retryDate);
}
}
protected void fireFailed() {
Date scheduleDate;
if (normalInterval == null) {
scheduleDate = null;
}
else {
scheduleDate = normalInterval.getFromDate();
}
for (Iterator<ScheduleListener> it = listeners.iterator(); it.hasNext(); ) {
ScheduleListener l = (ScheduleListener) it.next();
l.failed(scheduleDate);
}
}
private Schedule defaultSchedule() {
CountSchedule count = new CountSchedule();
count.setCount(1);
count.setRefinement(new NowSchedule());
return count;
}
}