it.sauronsoftware.cron4j.SchedulingPattern Maven / Gradle / Ivy
/*
* cron4j - A pure Java cron-like scheduler
*
* Copyright (C) 2007-2010 Carlo Pelliccia (www.sauronsoftware.it)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License version
* 2.1, as published by the Free Software Foundation.
*
* This program 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 2.1 for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License version 2.1 along with this program.
* If not, see .
*/
package it.sauronsoftware.cron4j;
import java.util.*;
/**
*
* A UNIX crontab-like pattern is a string split in five space separated parts.
* Each part is intented as:
*
*
* - Minutes sub-pattern. During which minutes of the hour
* should the task been launched? The values range is from 0 to 59.
* - Hours sub-pattern. During which hours of the day should
* the task been launched? The values range is from 0 to 23.
* - Days of month sub-pattern. During which days of the
* month should the task been launched? The values range is from 1 to 31. The
* special value L can be used to recognize the last day of month.
* - Months sub-pattern. During which months of the year
* should the task been launched? The values range is from 1 (January) to 12
* (December), otherwise this sub-pattern allows the aliases "jan",
* "feb", "mar", "apr", "may",
* "jun", "jul", "aug", "sep",
* "oct", "nov" and "dec".
* - Days of week sub-pattern. During which days of the week
* should the task been launched? The values range is from 0 (Sunday) to 6
* (Saturday), otherwise this sub-pattern allows the aliases "sun",
* "mon", "tue", "wed", "thu",
* "fri" and "sat".
*
*
* The star wildcard character is also admitted, indicating "every minute
* of the hour", "every hour of the day", "every day of the
* month", "every month of the year" and "every day of the
* week", according to the sub-pattern in which it is used.
*
*
* Once the scheduler is started, a task will be launched when the five parts in
* its scheduling pattern will be true at the same time.
*
*
* Some examples:
*
*
* 5 * * * *
* This pattern causes a task to be launched once every hour, at the begin of
* the fifth minute (00:05, 01:05, 02:05 etc.).
*
*
* * * * * *
* This pattern causes a task to be launched every minute.
*
*
* * 12 * * Mon
* This pattern causes a task to be launched every minute during the 12th hour
* of Monday.
*
*
* * 12 16 * Mon
* This pattern causes a task to be launched every minute during the 12th hour
* of Monday, 16th, but only if the day is the 16th of the month.
*
*
* Every sub-pattern can contain two or more comma separated values.
*
*
* 59 11 * * 1,2,3,4,5
* This pattern causes a task to be launched at 11:59AM on Monday, Tuesday,
* Wednesday, Thursday and Friday.
*
*
* Values intervals are admitted and defined using the minus character.
*
*
* 59 11 * * 1-5
* This pattern is equivalent to the previous one.
*
*
* The slash character can be used to identify step values within a range. It
* can be used both in the form */c and a-b/c. The
* subpattern is matched every c values of the range
* 0,maxvalue or a-b.
*
*
* */5 * * * *
* This pattern causes a task to be launched every 5 minutes (0:00, 0:05, 0:10,
* 0:15 and so on).
*
*
* 3-18/5 * * * *
* This pattern causes a task to be launched every 5 minutes starting from the
* third minute of the hour, up to the 18th (0:03, 0:08, 0:13, 0:18, 1:03, 1:08
* and so on).
*
*
* */15 9-17 * * *
* This pattern causes a task to be launched every 15 minutes between the 9th
* and 17th hour of the day (9:00, 9:15, 9:30, 9:45 and so on... note that the
* last execution will be at 17:45).
*
*
* All the fresh described syntax rules can be used together.
*
*
* * 12 10-16/2 * *
* This pattern causes a task to be launched every minute during the 12th hour
* of the day, but only if the day is the 10th, the 12th, the 14th or the 16th
* of the month.
*
*
* * 12 1-15,17,20-25 * *
* This pattern causes a task to be launched every minute during the 12th hour
* of the day, but the day of the month must be between the 1st and the 15th,
* the 20th and the 25, or at least it must be the 17th.
*
*
* Finally cron4j lets you combine more scheduling patterns into one, with the
* pipe character:
*
*
* 0 5 * * *|8 10 * * *|22 17 * * *
* This pattern causes a task to be launched every day at 05:00, 10:08 and
* 17:22.
*
*
* @author Carlo Pelliccia
* @since 2.0
*/
public class SchedulingPattern {
/**
* The parser for the minute values.
*/
private static final ValueParser MINUTE_VALUE_PARSER = new MinuteValueParser();
/**
* The parser for the hour values.
*/
private static final ValueParser HOUR_VALUE_PARSER = new HourValueParser();
/**
* The parser for the day of month values.
*/
private static final ValueParser DAY_OF_MONTH_VALUE_PARSER = new DayOfMonthValueParser();
/**
* The parser for the month values.
*/
private static final ValueParser MONTH_VALUE_PARSER = new MonthValueParser();
/**
* The parser for the day of week values.
*/
private static final ValueParser DAY_OF_WEEK_VALUE_PARSER = new DayOfWeekValueParser();
/**
* Validates a string as a scheduling pattern.
*
* @param schedulingPattern
* The pattern to validate.
* @return true if the given string represents a valid scheduling pattern;
* false otherwise.
*/
public static boolean validate(String schedulingPattern) {
try {
new SchedulingPattern(schedulingPattern);
} catch (InvalidPatternException e) {
return false;
}
return true;
}
/**
* The pattern as a string.
*/
private String asString;
/**
* The ValueMatcher list for the "minute" field.
*/
protected ArrayList minuteMatchers = new ArrayList();
/**
* The ValueMatcher list for the "hour" field.
*/
protected ArrayList hourMatchers = new ArrayList();
/**
* The ValueMatcher list for the "day of month" field.
*/
protected ArrayList dayOfMonthMatchers = new ArrayList();
/**
* The ValueMatcher list for the "month" field.
*/
protected ArrayList monthMatchers = new ArrayList();
/**
* The ValueMatcher list for the "day of week" field.
*/
protected ArrayList dayOfWeekMatchers = new ArrayList();
/**
* How many matcher groups in this pattern?
*/
protected int matcherSize = 0;
/**
* Builds a SchedulingPattern parsing it from a string.
*
* @param pattern
* The pattern as a crontab-like string.
* @throws InvalidPatternException
* If the supplied string is not a valid pattern.
*/
public SchedulingPattern(String pattern) throws InvalidPatternException {
this.asString = pattern;
StringTokenizer st1 = new StringTokenizer(pattern, "|");
if (st1.countTokens() < 1) {
throw new InvalidPatternException("invalid pattern: \"" + pattern + "\"");
}
while (st1.hasMoreTokens()) {
String localPattern = st1.nextToken();
StringTokenizer st2 = new StringTokenizer(localPattern, " \t");
if (st2.countTokens() != 5) {
throw new InvalidPatternException("invalid pattern: \"" + localPattern + "\"");
}
try {
minuteMatchers.add(buildValueMatcher(st2.nextToken(), MINUTE_VALUE_PARSER));
} catch (Exception e) {
throw new InvalidPatternException("invalid pattern \""
+ localPattern + "\". Error parsing minutes field: "
+ e.getMessage() + ".");
}
try {
hourMatchers.add(buildValueMatcher(st2.nextToken(), HOUR_VALUE_PARSER));
} catch (Exception e) {
throw new InvalidPatternException("invalid pattern \""
+ localPattern + "\". Error parsing hours field: "
+ e.getMessage() + ".");
}
try {
dayOfMonthMatchers.add(buildValueMatcher(st2.nextToken(), DAY_OF_MONTH_VALUE_PARSER));
} catch (Exception e) {
throw new InvalidPatternException("invalid pattern \""
+ localPattern
+ "\". Error parsing days of month field: "
+ e.getMessage() + ".");
}
try {
monthMatchers.add(buildValueMatcher(st2.nextToken(), MONTH_VALUE_PARSER));
} catch (Exception e) {
throw new InvalidPatternException("invalid pattern \""
+ localPattern + "\". Error parsing months field: "
+ e.getMessage() + ".");
}
try {
dayOfWeekMatchers.add(buildValueMatcher(st2.nextToken(), DAY_OF_WEEK_VALUE_PARSER));
} catch (Exception e) {
throw new InvalidPatternException("invalid pattern \""
+ localPattern
+ "\". Error parsing days of week field: "
+ e.getMessage() + ".");
}
matcherSize++;
}
}
/**
* A ValueMatcher utility builder.
*
* @param str
* The pattern part for the ValueMatcher creation.
* @param parser
* The parser used to parse the values.
* @return The requested ValueMatcher.
* @throws Exception
* If the supplied pattern part is not valid.
*/
private ValueMatcher buildValueMatcher(String str, ValueParser parser)
throws Exception {
if (str.length() == 1 && str.equals("*")) {
return new AlwaysTrueValueMatcher();
}
ArrayList values = new ArrayList();
StringTokenizer st = new StringTokenizer(str, ",");
while (st.hasMoreTokens()) {
String element = st.nextToken();
ArrayList local;
try {
local = parseListElement(element, parser);
} catch (Exception e) {
throw new Exception("invalid field \"" + str
+ "\", invalid element \"" + element + "\", "
+ e.getMessage());
}
for (Iterator i = local.iterator(); i.hasNext();) {
Object value = i.next();
if (!values.contains(value)) {
values.add(value);
}
}
}
if (values.size() == 0) {
throw new Exception("invalid field \"" + str + "\"");
}
if (parser == DAY_OF_MONTH_VALUE_PARSER) {
return new DayOfMonthValueMatcher(values);
} else {
return new IntArrayValueMatcher(values);
}
}
/**
* Parses an element of a list of values of the pattern.
*
* @param str
* The element string.
* @param parser
* The parser used to parse the values.
* @return A list of integers representing the allowed values.
* @throws Exception
* If the supplied pattern part is not valid.
*/
private ArrayList parseListElement(String str, ValueParser parser)
throws Exception {
StringTokenizer st = new StringTokenizer(str, "/");
int size = st.countTokens();
if (size < 1 || size > 2) {
throw new Exception("syntax error");
}
ArrayList values;
try {
values = parseRange(st.nextToken(), parser);
} catch (Exception e) {
throw new Exception("invalid range, " + e.getMessage());
}
if (size == 2) {
String dStr = st.nextToken();
int div;
try {
div = Integer.parseInt(dStr);
} catch (NumberFormatException e) {
throw new Exception("invalid divisor \"" + dStr + "\"");
}
if (div < 1) {
throw new Exception("non positive divisor \"" + div + "\"");
}
ArrayList values2 = new ArrayList();
for (int i = 0; i < values.size(); i += div) {
values2.add(values.get(i));
}
return values2;
} else {
return values;
}
}
/**
* Parses a range of values.
*
* @param str
* The range string.
* @param parser
* The parser used to parse the values.
* @return A list of integers representing the allowed values.
* @throws Exception
* If the supplied pattern part is not valid.
*/
private ArrayList parseRange(String str, ValueParser parser)
throws Exception {
if (str.equals("*")) {
int min = parser.getMinValue();
int max = parser.getMaxValue();
ArrayList values = new ArrayList();
for (int i = min; i <= max; i++) {
values.add(new Integer(i));
}
return values;
}
StringTokenizer st = new StringTokenizer(str, "-");
int size = st.countTokens();
if (size < 1 || size > 2) {
throw new Exception("syntax error");
}
String v1Str = st.nextToken();
int v1;
try {
v1 = parser.parse(v1Str);
} catch (Exception e) {
throw new Exception("invalid value \"" + v1Str + "\", "
+ e.getMessage());
}
if (size == 1) {
ArrayList values = new ArrayList();
values.add(new Integer(v1));
return values;
} else {
String v2Str = st.nextToken();
int v2;
try {
v2 = parser.parse(v2Str);
} catch (Exception e) {
throw new Exception("invalid value \"" + v2Str + "\", "
+ e.getMessage());
}
ArrayList values = new ArrayList();
if (v1 < v2) {
for (int i = v1; i <= v2; i++) {
values.add(new Integer(i));
}
} else if (v1 > v2) {
int min = parser.getMinValue();
int max = parser.getMaxValue();
for (int i = v1; i <= max; i++) {
values.add(new Integer(i));
}
for (int i = min; i <= v2; i++) {
values.add(new Integer(i));
}
} else {
// v1 == v2
values.add(new Integer(v1));
}
return values;
}
}
/**
* This methods returns true if the given timestamp (expressed as a UNIX-era
* millis value) matches the pattern, according to the given time zone.
*
* @param timezone
* A time zone.
* @param millis
* The timestamp, as a UNIX-era millis value.
* @return true if the given timestamp matches the pattern.
*/
public boolean match(TimeZone timezone, long millis) {
GregorianCalendar gc = new GregorianCalendar();
gc.setTimeInMillis(millis);
gc.setTimeZone(timezone);
int minute = gc.get(Calendar.MINUTE);
int hour = gc.get(Calendar.HOUR_OF_DAY);
int dayOfMonth = gc.get(Calendar.DAY_OF_MONTH);
int month = gc.get(Calendar.MONTH) + 1;
int dayOfWeek = gc.get(Calendar.DAY_OF_WEEK) - 1;
int year = gc.get(Calendar.YEAR);
for (int i = 0; i < matcherSize; i++) {
ValueMatcher minuteMatcher = (ValueMatcher) minuteMatchers.get(i);
ValueMatcher hourMatcher = (ValueMatcher) hourMatchers.get(i);
ValueMatcher dayOfMonthMatcher = (ValueMatcher) dayOfMonthMatchers.get(i);
ValueMatcher monthMatcher = (ValueMatcher) monthMatchers.get(i);
ValueMatcher dayOfWeekMatcher = (ValueMatcher) dayOfWeekMatchers.get(i);
boolean eval = minuteMatcher.match(minute)
&& hourMatcher.match(hour)
&& ((dayOfMonthMatcher instanceof DayOfMonthValueMatcher) ? ((DayOfMonthValueMatcher) dayOfMonthMatcher)
.match(dayOfMonth, month, gc.isLeapYear(year))
: dayOfMonthMatcher.match(dayOfMonth))
&& monthMatcher.match(month)
&& dayOfWeekMatcher.match(dayOfWeek);
if (eval) {
return true;
}
}
return false;
}
/**
* This methods returns true if the given timestamp (expressed as a UNIX-era
* millis value) matches the pattern, according to the system default time
* zone.
*
* @param millis
* The timestamp, as a UNIX-era millis value.
* @return true if the given timestamp matches the pattern.
*/
public boolean match(long millis) {
return match(TimeZone.getDefault(), millis);
}
/**
* Returns the pattern as a string.
*
* @return The pattern as a string.
*/
public String toString() {
return asString;
}
/**
* This utility method changes an alias to an int value.
*
* @param value
* The value.
* @param aliases
* The aliases list.
* @param offset
* The offset appplied to the aliases list indices.
* @return The parsed value.
* @throws Exception
* If the expressed values doesn't match any alias.
*/
private static int parseAlias(String value, String[] aliases, int offset)
throws Exception {
for (int i = 0; i < aliases.length; i++) {
if (aliases[i].equalsIgnoreCase(value)) {
return offset + i;
}
}
throw new Exception("invalid alias \"" + value + "\"");
}
/**
* Definition for a value parser.
*/
private static interface ValueParser {
/**
* Attempts to parse a value.
*
* @param value
* The value.
* @return The parsed value.
* @throws Exception
* If the value can't be parsed.
*/
public int parse(String value) throws Exception;
/**
* Returns the minimum value accepred by the parser.
*
* @return The minimum value accepred by the parser.
*/
public int getMinValue();
/**
* Returns the maximum value accepred by the parser.
*
* @return The maximum value accepred by the parser.
*/
public int getMaxValue();
}
/**
* A simple value parser.
*/
private static class SimpleValueParser implements ValueParser {
/**
* The minimum allowed value.
*/
protected int minValue;
/**
* The maximum allowed value.
*/
protected int maxValue;
/**
* Builds the value parser.
*
* @param minValue
* The minimum allowed value.
* @param maxValue
* The maximum allowed value.
*/
public SimpleValueParser(int minValue, int maxValue) {
this.minValue = minValue;
this.maxValue = maxValue;
}
public int parse(String value) throws Exception {
int i;
try {
i = Integer.parseInt(value);
} catch (NumberFormatException e) {
throw new Exception("invalid integer value");
}
if (i < minValue || i > maxValue) {
throw new Exception("value out of range");
}
return i;
}
public int getMinValue() {
return minValue;
}
public int getMaxValue() {
return maxValue;
}
}
/**
* The minutes value parser.
*/
private static class MinuteValueParser extends SimpleValueParser {
/**
* Builds the value parser.
*/
public MinuteValueParser() {
super(0, 59);
}
}
/**
* The hours value parser.
*/
private static class HourValueParser extends SimpleValueParser {
/**
* Builds the value parser.
*/
public HourValueParser() {
super(0, 23);
}
}
/**
* The days of month value parser.
*/
private static class DayOfMonthValueParser extends SimpleValueParser {
/**
* Builds the value parser.
*/
public DayOfMonthValueParser() {
super(1, 31);
}
/**
* Added to support last-day-of-month.
*
* @param value
* The value to be parsed
* @return the integer day of the month or 32 for last day of the month
* @throws Exception
* if the input value is invalid
*/
public int parse(String value) throws Exception {
if (value.equalsIgnoreCase("L")) {
return 32;
} else {
return super.parse(value);
}
}
}
/**
* The value parser for the months field.
*/
private static class MonthValueParser extends SimpleValueParser {
/**
* Months aliases.
*/
private static String[] ALIASES = { "jan", "feb", "mar", "apr", "may",
"jun", "jul", "aug", "sep", "oct", "nov", "dec" };
/**
* Builds the months value parser.
*/
public MonthValueParser() {
super(1, 12);
}
public int parse(String value) throws Exception {
try {
// try as a simple value
return super.parse(value);
} catch (Exception e) {
// try as an alias
return parseAlias(value, ALIASES, 1);
}
}
}
/**
* The value parser for the months field.
*/
private static class DayOfWeekValueParser extends SimpleValueParser {
/**
* Days of week aliases.
*/
private static String[] ALIASES = { "sun", "mon", "tue", "wed", "thu", "fri", "sat" };
/**
* Builds the months value parser.
*/
public DayOfWeekValueParser() {
super(0, 7);
}
public int parse(String value) throws Exception {
try {
// try as a simple value
return super.parse(value) % 7;
} catch (Exception e) {
// try as an alias
return parseAlias(value, ALIASES, 0);
}
}
}
}