/*
* Copyright 2001-2008 Geert Bevin <gbevin[remove] at uwyn dot com>
* Licensed under the Apache License, Version 2.0 (the "License")
* $Id: Frequency.java 3957 2008-05-26 07:57:51Z gbevin $
*/
package com.uwyn.rife.scheduler;
import com.uwyn.rife.config.RifeConfig;
import com.uwyn.rife.scheduler.exceptions.FrequencyException;
import com.uwyn.rife.tools.Localization;
import com.uwyn.rife.tools.StringUtils;
import java.util.Arrays;
import java.util.Calendar;
import java.util.List;
class Frequency
{
final static private int MAX_YEAR = 2050;
final static private byte[] ALL_MINUTES = new byte[] {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59};
final static private byte[] ALL_HOURS = new byte[] {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23};
final static private byte[] ALL_DATES = new byte[] {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31};
final static private byte[] ALL_MONTHS = new byte[] {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12};
final static private byte[] ALL_WEEKDAYS = new byte[] {1, 2, 3, 4, 5, 6, 7};
final static private byte[] EMPTY_DATE_OVERFLOW = new byte[] {-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1};
private String mFrequency = null;
private byte[] mMinutes = null;
private byte[] mHours = null;
private byte[] mDates = null;
private byte[] mDatesUnderflow = null;
private byte[] mDatesOverflow = null;
private byte[] mMonths = null;
private byte[] mWeekdays = null;
private boolean mParsed = false;
Frequency(String frequency)
throws FrequencyException
{
parse(frequency);
}
long getNextDate(long start)
throws FrequencyException
{
if (start < 0) throw new IllegalArgumentException("start should be positive");
Calendar calendar = Calendar.getInstance(RifeConfig.Tools.getDefaultTimeZone(), Localization.getLocale());
calendar.setTimeInMillis(start);
int minute = calendar.get(Calendar.MINUTE);
int hour = calendar.get(Calendar.HOUR_OF_DAY);
int date = calendar.get(Calendar.DATE);
int month = calendar.get(Calendar.MONTH)+1;
int year = calendar.get(Calendar.YEAR);
// got to next valid time
minute++;
if (-1 == (minute = getNextValidMinute(minute)) ||
-1 == mHours[hour] ||
-1 == mMonths[month-1] ||
-1 == getDates(month, year)[date-1])
{
hour++;
if (-1 == (hour = getNextValidHour(hour)) ||
-1 == mMonths[month-1] ||
-1 == getDates(month, year)[date-1])
{
date++;
hour = getFirstValidHour();
}
minute = getFirstValidMinute();
}
// got to next valid date
while (year < MAX_YEAR)
{
if (-1 == (date = getNextValidDate(date, month, year)) ||
-1 == mMonths[month-1])
{
month++;
if (-1 == (month = getNextValidMonth(month)))
{
year++;
month = getFirstValidMonth();
}
date = getFirstValidDate(month, year);
if (-1 == date)
{
date = 1;
continue;
}
}
calendar.set(year, month-1, date, hour, minute);
if (year == calendar.get(Calendar.YEAR) &&
month == calendar.get(Calendar.MONTH)+1)
{
int weekday = calendar.get(Calendar.DAY_OF_WEEK)-2;
if (-1 == weekday)
{
weekday = 6;
}
if (mWeekdays[weekday] != -1)
{
return calendar.getTimeInMillis();
}
}
date++;
}
throw new FrequencyException("no valid next date available");
}
private int getFirstValidMinute()
{
return getNextValidMinute(0);
}
private int getNextValidMinute(int minute)
{
assert minute >= 0;
for (int i = minute; i < mMinutes.length; i++)
{
if (mMinutes[i] != -1)
{
return mMinutes[i];
}
}
return -1;
}
private int getFirstValidHour()
{
return getNextValidHour(0);
}
private int getNextValidHour(int hour)
{
assert hour >= 0;
for (int i = hour; i < mHours.length; i++)
{
if (mHours[i] != -1)
{
return mHours[i];
}
}
return -1;
}
private byte[] getDates(int month, int year)
{
assert month >= 1;
assert year >= 0;
Calendar calendar = Calendar.getInstance(RifeConfig.Tools.getDefaultTimeZone(), Localization.getLocale());
calendar.set(year, month-1, 1);
byte maximum_date = (byte)calendar.getActualMaximum(Calendar.DATE);
byte[] dates = null;
// only retain the dates that are valid for this month
dates = new byte[ALL_DATES.length];
Arrays.fill(dates, (byte)-1);
System.arraycopy(mDates, 0, dates, 0, maximum_date);
if (mDatesUnderflow != null &&
mDatesOverflow != null)
{
// get the maximum date of the previous month
calendar.roll(Calendar.MONTH, -1);
byte maximum_date_previous = (byte)calendar.getActualMaximum(Calendar.DATE);
// integrate overflowed dates
byte end_value = ALL_DATES[ALL_DATES.length-1];
byte difference = (byte)(end_value-maximum_date_previous);
int start_position = ALL_DATES.length-1;
int target_position = 0;
for (int i = start_position; i >= 0; i--)
{
if (mDatesUnderflow[i] != 0)
{
// handle the possibility where due to the difference,
// the underflow turns into an overflow
if (i > maximum_date_previous-1)
{
target_position = i-maximum_date_previous;
if (target_position < mDatesUnderflow[i] &&
target_position < maximum_date)
{
dates[target_position] = ALL_DATES[target_position];
}
}
}
if (mDatesOverflow[i] != 0)
{
// handle the overflow of the end of the previous month
target_position = i+difference;
if (target_position < mDatesOverflow[i] &&
target_position < maximum_date)
{
dates[target_position] = ALL_DATES[target_position];
}
}
}
}
return dates;
}
private int getFirstValidDate(int month, int year)
{
return getNextValidDate(1, month, year);
}
private int getNextValidDate(int date, int month, int year)
{
assert date >= 1;
assert month >= 1;
assert year >= 0;
byte[] dates = getDates(month, year);
for (int i = date-1; i < dates.length; i++)
{
if (dates[i] != -1)
{
return dates[i];
}
}
return -1;
}
private int getFirstValidMonth()
{
return getNextValidMonth(1);
}
private int getNextValidMonth(int month)
{
assert month >= 1;
for (int i = month-1; i < mMonths.length; i++)
{
if (mMonths[i] != -1)
{
return mMonths[i];
}
}
return -1;
}
boolean isParsed()
{
return mParsed;
}
String getFrequency()
{
return mFrequency;
}
byte[] getMinutes()
{
return mMinutes;
}
byte[] getHours()
{
return mHours;
}
byte[] getDates()
{
return mDates;
}
byte[] getDatesUnderflow()
{
return mDatesUnderflow;
}
byte[] getDatesOverflow()
{
return mDatesOverflow;
}
byte[] getMonths()
{
return mMonths;
}
byte[] getWeekdays()
{
return mWeekdays;
}
void parse(String frequency)
throws FrequencyException
{
if (null == frequency) throw new IllegalArgumentException("frequency can't be null");
if (0 == frequency.length()) throw new IllegalArgumentException("frequency can't be empty");
mFrequency = frequency;
mParsed = false;
mMinutes = null;
mHours = null;
mDates = null;
mDatesUnderflow = new byte[ALL_DATES.length];
mDatesOverflow = new byte[ALL_DATES.length];
mMonths = null;
mWeekdays = null;
List<String> frequency_parts = StringUtils.split(frequency, " ");
if (frequency_parts.size() != 5)
{
throw new FrequencyException("invalid frequency, should be 5 fields seperated by a space");
}
String minutes = frequency_parts.get(0);
String hours = frequency_parts.get(1);
String dates = frequency_parts.get(2);
String months = frequency_parts.get(3);
String weekdays = frequency_parts.get(4);
mMinutes = processParts(StringUtils.split(minutes, ","), ALL_MINUTES, false, null, null);
mHours = processParts(StringUtils.split(hours, ","), ALL_HOURS, false, null, null);
mDates = processParts(StringUtils.split(dates, ","), ALL_DATES, true, mDatesUnderflow, mDatesOverflow);
if (Arrays.equals(mDatesUnderflow, EMPTY_DATE_OVERFLOW))
{
mDatesUnderflow = null;
}
if (Arrays.equals(mDatesOverflow, EMPTY_DATE_OVERFLOW))
{
mDatesOverflow = null;
}
mMonths = processParts(StringUtils.split(months, ","), ALL_MONTHS, false, null, null);
mWeekdays = processParts(StringUtils.split(weekdays, ","), ALL_WEEKDAYS, false, null, null);
mParsed = true;
}
private byte[] processParts(List<String> parts, byte[] allValues, boolean deferOverflowProcessing, byte[] underflowStorage, byte[] overflowStorage)
throws FrequencyException
{
assert parts != null;
assert parts.size() > 0;
assert allValues != null;
assert allValues.length > 0;
assert !deferOverflowProcessing || (deferOverflowProcessing && underflowStorage != null && overflowStorage != null);
String part = null;
byte[] result_values = null;
// initialize the values to -1, the frequency syntax
// will enable the specified array positions by copying them
// from the reference array
result_values = new byte[allValues.length];
Arrays.fill(result_values, (byte)-1);
if (underflowStorage != null)
{
Arrays.fill(underflowStorage, (byte)-1);
}
if (overflowStorage != null)
{
Arrays.fill(overflowStorage, (byte)-1);
}
byte begin = allValues[0];
byte end = allValues[allValues.length-1];
for (String current_part : parts)
{
part = current_part;
// plain wildcard
if (current_part.equals("*"))
{
result_values = allValues;
return result_values;
}
try
{
int seperator = -1;
byte divider = -1;
// divider
if ((seperator = current_part.indexOf("/")) != -1)
{
divider = Byte.parseByte(current_part.substring(seperator+1));
current_part = current_part.substring(0, seperator);
}
// wildcard
if (current_part.equals("*"))
{
if (-1 == divider)
{
throw new FrequencyException("invalid frequency part '"+part+"'");
}
for (byte i = 0; i < allValues.length; i += divider)
{
result_values[i] = allValues[i];
}
continue;
}
// range
else if ((seperator = current_part.indexOf("-")) != -1)
{
byte left = Byte.parseByte(current_part.substring(0, seperator));
byte right = Byte.parseByte(current_part.substring(seperator+1));
if (left < begin ||
left > end)
{
throw new FrequencyException("value out of range '"+left+"'");
}
if (right < begin ||
right > end)
{
throw new FrequencyException("value out of range '"+right+"'");
}
if (left == right)
{
if (divider != -1)
{
throw new FrequencyException("invalid frequency part '"+part+"'");
}
result_values[left-begin] = allValues[left-begin];
continue;
}
if (-1 == divider)
{
divider = 1;
}
if (right < left)
{
if (deferOverflowProcessing)
{
// the overflow processing should be done later
// store the underflow both in the regular fashion and
// preserve it seperately for later underflow processing
// since it might bleed into overflow
while (left <= end)
{
result_values[left-begin] = allValues[left-begin];
// don't store the actual values after the overflow breakpoint
// but store the value of the rightmost
// limit of the corresponding range
if (underflowStorage[left-begin] < right)
{
underflowStorage[left-begin] = right;
}
left += divider;
}
left = (byte)(begin+(left-end)-1);
// store the positions at which entries are located
// the positions contain the value of the rightmost
// limit of the corresponding range
// this is needed to be able to calculate the correct
// transformations later
while (left <= right)
{
// preserve a later right limit
if (overflowStorage[left-begin] < right)
{
overflowStorage[left-begin] = right;
}
left += divider;
}
continue;
}
else
{
while (left <= end)
{
result_values[left-begin] = allValues[left-begin];
left += divider;
}
left = (byte)(begin+(left-end)-1);
}
}
while (left <= right)
{
result_values[left-begin] = allValues[left-begin];
left += divider;
}
continue;
}
// one number
else
{
if (divider != -1)
{
throw new FrequencyException("invalid frequency part '"+part+"'");
}
byte minute = Byte.parseByte(current_part);
if (minute < begin ||
minute > end)
{
throw new FrequencyException("value out of range '"+minute+"'");
}
result_values[minute-begin] = allValues[minute-begin];
}
}
catch (NumberFormatException e)
{
throw new FrequencyException("invalid frequency part '"+part+"'", e);
}
}
return result_values;
}
}