com.espertech.esper.schedule.ScheduleComputeHelper.java Source code

Java tutorial

Introduction

Here is the source code for com.espertech.esper.schedule.ScheduleComputeHelper.java

Source

/**************************************************************************************
 * Copyright (C) 2008 EsperTech, Inc. All rights reserved.                            *
 * http://esper.codehaus.org                                                          *
 * http://www.espertech.com                                                           *
 * ---------------------------------------------------------------------------------- *
 * The software in this package is published under the terms of the GPL license       *
 * a copy of which has been included with this distribution in the license.txt file.  *
 **************************************************************************************/
package com.espertech.esper.schedule;

import com.espertech.esper.type.CronOperatorEnum;
import com.espertech.esper.type.CronParameter;
import com.espertech.esper.type.ScheduleUnit;
import com.espertech.esper.util.ExecutionPathDebugLog;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import java.util.Calendar;
import java.util.Date;
import java.util.SortedSet;
import java.util.TimeZone;

/**
 * For a crontab-like schedule, this class computes the next occurance given a start time and a specification of
 * what the schedule looks like.
 * The resolution at which this works is at the second level. The next occurance
 * is always at least 1 second ahead.
 * The class implements an algorithm that starts at the highest precision (seconds) and
 * continues to the lowest precicion (month). For each precision level the
 * algorithm looks at the list of valid values and finds a value for each that is equal to or greater then
 * the valid values supplied. If no equal or
 * greater value was supplied, it will reset all higher precision elements to its minimum value.
 */
public final class ScheduleComputeHelper {
    private static final Log log = LogFactory.getLog(ScheduleComputeHelper.class);

    private static int[] DAY_OF_WEEK_ARRAY = new int[] { Calendar.SUNDAY, Calendar.MONDAY, Calendar.TUESDAY,
            Calendar.WEDNESDAY, Calendar.THURSDAY, Calendar.FRIDAY, Calendar.SATURDAY };

    /**
     * Minimum time to next occurance.
     */
    private static final int MIN_OFFSET_MSEC = 1000;

    /**
     * Computes the next lowest date in milliseconds based on a specification and the
     * from-time passed in.
     * @param spec defines the schedule
     * @param afterTimeInMillis defines the start time
     * @return a long date millisecond value for the next schedule occurance matching the spec
     */
    public static long computeNextOccurance(ScheduleSpec spec, long afterTimeInMillis) {
        if ((ExecutionPathDebugLog.isDebugEnabled) && (log.isDebugEnabled())) {
            log.debug(".computeNextOccurance Computing next occurance, afterTimeInMillis="
                    + (new Date(afterTimeInMillis)) + "  as long=" + afterTimeInMillis + "  spec=" + spec);
        }

        // Add the minimum resolution to the start time to ensure we don't get the same exact time
        if (spec.getUnitValues().containsKey(ScheduleUnit.SECONDS)) {
            afterTimeInMillis += MIN_OFFSET_MSEC;
        } else {
            afterTimeInMillis += 60 * MIN_OFFSET_MSEC;
        }

        return compute(spec, afterTimeInMillis);
    }

    /**
     * Computes the next lowest date in milliseconds based on a specification and the
     * from-time passed in and returns the delta from the current time.
     * @param spec defines the schedule
     * @param afterTimeInMillis defines the start time
     * @return a long millisecond value representing the delta between current time and the next schedule occurance matching the spec
     */
    public static long computeDeltaNextOccurance(ScheduleSpec spec, long afterTimeInMillis) {
        return computeNextOccurance(spec, afterTimeInMillis) - afterTimeInMillis;
    }

