- * A cron expressions consists of 6 mandatory fields separated by space.
+ * A cron expressions consists of 5 or 6 mandatory fields (seconds may be omitted) separated by space.
* These are:
*
*
| Special Characters | * *||||
|---|---|---|---|---|
Seconds |
+ * Seconds (may be omitted) |
* * | 0-59 |
* @@ -143,7 +143,6 @@ public class CronExpression { this.to = to; this.names = names; } - } private final String expr; @@ -154,26 +153,56 @@ public class CronExpression { private final SimpleField monthField; private final DayOfMonthField dayOfMonthField; - public CronExpression(String expr) { + public CronExpression(final String expr) { + this(expr, true); + } + + public CronExpression(final String expr, final boolean withSeconds) { if (expr == null) { - throw new IllegalArgumentException("expr is null"); - } - this.expr = expr; - String[] parts = expr.split("\\s+"); - if (parts.length != 6) { - throw new IllegalArgumentException(String.format("Invalid cronexpression [%s], expected %s felt, got %s" - , expr, CronFieldType.values().length, parts.length)); + throw new IllegalArgumentException("expr is null"); //$NON-NLS-1$ } - this.secondField = new SimpleField(CronFieldType.SECOND, parts[0]); - this.minuteField = new SimpleField(CronFieldType.MINUTE, parts[1]); - this.hourField = new SimpleField(CronFieldType.HOUR, parts[2]); - this.dayOfMonthField = new DayOfMonthField(parts[3]); - this.monthField = new SimpleField(CronFieldType.MONTH, parts[4]); - this.dayOfWeekField = new DayOfWeekField(parts[5]); + this.expr = expr; + + final int expectedParts = withSeconds ? 6 : 5; + final String[] parts = expr.split("\\s+"); //$NON-NLS-1$ + if (parts.length != expectedParts) { + throw new IllegalArgumentException(String.format("Invalid cron expression [%s], expected %s felt, got %s" + , expr, expectedParts, parts.length)); + } + + int ix = withSeconds ? 1 : 0; + this.secondField = new SimpleField(CronFieldType.SECOND, withSeconds ? parts[0] : "0"); + this.minuteField = new SimpleField(CronFieldType.MINUTE, parts[ix++]); + this.hourField = new SimpleField(CronFieldType.HOUR, parts[ix++]); + this.dayOfMonthField = new DayOfMonthField(parts[ix++]); + this.monthField = new SimpleField(CronFieldType.MONTH, parts[ix++]); + this.dayOfWeekField = new DayOfWeekField(parts[ix++]); + } + + public static CronExpression create(final String expr) { + return new CronExpression(expr, true); + } + + public static CronExpression createWithoutSeconds(final String expr) { + return new CronExpression(expr, false); } public DateTime nextTimeAfter(DateTime afterTime) { + // will search for the next time within the next 4 years. If there is no + // time matching, an InvalidArgumentException will be thrown (it is very + // likely that the cron expression is invalid, like the February 30th). + return nextTimeAfter(afterTime, afterTime.plusYears(4)); + } + + public DateTime nextTimeAfter(DateTime afterTime, long durationInMillis) { + // will search for the next time within the next durationInMillis + // millisecond. Be aware that the duration is specified in millis, + // but in fact the limit is checked on a day-to-day basis. + return nextTimeAfter(afterTime, afterTime.plus(durationInMillis)); + } + + public DateTime nextTimeAfter(DateTime afterTime, DateTime dateTimeBarrier) { MutableDateTime nextTime = new MutableDateTime(afterTime); nextTime.setMillisOfSecond(0); nextTime.secondOfDay().add(1); @@ -207,6 +236,7 @@ public class CronExpression { } nextTime.addDays(1); nextTime.setTime(0, 0, 0, 0); + checkIfDateTimeBarrierIsReached(nextTime, dateTimeBarrier); } if (monthField.matches(nextTime.getMonthOfYear())) { break; @@ -214,17 +244,25 @@ public class CronExpression { nextTime.addMonths(1); nextTime.setDayOfMonth(1); nextTime.setTime(0, 0, 0, 0); + checkIfDateTimeBarrierIsReached(nextTime, dateTimeBarrier); } if (dayOfWeekField.matches(new LocalDate(nextTime))) { break; } nextTime.addDays(1); nextTime.setTime(0, 0, 0, 0); + checkIfDateTimeBarrierIsReached(nextTime, dateTimeBarrier); } return nextTime.toDateTime(); } + private static void checkIfDateTimeBarrierIsReached(MutableDateTime nextTime, DateTime dateTimeBarrier) { + if (nextTime.isAfter(dateTimeBarrier)) { + throw new IllegalArgumentException("No next execution time could be determined that is before the limit of " + dateTimeBarrier); + } + } + @Override public String toString() { return getClass().getSimpleName() + "<" + expr + ">"; @@ -442,6 +480,5 @@ public class CronExpression { protected boolean matches(int val, FieldPart part) { return "?".equals(part.modifier) || super.matches(val, part); } - } } diff --git a/src/test/java/fc/cron/CronExpressionTest.java b/src/test/java/fc/cron/CronExpressionTest.java index f8085a1..482fdea 100644 --- a/src/test/java/fc/cron/CronExpressionTest.java +++ b/src/test/java/fc/cron/CronExpressionTest.java @@ -442,4 +442,38 @@ public class CronExpressionTest { public void shall_not_not_support_rolling_period() throws Exception { new CronExpression("* * 5-1 * * *"); } + + @Test(expected = IllegalArgumentException.class) + public void non_existing_date_throws_exception() throws Exception { + // Will check for the next 4 years - no 30th of February is found so a IAE is thrown. + new CronExpression("* * * 30 2 *").nextTimeAfter(DateTime.now()); + } + + @Test + public void test_default_barrier() throws Exception { + // the default barrier is 4 years - so leap years are considered. + assertThat(new CronExpression("* * * 29 2 *").nextTimeAfter(new DateTime(2012, 3, 1, 00, 00))).isEqualTo(new DateTime(2016, 2, 29, 00, 00)); + } + + @Test(expected = IllegalArgumentException.class) + public void test_one_year_barrier() throws Exception { + // The next leap year is 2016, so an IllegalArgumentException is expected. + new CronExpression("* * * 29 2 *").nextTimeAfter(new DateTime(2012, 3, 1, 00, 00), new DateTime(2013, 3, 1, 00, 00)); + } + + @Test(expected = IllegalArgumentException.class) + public void test_two_year_barrier() throws Exception { + // The next leap year is 2016, so an IllegalArgumentException is expected. + new CronExpression("* * * 29 2 *").nextTimeAfter(new DateTime(2012, 3, 1, 00, 00), 1000 * 60 * 60 * 24 * 356 * 2); + } + + @Test(expected = IllegalArgumentException.class) + public void test_seconds_specified_but_should_be_omitted() throws Exception { + CronExpression.createWithoutSeconds("* * * 29 2 *"); + } + + @Test + public void test_without_seconds() throws Exception { + assertThat(CronExpression.createWithoutSeconds("* * 29 2 *").nextTimeAfter(new DateTime(2012, 3, 1, 00, 00))).isEqualTo(new DateTime(2016, 2, 29, 00, 00)); + } } |