
hudson.scheduler.CronTab Maven / Gradle / Ivy
Show all versions of hudson-core Show documentation
/*******************************************************************************
*
* 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 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<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<
* 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 ANTLRException {
set(format,1);
}
/**
* Returns true if n-th bit is on.
*/
private boolean checkBits(long bitMask, int n) {
return (bitMask|(1L<
* 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};
}