From b2e7da3170d34a6c1478ef7b162fd31392464d16 Mon Sep 17 00:00:00 2001 From: unknown Date: Wed, 1 Jul 2015 18:55:08 +0200 Subject: [PATCH 1/5] The search for the next execution time can be limited by a date/time barrier so searches for february 30th will no longer result in an endless loop --- src/main/java/fc/cron/CronExpression.java | 23 ++++++++++++++++++ src/test/java/fc/cron/CronExpressionTest.java | 24 +++++++++++++++++++ 2 files changed, 47 insertions(+) diff --git a/src/main/java/fc/cron/CronExpression.java b/src/main/java/fc/cron/CronExpression.java index 4569f48..3c15b34 100644 --- a/src/main/java/fc/cron/CronExpression.java +++ b/src/main/java/fc/cron/CronExpression.java @@ -174,6 +174,20 @@ public class CronExpression { } 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 +221,7 @@ public class CronExpression { } nextTime.addDays(1); nextTime.setTime(0, 0, 0, 0); + checkIfDateTimeBarrierIsReached(nextTime, dateTimeBarrier); } if (monthField.matches(nextTime.getMonthOfYear())) { break; @@ -214,17 +229,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 + ">"; diff --git a/src/test/java/fc/cron/CronExpressionTest.java b/src/test/java/fc/cron/CronExpressionTest.java index f8085a1..9ffa43d 100644 --- a/src/test/java/fc/cron/CronExpressionTest.java +++ b/src/test/java/fc/cron/CronExpressionTest.java @@ -442,4 +442,28 @@ 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); + } } From 605745991e30cc0df224b2c361380ad3c466e8bb Mon Sep 17 00:00:00 2001 From: unknown Date: Thu, 2 Jul 2015 09:02:29 +0200 Subject: [PATCH 2/5] Version set to 1.1 --- pom.xml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/pom.xml b/pom.xml index 0afa913..c542448 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ fc.cron cron jar - 1.0 + 1.1 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 From 3ab418b9793fe0ba73fb247f75d46dd3e33ec5bd Mon Sep 17 00:00:00 2001 From: unknown Date: Thu, 2 Jul 2015 09:15:05 +0200 Subject: [PATCH 3/5] Specifying seconds is now optional. Version set to 1.2 --- pom.xml | 2 +- src/main/java/fc/cron/CronExpression.java | 50 ++++++++++++------- src/test/java/fc/cron/CronExpressionTest.java | 10 ++++ 3 files changed, 43 insertions(+), 19 deletions(-) diff --git a/pom.xml b/pom.xml index c542448..0d0d263 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ fc.cron cron jar - 1.1 + 1.2 cron https://github.com/frode-carlsen/cron diff --git a/src/main/java/fc/cron/CronExpression.java b/src/main/java/fc/cron/CronExpression.java index 3c15b34..8d641bd 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,23 +153,39 @@ 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] : "*"); + 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) { @@ -465,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 9ffa43d..482fdea 100644 --- a/src/test/java/fc/cron/CronExpressionTest.java +++ b/src/test/java/fc/cron/CronExpressionTest.java @@ -466,4 +466,14 @@ public class CronExpressionTest { // 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)); + } } From 9c51baacbda0a765c2c094a2e3094dbe444796c2 Mon Sep 17 00:00:00 2001 From: unknown Date: Thu, 2 Jul 2015 09:38:07 +0200 Subject: [PATCH 4/5] README.md updated to reflect the changes of this fork --- README.md | 9 +++++++++ 1 file changed, 9 insertions(+) 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 From e2077cfa4e37d732c75790901e119e0238073f88 Mon Sep 17 00:00:00 2001 From: unknown Date: Fri, 3 Jul 2015 16:03:23 +0200 Subject: [PATCH 5/5] When omitting the seconds, now 0 will be used for the seconds instead of * --- pom.xml | 2 +- src/main/java/fc/cron/CronExpression.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 0d0d263..4d3be55 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ fc.cron cron jar - 1.2 + 1.3 cron https://github.com/frode-carlsen/cron diff --git a/src/main/java/fc/cron/CronExpression.java b/src/main/java/fc/cron/CronExpression.java index 8d641bd..55d16c9 100644 --- a/src/main/java/fc/cron/CronExpression.java +++ b/src/main/java/fc/cron/CronExpression.java @@ -172,7 +172,7 @@ public class CronExpression { } int ix = withSeconds ? 1 : 0; - this.secondField = new SimpleField(CronFieldType.SECOND, withSeconds ? parts[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++]);