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

hudson.scheduler.CronTab Maven / Gradle / Ivy

The newest version!
/*******************************************************************************
 *
 * Copyright (c) 2004-2009 Oracle Corporation.
 *
 * 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:
 * 
 *    Kohsuke Kawaguchi, InfraDNA, Inc.
 *
 *
 *******************************************************************************/ 

package hudson.scheduler;

import java.util.Calendar;
import java.util.GregorianCalendar;
import java.util.Locale;

import static java.util.Calendar.*;

import org.antlr.runtime.ANTLRStringStream;
import org.antlr.runtime.CommonTokenStream;
import org.antlr.runtime.RecognitionException;

/**
 * Table for driving scheduled tasks.
 *
 * @author Kohsuke Kawaguchi
 */
public final class CronTab {

    /**
     * bits[0]: minutes bits[1]: hours bits[2]: days bits[3]: months
     *
     * false:not scheduled <-> true scheduled
     */
    final long[] bits = new long[4];
    int dayOfWeek;
    /**
     * Textual representation.
     */
    private String spec;

    public CronTab(String format) throws RecognitionException {
        this(format, 1);
    }

    public CronTab(String format, int line) throws RecognitionException {
        set(format, line);
    }

    private void set(String format, int line) throws RecognitionException {
        ANTLRStringStream stream = new ANTLRStringStream(format);
        stream.setLine(line);
        CrontabLexer lexer = new CrontabLexer(stream);
        CrontabParser parser = new CrontabParser(new CommonTokenStream(lexer));
        spec = format;

        parser.startRule(this);
        if ((dayOfWeek & (1 << 7)) != 0) {
            dayOfWeek |= 1; // copy bit 7 over to bit 0
        }
    }

    /**
     * Returns true if the given calendar matches
     */
    boolean check(Calendar cal) {
        if (!checkBits(bits[0], cal.get(MINUTE))) {
            return false;
        }
        if (!checkBits(bits[1], cal.get(HOUR_OF_DAY))) {
            return false;
        }
        if (!checkBits(bits[2], cal.get(DAY_OF_MONTH))) {
            return false;
        }
        if (!checkBits(bits[3], cal.get(MONTH) + 1)) {
            return false;
        }
        if (!checkBits(dayOfWeek, cal.get(Calendar.DAY_OF_WEEK) - 1)) {
            return false;
        }

        return true;
    }

    private static abstract class CalendarField {

        /**
         * {@link Calendar} field ID.
         */
        final int field;
        /**
         * Lower field is a calendar field whose value needs to be reset when we
         * change the value in this field. For example, if we modify the value
         * in HOUR, MINUTES must be reset.
         */
        final CalendarField lowerField;
        /**
         * Whether this field is 0-origin or 1-origin differs between Crontab
         * and {@link Calendar}, so this field adjusts that. If crontab is 1
         * origin and calendar is 0 origin, this field is 1 that is the value is
         * {@code (cronOrigin-calendarOrigin)}
         */
        final int offset;
        /**
         * When we reset this field, we set the field to this value. For
         * example, resetting {@link Calendar#DAY_OF_MONTH} means setting it to
         * 1.
         */
        final int min;
        /**
         * If this calendar field has other aliases such that a change in this
         * field modifies other field values, then true.
         */
        final boolean redoAdjustmentIfModified;
        /**
         * What is this field? Useful for debugging
         */
        private final String displayName;

        private CalendarField(String displayName, int field, int min, int offset, boolean redoAdjustmentIfModified, CalendarField lowerField) {
            this.displayName = displayName;
            this.field = field;
            this.min = min;
            this.redoAdjustmentIfModified = redoAdjustmentIfModified;
            this.lowerField = lowerField;
            this.offset = offset;
        }

        /**
         * Gets the current value of this field in the given calendar.
         */
        int valueOf(Calendar c) {
            return c.get(field) + offset;
        }

        void addTo(Calendar c, int i) {
            c.add(field, i);
        }

        void setTo(Calendar c, int i) {
            c.set(field, i - offset);
        }

        void clear(Calendar c) {
            setTo(c, min);
        }

