/*
* ============================================================================
* GNU Lesser General Public License
* ============================================================================
*
* Beanlet - JSE Application Container.
* Copyright (C) 2006 Leon van Zantvoort
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA.
*
* Leon van Zantvoort
* 243 Acalanes Drive #11
* Sunnyvale, CA 94086
* USA
*
* zantvoort@users.sourceforge.net
* http://beanlet.org
*/
package org.beanlet.impl;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Date;
import java.util.EnumSet;
import java.util.List;
import java.util.ListIterator;
import java.util.Set;
import java.util.TimeZone;
import static org.beanlet.impl.CronExpression.CronCharacter.*;
import static org.beanlet.impl.CronExpression.CronField.*;
/**
* @author Leon van Zantvoort
*/
public final class CronExpression {
private final String expression;
private final String[] fields;
private final List<CronField> fieldList;
private final TimeZone tz;
public CronExpression(String expression) throws IllegalArgumentException {
this.expression = expression;
this.fields = expression.split(" ");
if (fields.length < 6 || fields.length > 8) {
throw new IllegalArgumentException("Cron expression MUST specify " +
"at least 6, but no more than 8 options.");
}
this.fieldList = new ArrayList<CronField>(Arrays.asList(CronField.values()));
if (fields.length < 7) {
fieldList.remove(0);
}
if (fields.length == 8) {
if (fields[7].equalsIgnoreCase(ALL.character)) {
this.tz = TimeZone.getDefault();
} else {
this.tz = TimeZone.getTimeZone(fields[7]);
}
} else {
this.tz = TimeZone.getDefault();
}
// Call to check expression.
nextFireTime();
}
/**
* Returns next fire time computed from now.
*/
public Date nextFireTime() {
return nextFireTime(new Date());
}
/**
* Returns next fire time computed from given date.
*/
public Date nextFireTime(Date fromDate) {
Calendar calendar = Calendar.getInstance(tz);
calendar.setTime(fromDate);
calendar.set(Calendar.MILLISECOND, 0);
calendar.add(Calendar.SECOND, 1);
CronField prev = null;
for (ListIterator<CronField> i = fieldList.listIterator();
i.hasNext();) {
CronField field = i.next();
Integer prevValue = null;
if (prev != null) {
prevValue = calendar.get(prev.calendar);
}
int added = setField(calendar, field);
Integer unitSize = field.getUnitSize(calendar);
if (unitSize == null && added < 0) {
return null;
}
if (added > 0) {
clear(calendar, field);
}
if (prevValue != null) {
if (!prevValue.equals(calendar.get(prev.calendar))) {
i = fieldList.listIterator();
prev = null;
continue;
}
}
prev = field;
}
return calendar.getTime();
}
private int setField(Calendar calendar, CronField field) {
String fieldValue = fields[field.index];
CronCharacter character = CronCharacter.parse(fieldValue);
if (character == null) {
return setNoneField(calendar, field);
} else {
if (!field.allowedCharacters.contains(character)) {
throw new IllegalArgumentException("Option '" + character.character + "' " +
"not supported for field " + field + ".");
}
switch (character) {
case ALL:
return setAllField(calendar, field);
case ANY:
return setAnyField(calendar, field);
case RANGE:
return setRangeField(calendar, field);
case ADDITIONAL:
return setAdditionalField(calendar, field);
case INCREMENT:
return setIncrementField(calendar, field);
case LAST:
return setLastField(calendar, field);
case WEEKDAY:
return setWeekdayField(calendar, field);
case NTH_DAY_OF_MONTH:
return setNthDayOfMonthField(calendar, field);
}
}
return 0;
}
private int setNoneField(Calendar calendar, CronField field) {
int current = calendar.get(field.calendar);
int value = field.getValue(fields[field.index]);
final int add;
Integer unitSize = field.getUnitSize(calendar);
if (unitSize == null) {
add = value - current;
} else {
add = (unitSize - current + value) % unitSize;
}
calendar.add(field.calendar, add);
return add;
}
private int setAllField(Calendar calendar, CronField field) {
// Do nothing.
return 0;
}
private int setAnyField(Calendar calendar, CronField field) {
// Do nothing.
return 0;
}
private int setRangeField(Calendar calendar, CronField field) {
String[] range = fields[field.index].split(RANGE.character);
if (range.length != 2) {
throw new IllegalArgumentException("Invalid " + field + " value: '" +
fields[field.index] + "'.");
}
int lowerBound = field.getValue(range[0]);
int upperBound = field.getValue(range[1]);
int current = calendar.get(field.calendar);
final int value;
if (current < lowerBound || current > upperBound) {
value = lowerBound;
} else {
value = current;
}
final int add;
Integer unitSize = field.getUnitSize(calendar);
if (unitSize == null) {
add = value - current;
} else {
add = (unitSize - current + value) % unitSize;
}
calendar.add(field.calendar, add);
return add;
}
private int setAdditionalField(Calendar calendar, CronField field) {
String[] tmp = fields[field.index].split(ADDITIONAL.character);
if (tmp.length == 0) {
throw new IllegalArgumentException("Invalid " + field + " value: '" +
fields[field.index] + "'.");
}
int[] values = new int[tmp.length];
for (int i = 0; i < values.length; i++) {
values[i] = field.getValue(tmp[i]);
}
Arrays.sort(values);
int current = calendar.get(field.calendar);
int value = values[0];
for (int i = 0; i < values.length; i++) {
if (values[i] >= current) {
value = values[i];
break;
}
}
final int add;
Integer unitSize = field.getUnitSize(calendar);
if (unitSize == null) {
add = value - current;
} else {
add = (unitSize - current + value) % unitSize;
}
calendar.add(field.calendar, add);
return add;
}
private int setIncrementField(Calendar calendar, CronField field) {
String[] tmp = fields[field.index].split(INCREMENT.character);
if (tmp.length != 2) {
throw new IllegalArgumentException("Invalid " + field + " value: '" +
fields[field.index] + "'.");
}
int initial = tmp[0].equals("*") ? 0 : field.getValue(tmp[0]);
int increment = field.getValue(tmp[1]);
int current = calendar.get(field.calendar);
final int add;
Integer unitSize = field.getUnitSize(calendar);
if (unitSize == null) {
int start = current - ((current % increment) - initial);
int value = start;
while (value < current) {
value += increment;
}
add = value - current;
} else {
int value = initial;
for (int i = initial; i <= unitSize; i+=increment) {
if (i >= current) {
value = i;
break;
}
}
add = (unitSize - current + value) % unitSize;
}
calendar.add(field.calendar, add);
return add;
}
private int setLastField(Calendar calendar, CronField field) {
String last = fields[field.index].toUpperCase();
int current = calendar.get(field.calendar);
int add = 0;
if (last.equalsIgnoreCase("LW")) {
if (field != DAY_OF_MONTH) {
throw new IllegalArgumentException("'LW' option only supported " +
"for Day-of-Month.");
}
int now = calendar.get(DAY_OF_MONTH.calendar);
int max = calendar.getActualMaximum(DAY_OF_MONTH.calendar);
for (int i = max ; i > 0; i++) {
calendar.set(DAY_OF_MONTH.calendar, i);
int day = calendar.get(DAY_OF_WEEK.calendar);
if (day >= Calendar.MONDAY || day <= Calendar.FRIDAY) {
break;
}
}
add = calendar.get(DAY_OF_MONTH.calendar) - now;
assert add >= 0;
} else if (!last.startsWith("L")) {
if (field != DAY_OF_WEEK) {
throw new IllegalArgumentException(
"'L' option only supported for Day-of-Week.");
}
int day = field.getValue(last.substring(0, last.length() - 1));
int now = calendar.get(DAY_OF_MONTH.calendar);
int value = ((7 + day - current) % 7);
add += value;
calendar.add(DAY_OF_MONTH.calendar, value);
while (calendar.getActualMaximum(DAY_OF_MONTH.calendar) - 7 >=
calendar.get(DAY_OF_MONTH.calendar)) {
add += 7;
calendar.add(DAY_OF_MONTH.calendar, 7);
}
assert add >= 0;
} else {
int value = calendar.getActualMaximum(field.calendar);
calendar.set(field.calendar, value);
add = value - current;
assert add >= 0;
}
return add;
}
private int setWeekdayField(Calendar calendar, CronField field) {
if (field != DAY_OF_MONTH) {
throw new IllegalArgumentException(
"'W' option only supported for Day-of-Month.");
}
String tmp = fields[field.index];
int current = calendar.get(field.calendar);
int max = calendar.getActualMaximum(DAY_OF_MONTH.calendar);
int weekday = Math.min(max,
field.getValue(tmp.substring(0, tmp.length() - 1)));
calendar.set(DAY_OF_MONTH.calendar, weekday);
int day = calendar.get(DAY_OF_WEEK.calendar);
final int value;
if (day >= Calendar.MONDAY || day <= Calendar.FRIDAY) {
value = weekday;
} else {
if (day == Calendar.SATURDAY) {
if (weekday == 1) {
value = 3;
} else {
value = weekday - 1;
}
} else if (day == Calendar.SUNDAY) {
if (weekday == max) {
value = weekday - 2;
} else {
value = weekday + 1;
}
} else {
assert false;
value = weekday;
}
calendar.set(DAY_OF_MONTH.calendar, value);
}
int add = value - current;
assert add >= 0;
return add;
}
private int setNthDayOfMonthField(Calendar calendar, CronField field) {
String[] tmp = fields[field.index].split(NTH_DAY_OF_MONTH.character);
if (tmp.length != 2) {
throw new IllegalArgumentException("Invalid " + field + " value: '" +
fields[field.index] + "'.");
}
int day = field.getValue(tmp[0]);
int nth = 0;
try {
nth = Integer.parseInt(tmp[1]);
} catch (NumberFormatException e) {
throw new IllegalArgumentException(
"Value after the '#' option must be an integer: '" +
tmp[1] + "''.");
}
if (nth < 1 || nth > 5) {
throw new IllegalArgumentException(
"Value after the '#' option must be between 1 and 5.");
}
int current = calendar.get(DAY_OF_MONTH.calendar);
int add = -current;
while (true) {
calendar.set(DAY_OF_MONTH.calendar, 1);
int max = calendar.getActualMaximum(DAY_OF_MONTH.calendar);
int firstDay = calendar.get(DAY_OF_WEEK.calendar);
int v = 1 + (7 + day - firstDay) % 7;
int i = 1;
while (i < nth && (v + 7) <= max) {
i++;
v += 7;
}
if (i == nth) {
add += v;
calendar.set(DAY_OF_MONTH.calendar, v);
if (add >= 0) {
break;
}
}
calendar.add(MONTH.calendar, 1);
add += max;
}
return add;
}
private void clear(Calendar calendar, CronField field) {
if (field == YEAR) {
calendar.set(MONTH.calendar, calendar.getActualMinimum(MONTH.calendar));
field = MONTH;
}
if (field == MONTH) {
calendar.set(DAY_OF_MONTH.calendar, calendar.getActualMinimum(DAY_OF_MONTH.calendar));
field = DAY_OF_MONTH;
}
if (field == DAY_OF_MONTH) {
field = DAY_OF_WEEK;
}
if (field == DAY_OF_WEEK) {
calendar.set(HOUR.calendar, calendar.getActualMinimum(HOUR.calendar));
field = HOUR;
}
if (field == HOUR) {
calendar.set(MINUTE.calendar, calendar.getActualMinimum(MINUTE.calendar));
field = MINUTE;
}
if (field == MINUTE) {
calendar.set(SECOND.calendar, calendar.getActualMinimum(SECOND.calendar));
}
}
enum CronField {
YEAR(6, Calendar.YEAR, EnumSet.<CronCharacter>of(
ALL, RANGE, ADDITIONAL, INCREMENT)),
MONTH(4, Calendar.MONTH, EnumSet.<CronCharacter>of(
ALL, RANGE, ADDITIONAL, INCREMENT)),
DAY_OF_WEEK(5, Calendar.DAY_OF_WEEK, EnumSet.<CronCharacter>of(
ALL, RANGE, ADDITIONAL, INCREMENT, ANY, LAST, NTH_DAY_OF_MONTH)),
DAY_OF_MONTH(3, Calendar.DAY_OF_MONTH, EnumSet.<CronCharacter>of(
ALL, RANGE, ADDITIONAL, INCREMENT, ANY, LAST, WEEKDAY)),
HOUR(2, Calendar.HOUR_OF_DAY, EnumSet.<CronCharacter>of(
ALL, RANGE, ADDITIONAL, INCREMENT)),
MINUTE(1, Calendar.MINUTE, EnumSet.<CronCharacter>of(
ALL, RANGE, ADDITIONAL, INCREMENT)),
SECOND(0, Calendar.SECOND, EnumSet.<CronCharacter>of(
ALL, RANGE, ADDITIONAL, INCREMENT));
private final int index;
private final int calendar;
private final Set<CronCharacter> allowedCharacters;
CronField(int index, int calendar,
Set<CronCharacter> allowedCharacters) {
this.index = index;
this.calendar = calendar;
this.allowedCharacters = allowedCharacters;
}
public int getValue(String str) {
final int value;
try {
switch (this) {
case YEAR:
value = Integer.parseInt(str);
if (value < 1970) {
throw new IllegalArgumentException(this +
" value < 1970.");
}
if (value > 2099) {
throw new IllegalArgumentException(this +
" value > 2099.");
}
break;
case MONTH:
if (Character.isDigit(str.charAt(0))) {
int tmp = Integer.parseInt(str);
if (tmp < 1) {
throw new IllegalArgumentException(this +
" value < 1.");
}
if (tmp > 12) {
throw new IllegalArgumentException(this +
" value > 12.");
}
value = tmp - 1;
} else {
value = CronMonth.valueOf(str.toUpperCase()).month;
}
break;
case DAY_OF_WEEK:
if (Character.isDigit(str.charAt(0))) {
value = Integer.parseInt(str);
} else {
value = CronDay.valueOf(str.toUpperCase()).day;
}
if (value < 1) {
throw new IllegalArgumentException(this +
" value < 1.");
}
if (value > 7) {
throw new IllegalArgumentException(this +
" value > 7.");
}
break;
case DAY_OF_MONTH:
value = Integer.parseInt(str);
if (value < 1) {
throw new IllegalArgumentException(this +
" value < 1.");
}
if (value > 31) {
throw new IllegalArgumentException(this +
" value > 31.");
}
break;
case HOUR:
value = Integer.parseInt(str);
if (value < 0) {
throw new IllegalArgumentException(this +
" value < 0.");
}
if (value > 23) {
throw new IllegalArgumentException(this +
" > 23.");
}
break;
case MINUTE:
value = Integer.parseInt(str);
if (value < 0) {
throw new IllegalArgumentException(this +
" value < 0.");
}
if (value > 59) {
throw new IllegalArgumentException(this +
" value > 59.");
}
break;
case SECOND:
value = Integer.parseInt(str);
if (value < 0) {
throw new IllegalArgumentException(this +
" value < 0.");
}
if (value > 59) {
throw new IllegalArgumentException(this +
" value > 59.");
}
break;
default:
value = Integer.parseInt(str);
}
return value;
} catch (NumberFormatException e) {
throw new IllegalArgumentException(this +
" value not an integer: '" + str + "'.");
}
}
public Integer getUnitSize(Calendar c) {
switch (this) {
case YEAR:
return null;
case MONTH:
return 12;
case DAY_OF_WEEK:
return 7;
case DAY_OF_MONTH:
return c.getActualMaximum(calendar);
case HOUR:
return 24;
case MINUTE:
return 60;
case SECOND:
return 60;
default:
return null;
}
}
}
enum CronCharacter {
RANGE("-"),
ADDITIONAL(","),
INCREMENT("/"),
NTH_DAY_OF_MONTH("#"),
LAST("L"),
WEEKDAY("W"),
ALL("*"),
ANY("?");
private String character;
CronCharacter(String character) {
this.character = character;
}
public static CronCharacter parse(String value) throws
IllegalArgumentException {
CronCharacter c = null;
Set<CronCharacter> all = EnumSet.allOf(CronCharacter.class);
for (CronCharacter cc : all) {
if (value.toUpperCase().indexOf(cc.character) != -1) {
if (value.startsWith("WED")) {
continue;
}
if (c != null) {
// Special cases:
if (value.equalsIgnoreCase("LW")) {
break;
} else if (value.startsWith("*/")) {
break;
} else if (value.indexOf("#") != -1) {
break;
}
throw new IllegalArgumentException(value);
}
c = cc;
}
}
return c;
}
}
enum CronMonth {
JAN(0),
FEB(1),
MAR(2),
APR(3),
MAY(4),
JUN(5),
JUL(6),
AUG(7),
SEP(8),
OCT(9),
NOV(10),
DEC(11),
JANUARY(0),
FEBRUARY(1),
MARCH(2),
APRIL(3),
JUNE(5),
JULY(6),
AUGUST(7),
SEPTEMBER(8),
OCTOBER(9),
NOVEMBER(10),
DECEMBER(11);
private final int month;
CronMonth(int month) {
this.month = month;
}
}
enum CronDay{
SUN(1),
MON(2),
TUE(3),
WED(4),
THU(5),
FRI(6),
SAT(7),
SUNDAY(1),
MONDAY(2),
TUESDAY(3),
WEDNESDAY(4),
THURSDAY(5),
FRIDAY(6),
SATURDAY(7);
private final int day;
CronDay(int day) {
this.day = day;
}
}
}