package org.openntf.domino.thread;
import java.util.Calendar;
import java.util.StringTokenizer;
import java.util.concurrent.TimeUnit;
import java.util.logging.Logger;
import com.ibm.commons.util.StringUtil;
public class PeriodicScheduler implements Scheduler {
private static final Logger log_ = Logger.getLogger(DominoExecutor.class.getName());
// computation is done on an calendar object, as this is easier.
final Calendar nextExecTime = Calendar.getInstance();
int periodSecond = 0;
// startSecond & endSecond defines a time window
int startSecond = 0;
int endSecond = 24 * 60 * 60;
// day bits for all days
private static final int ALL_DAYS = 1 << Calendar.SUNDAY | 1 << Calendar.MONDAY | 1 << Calendar.TUESDAY | 1 << Calendar.WEDNESDAY
| 1 << Calendar.THURSDAY | 1 << Calendar.FRIDAY | 1 << Calendar.SATURDAY;
int dayBits = ALL_DAYS;
/**
* Constructs a new periodic scheduler
*
* @param start
* the next possible start time
* @param period
* If positive: the period, if negative: the delay, if zero: not periodic
* @param timeUnit
* the timeUnit of start & period
*/
public PeriodicScheduler(final long delay, final long period, final TimeUnit timeUnit) {
super();
this.nextExecTime.setTimeInMillis(System.currentTimeMillis() + timeUnit.toMillis(delay));
this.periodSecond = (int) timeUnit.toSeconds(period);
}
/**
* Constructs a new periodic scheduler
*
*/
public PeriodicScheduler(String defString) {
int sign;
if (defString.startsWith("period:")) {
defString = defString.substring(7);
sign = +1;
} else if (defString.startsWith("delay:")) {
defString = defString.substring(6);
sign = -1;
} else {
throw new NumberFormatException("Invalid Time Definition String: " + defString);
}
StringTokenizer strTok = new StringTokenizer(defString);
if (!strTok.hasMoreTokens()) { // delay value
throw new NumberFormatException("Invalid Time Definition String: " + defString);
}
this.nextExecTime.setTimeInMillis(System.currentTimeMillis());
this.periodSecond = parseToSeconds(strTok.nextToken()) * sign;
addJitter(1.0);
if (strTok.hasMoreTokens()) { // time window
String[] parts = StringUtil.splitString(strTok.nextToken(), '-');
if (parts.length != 2)
throw new NumberFormatException("Invalid Time Definition String: " + defString);
this.startSecond = parseToSeconds(parts[0]);
this.endSecond = parseToSeconds(parts[1]);
if (strTok.hasMoreTokens()) { // days
setDays(strTok.nextToken());
}
}
shiftToNextTimeWindow();
}
private void addJitter(final double d) {
this.nextExecTime.add(Calendar.SECOND, (int) (Math.abs(this.periodSecond) * Math.random() * d)); // add some random jitter
}
/**
* Set the time window
*
* @param startSecond
* the start of the time window
* @param endSecond
* the end of the time window
* @param days
* the days {@link Calendar#SUNDAY} - {@link Calendar#SATURDAY}
*/
public void setTimeWindow(final int startSecond, final int endSecond, final int[] days) {
this.startSecond = startSecond;
this.endSecond = endSecond;
this.dayBits = 0;
for (int day : days) {
this.dayBits |= 1 << day;
}
if (dayBits < (1 << Calendar.SUNDAY))
throw new IllegalStateException("No valid day bits set");
}
/**
* Parses a time string to seconds
*
* @param str
* 13:37 => 13*3600 + 37*60
* @return
*/
private int parseToSeconds(final String str) {
int parts[] = new int[3];
if (str.indexOf(':') != -1) {
String[] strParts = StringUtil.splitString(str, ':');
if (strParts.length == 3) { // 00:00:00
parts[0] = Integer.valueOf(strParts[0]); // hour
parts[1] = Integer.valueOf(strParts[1]); // minute
parts[2] = Integer.valueOf(strParts[2]); // seconds
} else if (strParts.length == 2) {
parts[0] = Integer.valueOf(strParts[0]); // hour
parts[1] = Integer.valueOf(strParts[1]); // minute
parts[2] = 0; // seconds
} else {
throw new NumberFormatException(str + " is not in the format 00:00 or 00:00:00");
}
} else {
// the string is in the format 90m30s or 2h30
int tmp = 0;
int idx = 0; // Hour
for (int i = 0; i < str.length(); i++) {
char ch = str.charAt(i);
if ('0' <= ch & ch <= '9') { // numeric
tmp = tmp * 10 + (ch - '0');
parts[idx] = tmp; // save the computed value at current parts position
} else if (ch == 'h' & idx == 0) { // h is only allowed on first position
tmp = 0;
idx = 1;
} else if (ch == 'm' & idx < 1) {
tmp = 0;
if (idx == 0) { // no hour specified
parts[1] = parts[0];
parts[0] = 0;
}
idx = 1;
} else if (ch == 's' & idx < 2) {
tmp = 0;
if (idx == 0) { // no hour specified
parts[2] = parts[0];
parts[1] = 0;
parts[0] = 0;
} else if (idx == 1) { // no minute specified
parts[2] = parts[1];
parts[1] = 0;
}
idx = 2;
} else {
throw new NumberFormatException(str + " is not in the format 00, 00h 00h00m or 00h00m00s");
}
}
}
return parts[0] * 3600 + parts[1] * 60 + parts[2];
}
public void setDays(final String days) {
dayBits = 0;
for (int i = 0; i < days.length(); i++) {
char ch = days.charAt(i);
switch (ch) {
case 'M':
case 'm':
dayBits |= 1 << Calendar.MONDAY;
break;
case 'T':
case 't':
dayBits |= 1 << Calendar.TUESDAY;
break;
case 'W':
case 'w':
dayBits |= 1 << Calendar.WEDNESDAY;
break;
case 'R':
case 'r':
dayBits |= 1 << Calendar.THURSDAY;
break;
case 'F':
case 'f':
dayBits |= 1 << Calendar.FRIDAY;
break;
case 'S':
case 's':
dayBits |= 1 << Calendar.SATURDAY;
break;
case 'U':
case 'u':
dayBits |= 1 << Calendar.SUNDAY;
break;
}
}
if (dayBits < (1 << Calendar.SUNDAY))
throw new IllegalStateException("No valid day bits set");
}
@Override
public void eventStart(final Calendar now) {
if (periodSecond > 0) {
synchronized (nextExecTime) {
nextExecTime.add(Calendar.SECOND, periodSecond);
if (nextExecTime.before(now)) {
nextExecTime.setTimeInMillis(now.getTimeInMillis());
addJitter(0.1);
log_.info("Misfire detected, setting next execution time to: " + nextExecTime);
}
shiftToNextTimeWindow();
}
}
}
@Override
public void eventStop(final Calendar now) {
if (periodSecond < 0) {
synchronized (nextExecTime) {
nextExecTime.setTimeInMillis(now.getTimeInMillis());
nextExecTime.add(Calendar.SECOND, -periodSecond);
shiftToNextTimeWindow();
}
}
}
protected void shiftToNextTimeWindow() {
if (startSecond == 0 && endSecond == 24 * 60 * 60 && dayBits == ALL_DAYS) {
return; // nothing to do
}
//int jitter = (int) TimeUnit.SECONDS.convert((long) (this.period * Math.random()), TimeUnit.NANOSECONDS);
int secondOfTheDay = nextExecTime.get(Calendar.HOUR_OF_DAY) * 3600 + nextExecTime.get(Calendar.MINUTE) * 60
+ nextExecTime.get(Calendar.SECOND);
if ((dayBits & (1 << nextExecTime.get(Calendar.DAY_OF_WEEK))) == 0 || secondOfTheDay > startSecond) {
// not allowed for this day, so goto next day
nextExecTime.set(Calendar.HOUR_OF_DAY, 0);
nextExecTime.set(Calendar.MINUTE, 0);
nextExecTime.set(Calendar.MILLISECOND, 0);
nextExecTime.set(Calendar.SECOND, startSecond); // somewhere on next start window
addJitter(0.5);
// and now, find next day
int i = 0;
do {
nextExecTime.add(Calendar.DAY_OF_MONTH, 1);
if (i++ > 7) {
throw new IllegalStateException("Could not determine next execution time for the next 7 days");
}
} while ((dayBits & (1 << nextExecTime.get(Calendar.DAY_OF_WEEK))) == 0);
} else if (secondOfTheDay < startSecond) {
// before allowed execution (at this day)
nextExecTime.set(Calendar.SECOND, startSecond);
addJitter(0.5);
}
}
@Override
public long getNextExecutionTimeInMillis() {
return nextExecTime.getTimeInMillis();
}
@Override
public boolean isPeriodic() {
return periodSecond != 0L;
}
private String getDayString() {
if (dayBits == ALL_DAYS)
return "every day";
String[] namesOfDays = new String[] { "Spare-day", "SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT" };
StringBuilder sb = new StringBuilder();
for (int i = 1; i <= 7; i++) {
if ((dayBits & (1 << i)) != 0) {
if (sb.length() > 0)
sb.append(',');
sb.append(namesOfDays[i]);
}
}
return sb.toString();
}
private String getTime(final int seconds) {
int hr = seconds / 3600;
int rem = seconds % 3600;
int mn = rem / 60;
int sec = rem % 60;
StringBuilder sb = new StringBuilder();
if (hr < 10)
sb.append('0');
sb.append(hr);
sb.append(':');
if (mn < 10)
sb.append('0');
sb.append(mn);
sb.append(':');
if (sec < 10)
sb.append('0');
sb.append(sec);
return sb.toString();
}
private String getTimeWindow() {
if (startSecond == 0 && endSecond == 24 * 60 * 60)
return "all time";
return getTime(startSecond) + "-" + getTime(endSecond);
}
@Override
public String toString() {
return "Next Exec: " + nextExecTime.getTime() + ", period: " + getTime(periodSecond) + ", " + getTimeWindow() + " "
+ getDayString();
}
}