        /**
         * Given the value 'n' (which represents the current value), finds the
         * smallest x such that: 1) x matches the specified {@link CronTab} (as
         * far as this field is concerned.) 2) x>=n (inclusive)
         *
         * If there's no such bit, return -1. Note that if 'n' already matches
         * the crontab, the same n will be returned.
         */
        private int ceil(CronTab c, int n) {
            long bits = bits(c);
            while ((bits | (1L << n)) != bits) {
                if (n > 60) {
                    return -1;
                }
                n++;
            }
            return n;
        }

        /**
         * Given a bit mask, finds the first bit that's on, and return its
         * index.
         */
        private int first(CronTab c) {
            return ceil(c, 0);
        }

        private int floor(CronTab c, int n) {
            long bits = bits(c);
            while ((bits | (1L << n)) != bits) {
                if (n == 0) {
                    return -1;
                }
                n--;
            }
            return n;
        }

        private int last(CronTab c) {
            return floor(c, 63);
        }

        /**
         * Extracts the bit masks from the given {@link CronTab} that matches
         * this field.
         */
        abstract long bits(CronTab c);

        /**
         * Increment the next field.
         */
        abstract void rollUp(Calendar cal, int i);
        private static final CalendarField MINUTE = new CalendarField("minute", Calendar.MINUTE, 0, 0, false, null) {
            long bits(CronTab c) {
                return c.bits[0];
            }

            void rollUp(Calendar cal, int i) {
                cal.add(Calendar.HOUR_OF_DAY, i);
            }
        };
        private static final CalendarField HOUR = new CalendarField("hour", Calendar.HOUR_OF_DAY, 0, 0, false, MINUTE) {
            long bits(CronTab c) {
                return c.bits[1];
            }

            void rollUp(Calendar cal, int i) {
                cal.add(Calendar.DAY_OF_MONTH, i);
            }
        };
        private static final CalendarField DAY_OF_MONTH = new CalendarField("day", Calendar.DAY_OF_MONTH, 1, 0, true, HOUR) {
            long bits(CronTab c) {
                return c.bits[2];
            }

            void rollUp(Calendar cal, int i) {
                cal.add(Calendar.MONTH, i);
            }
        };
        private static final CalendarField MONTH = new CalendarField("month", Calendar.MONTH, 1, 1, false, DAY_OF_MONTH) {
            long bits(CronTab c) {
                return c.bits[3];
            }

            void rollUp(Calendar cal, int i) {
                cal.add(Calendar.YEAR, i);
            }
        };
        private static final CalendarField DAY_OF_WEEK = new CalendarField("dow", Calendar.DAY_OF_WEEK, 1, -1, true, HOUR) {
            long bits(CronTab c) {
                return c.dayOfWeek;
            }

            void rollUp(Calendar cal, int i) {
                cal.add(Calendar.DAY_OF_WEEK, 7);
            }

            @Override
            void setTo(Calendar c, int i) {
                int v = i - offset;
                c.set(field, v);
                if (v < c.getFirstDayOfWeek()) {
                    // in crontab, the first DoW is always Sunday, but in Java, it can be Monday or in theory arbitrary other days.
                    // When first DoW is 1/2 Monday, calendar points to 1/2 Monday, setting the DoW to Sunday makes
                    // the calendar moves forward to 1/8 Sunday, instead of 1/1 Sunday. So we need to compensate that effect here.
                    addTo(c, -7);
                }
            }
        };
        private static final CalendarField[] ADJUST_ORDER = {
            MONTH, DAY_OF_MONTH, DAY_OF_WEEK, HOUR, MINUTE
        };
    }