    private static long compute(ScheduleSpec spec, long afterTimeInMillis) {
        while (true) {
            Calendar after;
            if (spec.getOptionalTimeZone() != null) {
                after = Calendar.getInstance(TimeZone.getTimeZone(spec.getOptionalTimeZone()));
            } else {
                after = Calendar.getInstance();
            }
            after.setTimeInMillis(afterTimeInMillis);

            ScheduleCalendar result = new ScheduleCalendar();
            result.setMilliseconds(after.get(Calendar.MILLISECOND));

            SortedSet<Integer> minutesSet = spec.getUnitValues().get(ScheduleUnit.MINUTES);
            SortedSet<Integer> hoursSet = spec.getUnitValues().get(ScheduleUnit.HOURS);
            SortedSet<Integer> monthsSet = spec.getUnitValues().get(ScheduleUnit.MONTHS);
            SortedSet<Integer> secondsSet = null;
            boolean isSecondsSpecified = false;

            if (spec.getUnitValues().containsKey(ScheduleUnit.SECONDS)) {
                isSecondsSpecified = true;
                secondsSet = spec.getUnitValues().get(ScheduleUnit.SECONDS);
            }

            if (isSecondsSpecified) {
                result.setSecond(nextValue(secondsSet, after.get(Calendar.SECOND)));
                if (result.getSecond() == -1) {
                    result.setSecond(nextValue(secondsSet, 0));
                    after.add(Calendar.MINUTE, 1);
                }
            }

            result.setMinute(nextValue(minutesSet, after.get(Calendar.MINUTE)));
            if (result.getMinute() != after.get(Calendar.MINUTE)) {
                result.setSecond(nextValue(secondsSet, 0));
            }
            if (result.getMinute() == -1) {
                result.setMinute(nextValue(minutesSet, 0));
                after.add(Calendar.HOUR_OF_DAY, 1);
            }

            result.setHour(nextValue(hoursSet, after.get(Calendar.HOUR_OF_DAY)));
            if (result.getHour() != after.get(Calendar.HOUR_OF_DAY)) {
                result.setSecond(nextValue(secondsSet, 0));
                result.setMinute(nextValue(minutesSet, 0));
            }
            if (result.getHour() == -1) {
                result.setHour(nextValue(hoursSet, 0));
                after.add(Calendar.DAY_OF_MONTH, 1);
            }

            // This call may change second, minute and/or hour parameters
            // They may be reset to minimum values if the day rolled
            result.setDayOfMonth(determineDayOfMonth(spec, after, result));

            boolean dayMatchRealDate = false;
            while (!dayMatchRealDate) {
                if (checkDayValidInMonth(result.getDayOfMonth(), after.get(Calendar.MONTH),
                        after.get(Calendar.YEAR))) {
                    dayMatchRealDate = true;
                } else {
                    after.add(Calendar.MONTH, 1);
                }
            }

            int currentMonth = after.get(Calendar.MONTH) + 1;
            result.setMonth(nextValue(monthsSet, currentMonth));
            if (result.getMonth() != currentMonth) {
                result.setSecond(nextValue(secondsSet, 0));
                result.setMinute(nextValue(minutesSet, 0));
                result.setHour(nextValue(hoursSet, 0));
                result.setDayOfMonth(determineDayOfMonth(spec, after, result));
            }
            if (result.getMonth() == -1) {
                result.setMonth(nextValue(monthsSet, 0));
                after.add(Calendar.YEAR, 1);
            }

            // Perform a last valid date check, if failing, try to compute a new date based on this altered after date
            int year = after.get(Calendar.YEAR);
            if (!(checkDayValidInMonth(result.getDayOfMonth(), result.getMonth() - 1, year))) {
                afterTimeInMillis = after.getTimeInMillis();
                continue;
            }

            return getTime(result, after.get(Calendar.YEAR), spec.getOptionalTimeZone());
        }
    }

