diff --git a/README.md b/README.md
index 46305f9..d0e2b5c 100644
--- a/README.md
+++ b/README.md
@@ -83,19 +83,24 @@ the job will be executed once 10 seconds after it has been scheduled.
### Cron
Schedules can be created using [cron expressions](https://en.wikipedia.org/wiki/Cron#CRON_expression).
-This feature is made possible by the use of [cron-utils](https://github.com/jmrozanec/cron-utils).
-So to use cron expression, cron-utils should be added in the project:
+This feature is made possible by the use of [cron library](https://github.com/frode-carlsen/cron). This library is very lightweight: it has no dependency and is made of a single Java class of 650 lines of code.
+
+So to use cron expression, this library has to be added:
```xml
- com.cronutils
- cron-utils
- 9.1.6
+ ch.eitchnet
+ cron
+ 1.6.2
```
+
Then to create a job which is executed every hour at the 30th minute,
-you can create the schedule: `CronSchedule.parseQuartzCron("0 30 * * * ? *")`.
+you can create the schedule: `CronExpressionSchedule.parse("30 * * * *")`.
+
+Cron expression should be checked using a tool like [Cronhub](https://crontab.cronhub.io/).
-Cron expression should be created and checked using a tool like [Cron Maker](http://www.cronmaker.com/).
+Cron-utils was the default Cron implementation before Wisp 2.2.2. This has [changed in version 2.3.0](/../../issues/14).
+Documentation about cron-utils implementation can be found at [Wisp 2.2.2](/../../tree/2.2.2#cron).
### Custom schedules
Custom schedules can be created,
diff --git a/pom.xml b/pom.xml
index fd4f9a6..0f3319f 100644
--- a/pom.xml
+++ b/pom.xml
@@ -4,7 +4,7 @@
com.coreoz
wisp
- 2.2.3-SNAPSHOT
+ 2.3.1-SNAPSHOT
jar
Wisp Scheduler
@@ -176,6 +176,12 @@
9.1.6
true
+
+ ch.eitchnet
+ cron
+ 1.6.2
+ true
+
junit
diff --git a/src/main/java/com/coreoz/wisp/schedule/cron/CronExpressionSchedule.java b/src/main/java/com/coreoz/wisp/schedule/cron/CronExpressionSchedule.java
new file mode 100644
index 0000000..f5b1936
--- /dev/null
+++ b/src/main/java/com/coreoz/wisp/schedule/cron/CronExpressionSchedule.java
@@ -0,0 +1,71 @@
+package com.coreoz.wisp.schedule.cron;
+
+import java.time.Instant;
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+
+import com.coreoz.wisp.schedule.Schedule;
+
+import fc.cron.CronExpression;
+
+/**
+ * A {@link Schedule} based on a
+ * Cron expression.
+ *
+ * This class depends on Cron library,
+ * so this dependency has to be in the classpath in order to be able to use {@link CronExpressionSchedule}.
+ * Since the Cron library is marked as optional in Wisp, it has to be
+ * explicitly referenced in the project dependency configuration
+ * (pom.xml, build.gradle, build.sbt etc.).
+ *
+ * See also {@link CronExpression} for format details and implementation.
+ */
+public class CronExpressionSchedule implements Schedule {
+
+ private final CronExpression cronExpression;
+ private final ZoneId zoneId;
+
+ public CronExpressionSchedule(CronExpression cronExpression, ZoneId zoneId) {
+ this.cronExpression = cronExpression;
+ this.zoneId = zoneId;
+ }
+
+ public CronExpressionSchedule(CronExpression cronExpression) {
+ this(cronExpression, ZoneId.systemDefault());
+ }
+
+ @Override
+ public long nextExecutionInMillis(long currentTimeInMillis, int executionsCount, Long lastExecutionTimeInMillis) {
+ Instant currentInstant = Instant.ofEpochMilli(currentTimeInMillis);
+ try {
+ return cronExpression.nextTimeAfter(ZonedDateTime.ofInstant(
+ currentInstant,
+ zoneId
+ )).toEpochSecond() * 1000L;
+ } catch (IllegalArgumentException e) {
+ return Schedule.WILL_NOT_BE_EXECUTED_AGAIN;
+ }
+ }
+
+ @Override
+ public String toString() {
+ return cronExpression.toString();
+ }
+
+ /**
+ * Create a {@link Schedule} from a cron expression based on the Unix format,
+ * e.g. 1 * * * * for each minute.
+ */
+ public static CronExpressionSchedule parse(String cronExpression) {
+ return new CronExpressionSchedule(CronExpression.createWithoutSeconds(cronExpression));
+ }
+
+ /**
+ * Create a {@link Schedule} from a cron expression based on the Unix format, but accepting a second field as the first one,
+ * e.g. 29 * * * * * for each minute at the second 29, for instance 12:05:29.
+ */
+ public static CronExpressionSchedule parseWithSeconds(String cronExpression) {
+ return new CronExpressionSchedule(CronExpression.create(cronExpression));
+ }
+
+}
diff --git a/src/main/java/com/coreoz/wisp/schedule/cron/CronSchedule.java b/src/main/java/com/coreoz/wisp/schedule/cron/CronSchedule.java
index 5c6cee6..0f46fa0 100644
--- a/src/main/java/com/coreoz/wisp/schedule/cron/CronSchedule.java
+++ b/src/main/java/com/coreoz/wisp/schedule/cron/CronSchedule.java
@@ -21,7 +21,12 @@
* so this dependency have to be in the classpath in order to be able to use {@link CronSchedule}.
* Since cron-utils is marked as optional, it has to be explicitly referenced in the
* project dependency configuration (pom.xml, build.gradle, build.sbt etc.).
+ *
+ * @deprecated Use {@link CronExpressionScheduleTest} instead.
+ * This class has been deprecated to move away from cron-utils. See
+ * issue #14 for details.
*/
+@Deprecated
public class CronSchedule implements Schedule {
private static final CronParser UNIX_CRON_PARSER = new CronParser(
@@ -68,7 +73,10 @@ public String toString() {
/**
* Create a {@link Schedule} from a cron expression based on the Unix format,
* e.g. 1 * * * * for each minute.
+ *
+ * @deprecated Use {@link CronExpressionScheduleTest#parse(String)} instead
*/
+ @Deprecated
public static CronSchedule parseUnixCron(String cronExpression) {
return new CronSchedule(UNIX_CRON_PARSER.parse(cronExpression));
}
@@ -76,7 +84,11 @@ public static CronSchedule parseUnixCron(String cronExpression) {
/**
* Create a {@link Schedule} from a cron expression based on the Quartz format,
* e.g. 0 * * * * ? * for each minute.
+ *
+ * @deprecated Use {@link CronExpressionScheduleTest#parse(String)}
+ * or {@link CronExpressionScheduleTest#parseWithSeconds(String)} instead
*/
+ @Deprecated
public static CronSchedule parseQuartzCron(String cronExpression) {
return new CronSchedule(QUARTZ_CRON_PARSER.parse(cronExpression));
}
diff --git a/src/test/java/com/coreoz/wisp/schedule/cron/CronExpressionScheduleTest.java b/src/test/java/com/coreoz/wisp/schedule/cron/CronExpressionScheduleTest.java
new file mode 100644
index 0000000..7617918
--- /dev/null
+++ b/src/test/java/com/coreoz/wisp/schedule/cron/CronExpressionScheduleTest.java
@@ -0,0 +1,52 @@
+package com.coreoz.wisp.schedule.cron;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.time.Duration;
+import java.time.LocalDate;
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+
+import org.junit.Test;
+
+public class CronExpressionScheduleTest {
+
+ @Test
+ public void should_calcule_the_next_execution_time_based_on_a_unix_cron_expression() {
+ CronExpressionSchedule everyMinuteScheduler = CronExpressionSchedule.parse("* * * * *");
+
+ // To ease calculations, next execution time are calculated from the timestamp "0".
+ // So here, the absolute timestamp for an execution in 1 minute will be 60
+ assertThat(everyMinuteScheduler.nextExecutionInMillis(0, 0, null))
+ .isEqualTo(Duration.ofMinutes(1).toMillis());
+ }
+
+ @Test
+ public void should_calcule_the_next_execution_time_based_on_a_unix_cron_expression_with_seconds() {
+ CronExpressionSchedule everyMinuteScheduler = CronExpressionSchedule.parseWithSeconds("29 * * * * *");
+
+ assertThat(everyMinuteScheduler.nextExecutionInMillis(0, 0, null))
+ // the first iteration will be the absolute timestamp "29"
+ .isEqualTo(Duration.ofSeconds(29).toMillis());
+ assertThat(everyMinuteScheduler.nextExecutionInMillis(29000 , 1, null))
+ // the second iteration will be the absolute timestamp "89"
+ .isEqualTo(Duration.ofSeconds(60 + 29).toMillis());
+ }
+
+ @Test
+ public void should_not_executed_daily_jobs_twice_a_day() {
+ CronExpressionSchedule everyMinuteScheduler = CronExpressionSchedule.parse("0 12 * * *");
+
+ ZonedDateTime augustMidday = LocalDate
+ .of(2016, 8, 31)
+ .atTime(12, 0)
+ .atZone(ZoneId.systemDefault());
+ long midday = augustMidday.toEpochSecond() * 1000;
+
+ assertThat(everyMinuteScheduler.nextExecutionInMillis(midday-1, 0, null))
+ .isEqualTo(midday);
+ assertThat(everyMinuteScheduler.nextExecutionInMillis(midday, 0, null))
+ .isEqualTo(midday + Duration.ofDays(1).toMillis());
+ }
+
+}