/*
* The MIT License
*
* Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi, InfraDNA, Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package hudson.scheduler;
import antlr.ANTLRException;
import java.io.StringReader;
import java.util.Calendar;
import java.util.GregorianCalendar;
import java.util.Locale;
import static java.util.Calendar.*;
/**
* 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 ANTLRException {
this(format,1);
}
public CronTab(String format, int line) throws ANTLRException {
set(format, line);
}
private void set(String format, int line) throws ANTLRException {
CrontabLexer lexer = new CrontabLexer(new StringReader(format));
lexer.setLine(line);
CrontabParser parser = new CrontabParser(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 ANTLRException {
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};
}