Added CronSequenceGenerator. CronTrigger now delegates to the generator, and thus we no longer have a dependency on quartz.

This commit is contained in:
Mark Fisher
2008-10-07 14:19:13 +00:00
parent 05d9528024
commit 71b17fa8a4
6 changed files with 571 additions and 20 deletions

View File

@@ -15,6 +15,5 @@
<classpathentry kind="var" path="IVY_CACHE/org.springframework/org.springframework.core/2.5.5.A/org.springframework.core-2.5.5.A.jar" sourcepath="/IVY_CACHE/org.springframework/org.springframework.core/2.5.5.A/org.springframework.core-sources-2.5.5.A.jar"/>
<classpathentry kind="var" path="IVY_CACHE/org.springframework/org.springframework.transaction/2.5.5.A/org.springframework.transaction-2.5.5.A.jar" sourcepath="/IVY_CACHE/org.springframework/org.springframework.transaction/2.5.5.A/org.springframework.transaction-sources-2.5.5.A.jar"/>
<classpathentry kind="var" path="IVY_CACHE/org.springframework/org.springframework.test/2.5.5.A/org.springframework.test-2.5.5.A.jar" sourcepath="IVY_CACHE/org.springframework/org.springframework.test/2.5.5.A/org.springframework.test-sources-2.5.5.A.jar"/>
<classpathentry kind="var" path="IVY_CACHE/com.opensymphony.quartz/com.springsource.org.quartz/1.6.0/com.springsource.org.quartz-1.6.0.jar" sourcepath="/IVY_CACHE/com.opensymphony.quartz/com.springsource.org.quartz/1.6.0/com.springsource.org.quartz-sources-1.6.0.jar"/>
<classpathentry kind="output" path="target/classes"/>
</classpath>

View File

@@ -26,7 +26,6 @@
<dependency org="org.springframework" name="org.springframework.context" rev="2.5.5.A" conf="compile->runtime"/>
<dependency org="org.springframework" name="org.springframework.transaction" rev="2.5.5.A" conf="compile->runtime"/>
<dependency org="org.springframework" name="org.springframework.test" rev="2.5.5.A" conf="test->runtime"/>
<dependency org="com.opensymphony.quartz" name="com.springsource.org.quartz" rev="1.6.0" conf="compile->runtime"/>
</dependencies>
</ivy-module>

View File

@@ -0,0 +1,307 @@
/*
* Copyright 2002-2008 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.integration.scheduling;
import java.util.BitSet;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import org.springframework.util.StringUtils;
/**
* Date sequence generator for a <a
* href="http://www.manpagez.com/man/5/crontab/">Crontab pattern</a> allowing
* client to specify a pattern that the sequence matches. The pattern is a list
* of 6 single space separated fields representing (second, minute, hour, day,
* month, weekday). Month and weekday names can be given as the first three
* letters of the English names.<br/><br/>
*
* Example patterns
* <ul>
* <li>"0 0 * * * *" = the top of every hour of every day.</li>
* <li>"*&#47;10 * * * * *" = every ten seconds.</li>
* <li>"0 0 8-10 * * *" = 8, 9 and 10 o'clock of every day.</li>
* <li>"0 0 8-10/30 * * *" = 8:00, 8:30, 9:00, 9:30 and 10 o'clock every day.</li>
* <li>"0 0 9-17 * * MON-FRI" = on the hour nine-to-five weekdays</li>
* <li>"0 0 0 25 12 ?" = every Christmas Day at midnight</li>
* </ul>
*
* @author Dave Syer
*/
public class CronSequenceGenerator {
private final BitSet seconds = new BitSet(60);
private final BitSet minutes = new BitSet(60);
private final BitSet hours = new BitSet(24);
private final BitSet daysOfWeek = new BitSet(7);
private final BitSet daysOfMonth = new BitSet(31);
private final BitSet months = new BitSet(12);
private final String pattern;
/**
* Construct a {@link CronSequenceGenerator} from the pattern provided.
*
* @param pattern a single space separated list of time fields
*
* @throws IllegalArgumentException if the pattern cannot be parsed
*/
public CronSequenceGenerator(String pattern) throws IllegalArgumentException {
this.pattern = pattern;
parse(pattern);
}
/**
* Get the next {@link Date} in the sequence matching the Cron pattern and
* after the value provided. The return value will have a whole number of
* seconds, and will be after the input value.
*
* @param date a seed value
* @return the next value matching the pattern
*/
public Date next(Date date) {
Calendar calendar = new GregorianCalendar();
calendar.setTime(date);
// Truncate to the next whole second
calendar.add(Calendar.SECOND, 1);
calendar.set(Calendar.MILLISECOND, 0);
int second = calendar.get(Calendar.SECOND);
int minute = calendar.get(Calendar.MINUTE);
int hour = calendar.get(Calendar.HOUR_OF_DAY);
int dayOfWeek = calendar.get(Calendar.DAY_OF_WEEK);
int dayOfMonth = calendar.get(Calendar.DAY_OF_MONTH);
int month = calendar.get(Calendar.MONTH);
month = findNext(months, month, 12, calendar, Calendar.MONTH, Calendar.DAY_OF_MONTH, Calendar.HOUR_OF_DAY,
Calendar.MINUTE, Calendar.SECOND);
dayOfMonth = findNextDay(calendar, daysOfMonth, dayOfMonth, daysOfWeek, dayOfWeek, 366);
hour = findNext(hours, hour, 24, calendar, Calendar.HOUR, Calendar.MINUTE, Calendar.SECOND);
minute = findNext(minutes, minute, 60, calendar, Calendar.MINUTE, Calendar.SECOND);
second = findNext(seconds, second, 60, calendar, Calendar.SECOND);
return calendar.getTime();
}
/**
* @param calendar
* @return
*/
private int findNextDay(Calendar calendar, BitSet daysOfMonth, int dayOfMonth, BitSet daysOfWeek, int dayOfWeek,
int max) {
int count = 0;
while ((!daysOfMonth.get(dayOfMonth) || !daysOfWeek.get(dayOfWeek)) && count++ < max) {
calendar.add(Calendar.DAY_OF_MONTH, 1);
dayOfMonth = calendar.get(Calendar.DAY_OF_MONTH);
dayOfWeek = calendar.get(Calendar.DAY_OF_WEEK);
reset(calendar, Calendar.HOUR_OF_DAY, Calendar.MINUTE, Calendar.SECOND);
}
if (count > max) {
throw new IllegalStateException("Overflow in day for expression=" + pattern);
}
return dayOfMonth;
}
/**
* Search the bits provided for the next set bit after the value provided,
* and reset the calendar.
*
* @param bits a {@link BitSet} representing the allowed values of the field
* @param value the current value of the field
* @param max the largest value that the field can have
* @param calendar the calendar to increment as we move through the bits
* @param field the field to increment in the calendar (@see
* {@link Calendar} for the static constants defining valid fields)
* @param lowerOrders the Calendar field ids that should be reset (i.e. the
* ones of lower significance than the field of interest)
*
* @return the value of the calendar field that is next in the sequence
*/
private int findNext(BitSet bits, int value, int max, Calendar calendar, int field, int... lowerOrders) {
// TODO: more efficient to use BitSet.nextSet(int)
int count = 0;
while (!bits.get(value) && count++ < max) {
calendar.add(field, 1);
value = calendar.get(field);
reset(calendar, lowerOrders);
}
if (count > max) {
throw new IllegalStateException(String.format("Overflow in field=%d for expression=%s", field, pattern));
}
return value;
}
/**
* Reset the calendar setting all the fields provided to zero.
*
* @param calendar
* @param fields
*/
private void reset(Calendar calendar, int... fields) {
for (int field : fields) {
calendar.set(field, 0);
}
}
/**
* @param expression
*/
private void parse(String expression) throws IllegalArgumentException {
String[] fields = StringUtils.delimitedListToStringArray(expression, " ");
if (fields.length != 6) {
throw new IllegalArgumentException(String.format("Expression must consist of 6 fields (found %d in %s)",
fields.length, fields));
}
setNumberHits(seconds, fields[0], 60);
setNumberHits(minutes, fields[1], 60);
setNumberHits(hours, fields[2], 24);
setDays(daysOfMonth, fields[3], 31);
setNumberHits(months, replaceOrdinals(fields[4], "JAN,FEB,MAR,APR,MAY,JUN,JUL,AUG,SEP,OCT,NOV,DEC"), 12);
setDays(daysOfWeek, replaceOrdinals(fields[5], "SUN,MON,TUE,WED,THU,FRI,SAT"), 8);
if (daysOfWeek.get(7)) {
// Sunday can be represented as 0 or 7
daysOfWeek.set(0);
daysOfWeek.clear(7);
}
}
/**
* Replace the values in the commaSeparatedList (case insensitive) with
* their index in the list.
*
* @param value
* @param commaSeparatedList
* @return a new string with the values from the list replaced
*/
private String replaceOrdinals(String value, String commaSeparatedList) {
String[] list = StringUtils.commaDelimitedListToStringArray(commaSeparatedList);
for (int i = 0; i < list.length; i++) {
String item = list[i].toUpperCase();
value = StringUtils.replace(value.toUpperCase(), item, "" + i);
}
return value;
}
/**
* @param bits
* @param field
* @param max
*/
private void setDays(BitSet bits, String field, int max) {
if (field.contains("?")) {
field = "*";
}
setNumberHits(bits, field, max);
}
/**
* @param bits
* @param value
* @param max
* @return
*/
private void setNumberHits(BitSet bits, String value, int max) {
String[] fields = StringUtils.delimitedListToStringArray(value, ",");
for (String field : fields) {
if (!field.contains("/")) {
// Not an incrementer so it must be a range (possibly empty)
int[] range = getRange(field, max);
bits.set(range[0], range[1] + 1);
}
else {
String[] split = StringUtils.delimitedListToStringArray(field, "/");
if (split.length > 2) {
throw new IllegalArgumentException("Incrementer has more than two fields: " + field);
}
int[] range = getRange(split[0], max);
if (!split[0].contains("-")) {
range[1] = max - 1;
}
int delta = Integer.valueOf(split[1]);
for (int i = range[0]; i <= range[1]; i += delta) {
bits.set(i);
}
}
}
}
/**
* @param field
* @return
*/
private int[] getRange(String field, int max) {
int[] result = new int[2];
if (field.contains("*")) {
result[0] = 0;
result[1] = max - 1;
return result;
}
if (!field.contains("-")) {
result[0] = result[1] = Integer.valueOf(field);
}
else {
String[] split = StringUtils.delimitedListToStringArray(field, "-");
if (split.length > 2) {
throw new IllegalArgumentException("Range has more than two fields: " + field);
}
result[0] = Integer.valueOf(split[0]);
result[1] = Integer.valueOf(split[1]);
}
return result;
}
/**
* @see Object#equals(Object)
*/
@Override
public boolean equals(Object obj) {
if (!(obj instanceof CronSequenceGenerator)) {
return false;
}
CronSequenceGenerator cron = (CronSequenceGenerator) obj;
return cron.months.equals(months) && cron.daysOfMonth.equals(daysOfMonth) && cron.daysOfWeek.equals(daysOfWeek)
&& cron.hours.equals(hours) && cron.minutes.equals(minutes) && cron.seconds.equals(seconds);
}
/**
* @see Object#hashCode()
*/
@Override
public int hashCode() {
return 37 + 17 * months.hashCode() + 29 * daysOfMonth.hashCode() + 37 * daysOfWeek.hashCode() + 41
* hours.hashCode() + 53 * minutes.hashCode() + 61 * seconds.hashCode();
}
}

