All Downloads are FREE. Search and download functionalities are using the official Maven repository.

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: *

*
    *
  1. Minutes sub-pattern. During which minutes of the hour * should the task been launched? The values range is from 0 to 59.
  2. *
  3. Hours sub-pattern. During which hours of the day should * the task been launched? The values range is from 0 to 23.
  4. *
  5. 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.
  6. *
  7. 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".
  8. *
  9. 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".
  10. *
*

* 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); } } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy