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()); + } + +}