From 1a8906bdc3a9e78e5036ef4874345a742fb151f7 Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Fri, 24 Jul 2020 11:29:49 +0200 Subject: [PATCH] Support macros in CronExpression This commit introduces supports for macros like "@yearly", "@monthly", etc. in CronExpression. Closes gh-25471 --- .../scheduling/support/CronExpression.java | 32 +++++ .../support/CronExpressionTests.java | 121 ++++++++++++++++++ 2 files changed, 153 insertions(+) diff --git a/spring-context/src/main/java/org/springframework/scheduling/support/CronExpression.java b/spring-context/src/main/java/org/springframework/scheduling/support/CronExpression.java index c3f8990df3..ff0a8732d0 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/support/CronExpression.java +++ b/spring-context/src/main/java/org/springframework/scheduling/support/CronExpression.java @@ -41,6 +41,16 @@ public final class CronExpression { static final int MAX_ATTEMPTS = 366; + private static final String[] MACROS = new String[] { + "@yearly", "0 0 0 1 1 *", + "@annually", "0 0 0 1 1 *", + "@monthly", "0 0 0 1 * *", + "@weekly", "0 0 0 * * 0", + "@daily", "0 0 0 * * *", + "@midnight", "0 0 0 * * *", + "@hourly", "0 0 * * * *" + }; + private final CronField[] fields; @@ -111,6 +121,15 @@ public final class CronExpression { *
  • {@code "0 0 0 25 12 ?"} = every Christmas Day at midnight
  • * * + *

    The following macros are also supported: + *

    + * * @param expression the expression string to parse * @return the parsed {@code CronExpression} object * @throws IllegalArgumentException in the expression does not conform to @@ -119,6 +138,8 @@ public final class CronExpression { public static CronExpression parse(String expression) { Assert.hasLength(expression, "Expression string must not be empty"); + expression = resolveMacros(expression); + String[] fields = StringUtils.tokenizeToStringArray(expression, " "); if (fields.length != 6) { throw new IllegalArgumentException(String.format( @@ -141,6 +162,17 @@ public final class CronExpression { } + private static String resolveMacros(String expression) { + expression = expression.trim(); + for (int i = 0; i < MACROS.length; i = i + 2) { + if (MACROS[i].equalsIgnoreCase(expression)) { + return MACROS[i + 1]; + } + } + return expression; + } + + /** * Calculate the next {@link Temporal} that matches this expression. * @param temporal the seed value diff --git a/spring-context/src/test/java/org/springframework/scheduling/support/CronExpressionTests.java b/spring-context/src/test/java/org/springframework/scheduling/support/CronExpressionTests.java index 0e6fd69076..e77fd644d0 100644 --- a/spring-context/src/test/java/org/springframework/scheduling/support/CronExpressionTests.java +++ b/spring-context/src/test/java/org/springframework/scheduling/support/CronExpressionTests.java @@ -27,6 +27,7 @@ import org.junit.jupiter.api.Test; import static java.time.DayOfWeek.FRIDAY; import static java.time.DayOfWeek.MONDAY; +import static java.time.DayOfWeek.SUNDAY; import static java.time.DayOfWeek.TUESDAY; import static java.time.DayOfWeek.WEDNESDAY; import static java.time.temporal.TemporalAdjusters.next; @@ -462,4 +463,124 @@ class CronExpressionTests { assertThat(actual.getDayOfMonth()).isEqualTo(13); } + @Test + void yearly() { + CronExpression expression = CronExpression.parse("@yearly"); + assertThat(expression).isEqualTo(CronExpression.parse("0 0 0 1 1 *")); + + LocalDateTime last = LocalDateTime.now().withMonth(10).withDayOfMonth(10); + LocalDateTime expected = LocalDateTime.of(last.getYear() + 1, 1, 1, 0, 0); + + LocalDateTime actual = expression.next(last); + assertThat(actual).isEqualTo(expected); + + last = actual; + expected = expected.plusYears(1); + actual = expression.next(last); + assertThat(actual).isEqualTo(expected); + + last = actual; + expected = expected.plusYears(1); + assertThat(expression.next(last)).isEqualTo(expected); + } + + @Test + void annually() { + CronExpression expression = CronExpression.parse("@annually"); + assertThat(expression).isEqualTo(CronExpression.parse("0 0 0 1 1 *")); + assertThat(expression).isEqualTo(CronExpression.parse("@yearly")); + } + + @Test + void monthly() { + CronExpression expression = CronExpression.parse("@monthly"); + assertThat(expression).isEqualTo(CronExpression.parse("0 0 0 1 * *")); + + LocalDateTime last = LocalDateTime.now().withMonth(10).withDayOfMonth(10); + LocalDateTime expected = LocalDateTime.of(last.getYear(), 11, 1, 0, 0); + + LocalDateTime actual = expression.next(last); + assertThat(actual).isEqualTo(expected); + + last = actual; + expected = expected.plusMonths(1); + actual = expression.next(last); + assertThat(actual).isEqualTo(expected); + + last = actual; + expected = expected.plusMonths(1); + assertThat(expression.next(last)).isEqualTo(expected); + } + + @Test + void weekly() { + CronExpression expression = CronExpression.parse("@weekly"); + assertThat(expression).isEqualTo(CronExpression.parse("0 0 0 * * 0")); + + LocalDateTime last = LocalDateTime.now(); + LocalDateTime expected = last.with(next(SUNDAY)).withHour(0).withMinute(0).withSecond(0).withNano(0); + + LocalDateTime actual = expression.next(last); + assertThat(actual).isEqualTo(expected); + + last = actual; + expected = expected.plusWeeks(1); + actual = expression.next(last); + assertThat(actual).isEqualTo(expected); + + last = actual; + expected = expected.plusWeeks(1); + assertThat(expression.next(last)).isEqualTo(expected); + } + + @Test + void daily() { + CronExpression expression = CronExpression.parse("@daily"); + assertThat(expression).isEqualTo(CronExpression.parse("0 0 0 * * *")); + + LocalDateTime last = LocalDateTime.now(); + LocalDateTime expected = last.plusDays(1).withHour(0).withMinute(0).withSecond(0).withNano(0); + + LocalDateTime actual = expression.next(last); + assertThat(actual).isEqualTo(expected); + + last = actual; + expected = expected.plusDays(1); + actual = expression.next(last); + assertThat(actual).isEqualTo(expected); + + last = actual; + expected = expected.plusDays(1); + assertThat(expression.next(last)).isEqualTo(expected); + } + + @Test + void midnight() { + CronExpression expression = CronExpression.parse("@midnight"); + assertThat(expression).isEqualTo(CronExpression.parse("0 0 0 * * *")); + assertThat(expression).isEqualTo(CronExpression.parse("@daily")); + } + + @Test + void hourly() { + CronExpression expression = CronExpression.parse("@hourly"); + assertThat(expression).isEqualTo(CronExpression.parse("0 0 * * * *")); + + LocalDateTime last = LocalDateTime.now(); + LocalDateTime expected = last.plusHours(1).withMinute(0).withSecond(0).withNano(0); + + LocalDateTime actual = expression.next(last); + assertThat(actual).isEqualTo(expected); + + last = actual; + expected = expected.plusHours(1); + actual = expression.next(last); + assertThat(actual).isEqualTo(expected); + + last = actual; + expected = expected.plusHours(1); + assertThat(expression.next(last)).isEqualTo(expected); + } + + }