View File

@@ -16,41 +16,49 @@
package org.springframework.integration.scheduling;
import java.text.ParseException;
import java.util.Date;
import org.quartz.CronExpression;
/**
* A trigger that uses a cron expression.
* A trigger that uses a cron expression. See {@link CronSequenceGenerator}
* for a detailed description of the expression pattern syntax.
*
* @author Mark Fisher
*/
public class CronTrigger implements Trigger {
private final CronExpression expression;
private final CronSequenceGenerator cronSequenceGenerator;
/**
* Create a trigger for the given cron expression.
*/
public CronTrigger(String expression) {
try {
this.expression = new CronExpression(expression);
}
catch (ParseException e) {
throw new IllegalArgumentException(
"failed to parse cron expression: " + expression);
}
public CronTrigger(String expression) throws IllegalArgumentException {
this.cronSequenceGenerator = new CronSequenceGenerator(expression);
}
/**
* Return the next time a task should run. Determined by this trigger's
* cron expression.
* Return the next time a task should run. Determined by consulting this
* trigger's cron expression compared with the lastCompleteTime. If the
* lastCompleteTime is <code>null</code>, the current time is used.
*/
public Date getNextRunTime(Date lastScheduledRunTime, Date lastCompleteTime) {
return this.expression.getNextValidTimeAfter(new Date());
Date date = (lastCompleteTime != null) ? lastCompleteTime : new Date();
return this.cronSequenceGenerator.next(date);
}
@Override
public boolean equals(Object other) {
if (other == null || !(other instanceof CronTrigger)) {
return false;
}
return this.cronSequenceGenerator.equals(
((CronTrigger) other).cronSequenceGenerator);
}
@Override
public int hashCode() {
return this.cronSequenceGenerator.hashCode();
}
}

View File

@@ -0,0 +1,239 @@
/*
* Copyright 2002-2008 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.integration.scheduling;
import static org.junit.Assert.assertEquals;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import org.junit.Before;
import org.junit.Test;
/**
* @author Dave Syer
* @author Mark Fisher
*/
public class CronTriggerTests {
private Calendar calendar = new GregorianCalendar();
private Date date = new Date();
/**
* @param calendar
*/
private void roundup(Calendar calendar) {
calendar.add(Calendar.SECOND, 1);
calendar.set(Calendar.MILLISECOND, 0);
}
@Before
public void setUp() {
calendar.setTime(date);
roundup(calendar);
}
@Test
public void testMatchAll() throws Exception {
CronTrigger trigger = new CronTrigger("* * * * * *");
assertEquals(calendar.getTime(), trigger.getNextRunTime(null, date));
}
@Test
public void testMatchLastSecond() throws Exception {
CronTrigger trigger = new CronTrigger("* * * * * *");
GregorianCalendar calendar = new GregorianCalendar();
calendar.set(Calendar.SECOND, 58);
assertMatchesNextSecond(trigger, calendar);
}
@Test
public void testMatchSpecificSecond() throws Exception {
CronTrigger trigger = new CronTrigger("10 * * * * *");
GregorianCalendar calendar = new GregorianCalendar();
calendar.set(Calendar.SECOND, 9);
assertMatchesNextSecond(trigger, calendar);
}
@Test
public void testIncrementSecondByOne() throws Exception {
CronTrigger trigger = new CronTrigger("11 * * * * *");
calendar.set(Calendar.SECOND, 10);
Date date = calendar.getTime();
calendar.add(Calendar.SECOND, 1);
assertEquals(calendar.getTime(), trigger.getNextRunTime(null, date));
}
@Test
public void testIncrementSecondAndRollover() throws Exception {
CronTrigger trigger = new CronTrigger("10 * * * * *");
calendar.set(Calendar.SECOND, 11);
Date date = calendar.getTime();
calendar.add(Calendar.SECOND, 59);
assertEquals(calendar.getTime(), trigger.getNextRunTime(null, date));
}
@Test
public void testSecondRange() throws Exception {
CronTrigger trigger = new CronTrigger("10-15 * * * * *");
calendar.set(Calendar.SECOND, 9);
assertMatchesNextSecond(trigger, calendar);
calendar.set(Calendar.SECOND, 14);
assertMatchesNextSecond(trigger, calendar);
}
@Test
public void testIncrementMinuteByOne() throws Exception {
CronTrigger trigger = new CronTrigger("* 11 * * * *");
calendar.set(Calendar.MINUTE, 10);
Date date = calendar.getTime();
calendar.add(Calendar.MINUTE, 1);
calendar.set(Calendar.SECOND, 0);
assertEquals(calendar.getTime(), trigger.getNextRunTime(null, date));
}
@Test
public void testIncrementMinuteAndRollover() throws Exception {
CronTrigger trigger = new CronTrigger("* 10 * * * *");
calendar.set(Calendar.MINUTE, 11);
Date date = calendar.getTime();
calendar.add(Calendar.MINUTE, 59);
calendar.set(Calendar.SECOND, 0);
assertEquals(calendar.getTime(), trigger.getNextRunTime(null, date));
}
@Test
public void testIncrementDayOfMonthByOne() throws Exception {
CronTrigger trigger = new CronTrigger("* * * 10 * *");
calendar.set(Calendar.DAY_OF_MONTH, 9);
Date date = calendar.getTime();
calendar.add(Calendar.DAY_OF_MONTH, 1);
calendar.set(Calendar.HOUR_OF_DAY, 0);
calendar.set(Calendar.MINUTE, 0);
calendar.set(Calendar.SECOND, 0);
assertEquals(calendar.getTime(), trigger.getNextRunTime(null, date));
}
@Test
public void testIncrementDayOfMonthAndRollover() throws Exception {
CronTrigger trigger = new CronTrigger("* * * 10 * *");
calendar.set(Calendar.DAY_OF_MONTH, 11);
Date date = calendar.getTime();
calendar.add(Calendar.MONTH, 1);
calendar.set(Calendar.DAY_OF_MONTH, 10);
calendar.set(Calendar.HOUR_OF_DAY, 0);
calendar.set(Calendar.MINUTE, 0);
calendar.set(Calendar.SECOND, 0);
assertEquals(calendar.getTime(), trigger.getNextRunTime(null, date));
}
@Test
public void testIncrementDayOfWeekByOne() throws Exception {
CronTrigger trigger = new CronTrigger("* * * * * 2");
calendar.set(Calendar.DAY_OF_WEEK, 1);
Date date = calendar.getTime();
calendar.add(Calendar.DAY_OF_WEEK, 1);
calendar.set(Calendar.HOUR_OF_DAY, 0);
calendar.set(Calendar.MINUTE, 0);
calendar.set(Calendar.SECOND, 0);
assertEquals(calendar.getTime(), trigger.getNextRunTime(null, date));
}
@Test
public void testIncrementDayOfWeekAndRollover() throws Exception {
CronTrigger trigger = new CronTrigger("* * * * * 2");
calendar.set(Calendar.DAY_OF_WEEK, 3);
Date date = calendar.getTime();
calendar.add(Calendar.DAY_OF_MONTH, 6);
calendar.set(Calendar.HOUR_OF_DAY, 0);
calendar.set(Calendar.MINUTE, 0);
calendar.set(Calendar.SECOND, 0);
assertEquals(calendar.getTime(), trigger.getNextRunTime(null, date));
}
@Test
public void testDayOfWeekIndifferent() throws Exception {
CronTrigger trigger1 = new CronTrigger("* * * 2 * *");
CronTrigger trigger2 = new CronTrigger("* * * 2 * ?");
assertEquals(trigger1, trigger2);
}
@Test
public void testSecondIncrementer() throws Exception {
CronTrigger trigger1 = new CronTrigger("57,59 * * * * *");
CronTrigger trigger2 = new CronTrigger("57/2 * * * * *");
assertEquals(trigger1, trigger2);
}
@Test
public void testSecondIncrementerWithRange() throws Exception {
CronTrigger trigger1 = new CronTrigger("1,3,5 * * * * *");
CronTrigger trigger2 = new CronTrigger("1-6/2 * * * * *");
assertEquals(trigger1, trigger2);
}
@Test
public void testHourIncrementer() throws Exception {
CronTrigger trigger1 = new CronTrigger("* * 4,8,12,16,20 * * *");
CronTrigger trigger2 = new CronTrigger("* * 4/4 * * *");
assertEquals(trigger1, trigger2);
}
@Test
public void testDayNames() throws Exception {
CronTrigger trigger1 = new CronTrigger("* * * * * 0-6");
CronTrigger trigger2 = new CronTrigger("* * * * * TUE,WED,THU,FRI,SAT,SUN,MON");
assertEquals(trigger1, trigger2);
}
@Test
public void testSundaySynonym() throws Exception {
CronTrigger trigger1 = new CronTrigger("* * * * * 0");
CronTrigger trigger2 = new CronTrigger("* * * * * 7");
assertEquals(trigger1, trigger2);
}
@Test
public void testMonthNames() throws Exception {
CronTrigger trigger1 = new CronTrigger("* * * * 0-11 *");
CronTrigger trigger2 = new CronTrigger("* * * * FEB,JAN,MAR,APR,MAY,JUN,JUL,AUG,SEP,OCT,NOV,DEC *");
assertEquals(trigger1, trigger2);
}
@Test
public void testMonthNamesMixedCase() throws Exception {
CronTrigger trigger1 = new CronTrigger("* * * * 1 *");
CronTrigger trigger2 = new CronTrigger("* * * * Feb *");
assertEquals(trigger1, trigger2);
}
/**
* @param trigger
* @param calendar
*/
private void assertMatchesNextSecond(CronTrigger trigger, Calendar calendar) {
Date date = calendar.getTime();
roundup(calendar);
assertEquals(calendar.getTime(), trigger.getNextRunTime(null, date));
}
}

View File

@@ -5,8 +5,7 @@ Bundle-ManifestVersion: 2
Import-Template:
org.springframework.*;version="[2.5.5.A, 3.0.0)",
org.apache.commons.logging;version="[1.1.1, 2.0.0)",
org.aopalliance.*;version="[1.0.0,2.0.0)",
org.quartz.*;version="[1.6.0, 2.0.0)";resolution:=optional
org.aopalliance.*;version="[1.0.0,2.0.0)"
Unversioned-Imports:
org.w3c.dom