/******************************************************************************* * Copyright (c) 2010-2014 SAP AG and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * SAP AG - initial API and implementation *******************************************************************************/ package org.eclipse.skalli.services.scheduler; import java.util.Arrays; import java.util.Calendar; import java.util.GregorianCalendar; import java.util.Locale; import java.util.TimeZone; import org.apache.commons.lang.StringUtils; /** * Defines a schedule for recurring tasks. * <p> * This class supports a cron-like notation for specifying the day of week, hour and minute * when a task should be executed. Note this class uses 24-hour time format and treats Sunday as the * first day of the week. */ public class Schedule { public static final String ASTERISK = "*"; //$NON-NLS-1$ /** Days of a week, see {@link #Schedule(String, String, String). */ @SuppressWarnings("nls") public static final String[] WEEKDAYS = { "SUNDAY", "MONDAY", "TUESDAY", "WEDNESDAY", "THURSDAY", "FRIDAY", "SATURDAY" }; /** Three-letter abbreviations for the days of a week, see {@link #Schedule(String, String, String). */ @SuppressWarnings("nls") public static final String[] WEEKDAYS_SHORT = { "SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT" }; private String daysOfWeek; private String hours; private String minutes; // transient so that Schedule can be XStreamed private transient SortedIntSet daysOfWeekSet; private transient SortedIntSet hoursSet; private transient SortedIntSet minutesSet; /** * Creates a schedule for the current day of week and time. * {@link #isDue(Calendar)} returns <code>true</code> immediately when * applied to this schedule. */ public static final Schedule NOW = new Schedule(new GregorianCalendar(TimeZone.getTimeZone("UTC"), Locale.ENGLISH)); //$NON-NLS-1$ /** * Creates a <tt>"* * *"</tt> schedule, i.e. a schedule that * is due every minute. */ public Schedule() { this(ASTERISK, ASTERISK, ASTERISK); } /** * Creates a schedule with the day of week, hour of day and minute * as specified by the given calendar. * * @param date the date at which the schedule is due. */ public Schedule(Calendar date) { this(Integer.toString(date.get(Calendar.DAY_OF_WEEK) - 1), Integer.toString(date.get(Calendar.HOUR_OF_DAY)), Integer.toString(date.get(Calendar.MINUTE))); } /** * Creates a schedule from a given day of week and time. * <p> * Each of the parameters can be a comma-separated list of ranges, where a range is either * a single integer number, or two integer numbers separated by <tt>"-"</tt>. Additionally, * <tt>"*"</tt> denotes the range <tt>"first-last"</tt>, where <tt>first</tt> and <tt>last</tt> * are the minium and maximum value of a parameter, respectively. For example, <tt>hour="*"</tt> * is equivalent to <tt>hour="0-23"</tt>. A range can be followed by <tt>"/n"</tt> with * <tt>n</tt> a positive number, which means "every nth ..." (e.g. <tt>hour="0-23/2"</tt> means * "every second hour"). * * @param daysOfWeek * the day of the week, denoted as number between 0 and 7 (0=Sunday,1=Monday,...,7=Sunday), * with one of the long names from {@link #WEEKDAYS} or with a short name from * {@link WEEKDAYS_SHORT}. Numbers and names can be mixed. * @param hours * the hour of day, denoted as number between 0 and 23. * @param minutes * the minute in an hour, denotes as number between 0 and 59. */ public Schedule(String daysOfWeek, String hours, String minutes) { setDaysOfWeek(daysOfWeek); setHours(hours); setMinutes(minutes); } /** * Creates a schedule and copies all data from a given schedule. * * @param schedule the schedule to initialize from. */ public Schedule(Schedule schedule) { this(); if (schedule != null) { setDaysOfWeek(schedule.getDaysOfWeek()); setHours(schedule.getHours()); setMinutes(schedule.getMinutes()); } } /** * Returns <code>true</code> if the recurring task that this schedule describes * is due, i.e. the runnable associated with the task (see {@link #getRunnable()}) * should be executed <code>now</code>. * * @param now the current day of week/hour/minute. * @return <code>true</code>, if the task is due. */ public boolean isDue(Calendar now) { return getDaysOfWeekSet().contains(now.get(Calendar.DAY_OF_WEEK)) && getHoursSet().contains(now.get(Calendar.HOUR_OF_DAY)) && getMinutesSet().contains(now.get(Calendar.MINUTE)); } /** * Returns <code>true</code> if the recurring task that this schedule describes * was due somewhen between <code>first</code> and <code>last</code> (including the * interval boundaries). * * @param first the begin of the interval to check, or <code>null</code>. In that case * the method is equivalent to {@link #isDue(Calendar)}. * @param last the end of the interval to check * * @return <code>true</code>, if the task is due. */ public boolean isDue(Calendar first, Calendar last) { if (isDue(last)) { return true; } if (first == null) { return false; } Calendar i = new GregorianCalendar(first.getTimeZone()); i.setTime(first.getTime()); i.add(Calendar.MINUTE, 1); while (i.before(last)) { if (isDue(i)) { return true; } i.add(Calendar.MINUTE, 1); } return false; } /** * Returns the minutes setting of the schedule. * * @return either <tt>"*"</tt> or <tt>"*/n"</tt>, or a comma-separated list of ranges. * A range is either a number between 0 and 59 or two numbers separated by <tt>"-"</tt>, * optionally followed by <tt>"/n"</tt> which means: "every n minutes". * Examples: <tt>"*/10"</tt> denotes a task scheduled every 10 minutes. */ public String getMinutes() { if (StringUtils.isBlank(minutes)) { setMinutes(ASTERISK); } return minutes; } /** * Specifies the minutes setting of the schedule. * * @param minutes * either <tt>"*"</tt> or <tt>"*/n"</tt>, or a comma-separated list of ranges. * A range is either a number between 0 and 59 or two numbers separated by <tt>"-"</tt>, * optionally followed by <tt>"/n"</tt> which means: "every n minutes". */ public void setMinutes(String minutes) { this.minutes = StringUtils.isNotBlank(minutes)? minutes : ASTERISK; initMinutesSet(); } /** * Returns the hours setting of the schedule. * * @return either <tt>"*"</tt> or <tt>"*/n"</tt>, or a comma-separated list of ranges. * A range is either a number between 0 and 23 (24-hour format!) or two numbers separated * by <tt>"-"</tt> optionally followed by <tt>"/n"</tt> which means: "every nth minute". * Examples: <tt>"*/10"</tt> denotes a task scheduled every 10 minutes. */ public String getHours() { if (StringUtils.isBlank(hours)) { setHours(ASTERISK); } return hours; } /** * Specifies the hours setting of the schedule. * * @param hours * either <tt>"*"</tt> or <tt>"*/n"</tt>, or a comma-separated list of ranges. * A range is either a number between 0 and 23 (24-hour format!) or two numbers separated * by <tt>"-"</tt>, optionally followed by <tt>"/n"</tt> which means: "every nth hour". */ public void setHours(String hours) { this.hours = StringUtils.isNotBlank(hours)? hours : ASTERISK; initHoursSet(); } /** * Returns the days of the week setting of the schedule. * * @return either <tt>"*"</tt> or <tt>"*/n"</tt>, or a comma-separated list of ranges. * A range is either a number between 0 and 7 (0=Sunday,1=Monday,...,7=Sunday), a long name from * {@link #WEEKDAYS}, a short name from {@link WEEKDAYS_SHORT}, or two numbers/names separated * by <tt>"-"</tt> optionally followed by <tt>"/n"</tt> which means: "every nth day". * Numbers and symbolic names can be mixed arbitrarily. */ public String getDaysOfWeek() { if (StringUtils.isBlank(daysOfWeek)) { setDaysOfWeek(ASTERISK); } return daysOfWeek; } /** * Specifies the days of the week setting of the schedule. * * @param daysOfWeek * either <tt>"*"</tt> or <tt>"*/n"</tt>, or a comma-separated list of ranges. * A range is either a number between 0 and 7 (0=Sunday,1=Monday,...,7=Sunday), a long name from * {@link #WEEKDAYS}, a short name from {@link WEEKDAYS_SHORT}, or two numbers/names separated * by <tt>"-"</tt> optionally followed by <tt>"/n"</tt> which means: "every nth day". * Numbers and symbolic names can be mixed arbitrarily. */ public void setDaysOfWeek(String daysOfWeek) { this.daysOfWeek = StringUtils.isNotBlank(daysOfWeek)? daysOfWeek : ASTERISK; initDaysOfWeekSet(); } public String getSchedule() { StringBuilder sb = new StringBuilder(); sb.append(getDaysOfWeek()).append(' '); sb.append(getHours()).append(' '); sb.append(getMinutes()); return sb.toString(); } @Override public String toString() { return getSchedule(); } @Override public int hashCode() { int result = 31 + getDaysOfWeek().hashCode(); result = 31 * result + getHours().hashCode(); result = 31 * result + getMinutes().hashCode(); return result; } @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (obj == null) { return false; } if (obj instanceof Schedule) { Schedule s = (Schedule) obj; return Arrays.equals(getDaysOfWeekSet().toArray(), s.getDaysOfWeekSet().toArray()) && Arrays.equals(getHoursSet().toArray(), s.getHoursSet().toArray()) && Arrays.equals(getMinutesSet().toArray(), s.getMinutesSet().toArray()); } return super.equals(obj); } private void initDaysOfWeekSet() { daysOfWeekSet = normalizeDaysOfWeek(getDaysOfWeekSet(getDaysOfWeek(), 0, 7, Schedule.WEEKDAYS, Schedule.WEEKDAYS_SHORT)); } private SortedIntSet getDaysOfWeekSet() { if (daysOfWeekSet == null) { initDaysOfWeekSet(); } return daysOfWeekSet; } private void initHoursSet() { hoursSet = getTimeSet(getHours(), 0, 23); } private SortedIntSet getHoursSet() { if (hoursSet == null) { initHoursSet(); } return hoursSet; } private void initMinutesSet() { minutesSet = getTimeSet(getMinutes(), 0, 59); } private SortedIntSet getMinutesSet() { if (minutesSet == null) { initMinutesSet(); } return minutesSet; } static SortedIntSet getTimeSet(String s, int min, int max) { return getIntSet(s, min, max, null, null); } static SortedIntSet getDaysOfWeekSet(String s, int min, int max, String[] names, String[] shortNames) { return getIntSet(s, min, max, names, shortNames); } private static SortedIntSet getIntSet(String s, int min, int max, String[] names, String[] shortNames) { SortedIntSet result = new SortedIntSet(); String[] ranges = StringUtils.split(s, ','); for (String range : ranges) { int first = min; int last = max; int step = 1; if (range.startsWith("*/")) { //$NON-NLS-1$ step = Integer.parseInt(range.substring(2)); } else if (!"*".equals(range)) { //$NON-NLS-1$ int m = range.indexOf('/'); if (m > 0) { step = Integer.parseInt(range.substring(m + 1)); range = range.substring(0, m); } int n = range.indexOf('-'); String left = n > 0 ? range.substring(0, n) : range; String right = n > 0 ? range.substring(n + 1) : range; first = indexOf(left, names, shortNames, min); last = indexOf(right, names, shortNames, min); if (first < 0) { first = Math.min(Math.max(Integer.parseInt(left), min), max); } if (last < 0) { last = Math.min(Math.max(Integer.parseInt(right), min), max); } } if (first > last) { // e.g. FRI-TUE equivalent to FRI-SUN + MON-TUE result.addAll(first, max, step); result.addAll(min, last, step); } else { result.addAll(first, last, step); } } return result; } private static int indexOf(String s, String[] names, String[] shortNames, int min) { int i = indexOf(s, names, min); return i < 0 ? indexOf(s, shortNames, min) : i; } private static int indexOf(String s, String[] names, int min) { if (names != null) { for (int i = 0; i < names.length; ++i) { if (s.equalsIgnoreCase(names[i])) { return i + min; } } } return -1; } // Calendar needs 1=Sunday,2=Monday,..,7=Saturday, while cron uses 0=Sunday,1=Monday,...7=Sunday; // so we map 7 to 1 and add +1 otherwise static SortedIntSet normalizeDaysOfWeek(SortedIntSet daysOfWeek) { SortedIntSet result = new SortedIntSet(); for (int i = 0; i < daysOfWeek.size(); ++i) { int dayOfWeek = daysOfWeek.get(i); result.add(dayOfWeek == 7 ? 1 : dayOfWeek + 1); } return result; } /** * Simple implementations of a resizeable, sorted integer set. */ static class SortedIntSet { private int[] array = new int[8]; private int count = 0; public void add(int value) { if (!contains(value)) { if (count == array.length) { int[] newarray = new int[array.length * 2]; System.arraycopy(array, 0, newarray, 0, count); array = newarray; } array[count++] = value; Arrays.sort(array, 0, count); } } public void addAll(int first, int last, int step) { for (int value = first; value <= last; value += step) { add(value); } } public int size() { return count; } public int get(int index) { if (index >= count) { throw new IndexOutOfBoundsException(); } return array[index]; } public boolean contains(int value) { return Arrays.binarySearch(array, 0, count, value) >= 0; } public int[] toArray() { return array; } } }