Java tutorial
/* * cron4joda - A pure Joda Time cron pattern parser * * This is a fork of cron4j: http://www.sauronsoftware.it/projects/cron4j/ * * Copyright (C) 2007-2010 Carlo Pelliccia (www.sauronsoftware.it) * Copyright (C) 2013 Christian Felde (cfelde.com) * * 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 <http://www.gnu.org/licenses/>. */ package com.cfelde.cron4joda; import java.util.ArrayList; import java.util.Iterator; import java.util.StringTokenizer; import org.joda.time.DateTime; /** * <p> * A UNIX crontab-like pattern is a string split in five space separated parts. * Each part is intented as: * </p> * <ol> * <li><strong>Minutes sub-pattern</strong>. During which minutes of the hour * should the task been launched? The values range is from 0 to 59.</li> * <li><strong>Hours sub-pattern</strong>. During which hours of the day should * the task been launched? The values range is from 0 to 23.</li> * <li><strong>Days of month sub-pattern</strong>. 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.</li> * <li><strong>Months sub-pattern</strong>. 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".</li> * <li><strong>Days of week sub-pattern</strong>. 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".</li> * </ol> * <p> * 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. * </p> * <p> * 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. * </p> * <p> * Some examples: * </p> * <p> * <strong>5 * * * *</strong><br /> * 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.). * </p> * <p> * <strong>* * * * *</strong><br /> * This pattern causes a task to be launched every minute. * </p> * <p> * <strong>* 12 * * Mon</strong><br /> * This pattern causes a task to be launched every minute during the 12th hour * of Monday. * </p> * <p> * <strong>* 12 16 * Mon</strong><br /> * 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. * </p> * <p> * Every sub-pattern can contain two or more comma separated values. * </p> * <p> * <strong>59 11 * * 1,2,3,4,5</strong><br /> * This pattern causes a task to be launched at 11:59AM on Monday, Tuesday, * Wednesday, Thursday and Friday. * </p> * <p> * Values intervals are admitted and defined using the minus character. * </p> * <p> * <strong>59 11 * * 1-5</strong><br /> * This pattern is equivalent to the previous one. * </p> * <p> * The slash character can be used to identify step values within a range. It * can be used both in the form <em>*/c</em> and <em>a-b/c</em>. The * subpattern is matched every <em>c</em> values of the range * <em>0,maxvalue</em> or <em>a-b</em>. * </p> * <p> * <strong>*/5 * * * *</strong><br /> * This pattern causes a task to be launched every 5 minutes (0:00, 0:05, 0:10, * 0:15 and so on). * </p> * <p> * <strong>3-18/5 * * * *</strong><br /> * 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). * </p> * <p> * <strong>*/15 9-17 * * *</strong><br /> * 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). * </p> * <p> * All the fresh described syntax rules can be used together. * </p> * <p> * <strong>* 12 10-16/2 * *</strong><br /> * 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. * </p> * <p> * <strong>* 12 1-15,17,20-25 * *</strong><br /> * 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. * </p> * <p> * Finally cron4j lets you combine more scheduling patterns into one, with the * pipe character: * </p> * <p> * <strong>0 5 * * *|8 10 * * *|22 17 * * *</strong><br /> * This pattern causes a task to be launched every day at 05:00, 10:08 and * 17:22. * </p> * * @author Carlo Pelliccia * @author Christian Felde * @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.isEmpty()) { 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(DateTime dt) { int minute = dt.getMinuteOfHour(); int hour = dt.getHourOfDay(); int dayOfMonth = dt.getDayOfMonth(); int month = dt.getMonthOfYear(); //gc.get(Calendar.MONTH) + 1; int dayOfWeek = dt.getDayOfWeek(); //gc.get(Calendar.DAY_OF_WEEK) - 1; 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, dt.year().getField().isLeap(dt.getMillis())) /*gc.isLeapYear(year))*/ : dayOfMonthMatcher.match(dayOfMonth)) && monthMatcher.match(month) && dayOfWeekMatcher.match(dayOfWeek); if (eval) { return true; } } return false; } /** * Returns the pattern as a string. * * @return The pattern as a string. */ @Override 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; } @Override 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; } @Override public int getMinValue() { return minValue; } @Override 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 */ @Override 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); } @Override 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 = { "mon", "tue", "wed", "thu", "fri", "sat", "sun" }; /** * Builds the months value parser. */ public DayOfWeekValueParser() { super(1, 7); } @Override 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); } } } }