/**************************************************************************************
* Copyright (C) 2008 EsperTech, Inc. All rights reserved. *
* http://esper.codehaus.org *
* 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.type.ScheduleUnit;
import com.espertech.esper.util.ExecutionPathDebugLog;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import java.util.Calendar;
import java.util.Date;
import java.util.SortedSet;
/**
* 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
{
/**
* Minimum time to next occurance.
*/
private static final int MIN_OFFSET_MSEC = 1000;
/**
* 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
* @return a long date millisecond value for the next schedule occurance matching the spec
*/
public static long computeNextOccurance(ScheduleSpec spec, long afterTimeInMillis)
{
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 += MIN_OFFSET_MSEC;
}
else
{
afterTimeInMillis += 60 * MIN_OFFSET_MSEC;
}
return compute(spec, afterTimeInMillis);
}
/**
* 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
* @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)
{
return computeNextOccurance(spec, afterTimeInMillis) - afterTimeInMillis;
}
private static long compute(ScheduleSpec spec, long afterTimeInMillis)
{
while (true)
{
Calendar after = Calendar.getInstance();
after.setTimeInMillis(afterTimeInMillis);
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(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(result.getDayOfMonth(), result.getMonth() - 1, year)))
{
afterTimeInMillis = after.getTimeInMillis();
continue;
}
return getTime(result, after.get(Calendar.YEAR));
}
}
/*
* 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 (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);
}
}
// If days of weeks is not a wildcard and days of month is a wildcard, go by days of week only
else if (daysOfMonthSet == null)
{
// 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;
}
}
}
// Both days of weeks and days of month are not a wildcard
else
{
// 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)
{
Calendar calendar = Calendar.getInstance();
calendar.set(year, result.getMonth() - 1, result.getDayOfMonth(), result.getHour(), result.getMinute(), result.getSecond());
calendar.set(Calendar.MILLISECOND, result.getMilliseconds());
return calendar.getTimeInMillis();
}
/*
* Check if this is a valid date.
*/
private static boolean checkDayValidInMonth(int day, int month, int year)
{
try
{
Calendar calendar = Calendar.getInstance();
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 final Log log = LogFactory.getLog(ScheduleComputeHelper.class);
}