diff --git a/README.md b/README.md index 98d337c..16776a0 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,15 @@ cron Since it's specified on Joda-time it allows for easy integration into unit testing and simulations, by adjusting the Joda-time offset to speed up executions. +fork +==== + + This project was forked from https://github.com/frode-carlsen/cron and has the following new features: + + - Seconds can be omitted in the cron expression. + + - No endless loop if a non existing date (february 30th) is specified in the cron expression. This fork uses a date/time barrier. If the search for the next execution date/time reaches this barrier, an InvalidArgumentException is thrown. This barrier can be user-defined, but has a built-in default of 4 years. + usage ===== See javadoc diff --git a/pom.xml b/pom.xml index 0afa913..4d3be55 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ fc.cron cron jar - 1.0 + 1.3 cron https://github.com/frode-carlsen/cron @@ -14,21 +14,21 @@ http://www.apache.org/licenses/LICENSE-2.0 - + joda-time joda-time 2.3 - - + + org.easytesting fest-assert 1.4 - test + test - + junit junit @@ -39,7 +39,7 @@ - + maven-compiler-plugin 3.1 @@ -53,7 +53,7 @@ - UTF-8 + UTF-8 UTF-8 diff --git a/src/main/java/fc/cron/CronExpression.java b/src/main/java/fc/cron/CronExpression.java index 4569f48..55d16c9 100644 --- a/src/main/java/fc/cron/CronExpression.java +++ b/src/main/java/fc/cron/CronExpression.java @@ -32,7 +32,7 @@ import org.joda.time.MutableDateTime; * Parser for unix-like cron expressions: Cron expressions allow specifying combinations of criteria for time * such as: "Each Monday-Friday at 08:00" or "Every last friday of the month at 01:30" *

- * 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: * * @@ -44,7 +44,7 @@ import org.joda.time.MutableDateTime; * * * - * + * * *
Special Characters
SecondsSeconds (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)); + } }