    /**
     * Computes the nearest future timestamp that matches this cron tab. 

* More precisely, given the time 't', computes another smallest time x such * that: * *

  • x >= t (inclusive)
  • x matches this crontab
* *

Note that if t already matches this cron, it's returned as is. */ public Calendar ceil(long t) { Calendar cal = new GregorianCalendar(Locale.US); cal.setTimeInMillis(t); return ceil(cal); } /** * See {@link #ceil(long)}. * * This method modifies the given calendar and returns the same object. */ public Calendar ceil(Calendar cal) { OUTER: while (true) { for (CalendarField f : CalendarField.ADJUST_ORDER) { int cur = f.valueOf(cal); int next = f.ceil(this, cur); if (cur == next) { continue; // this field is already in a good shape. move on to next } // we are modifying this field, so clear all the lower level fields for (CalendarField l = f.lowerField; l != null; l = l.lowerField) { l.clear(cal); } if (next < 0) { // we need to roll over to the next field. f.rollUp(cal, 1); f.setTo(cal, f.first(this)); // since higher order field is affected by this, we need to restart from all over continue OUTER; } else { f.setTo(cal, next); if (f.redoAdjustmentIfModified) { continue OUTER; // when we modify DAY_OF_MONTH and DAY_OF_WEEK, do it all over from the top } } } return cal; // all fields adjusted } } /** * Computes the nearest past timestamp that matched this cron tab.

More * precisely, given the time 't', computes another smallest time x such * that: * *

  • x <= t (inclusive)
  • x matches this crontab
* *

Note that if t already matches this cron, it's returned as is. */ public Calendar floor(long t) { Calendar cal = new GregorianCalendar(Locale.US); cal.setTimeInMillis(t); return floor(cal); } /** * See {@link #floor(long)} * * This method modifies the given calendar and returns the same object. */ public Calendar floor(Calendar cal) { OUTER: while (true) { for (CalendarField f : CalendarField.ADJUST_ORDER) { int cur = f.valueOf(cal); int next = f.floor(this, cur); if (cur == next) { continue; // this field is already in a good shape. move on to next } // we are modifying this field, so clear all the lower level fields for (CalendarField l = f.lowerField; l != null; l = l.lowerField) { l.clear(cal); } if (next < 0) { // we need to borrow from the next field. f.rollUp(cal, -1); // the problem here, in contrast with the ceil method, is that // the maximum value of the field is not always a fixed value (that is, day of month) // so we zero-clear all the lower fields, set the desired value +1, f.setTo(cal, f.last(this)); f.addTo(cal, 1); // then subtract a minute to achieve maximum values on all the lower fields, // with the desired value in 'f' CalendarField.MINUTE.addTo(cal, -1); // since higher order field is affected by this, we need to restart from all over continue OUTER; } else { f.setTo(cal, next); f.addTo(cal, 1); CalendarField.MINUTE.addTo(cal, -1); if (f.redoAdjustmentIfModified) { continue OUTER; // when we modify DAY_OF_MONTH and DAY_OF_WEEK, do it all over from the top } } } return cal; // all fields adjusted } } void set(String format) throws RecognitionException { set(format, 1); } /** * Returns true if n-th bit is on. */ private boolean checkBits(long bitMask, int n) { return (bitMask | (1L << n)) == bitMask; } public String toString() { return super.toString() + "[" + toString("minute", bits[0]) + ',' + toString("hour", bits[1]) + ',' + toString("dayOfMonth", bits[2]) + ',' + toString("month", bits[3]) + ',' + toString("dayOfWeek", dayOfWeek) + ']'; } private String toString(String key, long bit) { return key + '=' + Long.toHexString(bit); } /** * Checks if this crontab entry looks reasonable, and if not, return an * warning message. * *

The point of this method is to catch syntactically correct but * semantically suspicious combinations, like "* 0 * * *" */ public String checkSanity() { for (int i = 0; i < 5; i++) { long bitMask = (i < 4) ? bits[i] : (long) dayOfWeek; for (int j = LOWER_BOUNDS[i]; j <= UPPER_BOUNDS[i]; j++) { if (!checkBits(bitMask, j)) { // this rank has a sparse entry. // if we have a sparse rank, one of them better be the left-most. if (i > 0) { return "Do you really mean \"every minute\" when you say \"" + spec + "\"? " + "Perhaps you meant \"0 " + spec.substring(spec.indexOf(' ') + 1) + "\""; } // once we find a sparse rank, upper ranks don't matter return null; } } } return null; } // lower/uppser bounds of fields private static final int[] LOWER_BOUNDS = new int[]{0, 0, 1, 0, 0}; private static final int[] UPPER_BOUNDS = new int[]{59, 23, 31, 12, 7}; }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy