diff --git a/org.springframework.integration/.classpath b/org.springframework.integration/.classpath index 25f781f33a..34c01dfac1 100644 --- a/org.springframework.integration/.classpath +++ b/org.springframework.integration/.classpath @@ -15,6 +15,5 @@ - diff --git a/org.springframework.integration/ivy.xml b/org.springframework.integration/ivy.xml index 4a191a1cf6..144dc2d43e 100644 --- a/org.springframework.integration/ivy.xml +++ b/org.springframework.integration/ivy.xml @@ -26,7 +26,6 @@ - \ No newline at end of file diff --git a/org.springframework.integration/src/main/java/org/springframework/integration/scheduling/CronSequenceGenerator.java b/org.springframework.integration/src/main/java/org/springframework/integration/scheduling/CronSequenceGenerator.java new file mode 100644 index 0000000000..1f53243cec --- /dev/null +++ b/org.springframework.integration/src/main/java/org/springframework/integration/scheduling/CronSequenceGenerator.java @@ -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 Crontab pattern 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.

+ * + * Example patterns + *
    + *
  • "0 0 * * * *" = the top of every hour of every day.
  • + *
  • "*/10 * * * * *" = every ten seconds.
  • + *
  • "0 0 8-10 * * *" = 8, 9 and 10 o'clock of every day.
  • + *
  • "0 0 8-10/30 * * *" = 8:00, 8:30, 9:00, 9:30 and 10 o'clock every day.
  • + *
  • "0 0 9-17 * * MON-FRI" = on the hour nine-to-five weekdays
  • + *
  • "0 0 0 25 12 ?" = every Christmas Day at midnight
  • + *
+ * + * @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(); + } + +} diff --git a/org.springframework.integration/src/main/java/org/springframework/integration/scheduling/CronTrigger.java b/org.springframework.integration/src/main/java/org/springframework/integration/scheduling/CronTrigger.java index dc922305d8..58dea9eb01 100644 --- a/org.springframework.integration/src/main/java/org/springframework/integration/scheduling/CronTrigger.java +++ b/org.springframework.integration/src/main/java/org/springframework/integration/scheduling/CronTrigger.java @@ -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 null, 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(); } } diff --git a/org.springframework.integration/src/test/java/org/springframework/integration/scheduling/CronTriggerTests.java b/org.springframework.integration/src/test/java/org/springframework/integration/scheduling/CronTriggerTests.java new file mode 100644 index 0000000000..fbebf41dd6 --- /dev/null +++ b/org.springframework.integration/src/test/java/org/springframework/integration/scheduling/CronTriggerTests.java @@ -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)); + } + +} diff --git a/org.springframework.integration/template.mf b/org.springframework.integration/template.mf index 17d65d0821..b8ed01ab5c 100644 --- a/org.springframework.integration/template.mf +++ b/org.springframework.integration/template.mf @@ -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