org.eclipse.skalli.services.scheduler.Schedule.java Source code

Java tutorial

Introduction

Here is the source code for org.eclipse.skalli.services.scheduler.Schedule.java

Source

/*******************************************************************************
 * 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>"&#42/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>"&#42/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>"&#42/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>"&#42/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>"&#42/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>"&#42/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>"&#42/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>"&#42/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;
        }
    }
}