/*
***************************************************************************************
* Copyright (C) 2006 EsperTech, Inc. All rights reserved. *
* http://www.espertech.com/esper *
* http://www.espertech.com *
* ---------------------------------------------------------------------------------- *
* The software in this package is published under the terms of the GPL license *
* a copy of which has been included with this distribution in the license.txt file. *
***************************************************************************************
*/
package com.espertech.esper.schedule;
import com.espertech.esper.epl.expression.time.TimeAbacus;
import com.espertech.esper.type.CronOperatorEnum;
import com.espertech.esper.type.CronParameter;
import com.espertech.esper.type.ScheduleUnit;
import com.espertech.esper.util.ExecutionPathDebugLog;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Calendar;
import java.util.Date;
import java.util.SortedSet;
import java.util.TimeZone;
/**
* For a crontab-like schedule, this class computes the next occurance given a start time and a specification of
* what the schedule looks like.
* The resolution at which this works is at the second level. The next occurance
* is always at least 1 second ahead.
* The class implements an algorithm that starts at the highest precision (seconds) and
* continues to the lowest precicion (month). For each precision level the
* algorithm looks at the list of valid values and finds a value for each that is equal to or greater then
* the valid values supplied. If no equal or
* greater value was supplied, it will reset all higher precision elements to its minimum value.
*/
public final class ScheduleComputeHelper {
private static final Logger log = LoggerFactory.getLogger(ScheduleComputeHelper.class);
private final static int[] DAY_OF_WEEK_ARRAY = new int[]{Calendar.SUNDAY,
Calendar.MONDAY, Calendar.TUESDAY, Calendar.WEDNESDAY, Calendar.THURSDAY,
Calendar.FRIDAY, Calendar.SATURDAY};
/**
* Computes the next lowest date in milliseconds based on a specification and the
* from-time passed in.
*
* @param spec defines the schedule
* @param afterTimeInMillis defines the start time
* @param timeZone time zone
* @param timeAbacus time abacus
* @return a long date millisecond value for the next schedule occurance matching the spec
*/
public static long computeNextOccurance(ScheduleSpec spec, long afterTimeInMillis, TimeZone timeZone, TimeAbacus timeAbacus) {
if ((ExecutionPathDebugLog.isDebugEnabled) && (log.isDebugEnabled())) {
log.debug(".computeNextOccurance Computing next occurance, afterTimeInMillis=" + (new Date(afterTimeInMillis)) +
" as long=" + afterTimeInMillis +
" spec=" + spec);
}
// Add the minimum resolution to the start time to ensure we don't get the same exact time
if (spec.getUnitValues().containsKey(ScheduleUnit.SECONDS)) {
afterTimeInMillis += timeAbacus.getOneSecond();
} else {
afterTimeInMillis += 60 * timeAbacus.getOneSecond();
}
return compute(spec, afterTimeInMillis, timeZone, timeAbacus);
}
/**
* Computes the next lowest date in milliseconds based on a specification and the
* from-time passed in and returns the delta from the current time.
*
* @param spec defines the schedule
* @param afterTimeInMillis defines the start time
* @param timeZone time zone
* @param timeAbacus time abacus
* @return a long millisecond value representing the delta between current time and the next schedule occurance matching the spec
*/
public static long computeDeltaNextOccurance(ScheduleSpec spec, long afterTimeInMillis, TimeZone timeZone, TimeAbacus timeAbacus) {
return computeNextOccurance(spec, afterTimeInMillis, timeZone, timeAbacus) - afterTimeInMillis;
}
private static long compute(ScheduleSpec spec, long afterTimeInMillis, TimeZone timeZone, TimeAbacus timeAbacus) {
long remainderMicros = -1;
while (true) {
Calendar after;
if (spec.getOptionalTimeZone() != null) {
after = Calendar.getInstance(TimeZone.getTimeZone(spec.getOptionalTimeZone()));
} else {
after = Calendar.getInstance(timeZone);
}
long remainder = timeAbacus.calendarSet(afterTimeInMillis, after);
if (remainderMicros == -1) {
remainderMicros = remainder;
}
ScheduleCalendar result = new ScheduleCalendar();
result.setMilliseconds(after.get(Calendar.MILLISECOND));
SortedSet<Integer> minutesSet = spec.getUnitValues().get(ScheduleUnit.MINUTES);
SortedSet<Integer> hoursSet = spec.getUnitValues().get(ScheduleUnit.HOURS);
SortedSet<Integer> monthsSet = spec.getUnitValues().get(ScheduleUnit.MONTHS);
SortedSet<Integer> secondsSet = null;
boolean isSecondsSpecified = false;
if (spec.getUnitValues().containsKey(ScheduleUnit.SECONDS)) {
isSecondsSpecified = true;
secondsSet = spec.getUnitValues().get(ScheduleUnit.SECONDS);
}
if (isSecondsSpecified) {
result.setSecond(nextValue(secondsSet, after.get(Calendar.SECOND)));
if (result.getSecond() == -1) {
result.setSecond(nextValue(secondsSet, 0));
after.add(Calendar.MINUTE, 1);
}
}
result.setMinute(nextValue(minutesSet, after.get(Calendar.MINUTE)));
if (result.getMinute() != after.get(Calendar.MINUTE)) {
result.setSecond(nextValue(secondsSet, 0));
}
if (result.getMinute() == -1) {
result.setMinute(nextValue(minutesSet, 0));
after.add(Calendar.HOUR_OF_DAY, 1);
}
result.setHour(nextValue(hoursSet, after.get(Calendar.HOUR_OF_DAY)));
if (result.getHour() != after.get(Calendar.HOUR_OF_DAY)) {
result.setSecond(nextValue(secondsSet, 0));
result.setMinute(nextValue(minutesSet, 0));
}
if (result.getHour() == -1) {
result.setHour(nextValue(hoursSet, 0));
after.add(Calendar.DAY_OF_MONTH, 1);
}
// This call may change second, minute and/or hour parameters
// They may be reset to minimum values if the day rolled
result.setDayOfMonth(determineDayOfMonth(spec, after, result));
boolean dayMatchRealDate = false;
while (!dayMatchRealDate) {
if (checkDayValidInMonth(timeZone, result.getDayOfMonth(), after.get(Calendar.MONTH), after.get(Calendar.YEAR))) {
dayMatchRealDate = true;
} else {
after.add(Calendar.MONTH, 1);
}
}
int currentMonth = after.get(Calendar.MONTH) + 1;
result.setMonth(nextValue(monthsSet, currentMonth));
if (result.getMonth() != currentMonth) {
result.setSecond(nextValue(secondsSet, 0));
result.setMinute(nextValue(minutesSet, 0));
result.setHour(nextValue(hoursSet, 0));
result.setDayOfMonth(determineDayOfMonth(spec, after, result));
}
if (result.getMonth() == -1) {
result.setMonth(nextValue(monthsSet, 0));
after.add(Calendar.YEAR, 1);
}
// Perform a last valid date check, if failing, try to compute a new date based on this altered after date
int year = after.get(Calendar.YEAR);
if (!(checkDayValidInMonth(timeZone, result.getDayOfMonth(), result.getMonth() - 1, year))) {
afterTimeInMillis = timeAbacus.calendarGet(after, remainder);
continue;
}
return getTime(result, after.get(Calendar.YEAR), spec.getOptionalTimeZone(), timeZone, timeAbacus, remainder);
}
}
/*
* Determine the next valid day of month based on the given specification of valid days in month and
* valid days in week. If both days in week and days in month are supplied, the days are OR-ed.
*/
private static int determineDayOfMonth(ScheduleSpec spec,
Calendar after,
ScheduleCalendar result) {
SortedSet<Integer> daysOfMonthSet = spec.getUnitValues().get(ScheduleUnit.DAYS_OF_MONTH);
SortedSet<Integer> daysOfWeekSet = spec.getUnitValues().get(ScheduleUnit.DAYS_OF_WEEK);
SortedSet<Integer> secondsSet = spec.getUnitValues().get(ScheduleUnit.SECONDS);
SortedSet<Integer> minutesSet = spec.getUnitValues().get(ScheduleUnit.MINUTES);
SortedSet<Integer> hoursSet = spec.getUnitValues().get(ScheduleUnit.HOURS);
int dayOfMonth;
// If days of week is a wildcard, just go by days of month
if (spec.getOptionalDayOfMonthOperator() != null || spec.getOptionalDayOfWeekOperator() != null) {
boolean isWeek = false;
CronParameter op = spec.getOptionalDayOfMonthOperator();
if (spec.getOptionalDayOfMonthOperator() == null) {
op = spec.getOptionalDayOfWeekOperator();
isWeek = true;
}
// may return the current day or a future day in the same month,
// and may advance the "after" date to the next month
int currentYYMMDD = getTimeYYYYMMDD(after);
increaseAfterDayOfMonthSpecialOp(op.getOperator(), op.getDay(), op.getMonth(), isWeek, after);
int rolledYYMMDD = getTimeYYYYMMDD(after);
// if rolled then reset time portion
if (rolledYYMMDD > currentYYMMDD) {
result.setSecond(nextValue(secondsSet, 0));
result.setMinute(nextValue(minutesSet, 0));
result.setHour(nextValue(hoursSet, 0));
return after.get(Calendar.DAY_OF_MONTH);
} else if (rolledYYMMDD < currentYYMMDD) {
// rolling backwards is not allowed
throw new IllegalStateException("Failed to evaluate special date op, rolled date less then current date");
} else {
Calendar work = (Calendar) after.clone();
work.set(Calendar.SECOND, result.getSecond());
work.set(Calendar.MINUTE, result.getMinute());
work.set(Calendar.HOUR_OF_DAY, result.getHour());
if (!work.after(after)) { // new date is not after current date, so bump
after.add(Calendar.DAY_OF_MONTH, 1);
result.setSecond(nextValue(secondsSet, 0));
result.setMinute(nextValue(minutesSet, 0));
result.setHour(nextValue(hoursSet, 0));
increaseAfterDayOfMonthSpecialOp(op.getOperator(), op.getDay(), op.getMonth(), isWeek, after);
}
return after.get(Calendar.DAY_OF_MONTH);
}
} else if (daysOfWeekSet == null) {
dayOfMonth = nextValue(daysOfMonthSet, after.get(Calendar.DAY_OF_MONTH));
if (dayOfMonth != after.get(Calendar.DAY_OF_MONTH)) {
result.setSecond(nextValue(secondsSet, 0));
result.setMinute(nextValue(minutesSet, 0));
result.setHour(nextValue(hoursSet, 0));
}
if (dayOfMonth == -1) {
dayOfMonth = nextValue(daysOfMonthSet, 0);
after.add(Calendar.MONTH, 1);
}
} else if (daysOfMonthSet == null) {
// If days of weeks is not a wildcard and days of month is a wildcard, go by days of week only
// Loop to find the next day of month that works for the specified day of week values
while (true) {
dayOfMonth = after.get(Calendar.DAY_OF_MONTH);
int dayOfWeek = after.get(Calendar.DAY_OF_WEEK) - 1;
// If the day matches neither the day of month nor the day of week
if (!daysOfWeekSet.contains(dayOfWeek)) {
result.setSecond(nextValue(secondsSet, 0));
result.setMinute(nextValue(minutesSet, 0));
result.setHour(nextValue(hoursSet, 0));
after.add(Calendar.DAY_OF_MONTH, 1);
} else {
break;
}
}
} else {
// Both days of weeks and days of month are not a wildcard
// Loop to find the next day of month that works for either day of month OR day of week
while (true) {
dayOfMonth = after.get(Calendar.DAY_OF_MONTH);
int dayOfWeek = after.get(Calendar.DAY_OF_WEEK) - 1;
// If the day matches neither the day of month nor the day of week
if ((!daysOfWeekSet.contains(dayOfWeek)) &&
(!daysOfMonthSet.contains(dayOfMonth))) {
result.setSecond(nextValue(secondsSet, 0));
result.setMinute(nextValue(minutesSet, 0));
result.setHour(nextValue(hoursSet, 0));
after.add(Calendar.DAY_OF_MONTH, 1);
} else {
break;
}
}
}
return dayOfMonth;
}
private static long getTime(ScheduleCalendar result, int year, String optionalTimeZone, TimeZone timeZone, TimeAbacus timeAbacus, long remainder) {
Calendar calendar;
if (optionalTimeZone != null) {
calendar = Calendar.getInstance(TimeZone.getTimeZone(optionalTimeZone));
} else {
calendar = Calendar.getInstance(timeZone);
}
calendar.set(year, result.getMonth() - 1, result.getDayOfMonth(), result.getHour(), result.getMinute(), result.getSecond());
calendar.set(Calendar.MILLISECOND, result.getMilliseconds());
return timeAbacus.calendarGet(calendar, remainder);
}
/*
* Check if this is a valid date.
*/
private static boolean checkDayValidInMonth(TimeZone timeZone, int day, int month, int year) {
try {
Calendar calendar = Calendar.getInstance(timeZone);
calendar.setLenient(false);
calendar.set(Calendar.YEAR, year);
calendar.set(Calendar.MONTH, month);
calendar.set(Calendar.DAY_OF_MONTH, day);
calendar.getTime();
} catch (IllegalArgumentException e) {
return false;
}
return true;
}
/*
* Determine if in the supplied valueSet there is a value after the given start value.
* Return -1 to indicate that there is no value after the given startValue.
* If the valueSet passed is null it is treated as a wildcard and the same startValue is returned
*/
private static int nextValue(SortedSet<Integer> valueSet, int startValue) {
if (valueSet == null) {
return startValue;
}
if (valueSet.contains(startValue)) {
return startValue;
}
SortedSet<Integer> tailSet = valueSet.tailSet(startValue + 1);
if (tailSet.isEmpty()) {
return -1;
} else {
return tailSet.first();
}
}
private static int getTimeYYYYMMDD(Calendar calendar) {
return 10000 * calendar.get(Calendar.YEAR) + (calendar.get(Calendar.MONTH) + 1) * 100 + calendar.get(Calendar.DAY_OF_MONTH);
}
private static void increaseAfterDayOfMonthSpecialOp(CronOperatorEnum operator, Integer day, Integer month, boolean week, Calendar after) {
DateChecker checker;
if (operator == CronOperatorEnum.LASTDAY) {
if (!week) {
checker = new DateCheckerLastDayOfMonth(day, month);
} else {
if (day == null) {
checker = new DateCheckerLastDayOfWeek(month);
} else {
checker = new DateCheckerLastSpecificDayWeek(day, month);
}
}
} else if (operator == CronOperatorEnum.LASTWEEKDAY) {
checker = new DateCheckerLastWeekday(day, month);
} else {
checker = new DateCheckerMonthWeekday(day, month);
}
int dayCount = 0;
while (!checker.fits(after)) {
after.add(Calendar.DAY_OF_MONTH, 1);
dayCount++;
if (dayCount > 10000) {
throw new IllegalArgumentException("Invalid crontab expression: failed to find match day");
}
}
}
private interface DateChecker {
public boolean fits(Calendar cal);
}
private static class DateCheckerLastSpecificDayWeek implements DateChecker {
private final int dayCode;
private final Integer month;
private DateCheckerLastSpecificDayWeek(int day, Integer month) {
if (day < 0 || day > 7) {
throw new IllegalArgumentException("Last xx day of the month has to be a day of week (0-7)");
}
dayCode = DAY_OF_WEEK_ARRAY[day];
this.month = month;
}
public boolean fits(Calendar cal) {
if (dayCode != cal.get(Calendar.DAY_OF_WEEK)) {
return false;
}
if (month != null && month != cal.get(Calendar.MONTH)) {
return false;
}
// e.g. 31=Sun,30=Sat,29=Fri,28=Thu,27=Wed,26=Tue,25=Mon
// e.g. 31-7 = 24
return cal.get(Calendar.DAY_OF_MONTH) > cal.getActualMaximum(Calendar.DAY_OF_MONTH) - 7;
}
}
private static class DateCheckerLastDayOfMonth implements DateChecker {
private final Integer dayCode;
private final Integer month;
private DateCheckerLastDayOfMonth(Integer day, Integer month) {
if (day != null) {
if (day < 0 || day > 7) {
throw new IllegalArgumentException("Last xx day of the month has to be a day of week (0-7)");
}
dayCode = DAY_OF_WEEK_ARRAY[day];
} else {
dayCode = null;
}
this.month = month;
}
public boolean fits(Calendar cal) {
if (dayCode != null && dayCode != cal.get(Calendar.DAY_OF_WEEK)) {
return false;
}
if (month != null && month != cal.get(Calendar.MONTH)) {
return false;
}
return cal.get(Calendar.DAY_OF_MONTH) == cal.getActualMaximum(Calendar.DAY_OF_MONTH);
}
}
private static class DateCheckerLastDayOfWeek implements DateChecker {
private final Integer month;
private DateCheckerLastDayOfWeek(Integer month) {
this.month = month;
}
public boolean fits(Calendar cal) {
if (month != null && month != cal.get(Calendar.MONTH)) {
return false;
}
return cal.get(Calendar.DAY_OF_WEEK) == Calendar.SATURDAY;
}
}
private static class DateCheckerLastWeekday implements DateChecker {
private final Integer dayCode;
private final Integer month;
private DateCheckerLastWeekday(Integer day, Integer month) {
if (day != null) {
if (day < 0 || day > 7) {
throw new IllegalArgumentException("Last xx day of the month has to be a day of week (0-7)");
}
dayCode = DAY_OF_WEEK_ARRAY[day];
} else {
dayCode = null;
}
this.month = month;
}
public boolean fits(Calendar cal) {
if (dayCode != null && dayCode != cal.get(Calendar.DAY_OF_WEEK)) {
return false;
}
if (month != null && month != cal.get(Calendar.MONTH)) {
return false;
}
if (!isWeekday(cal)) {
return false;
}
int day = cal.get(Calendar.DAY_OF_MONTH);
int max = cal.getActualMaximum(Calendar.DAY_OF_MONTH);
if (day == max) {
return true;
}
int dayOfWeek = cal.get(Calendar.DAY_OF_WEEK);
return day >= max - 2 && dayOfWeek == Calendar.FRIDAY;
}
}
public static class DateCheckerMonthWeekday implements DateChecker {
private final Integer day;
private final Integer month;
private DateCheckerMonthWeekday(Integer day, Integer month) {
if (day != null) {
if (day < 1 || day > 31) {
throw new IllegalArgumentException("xx day of the month has to be a in range (1-31)");
}
}
this.day = day;
this.month = month;
}
public boolean fits(Calendar cal) {
if (month != null && month != cal.get(Calendar.MONTH)) {
return false;
}
if (!isWeekday(cal)) {
return false;
}
if (day == null) {
return true;
}
Calendar work = (Calendar) cal.clone();
int target = computeNearestWeekdayDay(day, work);
return cal.get(Calendar.DAY_OF_MONTH) == target;
}
private static int computeNearestWeekdayDay(int day, Calendar work) {
int max = work.getActualMaximum(Calendar.DAY_OF_MONTH);
if (day <= max) {
work.set(Calendar.DAY_OF_MONTH, day);
} else {
work.set(Calendar.DAY_OF_MONTH, max);
}
if (isWeekday(work)) {
return work.get(Calendar.DAY_OF_MONTH);
}
if (work.get(Calendar.DAY_OF_WEEK) == Calendar.SATURDAY) {
if (work.get(Calendar.DAY_OF_MONTH) > 1) {
work.add(Calendar.DAY_OF_MONTH, -1);
return work.get(Calendar.DAY_OF_MONTH);
} else {
work.add(Calendar.DAY_OF_MONTH, 2);
return work.get(Calendar.DAY_OF_MONTH);
}
} else {
// handle Sunday
if (max == work.get(Calendar.DAY_OF_MONTH)) {
work.add(Calendar.DAY_OF_MONTH, -2);
return work.get(Calendar.DAY_OF_MONTH);
} else {
work.add(Calendar.DAY_OF_MONTH, 1);
return work.get(Calendar.DAY_OF_MONTH);
}
}
}
}
private static boolean isWeekday(Calendar cal) {
int dayOfWeek = cal.get(Calendar.DAY_OF_WEEK);
return !(dayOfWeek < Calendar.MONDAY || dayOfWeek > Calendar.FRIDAY);
}
}