    /*
     * Determine the next valid day of month based on the given specification of valid days in month and
     * valid days in week. If both days in week and days in month are supplied, the days are OR-ed.
     */
    private static int determineDayOfMonth(ScheduleSpec spec, Calendar after, ScheduleCalendar result) {
        SortedSet<Integer> daysOfMonthSet = spec.getUnitValues().get(ScheduleUnit.DAYS_OF_MONTH);
        SortedSet<Integer> daysOfWeekSet = spec.getUnitValues().get(ScheduleUnit.DAYS_OF_WEEK);
        SortedSet<Integer> secondsSet = spec.getUnitValues().get(ScheduleUnit.SECONDS);
        SortedSet<Integer> minutesSet = spec.getUnitValues().get(ScheduleUnit.MINUTES);
        SortedSet<Integer> hoursSet = spec.getUnitValues().get(ScheduleUnit.HOURS);

        int dayOfMonth;

        // If days of week is a wildcard, just go by days of month
        if (spec.getOptionalDayOfMonthOperator() != null || spec.getOptionalDayOfWeekOperator() != null) {
            boolean isWeek = false;
            CronParameter op = spec.getOptionalDayOfMonthOperator();
            if (spec.getOptionalDayOfMonthOperator() == null) {
                op = spec.getOptionalDayOfWeekOperator();
                isWeek = true;
            }

            // may return the current day or a future day in the same month,
            // and may advance the "after" date to the next month
            int currentYYMMDD = getTimeYYYYMMDD(after);
            increaseAfterDayOfMonthSpecialOp(op.getOperator(), op.getDay(), op.getMonth(), isWeek, after);
            int rolledYYMMDD = getTimeYYYYMMDD(after);

            // if rolled then reset time portion
            if (rolledYYMMDD > currentYYMMDD) {
                result.setSecond(nextValue(secondsSet, 0));
                result.setMinute(nextValue(minutesSet, 0));
                result.setHour(nextValue(hoursSet, 0));
                return after.get(Calendar.DAY_OF_MONTH);
            }
            // rolling backwards is not allowed
            else if (rolledYYMMDD < currentYYMMDD) {
                throw new IllegalStateException(
                        "Failed to evaluate special date op, rolled date less then current date");
            } else {
                Calendar work = (Calendar) after.clone();
                work.set(Calendar.SECOND, result.getSecond());
                work.set(Calendar.MINUTE, result.getMinute());
                work.set(Calendar.HOUR_OF_DAY, result.getHour());
                if (!work.after(after)) { // new date is not after current date, so bump
                    after.add(Calendar.DAY_OF_MONTH, 1);
                    result.setSecond(nextValue(secondsSet, 0));
                    result.setMinute(nextValue(minutesSet, 0));
                    result.setHour(nextValue(hoursSet, 0));
                    increaseAfterDayOfMonthSpecialOp(op.getOperator(), op.getDay(), op.getMonth(), isWeek, after);
                }
                return after.get(Calendar.DAY_OF_MONTH);
            }
        } else if (daysOfWeekSet == null) {
            dayOfMonth = nextValue(daysOfMonthSet, after.get(Calendar.DAY_OF_MONTH));
            if (dayOfMonth != after.get(Calendar.DAY_OF_MONTH)) {
                result.setSecond(nextValue(secondsSet, 0));
                result.setMinute(nextValue(minutesSet, 0));
                result.setHour(nextValue(hoursSet, 0));
            }
            if (dayOfMonth == -1) {
                dayOfMonth = nextValue(daysOfMonthSet, 0);
                after.add(Calendar.MONTH, 1);
            }
        }
        // If days of weeks is not a wildcard and days of month is a wildcard, go by days of week only
        else if (daysOfMonthSet == null) {
            // Loop to find the next day of month that works for the specified day of week values
            while (true) {
                dayOfMonth = after.get(Calendar.DAY_OF_MONTH);
                int dayOfWeek = after.get(Calendar.DAY_OF_WEEK) - 1;

                // If the day matches neither the day of month nor the day of week
                if (!daysOfWeekSet.contains(dayOfWeek)) {
                    result.setSecond(nextValue(secondsSet, 0));
                    result.setMinute(nextValue(minutesSet, 0));
                    result.setHour(nextValue(hoursSet, 0));
                    after.add(Calendar.DAY_OF_MONTH, 1);
                } else {
                    break;
                }
            }
        }
        // Both days of weeks and days of month are not a wildcard
        else {
            // Loop to find the next day of month that works for either day of month  OR   day of week
            while (true) {
                dayOfMonth = after.get(Calendar.DAY_OF_MONTH);
                int dayOfWeek = after.get(Calendar.DAY_OF_WEEK) - 1;

                // If the day matches neither the day of month nor the day of week
                if ((!daysOfWeekSet.contains(dayOfWeek)) && (!daysOfMonthSet.contains(dayOfMonth))) {
                    result.setSecond(nextValue(secondsSet, 0));
                    result.setMinute(nextValue(minutesSet, 0));
                    result.setHour(nextValue(hoursSet, 0));
                    after.add(Calendar.DAY_OF_MONTH, 1);
                } else {
                    break;
                }
            }
        }

        return dayOfMonth;
    }

