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
new file mode 100644
index 00000000000..c3f8990df34
--- /dev/null
+++ b/spring-context/src/main/java/org/springframework/scheduling/support/CronExpression.java
@@ -0,0 +1,209 @@
+/*
+ * Copyright 2002-2020 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.scheduling.support;
+
+import java.time.temporal.ChronoUnit;
+import java.time.temporal.Temporal;
+import java.util.Arrays;
+
+import org.springframework.lang.Nullable;
+import org.springframework.util.Assert;
+import org.springframework.util.StringUtils;
+
+/**
+ * Representation of a
+ * crontab expression
+ * that can calculate the next time it matches.
+ *
+ *
{@code CronExpression} instances are created through
+ * {@link #parse(String)}; the next match is determined with
+ * {@link #next(Temporal)}.
+ *
+ * @author Arjen Poutsma
+ * @since 5.3
+ * @see CronTrigger
+ */
+public final class CronExpression {
+
+ static final int MAX_ATTEMPTS = 366;
+
+
+ private final CronField[] fields;
+
+ private final String expression;
+
+
+ private CronExpression(
+ CronField seconds,
+ CronField minutes,
+ CronField hours,
+ CronField daysOfMonth,
+ CronField months,
+ CronField daysOfWeek,
+ String expression) {
+
+ // to make sure we end up at 0 nanos, we add an extra field
+ this.fields = new CronField[]{daysOfWeek, months, daysOfMonth, hours, minutes, seconds, CronField.zeroNanos()};
+ this.expression = expression;
+ }
+
+
+ /**
+ * Parse the given
+ * crontab expression
+ * string into a {@code CronExpression}.
+ * The string has six single space-separated time and date fields:
+ *
+ * ┌───────────── second (0-59)
+ * │ ┌───────────── minute (0 - 59)
+ * │ │ ┌───────────── hour (0 - 23)
+ * │ │ │ ┌───────────── day of the month (1 - 31)
+ * │ │ │ │ ┌───────────── month (1 - 12) (or JAN-DEC)
+ * │ │ │ │ │ ┌───────────── day of the week (0 - 7)
+ * │ │ │ │ │ │ (0 or 7 is Sunday, or MON-SUN)
+ * │ │ │ │ │ │
+ * * * * * * *
+ *
+ *
+ * The following rules apply:
+ *
+ * -
+ * A field may be an asterisk ({@code *}), which always stands for
+ * "first-last". For the "day of the month" or "day of the week" fields, a
+ * question mark ({@code ?}) may be used instead of an asterisk.
+ *
+ * -
+ * Ranges of numbers are expressed by two numbers separated with a hyphen
+ * ({@code -}). The specified range is inclusive.
+ *
+ * - Following a range (or {@code *}) with {@code "/n"} specifies
+ * skips of the number's value through the range.
+ *
+ * -
+ * English names can also be used for the "month" and "day of week" fields.
+ * Use the first three letters of the particular day or month (case does not
+ * matter).
+ *
+ *
+ *
+ * Example expressions:
+ *
+ * - {@code "0 0 * * * *"} = the top of every hour of every day.
+ * "*/10 * * * * *" = every ten seconds.
+ * - {@code "0 0 8-10 * * *"} = 8, 9 and 10 o'clock of every day.
+ * - {@code "0 0 6,19 * * *"} = 6:00 AM and 7:00 PM every day.
+ * - {@code "0 0/30 8-10 * * *"} = 8:00, 8:30, 9:00, 9:30, 10:00 and 10:30 every day.
+ * - {@code "0 0 9-17 * * MON-FRI"} = on the hour nine-to-five weekdays
+ * - {@code "0 0 0 25 12 ?"} = every Christmas Day at midnight
+ *
+ *
+ * @param expression the expression string to parse
+ * @return the parsed {@code CronExpression} object
+ * @throws IllegalArgumentException in the expression does not conform to
+ * the cron format
+ */
+ public static CronExpression parse(String expression) {
+ Assert.hasLength(expression, "Expression string must not be empty");
+
+ String[] fields = StringUtils.tokenizeToStringArray(expression, " ");
+ if (fields.length != 6) {
+ throw new IllegalArgumentException(String.format(
+ "Cron expression must consist of 6 fields (found %d in \"%s\")", fields.length, expression));
+ }
+ try {
+ CronField seconds = CronField.parseSeconds(fields[0]);
+ CronField minutes = CronField.parseMinutes(fields[1]);
+ CronField hours = CronField.parseHours(fields[2]);
+ CronField daysOfMonth = CronField.parseDaysOfMonth(fields[3]);
+ CronField months = CronField.parseMonth(fields[4]);
+ CronField daysOfWeek = CronField.parseDaysOfWeek(fields[5]);
+
+ return new CronExpression(seconds, minutes, hours, daysOfMonth, months, daysOfWeek, expression);
+ }
+ catch (IllegalArgumentException ex) {
+ String msg = ex.getMessage() + " in cron expression \"" + expression + "\"";
+ throw new IllegalArgumentException(msg, ex);
+ }
+ }
+
+
+ /**
+ * Calculate the next {@link Temporal} that matches this expression.
+ * @param temporal the seed value
+ * @param the type of temporal
+ * @return the next temporal that matches this expression, or {@code null}
+ * if no such temporal can be found
+ */
+ @Nullable
+ public T next(T temporal) {
+ return nextOrSame(ChronoUnit.NANOS.addTo(temporal, 1));
+ }
+
+
+ @Nullable
+ private T nextOrSame(T temporal) {
+ for (int i = 0; i < MAX_ATTEMPTS; i++) {
+ T result = nextOrSameInternal(temporal);
+ if (result == null || result.equals(temporal)) {
+ return result;
+ }
+ temporal = result;
+ }
+ return null;
+ }
+
+ @Nullable
+ private T nextOrSameInternal(T temporal) {
+ for (CronField field : this.fields) {
+ temporal = field.nextOrSame(temporal);
+ if (temporal == null) {
+ return null;
+ }
+ }
+ return temporal;
+ }
+
+
+ @Override
+ public int hashCode() {
+ return Arrays.hashCode(this.fields);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o instanceof CronExpression) {
+ CronExpression other = (CronExpression) o;
+ return Arrays.equals(this.fields, other.fields);
+ }
+ else {
+ return false;
+ }
+ }
+
+ /**
+ * Return the expression string used to create this {@code CronExpression}.
+ * @return the expression string
+ */
+ @Override
+ public String toString() {
+ return this.expression;
+ }
+
+}
diff --git a/spring-context/src/main/java/org/springframework/scheduling/support/CronField.java b/spring-context/src/main/java/org/springframework/scheduling/support/CronField.java
new file mode 100644
index 00000000000..866e507bb8f
--- /dev/null
+++ b/spring-context/src/main/java/org/springframework/scheduling/support/CronField.java
@@ -0,0 +1,385 @@
+/*
+ * Copyright 2002-2020 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.scheduling.support;
+
+import java.time.DateTimeException;
+import java.time.temporal.ChronoField;
+import java.time.temporal.Temporal;
+import java.time.temporal.ValueRange;
+import java.util.BitSet;
+
+import org.springframework.lang.Nullable;
+import org.springframework.util.Assert;
+import org.springframework.util.StringUtils;
+
+/**
+ * A single field in a cron pattern. Created using the {@code parse*} methods,
+ * main and only entry point is {@link #nextOrSame(Temporal)}.
+ *
+ * @author Arjen Poutsma
+ * @since 5.3
+ */
+final class CronField {
+
+ private static final String[] MONTHS = new String[]{"JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP",
+ "OCT", "NOV", "DEC"};
+
+ private static final String[] DAYS = new String[]{"MON", "TUE", "WED", "THU", "FRI", "SAT", "SUN"};
+
+ private static final CronField ZERO_NANOS;
+
+
+ static {
+ ZERO_NANOS = new CronField(Type.NANO);
+ ZERO_NANOS.bits.set(0);
+ }
+
+
+ private final Type type;
+
+ private final BitSet bits;
+
+
+ private CronField(Type type) {
+ this.type = type;
+ this.bits = new BitSet((int) type.range().getMaximum());
+ }
+
+
+ /**
+ * Return a {@code CronField} enabled for 0 nano seconds.
+ */
+ public static CronField zeroNanos() {
+ return ZERO_NANOS;
+ }
+
+ /**
+ * Parse the given value into a seconds {@code CronField}, the first entry of a cron expression.
+ */
+ public static CronField parseSeconds(String value) {
+ return parseField(value, Type.SECOND);
+ }
+
+ /**
+ * Parse the given value into a minutes {@code CronField}, the second entry of a cron expression.
+ */
+ public static CronField parseMinutes(String value) {
+ return parseField(value, Type.MINUTE);
+ }
+
+ /**
+ * Parse the given value into a hours {@code CronField}, the third entry of a cron expression.
+ */
+ public static CronField parseHours(String value) {
+ return parseField(value, Type.HOUR);
+ }
+
+ /**
+ * Parse the given value into a days of months {@code CronField}, the fourth entry of a cron expression.
+ */
+ public static CronField parseDaysOfMonth(String value) {
+ return parseDate(value, Type.DAY_OF_MONTH);
+ }
+
+ /**
+ * Parse the given value into a month {@code CronField}, the fifth entry of a cron expression.
+ */
+ public static CronField parseMonth(String value) {
+ value = replaceOrdinals(value, MONTHS);
+ return parseField(value, Type.MONTH);
+ }
+
+ /**
+ * Parse the given value into a days of week {@code CronField}, the sixth entry of a cron expression.
+ */
+ public static CronField parseDaysOfWeek(String value) {
+ value = replaceOrdinals(value, DAYS);
+ CronField result = parseDate(value, Type.DAY_OF_WEEK);
+ if (result.bits.get(0)) {
+ // cron supports 0 for Sunday; we use 7 like java.time
+ result.bits.set(7);
+ result.bits.clear(0);
+ }
+ return result;
+ }
+
+
+ private static CronField parseDate(String value, Type type) {
+ if (value.indexOf('?') != -1) {
+ value = "*";
+ }
+ return parseField(value, type);
+ }
+
+ private static CronField parseField(String value, Type type) {
+ Assert.hasLength(value, "Value must not be empty");
+ Assert.notNull(type, "Type must not be null");
+ try {
+ CronField result = new CronField(type);
+ String[] fields = StringUtils.delimitedListToStringArray(value, ",");
+ for (String field : fields) {
+ int slashPos = field.indexOf('/');
+ if (slashPos == -1) {
+ ValueRange range = parseRange(field, type);
+ result.setBits(range);
+ }
+ else {
+ String rangeStr = value.substring(0, slashPos);
+ String deltaStr = value.substring(slashPos + 1);
+ ValueRange range = parseRange(rangeStr, type);
+ if (rangeStr.indexOf('-') == -1) {
+ range = ValueRange.of(range.getMinimum(), type.range().getMaximum());
+ }
+ int delta = Integer.parseInt(deltaStr);
+ if (delta <= 0) {
+ throw new IllegalArgumentException("Incrementer delta must be 1 or higher");
+ }
+ result.setBits(range, delta);
+ }
+ }
+ return result;
+ }
+ catch (DateTimeException | IllegalArgumentException ex) {
+ String msg = ex.getMessage() + " '" + value + "'";
+ throw new IllegalArgumentException(msg, ex);
+ }
+ }
+
+ private static ValueRange parseRange(String value, Type type) {
+ if (value.indexOf('*') != -1) {
+ return type.range();
+ }
+ else {
+ int hyphenPos = value.indexOf('-');
+ if (hyphenPos == -1) {
+ int result = type.checkValidValue(Integer.parseInt(value));
+ return ValueRange.of(result, result);
+ }
+ else {
+ int min = Integer.parseInt(value.substring(0, hyphenPos));
+ int max = Integer.parseInt(value.substring(hyphenPos + 1));
+ min = type.checkValidValue(min);
+ max = type.checkValidValue(max);
+ return ValueRange.of(min, max);
+ }
+ }
+ }
+
+ private static String replaceOrdinals(String value, String[] list) {
+ value = value.toUpperCase();
+ for (int i = 0; i < list.length; i++) {
+ String replacement = Integer.toString(i + 1);
+ value = StringUtils.replace(value, list[i], replacement);
+ }
+ return value;
+ }
+
+
+ /**
+ * Get the next or same {@link Temporal} in the sequence matching this
+ * cron field.
+ * @param temporal the seed value
+ * @return the next or same temporal matching the pattern
+ */
+ @Nullable
+ public T nextOrSame(T temporal) {
+ int current = this.type.get(temporal);
+ int next = this.bits.nextSetBit(current);
+ if (next == -1) {
+ temporal = this.type.rollForward(temporal);
+ next = this.bits.nextSetBit(0);
+ }
+ if (next == current) {
+ return temporal;
+ }
+ else {
+ int count = 0;
+ current = this.type.get(temporal);
+ while (current != next && count++ < CronExpression.MAX_ATTEMPTS) {
+ temporal = this.type.elapseUntil(temporal, next);
+ current = this.type.get(temporal);
+ }
+ if (count >= CronExpression.MAX_ATTEMPTS) {
+ return null;
+ }
+ return this.type.reset(temporal);
+ }
+ }
+
+
+ BitSet bits() {
+ return this.bits;
+ }
+
+ private void setBits(ValueRange range) {
+ this.bits.set((int) range.getMinimum(), (int) range.getMaximum() + 1);
+ }
+
+ private void setBits(ValueRange range, int delta) {
+ for (int i = (int) range.getMinimum(); i <= range.getMaximum(); i += delta) {
+ this.bits.set(i);
+ }
+ }
+
+
+ @Override
+ public int hashCode() {
+ return this.bits.hashCode();
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (!(o instanceof CronField)) {
+ return false;
+ }
+ CronField other = (CronField) o;
+ return this.type == other.type &&
+ this.bits.equals(other.bits);
+ }
+
+ @Override
+ public String toString() {
+ return this.type + " " + this.bits;
+ }
+
+
+ /**
+ * Represents the type of cron field, i.e. seconds, minutes, hours,
+ * day-of-month, month, day-of-week.
+ */
+ private enum Type {
+ NANO(ChronoField.NANO_OF_SECOND),
+ SECOND(ChronoField.SECOND_OF_MINUTE, ChronoField.NANO_OF_SECOND),
+ MINUTE(ChronoField.MINUTE_OF_HOUR, ChronoField.SECOND_OF_MINUTE, ChronoField.NANO_OF_SECOND),
+ HOUR(ChronoField.HOUR_OF_DAY, ChronoField.MINUTE_OF_HOUR, ChronoField.SECOND_OF_MINUTE, ChronoField.NANO_OF_SECOND),
+ DAY_OF_MONTH(ChronoField.DAY_OF_MONTH, ChronoField.HOUR_OF_DAY, ChronoField.MINUTE_OF_HOUR, ChronoField.SECOND_OF_MINUTE, ChronoField.NANO_OF_SECOND),
+ MONTH(ChronoField.MONTH_OF_YEAR, ChronoField.DAY_OF_MONTH, ChronoField.HOUR_OF_DAY, ChronoField.MINUTE_OF_HOUR, ChronoField.SECOND_OF_MINUTE, ChronoField.NANO_OF_SECOND),
+ DAY_OF_WEEK(ChronoField.DAY_OF_WEEK, ChronoField.HOUR_OF_DAY, ChronoField.MINUTE_OF_HOUR, ChronoField.SECOND_OF_MINUTE, ChronoField.NANO_OF_SECOND);
+
+
+ private final ChronoField field;
+
+ private final ChronoField[] lowerOrders;
+
+
+ Type(ChronoField field, ChronoField... lowerOrders) {
+ this.field = field;
+ this.lowerOrders = lowerOrders;
+ }
+
+
+ /**
+ * Return the value of this type for the given temporal.
+ * @return the value of this type
+ */
+ public int get(Temporal date) {
+ return date.get(this.field);
+ }
+
+ /**
+ * Return the general range of this type. For instance, this methods
+ * will return 0-31 for {@link #MONTH}.
+ * @return the range of this field
+ */
+ public ValueRange range() {
+ return this.field.range();
+ }
+
+ /**
+ * Check whether the given value is valid, i.e. whether it falls in
+ * {@linkplain #range() range}.
+ * @param value the value to check
+ * @return the value that was passed in
+ * @throws IllegalArgumentException if the given value is invalid
+ */
+ public int checkValidValue(int value) {
+ if (this == DAY_OF_WEEK && value == 0) {
+ return value;
+ }
+ else {
+ try {
+ return this.field.checkValidIntValue(value);
+ }
+ catch (DateTimeException ex) {
+ throw new IllegalArgumentException(ex.getMessage(), ex);
+ }
+ }
+ }
+
+ /**
+ * Elapse the given temporal for the difference between the current
+ * value of this field and the goal value. Typically, the returned
+ * temporal will have the given goal as the current value for this type,
+ * but this is not the case for {@link #DAY_OF_MONTH}. For instance,
+ * if {@code goal} is 31, and {@code temporal} is April 16th,
+ * this method returns May 1st, because April 31st does not exist.
+ * @param temporal the temporal to elapse
+ * @param goal the goal value
+ * @param the type of temporal
+ * @return the elapsed temporal, typically with {@code goal} as value
+ * for this type.
+ */
+ public T elapseUntil(T temporal, int goal) {
+ int current = get(temporal);
+ if (current < goal) {
+ return this.field.getBaseUnit().addTo(temporal, goal - current);
+ }
+ else {
+ ValueRange range = temporal.range(this.field);
+ long amount = goal + range.getMaximum() - current + 1 - range.getMinimum();
+ return this.field.getBaseUnit().addTo(temporal, amount);
+ }
+ }
+
+ /**
+ * Roll forward the give temporal until it reaches the next higher
+ * order field. Calling this method is equivalent to calling
+ * {@link #elapseUntil(Temporal, int)} with goal set to the
+ * minimum value of this field's range.
+ * @param temporal the temporal to roll forward
+ * @param the type of temporal
+ * @return the rolled forward temporal
+ */
+ public T rollForward(T temporal) {
+ int current = get(temporal);
+ ValueRange range = temporal.range(this.field);
+ long amount = range.getMaximum() - current + 1;
+ return this.field.getBaseUnit().addTo(temporal, amount);
+ }
+
+ /**
+ * Reset this and all lower order fields of the given temporal to their
+ * minimum value. For instance for {@link #MINUTE}, this method
+ * resets nanos, seconds, and minutes to 0.
+ * @param temporal the temporal to reset
+ * @param the type of temporal
+ * @return the reset temporal
+ */
+ public T reset(T temporal) {
+ for (ChronoField lowerOrder : this.lowerOrders) {
+ if (temporal.isSupported(lowerOrder)) {
+ temporal = lowerOrder.adjustInto(temporal, temporal.range(lowerOrder).getMinimum());
+ }
+ }
+ return temporal;
+ }
+ }
+
+}
diff --git a/spring-context/src/main/java/org/springframework/scheduling/support/CronSequenceGenerator.java b/spring-context/src/main/java/org/springframework/scheduling/support/CronSequenceGenerator.java
index 9325136ac5a..1f2b90f7cf1 100644
--- a/spring-context/src/main/java/org/springframework/scheduling/support/CronSequenceGenerator.java
+++ b/spring-context/src/main/java/org/springframework/scheduling/support/CronSequenceGenerator.java
@@ -53,7 +53,9 @@ import org.springframework.util.StringUtils;
* @author Ruslan Sibgatullin
* @since 3.0
* @see CronTrigger
+ * @deprecated as of 5.3, in favor of {@link CronExpression}
*/
+@Deprecated
public class CronSequenceGenerator {
private final String expression;
@@ -75,23 +77,27 @@ public class CronSequenceGenerator {
/**
- * Construct a {@link CronSequenceGenerator} from the pattern provided,
+ * Construct a {@code CronSequenceGenerator} from the pattern provided,
* using the default {@link TimeZone}.
* @param expression a space-separated list of time fields
* @throws IllegalArgumentException if the pattern cannot be parsed
* @see java.util.TimeZone#getDefault()
+ * @deprecated as of 5.3, in favor of {@link CronExpression#parse(String)}
*/
+ @Deprecated
public CronSequenceGenerator(String expression) {
this(expression, TimeZone.getDefault());
}
/**
- * Construct a {@link CronSequenceGenerator} from the pattern provided,
+ * Construct a {@code CronSequenceGenerator} from the pattern provided,
* using the specified {@link TimeZone}.
* @param expression a space-separated list of time fields
* @param timeZone the TimeZone to use for generated trigger times
* @throws IllegalArgumentException if the pattern cannot be parsed
+ * @deprecated as of 5.3, in favor of {@link CronExpression#parse(String)}
*/
+ @Deprecated
public CronSequenceGenerator(String expression, TimeZone timeZone) {
this.expression = expression;
this.timeZone = timeZone;
diff --git a/spring-context/src/main/java/org/springframework/scheduling/support/CronTrigger.java b/spring-context/src/main/java/org/springframework/scheduling/support/CronTrigger.java
index c98a3134024..3af71481c27 100644
--- a/spring-context/src/main/java/org/springframework/scheduling/support/CronTrigger.java
+++ b/spring-context/src/main/java/org/springframework/scheduling/support/CronTrigger.java
@@ -16,43 +16,65 @@
package org.springframework.scheduling.support;
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
import java.util.Date;
import java.util.TimeZone;
import org.springframework.lang.Nullable;
import org.springframework.scheduling.Trigger;
import org.springframework.scheduling.TriggerContext;
+import org.springframework.util.Assert;
/**
* {@link Trigger} implementation for cron expressions.
- * Wraps a {@link CronSequenceGenerator}.
+ * Wraps a {@link CronExpression}.
*
* @author Juergen Hoeller
+ * @author Arjen Poutsma
* @since 3.0
- * @see CronSequenceGenerator
+ * @see CronExpression
*/
public class CronTrigger implements Trigger {
- private final CronSequenceGenerator sequenceGenerator;
+ private final CronExpression expression;
+
+ private final ZoneId zoneId;
/**
- * Build a {@link CronTrigger} from the pattern provided in the default time zone.
+ * Build a {@code CronTrigger} from the pattern provided in the default time zone.
* @param expression a space-separated list of time fields, following cron
* expression conventions
*/
public CronTrigger(String expression) {
- this.sequenceGenerator = new CronSequenceGenerator(expression);
+ this(expression, ZoneId.systemDefault());
}
/**
- * Build a {@link CronTrigger} from the pattern provided in the given time zone.
+ * Build a {@code CronTrigger} from the pattern provided in the given time zone.
* @param expression a space-separated list of time fields, following cron
* expression conventions
* @param timeZone a time zone in which the trigger times will be generated
*/
public CronTrigger(String expression, TimeZone timeZone) {
- this.sequenceGenerator = new CronSequenceGenerator(expression, timeZone);
+ this(expression, timeZone.toZoneId());
+ }
+
+ /**
+ * Build a {@code CronTrigger} from the pattern provided in the given time zone.
+ * @param expression a space-separated list of time fields, following cron
+ * expression conventions
+ * @param zoneId a time zone in which the trigger times will be generated
+ * @since 5.3
+ * @see CronExpression#parse(String)
+ */
+ public CronTrigger(String expression, ZoneId zoneId) {
+ Assert.hasLength(expression, "Expression must not be empty");
+ Assert.notNull(zoneId, "ZoneId must not be null");
+
+ this.expression = CronExpression.parse(expression);
+ this.zoneId = zoneId;
}
@@ -60,7 +82,7 @@ public class CronTrigger implements Trigger {
* Return the cron pattern that this trigger has been built with.
*/
public String getExpression() {
- return this.sequenceGenerator.getExpression();
+ return this.expression.toString();
}
@@ -85,24 +107,26 @@ public class CronTrigger implements Trigger {
else {
date = new Date();
}
- return this.sequenceGenerator.next(date);
+ ZonedDateTime dateTime = ZonedDateTime.ofInstant(date.toInstant(), this.zoneId);
+ ZonedDateTime next = this.expression.next(dateTime);
+ return next != null ? Date.from(next.toInstant()) : null;
}
@Override
public boolean equals(@Nullable Object other) {
return (this == other || (other instanceof CronTrigger &&
- this.sequenceGenerator.equals(((CronTrigger) other).sequenceGenerator)));
+ this.expression.equals(((CronTrigger) other).expression)));
}
@Override
public int hashCode() {
- return this.sequenceGenerator.hashCode();
+ return this.expression.hashCode();
}
@Override
public String toString() {
- return this.sequenceGenerator.toString();
+ return this.expression.toString();
}
}
diff --git a/spring-context/src/test/java/org/springframework/scheduling/support/BitSetAssert.java b/spring-context/src/test/java/org/springframework/scheduling/support/BitSetAssert.java
new file mode 100644
index 00000000000..a22ca83209e
--- /dev/null
+++ b/spring-context/src/test/java/org/springframework/scheduling/support/BitSetAssert.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright 2002-2020 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.scheduling.support;
+
+import java.util.BitSet;
+
+import org.assertj.core.api.AbstractAssert;
+
+/**
+ * @author Arjen Poutsma
+ */
+public class BitSetAssert extends AbstractAssert {
+
+ private BitSetAssert(BitSet bitSet) {
+ super(bitSet, BitSetAssert.class);
+ }
+
+ public static BitSetAssert assertThat(BitSet actual) {
+ return new BitSetAssert(actual);
+ }
+
+ public BitSetAssert hasSet(int... indices) {
+ isNotNull();
+
+ for (int index : indices) {
+ if (!this.actual.get(index)) {
+ failWithMessage("Invalid disabled bit at @%d", index);
+ }
+ }
+ return this;
+ }
+
+ public BitSetAssert hasSetRange(int min, int max) {
+ isNotNull();
+
+ for (int i = min; i < max; i++) {
+ if (!this.actual.get(i)) {
+ failWithMessage("Invalid disabled bit at @%d", i);
+ }
+ }
+ return this;
+ }
+
+ public BitSetAssert hasUnset(int... indices) {
+ isNotNull();
+
+ for (int index : indices) {
+ if (this.actual.get(index)) {
+ failWithMessage("Invalid enabled bit at @%d", index);
+ }
+ }
+ return this;
+ }
+
+ public BitSetAssert hasUnsetRange(int min, int max) {
+ isNotNull();
+
+ for (int i = min; i < max; i++) {
+ if (this.actual.get(i)) {
+ failWithMessage("Invalid enabled bit at @%d", i);
+ }
+ }
+ return this;
+ }
+
+}
+
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
new file mode 100644
index 00000000000..b1e2b54e7d1
--- /dev/null
+++ b/spring-context/src/test/java/org/springframework/scheduling/support/CronExpressionTests.java
@@ -0,0 +1,434 @@
+/*
+ * Copyright 2002-2020 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.scheduling.support;
+
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+import java.time.Year;
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+
+import org.junit.jupiter.api.Test;
+
+import static java.time.DayOfWeek.MONDAY;
+import static java.time.DayOfWeek.TUESDAY;
+import static java.time.DayOfWeek.WEDNESDAY;
+import static java.time.temporal.TemporalAdjusters.next;
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * @author Arjen Poutsma
+ */
+class CronExpressionTests {
+
+ @Test
+ void matchAll() {
+ CronExpression expression = CronExpression.parse("* * * * * *");
+
+ LocalDateTime last = LocalDateTime.now();
+ LocalDateTime expected = last.plusSeconds(1).withNano(0);
+ assertThat(expression.next(last)).isEqualTo(expected);
+ }
+
+ @Test
+ void matchLastSecond() {
+ CronExpression expression = CronExpression.parse("* * * * * *");
+
+ LocalDateTime last = LocalDateTime.now().withSecond(58);
+ LocalDateTime expected = last.plusSeconds(1).withNano(0);
+ assertThat(expression.next(last)).isEqualTo(expected);
+ }
+
+ @Test
+ void matchSpecificSecond() {
+ CronExpression expression = CronExpression.parse("10 * * * * *");
+
+ LocalDateTime now = LocalDateTime.now();
+ LocalDateTime last = now.withSecond(9);
+ LocalDateTime expected = last.withSecond(10).withNano(0);
+ assertThat(expression.next(last)).isEqualTo(expected);
+ }
+
+ @Test
+ void incrementSecondByOne() {
+ CronExpression expression = CronExpression.parse("11 * * * * *");
+
+ LocalDateTime last = LocalDateTime.now().withSecond(10);
+ LocalDateTime expected = last.plusSeconds(1).withNano(0);
+ assertThat(expression.next(last)).isEqualTo(expected);
+ }
+
+ @Test
+ void incrementSecondAndRollover() {
+ CronExpression expression = CronExpression.parse("10 * * * * *");
+
+ LocalDateTime last = LocalDateTime.now().withSecond(11);
+ LocalDateTime expected = last.plusMinutes(1).withSecond(10).withNano(0);
+ assertThat(expression.next(last)).isEqualTo(expected);
+ }
+
+ @Test
+ void secondRange() {
+ CronExpression expression = CronExpression.parse("10-15 * * * * *");
+ LocalDateTime now = LocalDateTime.now();
+
+ for (int i = 9; i < 15; i++) {
+ LocalDateTime last = now.withSecond(i);
+ LocalDateTime expected = last.plusSeconds(1).withNano(0);
+ assertThat(expression.next(last)).isEqualTo(expected);
+ }
+ }
+
+ @Test
+ void incrementMinute() {
+ CronExpression expression = CronExpression.parse("0 * * * * *");
+
+ LocalDateTime last = LocalDateTime.now().withMinute(10);
+ LocalDateTime expected = last.plusMinutes(1).withSecond(0).withNano(0);
+ LocalDateTime actual = expression.next(last);
+ assertThat(actual).isEqualTo(expected);
+
+ last = actual;
+ expected = expected.plusMinutes(1);
+ assertThat(expression.next(last)).isEqualTo(expected);
+ }
+
+ @Test
+ void incrementMinuteByOne() {
+ CronExpression expression = CronExpression.parse("0 11 * * * *");
+
+ LocalDateTime last = LocalDateTime.now().withMinute(10);
+ LocalDateTime expected = last.plusMinutes(1).withSecond(0).withNano(0);
+ assertThat(expression.next(last)).isEqualTo(expected);
+ }
+
+ @Test
+ void incrementMinuteAndRollover() {
+ CronExpression expression = CronExpression.parse("0 10 * * * *");
+
+ LocalDateTime last = LocalDateTime.now().withMinute(11).withSecond(0);
+ LocalDateTime expected = last.plusMinutes(59).withNano(0);
+ assertThat(expression.next(last)).isEqualTo(expected);
+ }
+
+ @Test
+ void incrementHour() {
+ CronExpression expression = CronExpression.parse("0 0 * * * *");
+
+ int year = Year.now().getValue();
+ LocalDateTime last = LocalDateTime.of(year, 10, 30, 11, 1);
+ LocalDateTime expected = last.withHour(12).withMinute(0);
+ LocalDateTime actual = expression.next(last);
+ assertThat(actual).isEqualTo(expected);
+
+ last = actual;
+ expected = expected.withHour(13);
+ assertThat(expression.next(last)).isEqualTo(expected);
+ }
+
+ @Test
+ void incrementHourAndRollover() {
+ CronExpression expression = CronExpression.parse("0 0 * * * *");
+
+ int year = Year.now().getValue();
+ LocalDateTime last = LocalDateTime.of(year, 9, 10, 23, 1);
+ LocalDateTime expected = last.withDayOfMonth(11).withHour(0).withMinute(0);
+ assertThat(expression.next(last)).isEqualTo(expected);
+ }
+
+ @Test
+ void incrementDayOfMonth() {
+ CronExpression expression = CronExpression.parse("0 0 0 * * *");
+
+ LocalDateTime last = LocalDateTime.now().withDayOfMonth(1);
+ 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);
+ assertThat(expression.next(last)).isEqualTo(expected);
+ }
+
+ @Test
+ void incrementDayOfMonthByOne() {
+ CronExpression expression = CronExpression.parse("* * * 10 * *");
+
+ LocalDateTime last = LocalDateTime.now().withDayOfMonth(9);
+ LocalDateTime expected = last.plusDays(1).withHour(0).withMinute(0).withSecond(0).withNano(0);
+ assertThat(expression.next(last)).isEqualTo(expected);
+ }
+
+ @Test
+ void incrementDayOfMonthAndRollover() {
+ CronExpression expression = CronExpression.parse("* * * 10 * *");
+
+ LocalDateTime last = LocalDateTime.now().withDayOfMonth(11);
+ LocalDateTime expected =
+ last.plusMonths(1).withDayOfMonth(10).withHour(0).withMinute(0).withSecond(0).withNano(0);
+ assertThat(expression.next(last)).isEqualTo(expected);
+ }
+
+ @Test
+ void dailyTriggerInShortMonth() {
+ CronExpression expression = CronExpression.parse("0 0 0 * * *");
+
+ // September: 30 days
+ LocalDateTime last = LocalDateTime.now().withMonth(9).withDayOfMonth(30);
+ LocalDateTime expected = LocalDateTime.of(last.getYear(), 10, 1, 0, 0);
+ LocalDateTime actual = expression.next(last);
+ assertThat(actual).isEqualTo(expected);
+
+ last = actual;
+ expected = expected.withDayOfMonth(2);
+ assertThat(expression.next(last)).isEqualTo(expected);
+ }
+
+ @Test
+ void dailyTriggerInLongMonth() {
+ CronExpression expression = CronExpression.parse("0 0 0 * * *");
+
+ // August: 31 days and not a daylight saving boundary
+ LocalDateTime last = LocalDateTime.now().withMonth(8).withDayOfMonth(30);
+ 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);
+ assertThat(expression.next(last)).isEqualTo(expected);
+ }
+
+ @Test
+ void dailyTriggerOnDaylightSavingBoundary() {
+ CronExpression expression = CronExpression.parse("0 0 0 * * *");
+
+ // October: 31 days and a daylight saving boundary in CET
+ ZonedDateTime last = ZonedDateTime.now(ZoneId.of("CET")).withMonth(10).withDayOfMonth(30);
+ ZonedDateTime expected = last.withDayOfMonth(31).withHour(0).withMinute(0).withSecond(0).withNano(0);
+ ZonedDateTime actual = expression.next(last);
+ assertThat(actual).isEqualTo(expected);
+
+ last = actual;
+ expected = expected.withMonth(11).withDayOfMonth(1);
+ assertThat(expression.next(last)).isEqualTo(expected);
+ }
+
+ @Test
+ void incrementMonth() {
+ CronExpression expression = CronExpression.parse("0 0 0 1 * *");
+
+ LocalDateTime last = LocalDateTime.now().withMonth(10).withDayOfMonth(30);
+ LocalDateTime expected = LocalDateTime.of(last.getYear(), 11, 1, 0, 0);
+ LocalDateTime actual = expression.next(last);
+ assertThat(actual).isEqualTo(expected);
+
+ last = actual;
+ expected = expected.withMonth(12);
+ assertThat(expression.next(last)).isEqualTo(expected);
+ }
+
+ @Test
+ void incrementMonthAndRollover() {
+ CronExpression expression = CronExpression.parse("0 0 0 1 * *");
+
+ LocalDateTime last = LocalDateTime.now().withYear(2010).withMonth(12).withDayOfMonth(31);
+ LocalDateTime expected = LocalDateTime.of(2011, 1, 1, 0, 0);
+ LocalDateTime actual = expression.next(last);
+ assertThat(actual).isEqualTo(expected);
+
+ last = actual;
+ expected = expected.plusMonths(1);
+ assertThat(expression.next(last)).isEqualTo(expected);
+ }
+
+ @Test
+ void monthlyTriggerInLongMonth() {
+ CronExpression expression = CronExpression.parse("0 0 0 31 * *");
+
+ LocalDateTime last = LocalDateTime.now().withMonth(10).withDayOfMonth(30);
+ LocalDateTime expected = last.withDayOfMonth(31).withHour(0).withMinute(0).withSecond(0).withNano(0);
+ assertThat(expression.next(last)).isEqualTo(expected);
+ }
+
+ @Test
+ void monthlyTriggerInShortMonth() {
+ CronExpression expression = CronExpression.parse("0 0 0 1 * *");
+
+ LocalDateTime last = LocalDateTime.now().withMonth(10).withDayOfMonth(30);
+ LocalDateTime expected = LocalDateTime.of(last.getYear(), 11, 1, 0, 0);
+ assertThat(expression.next(last)).isEqualTo(expected);
+ }
+
+ @Test
+ void incrementDayOfWeekByOne() {
+ CronExpression expression = CronExpression.parse("* * * * * 2");
+
+ LocalDateTime last = LocalDateTime.now().with(next(MONDAY));
+ LocalDateTime expected = last.plusDays(1).withHour(0).withMinute(0).withSecond(0).withNano(0);
+ LocalDateTime actual = expression.next(last);
+ assertThat(actual).isEqualTo(expected);
+ assertThat(actual.getDayOfWeek()).isEqualTo(TUESDAY);
+ }
+
+ @Test
+ void incrementDayOfWeekAndRollover() {
+ CronExpression expression = CronExpression.parse("* * * * * 2");
+
+ LocalDateTime last = LocalDateTime.now().with(next(WEDNESDAY));
+ LocalDateTime expected = last.plusDays(6).withHour(0).withMinute(0).withSecond(0).withNano(0);
+ LocalDateTime actual = expression.next(last);
+ assertThat(actual).isEqualTo(expected);
+ assertThat(actual.getDayOfWeek()).isEqualTo(TUESDAY);
+ }
+
+ @Test
+ void specificMinuteSecond() {
+ CronExpression expression = CronExpression.parse("55 5 * * * *");
+
+ LocalDateTime last = LocalDateTime.now().withMinute(4).withSecond(54);
+ LocalDateTime expected = last.plusMinutes(1).withSecond(55).withNano(0);
+ LocalDateTime actual = expression.next(last);
+ assertThat(actual).isEqualTo(expected);
+
+ last = actual;
+ expected = expected.plusHours(1);
+ assertThat(expression.next(last)).isEqualTo(expected);
+ }
+
+ @Test
+ void specificHourSecond() {
+ CronExpression expression = CronExpression.parse("55 * 10 * * *");
+
+ LocalDateTime last = LocalDateTime.now().withHour(9).withSecond(54);
+ LocalDateTime expected = last.plusHours(1).withMinute(0).withSecond(55).withNano(0);
+ LocalDateTime actual = expression.next(last);
+ assertThat(actual).isEqualTo(expected);
+
+ last = actual;
+ expected = expected.plusMinutes(1);
+ assertThat(expression.next(last)).isEqualTo(expected);
+ }
+
+ @Test
+ void specificMinuteHour() {
+ CronExpression expression = CronExpression.parse("* 5 10 * * *");
+
+ LocalDateTime last = LocalDateTime.now().withHour(9).withMinute(4);
+ LocalDateTime expected = last.plusHours(1).plusMinutes(1).withSecond(0).withNano(0);
+ LocalDateTime actual = expression.next(last);
+ assertThat(actual).isEqualTo(expected);
+
+ last = actual;
+ // next trigger is in one second because second is wildcard
+ expected = expected.plusSeconds(1);
+ assertThat(expression.next(last)).isEqualTo(expected);
+ }
+
+ @Test
+ void specificDayOfMonthSecond() {
+ CronExpression expression = CronExpression.parse("55 * * 3 * *");
+
+ LocalDateTime last = LocalDateTime.now().withDayOfMonth(2).withSecond(54);
+ LocalDateTime expected = last.plusDays(1).withHour(0).withMinute(0).withSecond(55).withNano(0);
+ LocalDateTime actual = expression.next(last);
+ assertThat(actual).isEqualTo(expected);
+
+ last = actual;
+ expected = expected.plusMinutes(1);
+ assertThat(expression.next(last)).isEqualTo(expected);
+ }
+
+ @Test
+ void specificDate() {
+ CronExpression expression = CronExpression.parse("* * * 3 11 *");
+
+ LocalDateTime last = LocalDateTime.now().withMonth(10).withDayOfMonth(2);
+ LocalDateTime expected = LocalDateTime.of(last.getYear(), 11, 3, 0, 0);
+ LocalDateTime actual = expression.next(last);
+ assertThat(actual).isEqualTo(expected);
+
+ last = actual;
+ expected = expected.plusSeconds(1);
+ assertThat(expression.next(last)).isEqualTo(expected);
+ }
+
+ @Test
+ void nonExistentSpecificDate() {
+ CronExpression expression = CronExpression.parse("0 0 0 31 6 *");
+
+ LocalDateTime last = LocalDateTime.now().withMonth(3).withDayOfMonth(10);
+ assertThat(expression.next(last)).isNull();
+ }
+
+ @Test
+ void leapYearSpecificDate() {
+ CronExpression expression = CronExpression.parse("0 0 0 29 2 *");
+
+ LocalDateTime last = LocalDateTime.now().withYear(2007).withMonth(2).withDayOfMonth(10);
+ LocalDateTime expected = LocalDateTime.of(2008, 2, 29, 0, 0);
+ LocalDateTime actual = expression.next(last);
+ assertThat(actual).isEqualTo(expected);
+
+ last = actual;
+ expected = expected.plusYears(4);
+ assertThat(expression.next(last)).isEqualTo(expected);
+ }
+
+ @Test
+ void weekDaySequence() {
+ CronExpression expression = CronExpression.parse("0 0 7 ? * MON-FRI");
+
+ // This is a Saturday
+ LocalDateTime last = LocalDateTime.of(LocalDate.of(2009, 9, 26), LocalTime.now());
+ LocalDateTime expected = last.plusDays(2).withHour(7).withMinute(0).withSecond(0).withNano(0);
+ LocalDateTime actual = expression.next(last);
+ assertThat(actual).isEqualTo(expected);
+
+ // Next day is a week day so add one
+ 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 monthSequence() {
+ CronExpression expression = CronExpression.parse("0 30 23 30 1/3 ?");
+
+ LocalDateTime last = LocalDateTime.of(LocalDate.of(2010, 12, 30), LocalTime.now());
+ LocalDateTime expected = last.plusMonths(1).withHour(23).withMinute(30).withSecond(0).withSecond(0).withNano(0);
+ LocalDateTime actual = expression.next(last);
+ assertThat(actual).isEqualTo(expected);
+
+ // Next trigger is 3 months later
+ last = actual;
+ expected = expected.plusMonths(3);
+ actual = expression.next(last);
+ assertThat(actual).isEqualTo(expected);
+
+ last = actual;
+ expected = expected.plusMonths(3);
+ assertThat(expression.next(last)).isEqualTo(expected);
+ }
+
+}
diff --git a/spring-context/src/test/java/org/springframework/scheduling/support/CronFieldTests.java b/spring-context/src/test/java/org/springframework/scheduling/support/CronFieldTests.java
new file mode 100644
index 00000000000..7e0d157b3a4
--- /dev/null
+++ b/spring-context/src/test/java/org/springframework/scheduling/support/CronFieldTests.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2002-2020 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.scheduling.support;
+
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
+import static org.springframework.scheduling.support.BitSetAssert.assertThat;
+
+/**
+ * @author Arjen Poutsma
+ */
+public class CronFieldTests {
+
+ @Test
+ void parse() {
+ assertThat(CronField.parseSeconds("42").bits()).hasUnsetRange(0, 41).hasSet(42).hasUnsetRange(43, 59);
+ assertThat(CronField.parseMinutes("1,2,5,9").bits()).hasUnset(0).hasSet(1, 2).hasUnset(3,4).hasSet(5).hasUnsetRange(6,8).hasSet(9).hasUnsetRange(10,59);
+ assertThat(CronField.parseSeconds("0-4,8-12").bits()).hasSetRange(0, 4).hasUnsetRange(5,7).hasSetRange(8, 12).hasUnsetRange(13,59);
+ assertThat(CronField.parseHours("0-23/2").bits()).hasSet(0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22).hasUnset(1,3,5,7,9,11,13,15,17,19,21,23);
+ assertThat(CronField.parseDaysOfWeek("0").bits()).hasUnsetRange(0, 6).hasSet(7, 7);
+ assertThat(CronField.parseSeconds("57/2").bits()).hasUnsetRange(0, 56).hasSet(57).hasUnset(58).hasSet(59);
+ }
+
+ @Test
+ void invalidRange() {
+ assertThatIllegalArgumentException().isThrownBy(() -> CronField.parseSeconds(""));
+ assertThatIllegalArgumentException().isThrownBy(() -> CronField.parseSeconds("0-12/0"));
+ assertThatIllegalArgumentException().isThrownBy(() -> CronField.parseSeconds("60"));
+ assertThatIllegalArgumentException().isThrownBy(() -> CronField.parseMinutes("60"));
+ assertThatIllegalArgumentException().isThrownBy(() -> CronField.parseDaysOfMonth("0"));
+ assertThatIllegalArgumentException().isThrownBy(() -> CronField.parseDaysOfMonth("32"));
+ assertThatIllegalArgumentException().isThrownBy(() -> CronField.parseMonth("0"));
+ assertThatIllegalArgumentException().isThrownBy(() -> CronField.parseMonth("13"));
+ assertThatIllegalArgumentException().isThrownBy(() -> CronField.parseDaysOfWeek("8"));
+ assertThatIllegalArgumentException().isThrownBy(() -> CronField.parseSeconds("20-10"));
+ }
+
+ @Test
+ void parseWildcards() {
+ assertThat(CronField.parseSeconds("*").bits()).hasSetRange(0, 60);
+ assertThat(CronField.parseMinutes("*").bits()).hasSetRange(0, 60);
+ assertThat(CronField.parseHours("*").bits()).hasSetRange(0, 23);
+ assertThat(CronField.parseDaysOfMonth("*").bits()).hasUnset(0).hasSetRange(1, 31);
+ assertThat(CronField.parseDaysOfMonth("?").bits()).hasUnset(0).hasSetRange(1, 31);
+ assertThat(CronField.parseMonth("*").bits()).hasUnset(0).hasSetRange(1, 12);
+ assertThat(CronField.parseDaysOfWeek("*").bits()).hasUnset(0).hasSetRange(1, 7);
+ assertThat(CronField.parseDaysOfWeek("?").bits()).hasUnset(0).hasSetRange(1, 7);
+ }
+
+ @Test
+ void names() {
+ assertThat(CronField.parseMonth("JAN,FEB,MAR,APR,MAY,JUN,JUL,AUG,SEP,OCT,NOV,DEC").bits())
+ .hasUnset(0).hasSetRange(1, 12);
+ assertThat(CronField.parseDaysOfWeek("SUN,MON,TUE,WED,THU,FRI,SAT").bits())
+ .hasUnset(0).hasSetRange(1, 7);
+ }
+
+}
diff --git a/spring-context/src/test/java/org/springframework/scheduling/support/CronTriggerTests.java b/spring-context/src/test/java/org/springframework/scheduling/support/CronTriggerTests.java
index 2f326ae74b9..119b5bdbd27 100644
--- a/spring-context/src/test/java/org/springframework/scheduling/support/CronTriggerTests.java
+++ b/spring-context/src/test/java/org/springframework/scheduling/support/CronTriggerTests.java
@@ -557,7 +557,7 @@ class CronTriggerTests {
this.calendar.set(Calendar.MONTH, 2);
Date localDate = this.calendar.getTime();
TriggerContext context1 = getTriggerContext(localDate);
- assertThatIllegalArgumentException().isThrownBy(() -> trigger.nextExecutionTime(context1));
+ assertThat(trigger.nextExecutionTime(context1)).isNull();
}
@ParameterizedCronTriggerTest