blob: 1320000f04413dc83315470d6e8cf09e909165b0 [file] [log] [blame]
package com.google.enterprise.adaptor;
import java.util.Arrays;
import java.util.BitSet;
import java.util.Calendar;
import java.util.Collections;
import java.util.Date;
import java.util.EnumMap;
import java.util.GregorianCalendar;
import java.util.List;
import java.util.TimeZone;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* Executes runnables based on whether the current time matches the provided
* cron time specification.
*/
class CronScheduler {
private static final Logger log
= Logger.getLogger(CronScheduler.class.getName());
private final ScheduledExecutorService executor;
private final TimeProvider timeProvider;
private final TimeZone timeZone;
public CronScheduler(ScheduledExecutorService executor) {
this(executor, new SystemTimeProvider(), TimeZone.getDefault());
}
CronScheduler(ScheduledExecutorService executor, TimeProvider timeProvider,
TimeZone timeZone) {
if (executor == null) {
throw new NullPointerException();
}
this.executor = executor;
this.timeProvider = timeProvider;
this.timeZone = timeZone;
}
/**
* Long-running runnables are not supported; the runnable must be completed by
* the time the next minute arrives.
*
* @return a Future representing pending completion of the task, and whose
* get() method will throw an exception upon cancellation.
*/
public Future<?> schedule(String pattern, Runnable runnable)
throws IllegalArgumentException {
CronPattern compiledPattern = CronPattern.create(pattern);
CronFilterRunnable toRun
= new CronFilterRunnable(compiledPattern, runnable);
final long minuteMillis = 60 * 1000;
long delayToNextMinuteMillis
= minuteMillis - (timeProvider.currentTimeMillis() % minuteMillis);
// Add a second to the delay, so we are less likely to fire before the
// minute starts.
long delayMillis = delayToNextMinuteMillis + 1000;
// If toRun takes a while to run, invocations to it will be queued and will
// be run as soon as it completes.
Future<?> future = executor.scheduleAtFixedRate(
toRun, delayMillis, minuteMillis, TimeUnit.MILLISECONDS);
return CronFuture.create(future, toRun);
}
public void reschedule(Future<?> future, String pattern) {
if (!(future instanceof CronFuture)) {
throw new IllegalArgumentException("Provided future is not a CronFuture");
}
CronFuture cronFuture = (CronFuture) future;
CronPattern compiledPattern = CronPattern.create(pattern);
cronFuture.getCronFilterRunnable().setPattern(compiledPattern);
}
/**
* Manager of per-schedule information.
*/
private class CronFilterRunnable implements Runnable {
private volatile CronPattern pattern;
private final Runnable delegate;
public CronFilterRunnable(CronPattern pattern, Runnable delegate) {
this.pattern = pattern;
this.delegate = delegate;
}
@Override
public void run() {
try {
Date now = new Date(timeProvider.currentTimeMillis());
if (pattern.matches(now, timeZone)) {
delegate.run();
}
} catch (Exception ex) {
// We need to prevent any exceptions from being thrown, because that
// would cause this task not to be run again.
log.log(Level.WARNING, "Exception during cron task", ex);
}
}
public CronPattern getPattern() {
return pattern;
}
public void setPattern(CronPattern pattern) {
this.pattern = pattern;
}
}
/**
* Forwards all calls to the delegate returned by {@link #delegate}.
*/
private abstract static class ForwardingFuture<V> implements Future<V> {
protected abstract Future<V> delegate();
@Override
public boolean cancel(boolean mayInterruptIfRunning) {
return delegate().cancel(mayInterruptIfRunning);
}
@Override
public V get() throws ExecutionException, InterruptedException {
return delegate().get();
}
@Override
public V get(long timeout, TimeUnit unit) throws ExecutionException,
InterruptedException, TimeoutException {
return delegate().get(timeout, unit);
}
@Override
public boolean isCancelled() {
return delegate().isCancelled();
}
@Override
public boolean isDone() {
return delegate().isDone();
}
}
private static class CronFuture<V> extends ForwardingFuture<V> {
private final Future<V> delegate;
private final CronFilterRunnable runnable;
public CronFuture(Future<V> delegate, CronFilterRunnable runnable) {
this.delegate = delegate;
this.runnable = runnable;
}
public static <V> CronFuture<V> create(
Future<V> delegate, CronFilterRunnable runnable) {
return new CronFuture<V>(delegate, runnable);
}
@Override
protected Future<V> delegate() {
return delegate;
}
public CronFilterRunnable getCronFilterRunnable() {
return runnable;
}
@Override
public String toString() {
return "CronFuture(" + runnable.getPattern().toString() + ")";
}
}
private static class CronPattern {
private static final List<Field> FIELDS
= Collections.unmodifiableList(Arrays.asList(Field.values()));
private static enum Field {
MINUTE(0, 60, Calendar.MINUTE, 0),
HOUR(0, 24, Calendar.HOUR_OF_DAY, 0),
DAY_OF_MONTH(1, 31, Calendar.DAY_OF_MONTH, 1),
MONTH(1, 12, Calendar.MONTH, 0),
DAY_OF_WEEK(0, 7, Calendar.DAY_OF_WEEK, 1) {
@Override
public int numberToIndex(int i) {
// Sunday is both 0 and 7.
if (i == 7) {
i = 0;
}
return super.numberToIndex(i);
}
};
private final int offset;
private final int numValues;
private final int calendarField;
private final int calendarOffset;
private Field(int offset, int numValues, int calendarField,
int calendarOffset) {
this.offset = offset;
this.numValues = numValues;
this.calendarField = calendarField;
this.calendarOffset = calendarOffset;
}
public int numberToIndex(int i) {
if (i < offset || i >= offset + numValues) {
throw new IllegalArgumentException("Bounds for " + name()
+ " is " + offset + "-" + (offset + numValues - 1) + ": " + i);
}
return i - offset;
}
public int calendarToIndex(Calendar calendar) {
return calendar.get(calendarField) - calendarOffset;
}
public int numValues() {
return numValues;
}
}
// minute: 0-59
// hour: 0-23
// day of month: 0-30, 0 is 1st day of month.
// month: 0-11, 0 in January.
// day of week: 0-6, 0 is Sunday.
// null for any value implies wildcard
private final EnumMap<Field, BitSet> fields;
private CronPattern(EnumMap<Field, BitSet> fields) {
this.fields = fields;
}
public boolean matches(Date date, TimeZone timeZone) {
GregorianCalendar calendar = new GregorianCalendar(timeZone);
calendar.setTime(date);
boolean dayMatches;
if (fields.get(Field.DAY_OF_MONTH) != null
&& fields.get(Field.DAY_OF_WEEK) != null) {
// Both day of month and day of week are restricted, so we OR the two
// instead of the typical AND. This is a special case of the cron
// format since both fields restrict the day.
dayMatches = matches(Field.DAY_OF_MONTH, calendar)
|| matches(Field.DAY_OF_WEEK, calendar);
} else {
dayMatches = matches(Field.DAY_OF_MONTH, calendar)
&& matches(Field.DAY_OF_WEEK, calendar);
}
return dayMatches
&& matches(Field.MINUTE, calendar)
&& matches(Field.HOUR, calendar)
&& matches(Field.MONTH, calendar);
}
private boolean matches(Field field, Calendar calendar) {
BitSet bitset = fields.get(field);
if (bitset == null) {
// Wildcard.
return true;
}
return bitset.get(field.calendarToIndex(calendar));
}
@Override
public String toString() {
return "" + fields.values();
}
public static CronPattern create(String timeSpecification)
throws IllegalArgumentException {
timeSpecification = timeSpecification.trim();
String[] stringFields = timeSpecification.split("\\s+", -1);
if (stringFields.length != FIELDS.size()) {
throw new IllegalArgumentException(
"Should have precisely 5 fields: "
+ "minute, hour, day of month, month, day of week");
}
EnumMap<Field, BitSet> fields = new EnumMap<Field, BitSet>(Field.class);
for (Field fieldType : FIELDS) {
String stringField = stringFields[fieldType.ordinal()];
if ("*".equals(stringField)) {
// Signal wildcard. Semantics of day of week and day of month vary
// based on raw wildcard vs any other value.
fields.put(fieldType, null);
continue;
}
BitSet set = new BitSet(fieldType.numValues());
for (String element : stringField.split(",", -1)) {
int step = 1;
{
String[] parts = element.split("/", 2);
if (parts.length == 2) {
element = parts[0];
step = Integer.parseInt(parts[1]);
}
}
if ("*".equals(element)) {
for (int j = 0; j < fieldType.numValues(); j += step) {
set.set(j);
}
} else { // Is numerical, which permits ranges.
int[] range = stringsToInts(element.split("-", 2));
if (range.length == 1) {
set.set(fieldType.numberToIndex(range[0]));
} else {
// Set each individually since after numberToIndex, range[0] may
// be larger than range[1] in the case of weekdays. This also
// causes trouble with steps with ranges (1-7/2 should become
// 0,1,3,5), so handle step at the same time.
for (int j = range[0]; j <= range[1]; j += step) {
set.set(fieldType.numberToIndex(j));
}
}
}
}
fields.put(fieldType, set);
}
return new CronPattern(fields);
}
private static int[] stringsToInts(String[] ss) {
int[] is = new int[ss.length];
for (int i = 0; i < ss.length; i++) {
is[i] = Integer.parseInt(ss[i]);
}
return is;
}
}
}