mirror of
https://github.com/frode-carlsen/cron.git
synced 2025-12-06 14:00:57 +03:00
Merge pull request #1 from michaelknigge/master
Time barrier (no endloess loops when looking up Feb 30th) & optional seconds
This commit is contained in:
@@ -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.
|
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
|
usage
|
||||||
=====
|
=====
|
||||||
See javadoc
|
See javadoc
|
||||||
|
|||||||
16
pom.xml
16
pom.xml
@@ -4,7 +4,7 @@
|
|||||||
<groupId>fc.cron</groupId>
|
<groupId>fc.cron</groupId>
|
||||||
<artifactId>cron</artifactId>
|
<artifactId>cron</artifactId>
|
||||||
<packaging>jar</packaging>
|
<packaging>jar</packaging>
|
||||||
<version>1.0</version>
|
<version>1.3</version>
|
||||||
<name>cron</name>
|
<name>cron</name>
|
||||||
<url>https://github.com/frode-carlsen/cron</url>
|
<url>https://github.com/frode-carlsen/cron</url>
|
||||||
|
|
||||||
@@ -14,21 +14,21 @@
|
|||||||
<url>http://www.apache.org/licenses/LICENSE-2.0</url>
|
<url>http://www.apache.org/licenses/LICENSE-2.0</url>
|
||||||
</license>
|
</license>
|
||||||
</licenses>
|
</licenses>
|
||||||
|
|
||||||
<dependencies>
|
<dependencies>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>joda-time</groupId>
|
<groupId>joda-time</groupId>
|
||||||
<artifactId>joda-time</artifactId>
|
<artifactId>joda-time</artifactId>
|
||||||
<version>2.3</version>
|
<version>2.3</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.easytesting</groupId>
|
<groupId>org.easytesting</groupId>
|
||||||
<artifactId>fest-assert</artifactId>
|
<artifactId>fest-assert</artifactId>
|
||||||
<version>1.4</version>
|
<version>1.4</version>
|
||||||
<scope>test</scope>
|
<scope>test</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>junit</groupId>
|
<groupId>junit</groupId>
|
||||||
<artifactId>junit</artifactId>
|
<artifactId>junit</artifactId>
|
||||||
@@ -39,7 +39,7 @@
|
|||||||
|
|
||||||
<build>
|
<build>
|
||||||
<plugins>
|
<plugins>
|
||||||
<plugin>
|
<plugin>
|
||||||
<artifactId>maven-compiler-plugin</artifactId>
|
<artifactId>maven-compiler-plugin</artifactId>
|
||||||
<version>3.1</version>
|
<version>3.1</version>
|
||||||
<configuration>
|
<configuration>
|
||||||
@@ -53,7 +53,7 @@
|
|||||||
</build>
|
</build>
|
||||||
|
|
||||||
<properties>
|
<properties>
|
||||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
|
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
|
||||||
</properties>
|
</properties>
|
||||||
</project>
|
</project>
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ import org.joda.time.MutableDateTime;
|
|||||||
* Parser for unix-like cron expressions: Cron expressions allow specifying combinations of criteria for time
|
* 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"
|
* such as: "Each Monday-Friday at 08:00" or "Every last friday of the month at 01:30"
|
||||||
* <p>
|
* <p>
|
||||||
* A cron expressions consists of 6 mandatory fields separated by space. <br>
|
* A cron expressions consists of 5 or 6 mandatory fields (seconds may be omitted) separated by space. <br>
|
||||||
* These are:
|
* These are:
|
||||||
*
|
*
|
||||||
* <table cellspacing="8">
|
* <table cellspacing="8">
|
||||||
@@ -44,7 +44,7 @@ import org.joda.time.MutableDateTime;
|
|||||||
* <th align="left">Special Characters</th>
|
* <th align="left">Special Characters</th>
|
||||||
* </tr>
|
* </tr>
|
||||||
* <tr>
|
* <tr>
|
||||||
* <td align="left"><code>Seconds</code></td>
|
* <td align="left"><code>Seconds (may be omitted)</code></td>
|
||||||
* <td align="left"> </th>
|
* <td align="left"> </th>
|
||||||
* <td align="left"><code>0-59</code></td>
|
* <td align="left"><code>0-59</code></td>
|
||||||
* <td align="left"> </th>
|
* <td align="left"> </th>
|
||||||
@@ -143,7 +143,6 @@ public class CronExpression {
|
|||||||
this.to = to;
|
this.to = to;
|
||||||
this.names = names;
|
this.names = names;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private final String expr;
|
private final String expr;
|
||||||
@@ -154,26 +153,56 @@ public class CronExpression {
|
|||||||
private final SimpleField monthField;
|
private final SimpleField monthField;
|
||||||
private final DayOfMonthField dayOfMonthField;
|
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) {
|
if (expr == null) {
|
||||||
throw new IllegalArgumentException("expr is null");
|
throw new IllegalArgumentException("expr is null"); //$NON-NLS-1$
|
||||||
}
|
|
||||||
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));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.secondField = new SimpleField(CronFieldType.SECOND, parts[0]);
|
this.expr = expr;
|
||||||
this.minuteField = new SimpleField(CronFieldType.MINUTE, parts[1]);
|
|
||||||
this.hourField = new SimpleField(CronFieldType.HOUR, parts[2]);
|
final int expectedParts = withSeconds ? 6 : 5;
|
||||||
this.dayOfMonthField = new DayOfMonthField(parts[3]);
|
final String[] parts = expr.split("\\s+"); //$NON-NLS-1$
|
||||||
this.monthField = new SimpleField(CronFieldType.MONTH, parts[4]);
|
if (parts.length != expectedParts) {
|
||||||
this.dayOfWeekField = new DayOfWeekField(parts[5]);
|
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) {
|
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);
|
MutableDateTime nextTime = new MutableDateTime(afterTime);
|
||||||
nextTime.setMillisOfSecond(0);
|
nextTime.setMillisOfSecond(0);
|
||||||
nextTime.secondOfDay().add(1);
|
nextTime.secondOfDay().add(1);
|
||||||
@@ -207,6 +236,7 @@ public class CronExpression {
|
|||||||
}
|
}
|
||||||
nextTime.addDays(1);
|
nextTime.addDays(1);
|
||||||
nextTime.setTime(0, 0, 0, 0);
|
nextTime.setTime(0, 0, 0, 0);
|
||||||
|
checkIfDateTimeBarrierIsReached(nextTime, dateTimeBarrier);
|
||||||
}
|
}
|
||||||
if (monthField.matches(nextTime.getMonthOfYear())) {
|
if (monthField.matches(nextTime.getMonthOfYear())) {
|
||||||
break;
|
break;
|
||||||
@@ -214,17 +244,25 @@ public class CronExpression {
|
|||||||
nextTime.addMonths(1);
|
nextTime.addMonths(1);
|
||||||
nextTime.setDayOfMonth(1);
|
nextTime.setDayOfMonth(1);
|
||||||
nextTime.setTime(0, 0, 0, 0);
|
nextTime.setTime(0, 0, 0, 0);
|
||||||
|
checkIfDateTimeBarrierIsReached(nextTime, dateTimeBarrier);
|
||||||
}
|
}
|
||||||
if (dayOfWeekField.matches(new LocalDate(nextTime))) {
|
if (dayOfWeekField.matches(new LocalDate(nextTime))) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
nextTime.addDays(1);
|
nextTime.addDays(1);
|
||||||
nextTime.setTime(0, 0, 0, 0);
|
nextTime.setTime(0, 0, 0, 0);
|
||||||
|
checkIfDateTimeBarrierIsReached(nextTime, dateTimeBarrier);
|
||||||
}
|
}
|
||||||
|
|
||||||
return nextTime.toDateTime();
|
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
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return getClass().getSimpleName() + "<" + expr + ">";
|
return getClass().getSimpleName() + "<" + expr + ">";
|
||||||
@@ -442,6 +480,5 @@ public class CronExpression {
|
|||||||
protected boolean matches(int val, FieldPart part) {
|
protected boolean matches(int val, FieldPart part) {
|
||||||
return "?".equals(part.modifier) || super.matches(val, part);
|
return "?".equals(part.modifier) || super.matches(val, part);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -442,4 +442,38 @@ public class CronExpressionTest {
|
|||||||
public void shall_not_not_support_rolling_period() throws Exception {
|
public void shall_not_not_support_rolling_period() throws Exception {
|
||||||
new CronExpression("* * 5-1 * * *");
|
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));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user