    private static long getTime(ScheduleCalendar result, int year, String optionalTimeZone) {
        Calendar calendar;
        if (optionalTimeZone != null) {
            calendar = Calendar.getInstance(TimeZone.getTimeZone(optionalTimeZone));
        } else {
            calendar = Calendar.getInstance();
        }
        calendar.set(year, result.getMonth() - 1, result.getDayOfMonth(), result.getHour(), result.getMinute(),
                result.getSecond());
        calendar.set(Calendar.MILLISECOND, result.getMilliseconds());
        return calendar.getTimeInMillis();
    }

    /*
     * Check if this is a valid date.
     */
    private static boolean checkDayValidInMonth(int day, int month, int year) {
        try {
            Calendar calendar = Calendar.getInstance();
            calendar.setLenient(false);
            calendar.set(Calendar.YEAR, year);
            calendar.set(Calendar.MONTH, month);
            calendar.set(Calendar.DAY_OF_MONTH, day);
            calendar.getTime();
        } catch (IllegalArgumentException e) {
            return false;
        }
        return true;
    }

    /*
     * Determine if in the supplied valueSet there is a value after the given start value.
     * Return -1 to indicate that there is no value after the given startValue.
     * If the valueSet passed is null it is treated as a wildcard and the same startValue is returned
     */
    private static int nextValue(SortedSet<Integer> valueSet, int startValue) {
        if (valueSet == null) {
            return startValue;
        }

        if (valueSet.contains(startValue)) {
            return startValue;
        }

        SortedSet<Integer> tailSet = valueSet.tailSet(startValue + 1);

        if (tailSet.isEmpty()) {
            return -1;
        } else {
            return tailSet.first();
        }
    }

    private static int getTimeYYYYMMDD(Calendar calendar) {
        return 10000 * calendar.get(Calendar.YEAR) + (calendar.get(Calendar.MONTH) + 1) * 100
                + calendar.get(Calendar.DAY_OF_MONTH);
    }

    private static void increaseAfterDayOfMonthSpecialOp(CronOperatorEnum operator, Integer day, Integer month,
            boolean week, Calendar after) {
        DateChecker checker;
        if (operator == CronOperatorEnum.LASTDAY) {
            if (!week) {
                checker = new DateCheckerLastDayOfMonth(day, month);
            } else {
                if (day == null) {
                    checker = new DateCheckerLastDayOfWeek(month);
                } else {
                    checker = new DateCheckerLastSpecificDayWeek(day, month);
                }
            }
        } else if (operator == CronOperatorEnum.LASTWEEKDAY) {
            checker = new DateCheckerLastWeekday(day, month);
        } else {
            checker = new DateCheckerMonthWeekday(day, month);
        }

        int dayCount = 0;
        while (!checker.fits(after)) {
            after.add(Calendar.DAY_OF_MONTH, 1);
            dayCount++;
            if (dayCount > 10000) {
                throw new IllegalArgumentException("Invalid crontab expression: failed to find match day");
            }
        }
    }

    private interface DateChecker {
        public boolean fits(Calendar cal);
    }

    private static class DateCheckerLastSpecificDayWeek implements DateChecker {

        private final int dayCode;
        private final Integer month;

        private DateCheckerLastSpecificDayWeek(int day, Integer month) {
            if (day < 0 || day > 7) {
                throw new IllegalArgumentException("Last xx day of the month has to be a day of week (0-7)");
            }
            dayCode = DAY_OF_WEEK_ARRAY[day];
            this.month = month;
        }

        public boolean fits(Calendar cal) {
            if (dayCode != cal.get(Calendar.DAY_OF_WEEK)) {
                return false;
            }
            if (month != null && month != cal.get(Calendar.MONTH)) {
                return false;
            }
            // e.g. 31=Sun,30=Sat,29=Fri,28=Thu,27=Wed,26=Tue,25=Mon
            // e.g. 31-7 = 24
            return cal.get(Calendar.DAY_OF_MONTH) > cal.getActualMaximum(Calendar.DAY_OF_MONTH) - 7;
        }
    }

    private static class DateCheckerLastDayOfMonth implements DateChecker {
        private final Integer dayCode;
        private final Integer month;

        private DateCheckerLastDayOfMonth(Integer day, Integer month) {
            if (day != null) {
                if (day < 0 || day > 7) {
                    throw new IllegalArgumentException("Last xx day of the month has to be a day of week (0-7)");
                }
                dayCode = DAY_OF_WEEK_ARRAY[day];
            } else {
                dayCode = null;
            }
            this.month = month;
        }

