/*******************************************************************************
*
* 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. <p>
* More precisely, given the time 't', computes another smallest time x such
* that:
*
* <ul> <li>x >= t (inclusive) <li>x matches this crontab </ul>
*
* <p> 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. <p> More
* precisely, given the time 't', computes another smallest time x such
* that:
*
* <ul> <li>x <= t (inclusive) <li>x matches this crontab </ul>
*
* <p> 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.
*
* <p> 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};
}