        public boolean fits(Calendar cal) {
            if (dayCode != null && dayCode != cal.get(Calendar.DAY_OF_WEEK)) {
                return false;
            }
            if (month != null && month != cal.get(Calendar.MONTH)) {
                return false;
            }
            return (cal.get(Calendar.DAY_OF_MONTH) == cal.getActualMaximum(Calendar.DAY_OF_MONTH));
        }
    }

    private static class DateCheckerLastDayOfWeek implements DateChecker {
        private final Integer month;

        private DateCheckerLastDayOfWeek(Integer month) {
            this.month = month;
        }

        public boolean fits(Calendar cal) {
            if (month != null && month != cal.get(Calendar.MONTH)) {
                return false;
            }
            return (cal.get(Calendar.DAY_OF_WEEK) == Calendar.SATURDAY);
        }
    }

    private static class DateCheckerLastWeekday implements DateChecker {
        private final Integer dayCode;
        private final Integer month;

        private DateCheckerLastWeekday(Integer day, Integer month) {
            if (day != null) {
                if (day < 0 || day > 7) {
                    throw new IllegalArgumentException("Last xx day of the month has to be a day of week (0-7)");
                }
                dayCode = DAY_OF_WEEK_ARRAY[day];
            } else {
                dayCode = null;
            }
            this.month = month;
        }

        public boolean fits(Calendar cal) {
            if (dayCode != null && dayCode != cal.get(Calendar.DAY_OF_WEEK)) {
                return false;
            }
            if (month != null && month != cal.get(Calendar.MONTH)) {
                return false;
            }
            if (!isWeekday(cal)) {
                return false;
            }
            int day = cal.get(Calendar.DAY_OF_MONTH);
            int max = cal.getActualMaximum(Calendar.DAY_OF_MONTH);
            if (day == max) {
                return true;
            }
            int dayOfWeek = cal.get(Calendar.DAY_OF_WEEK);
            return day >= max - 2 && dayOfWeek == Calendar.FRIDAY;
        }
    }

    public static class DateCheckerMonthWeekday implements DateChecker {
        private final Integer day;
        private final Integer month;

        private DateCheckerMonthWeekday(Integer day, Integer month) {
            if (day != null) {
                if (day < 1 || day > 31) {
                    throw new IllegalArgumentException("xx day of the month has to be a in range (1-31)");
                }
            }
            this.day = day;
            this.month = month;
        }

        public boolean fits(Calendar cal) {
            if (month != null && month != cal.get(Calendar.MONTH)) {
                return false;
            }
            if (!isWeekday(cal)) {
                return false;
            }
            if (day == null) {
                return true;
            }

            Calendar work = (Calendar) cal.clone();
            int target = computeNearestWeekdayDay(day, work);
            return cal.get(Calendar.DAY_OF_MONTH) == target;
        }

        private static int computeNearestWeekdayDay(int day, Calendar work) {
            int max = work.getActualMaximum(Calendar.DAY_OF_MONTH);
            if (day <= max) {
                work.set(Calendar.DAY_OF_MONTH, day);
            } else {
                work.set(Calendar.DAY_OF_MONTH, max);
            }

            if (isWeekday(work)) {
                return work.get(Calendar.DAY_OF_MONTH);
            }
            if (work.get(Calendar.DAY_OF_WEEK) == Calendar.SATURDAY) {
                if (work.get(Calendar.DAY_OF_MONTH) > 1) {
                    work.add(Calendar.DAY_OF_MONTH, -1);
                    return work.get(Calendar.DAY_OF_MONTH);
                } else {
                    work.add(Calendar.DAY_OF_MONTH, 2);
                    return work.get(Calendar.DAY_OF_MONTH);
                }
            } else {
                // handle Sunday
                if (max == work.get(Calendar.DAY_OF_MONTH)) {
                    work.add(Calendar.DAY_OF_MONTH, -2);
                    return work.get(Calendar.DAY_OF_MONTH);
                } else {
                    work.add(Calendar.DAY_OF_MONTH, 1);
                    return work.get(Calendar.DAY_OF_MONTH);
                }
            }
        }
    }

    private static boolean isWeekday(Calendar cal) {
        int dayOfWeek = cal.get(Calendar.DAY_OF_WEEK);
        return !(dayOfWeek < Calendar.MONDAY || dayOfWeek > Calendar.FRIDAY);
    }
}