Browse Source

Add support for Quartz features in CronExpression

This commit introduces support for Quartz-specific features in
CronExpression. This includes support for "L", "W", and "#".

Closes gh-20106
Closes gh-22436
pull/25528/head
Arjen Poutsma 5 years ago
parent
commit
93b53dae29
  1. 235
      spring-context/src/main/java/org/springframework/scheduling/support/BitsCronField.java
  2. 56
      spring-context/src/main/java/org/springframework/scheduling/support/CronExpression.java
  3. 176
      spring-context/src/main/java/org/springframework/scheduling/support/CronField.java
  4. 309
      spring-context/src/main/java/org/springframework/scheduling/support/QuartzCronField.java
  5. 73
      spring-context/src/test/java/org/springframework/scheduling/support/BitsCronFieldTests.java
  6. 515
      spring-context/src/test/java/org/springframework/scheduling/support/CronExpressionTests.java
  7. 73
      spring-context/src/test/java/org/springframework/scheduling/support/CronFieldTests.java
  8. 144
      spring-context/src/test/java/org/springframework/scheduling/support/QuartzCronFieldTests.java

235
spring-context/src/main/java/org/springframework/scheduling/support/BitsCronField.java

@ -0,0 +1,235 @@ @@ -0,0 +1,235 @@
/*
* 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.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;
/**
* Efficient {@link BitSet}-based extension of {@link CronField}.
* Created using the {@code parse*} methods.
*
* @author Arjen Poutsma
* @since 5.3
*/
final class BitsCronField extends CronField {
private static final BitsCronField ZERO_NANOS;
static {
ZERO_NANOS = new BitsCronField(Type.NANO);
ZERO_NANOS.bits.set(0);
}
private final BitSet bits;
private BitsCronField(Type type) {
super(type);
this.bits = new BitSet((int) type.range().getMaximum());
}
/**
* Return a {@code BitsCronField} enabled for 0 nano seconds.
*/
public static BitsCronField zeroNanos() {
return BitsCronField.ZERO_NANOS;
}
/**
* Parse the given value into a seconds {@code BitsCronField}, the first entry of a cron expression.
*/
public static BitsCronField parseSeconds(String value) {
return parseField(value, Type.SECOND);
}
/**
* Parse the given value into a minutes {@code BitsCronField}, the second entry of a cron expression.
*/
public static BitsCronField parseMinutes(String value) {
return BitsCronField.parseField(value, Type.MINUTE);
}
/**
* Parse the given value into a hours {@code BitsCronField}, the third entry of a cron expression.
*/
public static BitsCronField parseHours(String value) {
return BitsCronField.parseField(value, Type.HOUR);
}
/**
* Parse the given value into a days of months {@code BitsCronField}, the fourth entry of a cron expression.
*/
public static BitsCronField parseDaysOfMonth(String value) {
return parseDate(value, Type.DAY_OF_MONTH);
}
/**
* Parse the given value into a month {@code BitsCronField}, the fifth entry of a cron expression.
*/
public static BitsCronField parseMonth(String value) {
return BitsCronField.parseField(value, Type.MONTH);
}
/**
* Parse the given value into a days of week {@code BitsCronField}, the sixth entry of a cron expression.
*/
public static BitsCronField parseDaysOfWeek(String value) {
BitsCronField result = parseDate(value, Type.DAY_OF_WEEK);
BitSet bits = result.bits;
if (bits.get(0)) {
// cron supports 0 for Sunday; we use 7 like java.time
bits.set(7);
bits.clear(0);
}
return result;
}
private static BitsCronField parseDate(String value, BitsCronField.Type type) {
if (value.indexOf('?') != -1) {
value = "*";
}
return BitsCronField.parseField(value, type);
}
private static BitsCronField parseField(String value, Type type) {
Assert.hasLength(value, "Value must not be empty");
Assert.notNull(type, "Type must not be null");
try {
BitsCronField result = new BitsCronField(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);
}
}
}
@Nullable
@Override
public <T extends Temporal & Comparable<? super T>> T nextOrSame(T temporal) {
int current = type().get(temporal);
int next = this.bits.nextSetBit(current);
if (next == -1) {
temporal = type().rollForward(temporal);
next = this.bits.nextSetBit(0);
}
if (next == current) {
return temporal;
}
else {
int count = 0;
current = type().get(temporal);
while (current != next && count++ < CronExpression.MAX_ATTEMPTS) {
temporal = type().elapseUntil(temporal, next);
current = type().get(temporal);
}
if (count >= CronExpression.MAX_ATTEMPTS) {
return null;
}
return 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 BitsCronField)) {
return false;
}
BitsCronField other = (BitsCronField) o;
return type() == other.type() &&
this.bits.equals(other.bits);
}
@Override
public String toString() {
return type() + " " + this.bits;
}
}

56
spring-context/src/main/java/org/springframework/scheduling/support/CronExpression.java

@ -67,7 +67,7 @@ public final class CronExpression { @@ -67,7 +67,7 @@ public final class CronExpression {
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.fields = new CronField[]{CronField.zeroNanos(), seconds, minutes, hours, daysOfMonth, months, daysOfWeek};
this.expression = expression;
}
@ -100,14 +100,49 @@ public final class CronExpression { @@ -100,14 +100,49 @@ public final class CronExpression {
* Ranges of numbers are expressed by two numbers separated with a hyphen
* ({@code -}). The specified range is inclusive.
* </li>
* <li>Following a range (or {@code *}) with {@code "/n"} specifies
* skips of the number's value through the range.
* <li>Following a range (or {@code *}) with {@code /n} specifies
* the interval of the number's value through the range.
* </li>
* <li>
* 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).
* </li>
* <li>
* The "day of month" and "day of week" fields can contain a
* {@code L}-character, which stands for "last", and has a different meaning
* in each field:
* <ul>
* <li>
* In the "day of month" field, {@code L} stands for "the last day of the
* month". If followed by an negative offset (i.e. {@code L-n}), it means
* "{@code n}th-to-last day of the month". If followed by {@code W} (i.e.
* {@code LW}), it means "the last weekday of the month".
* </li>
* <li>
* In the "day of week" field, {@code L} stands for "the last day of the
* week", and uses the
* {@linkplain java.util.Locale#getDefault() system default locale}
* to determine which day that is (i.e. Sunday or Saturday).
* If prefixed by a number or three-letter name (i.e. {@code dL} or
* {@code DDDL}), it means "the last day of week {@code d} (or {@code DDD})
* in the month".
* </li>
* </ul>
* </li>
* <li>
* The "day of month" field can be {@code nW}, which stands for "the nearest
* weekday to day of the month {@code n}".
* If {@code n} falls on Saturday, this yields the Friday before it.
* If {@code n} falls on Sunday, this yields the Monday after,
* which also happens if {@code n} is {@code 1} and falls on a Saturday
* (i.e. {@code 1W} stands for "the first weekday of the month").
* </li>
* <li>
* The "day of week" field can be {@code d#n} (or {@code DDD#n}), which
* stands for "the {@code n}-th day of week {@code d} (or {@code DDD}) in
* the month".
* </li>
* </ul>
*
* <p>Example expressions:
@ -119,6 +154,15 @@ public final class CronExpression { @@ -119,6 +154,15 @@ public final class CronExpression {
* <li>{@code "0 0/30 8-10 * * *"} = 8:00, 8:30, 9:00, 9:30, 10:00 and 10:30 every day.</li>
* <li>{@code "0 0 9-17 * * MON-FRI"} = on the hour nine-to-five weekdays</li>
* <li>{@code "0 0 0 25 12 ?"} = every Christmas Day at midnight</li>
* <li>{@code "0 0 0 L * *"} = last day of the month at midnight</li>
* <li>{@code "0 0 0 L-3 * *"} = third-to-last day of the month at midnight</li>
* <li>{@code "0 0 0 1W * *"} = first weekday of the month at midnight</li>
* <li>{@code "0 0 0 LW * *"} = last weekday of the month at midnight</li>
* <li>{@code "0 0 0 * * L"} = last day of the week at midnight</li>
* <li>{@code "0 0 0 * * 5L"} = last Friday of the month at midnight</li>
* <li>{@code "0 0 0 * * THUL"} = last Thursday of the month at midnight</li>
* <li>{@code "0 0 0 ? * 5#2"} = the second Friday in the month at midnight</li>
* <li>{@code "0 0 0 ? * MON#1"} = the first Monday in the month at midnight</li>
* </ul>
*
* <p>The following macros are also supported:
@ -181,13 +225,13 @@ public final class CronExpression { @@ -181,13 +225,13 @@ public final class CronExpression {
* if no such temporal can be found
*/
@Nullable
public <T extends Temporal> T next(T temporal) {
public <T extends Temporal & Comparable<? super T>> T next(T temporal) {
return nextOrSame(ChronoUnit.NANOS.addTo(temporal, 1));
}
@Nullable
private <T extends Temporal> T nextOrSame(T temporal) {
private <T extends Temporal & Comparable<? super T>> T nextOrSame(T temporal) {
for (int i = 0; i < MAX_ATTEMPTS; i++) {
T result = nextOrSameInternal(temporal);
if (result == null || result.equals(temporal)) {
@ -199,7 +243,7 @@ public final class CronExpression { @@ -199,7 +243,7 @@ public final class CronExpression {
}
@Nullable
private <T extends Temporal> T nextOrSameInternal(T temporal) {
private <T extends Temporal & Comparable<? super T>> T nextOrSameInternal(T temporal) {
for (CronField field : this.fields) {
temporal = field.nextOrSame(temporal);
if (temporal == null) {

176
spring-context/src/main/java/org/springframework/scheduling/support/CronField.java

@ -20,79 +20,69 @@ import java.time.DateTimeException; @@ -20,79 +20,69 @@ 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,
* 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 {
abstract 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) {
protected 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;
return BitsCronField.zeroNanos();
}
/**
* 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);
return BitsCronField.parseSeconds(value);
}
/**
* 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);
return BitsCronField.parseMinutes(value);
}
/**
* 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);
return BitsCronField.parseHours(value);
}
/**
* 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);
if (value.contains("L") || value.contains("W")) {
return QuartzCronField.parseDaysOfMonth(value);
}
else {
return BitsCronField.parseDaysOfMonth(value);
}
}
/**
@ -100,7 +90,7 @@ final class CronField { @@ -100,7 +90,7 @@ final class CronField {
*/
public static CronField parseMonth(String value) {
value = replaceOrdinals(value, MONTHS);
return parseField(value, Type.MONTH);
return BitsCronField.parseMonth(value);
}
/**
@ -108,77 +98,15 @@ final class CronField { @@ -108,77 +98,15 @@ final class CronField {
*/
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();
if (value.contains("L") || value.contains("#")) {
return QuartzCronField.parseDaysOfWeek(value);
}
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);
}
return BitsCronField.parseDaysOfWeek(value);
}
}
private static String replaceOrdinals(String value, String[] list) {
value = value.toUpperCase();
for (int i = 0; i < list.length; i++) {
@ -196,67 +124,11 @@ final class CronField { @@ -196,67 +124,11 @@ final class CronField {
* @return the next or same temporal matching the pattern
*/
@Nullable
public <T extends Temporal> 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);
}
}
public abstract <T extends Temporal & Comparable<? super T>> T nextOrSame(T 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;
protected Type type() {
return this.type;
}
@ -264,7 +136,7 @@ final class CronField { @@ -264,7 +136,7 @@ final class CronField {
* Represents the type of cron field, i.e. seconds, minutes, hours,
* day-of-month, month, day-of-week.
*/
private enum Type {
protected 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),
@ -336,7 +208,7 @@ final class CronField { @@ -336,7 +208,7 @@ final class CronField {
* @return the elapsed temporal, typically with {@code goal} as value
* for this type.
*/
public <T extends Temporal> T elapseUntil(T temporal, int goal) {
public <T extends Temporal & Comparable<? super T>> T elapseUntil(T temporal, int goal) {
int current = get(temporal);
if (current < goal) {
return this.field.getBaseUnit().addTo(temporal, goal - current);
@ -357,7 +229,7 @@ final class CronField { @@ -357,7 +229,7 @@ final class CronField {
* @param <T> the type of temporal
* @return the rolled forward temporal
*/
public <T extends Temporal> T rollForward(T temporal) {
public <T extends Temporal & Comparable<? super T>> T rollForward(T temporal) {
int current = get(temporal);
ValueRange range = temporal.range(this.field);
long amount = range.getMaximum() - current + 1;

309
spring-context/src/main/java/org/springframework/scheduling/support/QuartzCronField.java

@ -0,0 +1,309 @@ @@ -0,0 +1,309 @@
/*
* 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.DayOfWeek;
import java.time.temporal.ChronoField;
import java.time.temporal.ChronoUnit;
import java.time.temporal.Temporal;
import java.time.temporal.TemporalAdjuster;
import java.time.temporal.TemporalAdjusters;
import java.time.temporal.TemporalField;
import java.time.temporal.WeekFields;
import java.util.Locale;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
/**
* Extension of {@link CronField} for
* <a href="https://www.quartz-scheduler.org>Quartz</a> -specific fields.
* Created using the {@code parse*} methods, uses a {@link TemporalAdjuster}
* internally.
*
* @author Arjen Poutsma
* @since 5.3
*/
final class QuartzCronField extends CronField {
/**
* Temporal adjuster that returns the last weekday of the month.
*/
private static final TemporalAdjuster lastWeekdayOfMonth = temporal -> {
Temporal lastDayOfMonth = TemporalAdjusters.lastDayOfMonth().adjustInto(temporal);
int dayOfWeek = lastDayOfMonth.get(ChronoField.DAY_OF_WEEK);
if (dayOfWeek == 6) { // Saturday
return lastDayOfMonth.minus(1, ChronoUnit.DAYS);
}
else if (dayOfWeek == 7) { // Sunday
return lastDayOfMonth.minus(2, ChronoUnit.DAYS);
}
else {
return lastDayOfMonth;
}
};
private final Type rollForwardType;
private final TemporalAdjuster adjuster;
private final String value;
private QuartzCronField(Type type, TemporalAdjuster adjuster, String value) {
this(type, type, adjuster, value);
}
/**
* Constructor for fields that need to roll forward over a different type
* than the type this field represents. See {@link #parseDaysOfWeek(String)}.
*/
private QuartzCronField(Type type, Type rollForwardType, TemporalAdjuster adjuster, String value) {
super(type);
this.adjuster = adjuster;
this.value = value;
this.rollForwardType = rollForwardType;
}
/**
* Parse the given value into a days of months {@code QuartzCronField}, the fourth entry of a cron expression.
* Expects a "L" or "W" in the given value.
*/
public static QuartzCronField parseDaysOfMonth(String value) {
int idx = value.lastIndexOf('L');
if (idx != -1) {
TemporalAdjuster adjuster;
if (idx != 0) {
throw new IllegalArgumentException("Unrecognized characters before 'L' in '" + value + "'");
}
else if (value.length() == 2 && value.charAt(1) == 'W') { // "LW"
adjuster = lastWeekdayOfMonth;
}
else {
if (value.length() == 1) { // "L"
adjuster = TemporalAdjusters.lastDayOfMonth();
}
else { // "L-[0-9]+"
int offset = Integer.parseInt(value.substring(idx + 1));
if (offset >= 0) {
throw new IllegalArgumentException("Offset '" + offset + " should be < 0 '" + value + "'");
}
adjuster = lastDayWithOffset(offset);
}
}
return new QuartzCronField(Type.DAY_OF_MONTH, adjuster, value);
}
idx = value.lastIndexOf('W');
if (idx != -1) {
if (idx == 0) {
throw new IllegalArgumentException("No day-of-month before 'W' in '" + value + "'");
}
else if (idx != value.length() - 1) {
throw new IllegalArgumentException("Unrecognized characters after 'W' in '" + value + "'");
}
else { // "[0-9]+W"
int dayOfMonth = Integer.parseInt(value.substring(0, idx));
dayOfMonth = Type.DAY_OF_MONTH.checkValidValue(dayOfMonth);
TemporalAdjuster adjuster = weekdayNearestTo(dayOfMonth);
return new QuartzCronField(Type.DAY_OF_MONTH, adjuster, value);
}
}
throw new IllegalArgumentException("No 'L' or 'W' found in '" + value + "'");
}
/**
* Parse the given value into a days of week {@code QuartzCronField}, the sixth entry of a cron expression.
* Expects a "L" or "#" in the given value.
*/
public static QuartzCronField parseDaysOfWeek(String value) {
int idx = value.lastIndexOf('L');
if (idx != -1) {
if (idx != value.length() - 1) {
throw new IllegalArgumentException("Unrecognized characters after 'L' in '" + value + "'");
}
else {
TemporalAdjuster adjuster;
if (idx == 0) { // "L"
adjuster = lastDayOfWeek(Locale.getDefault());
}
else { // "[0-7]L"
DayOfWeek dayOfWeek = parseDayOfWeek(value.substring(0, idx));
adjuster = TemporalAdjusters.lastInMonth(dayOfWeek);
}
return new QuartzCronField(Type.DAY_OF_WEEK, Type.DAY_OF_MONTH, adjuster, value);
}
}
idx = value.lastIndexOf('#');
if (idx != -1) {
if (idx == 0) {
throw new IllegalArgumentException("No day-of-week before '#' in '" + value + "'");
}
else if (idx == value.length() - 1) {
throw new IllegalArgumentException("No ordinal after '#' in '" + value + "'");
}
// "[0-7]#[0-9]+"
DayOfWeek dayOfWeek = parseDayOfWeek(value.substring(0, idx));
int ordinal = Integer.parseInt(value.substring(idx + 1));
TemporalAdjuster adjuster = TemporalAdjusters.dayOfWeekInMonth(ordinal, dayOfWeek);
return new QuartzCronField(Type.DAY_OF_WEEK, Type.DAY_OF_MONTH, adjuster, value);
}
throw new IllegalArgumentException("No 'L' or '#' found in '" + value + "'");
}
private static DayOfWeek parseDayOfWeek(String value) {
int dayOfWeek = Integer.parseInt(value);
if (dayOfWeek == 0) {
dayOfWeek = 7; // cron is 0 based; java.time 1 based
}
try {
return DayOfWeek.of(dayOfWeek);
}
catch (DateTimeException ex) {
String msg = ex.getMessage() + " '" + value + "'";
throw new IllegalArgumentException(msg, ex);
}
}
/**
* Return a temporal adjuster that finds the nth-to-last day of the month.
* @param offset the negative offset, i.e. -3 means third-to-last
* @return a nth-to-last day-of-month adjuster
*/
private static TemporalAdjuster lastDayWithOffset(int offset) {
Assert.isTrue(offset < 0, "Offset should be < 0");
return temporal -> {
Temporal lastDayOfMonth = TemporalAdjusters.lastDayOfMonth().adjustInto(temporal);
return lastDayOfMonth.plus(offset, ChronoUnit.DAYS);
};
}
/**
* Return a temporal adjuster that finds the last day-of-week, depending
* on the given locale.
* @param locale the locale to base the last day calculation on
* @return the last day-of-week adjuster
*/
private static TemporalAdjuster lastDayOfWeek(Locale locale) {
Assert.notNull(locale, "Locale must not be null");
TemporalField dayOfWeek = WeekFields.of(locale).dayOfWeek();
return temporal -> temporal.with(dayOfWeek, 7);
}
/**
* Return a temporal adjuster that finds the weekday nearest to the given
* day-of-month. If {@code dayOfMonth} falls on a Saturday, the date is
* moved back to Friday; if it falls on a Sunday (or if {@code dayOfMonth}
* is 1 and it falls on a Saturday), it is moved forward to Monday.
* @param dayOfMonth the goal day-of-month
* @return the weekday-nearest-to adjuster
*/
private static TemporalAdjuster weekdayNearestTo(int dayOfMonth) {
return temporal -> {
int current = Type.DAY_OF_MONTH.get(temporal);
int dayOfWeek = temporal.get(ChronoField.DAY_OF_WEEK);
if ((current == dayOfMonth && dayOfWeek < 6) || // dayOfMonth is a weekday
(dayOfWeek == 5 && current == dayOfMonth - 1) || // dayOfMonth is a Saturday, so Friday before
(dayOfWeek == 1 && current == dayOfMonth + 1) || // dayOfMonth is a Sunday, so Monday after
(dayOfWeek == 1 && dayOfMonth == 1 && current == 3)) { // dayOfMonth is the 1st, so Monday 3rd
return temporal;
}
int count = 0;
while (count++ < CronExpression.MAX_ATTEMPTS) {
temporal = Type.DAY_OF_MONTH.elapseUntil(cast(temporal), dayOfMonth);
current = Type.DAY_OF_MONTH.get(temporal);
if (current == dayOfMonth) {
dayOfWeek = temporal.get(ChronoField.DAY_OF_WEEK);
if (dayOfWeek == 6) { // Saturday
if (dayOfMonth != 1) {
return temporal.minus(1, ChronoUnit.DAYS);
}
else {
// exception for "1W" fields: execute on nearest Monday
return temporal.plus(2, ChronoUnit.DAYS);
}
}
else if (dayOfWeek == 7) { // Sunday
return temporal.plus(1, ChronoUnit.DAYS);
}
else {
return temporal;
}
}
}
return null;
};
}
@SuppressWarnings("unchecked")
private static <T extends Temporal & Comparable<? super T>> T cast(Temporal temporal) {
return (T) temporal;
}
@Override
public <T extends Temporal & Comparable<? super T>> T nextOrSame(T temporal) {
T result = adjust(temporal);
if (result != null) {
if (result.compareTo(temporal) < 0) {
// We ended up before the start, roll forward and try again
temporal = this.rollForwardType.rollForward(temporal);
result = adjust(temporal);
}
}
return result;
}
@Nullable
@SuppressWarnings("unchecked")
private <T extends Temporal & Comparable<? super T>> T adjust(T temporal) {
return (T) this.adjuster.adjustInto(temporal);
}
@Override
public int hashCode() {
return this.value.hashCode();
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof QuartzCronField)) {
return false;
}
QuartzCronField other = (QuartzCronField) o;
return type() == other.type() &&
this.value.equals(other.value);
}
@Override
public String toString() {
return type() + " '" + this.value + "'";
}
}

73
spring-context/src/test/java/org/springframework/scheduling/support/BitsCronFieldTests.java

@ -0,0 +1,73 @@ @@ -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 BitsCronFieldTests {
@Test
void parse() {
assertThat(BitsCronField.parseSeconds("42").bits()).hasUnsetRange(0, 41).hasSet(42).hasUnsetRange(43, 59);
assertThat(BitsCronField.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(BitsCronField.parseSeconds("0-4,8-12").bits()).hasSetRange(0, 4).hasUnsetRange(5,7).hasSetRange(8, 12).hasUnsetRange(13,59);
assertThat(BitsCronField.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(BitsCronField.parseDaysOfWeek("0").bits()).hasUnsetRange(0, 6).hasSet(7, 7);
assertThat(BitsCronField.parseSeconds("57/2").bits()).hasUnsetRange(0, 56).hasSet(57).hasUnset(58).hasSet(59);
}
@Test
void invalidRange() {
assertThatIllegalArgumentException().isThrownBy(() -> BitsCronField.parseSeconds(""));
assertThatIllegalArgumentException().isThrownBy(() -> BitsCronField.parseSeconds("0-12/0"));
assertThatIllegalArgumentException().isThrownBy(() -> BitsCronField.parseSeconds("60"));
assertThatIllegalArgumentException().isThrownBy(() -> BitsCronField.parseMinutes("60"));
assertThatIllegalArgumentException().isThrownBy(() -> BitsCronField.parseDaysOfMonth("0"));
assertThatIllegalArgumentException().isThrownBy(() -> BitsCronField.parseDaysOfMonth("32"));
assertThatIllegalArgumentException().isThrownBy(() -> BitsCronField.parseMonth("0"));
assertThatIllegalArgumentException().isThrownBy(() -> BitsCronField.parseMonth("13"));
assertThatIllegalArgumentException().isThrownBy(() -> BitsCronField.parseDaysOfWeek("8"));
assertThatIllegalArgumentException().isThrownBy(() -> BitsCronField.parseSeconds("20-10"));
}
@Test
void parseWildcards() {
assertThat(BitsCronField.parseSeconds("*").bits()).hasSetRange(0, 60);
assertThat(BitsCronField.parseMinutes("*").bits()).hasSetRange(0, 60);
assertThat(BitsCronField.parseHours("*").bits()).hasSetRange(0, 23);
assertThat(BitsCronField.parseDaysOfMonth("*").bits()).hasUnset(0).hasSetRange(1, 31);
assertThat(BitsCronField.parseDaysOfMonth("?").bits()).hasUnset(0).hasSetRange(1, 31);
assertThat(BitsCronField.parseMonth("*").bits()).hasUnset(0).hasSetRange(1, 12);
assertThat(BitsCronField.parseDaysOfWeek("*").bits()).hasUnset(0).hasSetRange(1, 7);
assertThat(BitsCronField.parseDaysOfWeek("?").bits()).hasUnset(0).hasSetRange(1, 7);
}
@Test
void names() {
assertThat(((BitsCronField)CronField.parseMonth("JAN,FEB,MAR,APR,MAY,JUN,JUL,AUG,SEP,OCT,NOV,DEC")).bits())
.hasUnset(0).hasSetRange(1, 12);
assertThat(((BitsCronField)CronField.parseDaysOfWeek("SUN,MON,TUE,WED,THU,FRI,SAT")).bits())
.hasUnset(0).hasSetRange(1, 7);
}
}

515
spring-context/src/test/java/org/springframework/scheduling/support/CronExpressionTests.java

@ -22,11 +22,16 @@ import java.time.LocalTime; @@ -22,11 +22,16 @@ import java.time.LocalTime;
import java.time.Year;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.temporal.ChronoField;
import java.time.temporal.Temporal;
import java.util.Locale;
import org.assertj.core.api.Condition;
import org.junit.jupiter.api.Test;
import static java.time.DayOfWeek.FRIDAY;
import static java.time.DayOfWeek.MONDAY;
import static java.time.DayOfWeek.SATURDAY;
import static java.time.DayOfWeek.SUNDAY;
import static java.time.DayOfWeek.TUESDAY;
import static java.time.DayOfWeek.WEDNESDAY;
@ -38,6 +43,16 @@ import static org.assertj.core.api.Assertions.assertThat; @@ -38,6 +43,16 @@ import static org.assertj.core.api.Assertions.assertThat;
*/
class CronExpressionTests {
private static final Condition<Temporal> weekday = new Condition<Temporal>("weekday") {
@Override
public boolean matches(Temporal value) {
int dayOfWeek = value.get(ChronoField.DAY_OF_WEEK);
return dayOfWeek != 6 && dayOfWeek != 7;
}
};
@Test
void matchAll() {
CronExpression expression = CronExpression.parse("* * * * * *");
@ -583,4 +598,504 @@ class CronExpressionTests { @@ -583,4 +598,504 @@ class CronExpressionTests {
}
@Test
void quartzLastDayOfMonth() {
CronExpression expression = CronExpression.parse("0 0 0 L * *");
LocalDateTime last = LocalDateTime.of(LocalDate.of(2008, 1, 4), LocalTime.now());
LocalDateTime expected = LocalDateTime.of(2008, 1, 31, 0, 0);
LocalDateTime actual = expression.next(last);
assertThat(actual).isEqualTo(expected);
last = actual;
expected = LocalDateTime.of(2008, 2, 29, 0, 0);
actual = expression.next(last);
assertThat(actual).isEqualTo(expected);
last = actual;
expected = LocalDateTime.of(2008, 3, 31, 0, 0);
actual = expression.next(last);
assertThat(actual).isEqualTo(expected);
last = actual;
expected = LocalDateTime.of(2008, 4, 30, 0, 0);
assertThat(expression.next(last)).isEqualTo(expected);
}
@Test
void quartzLastDayOfMonthOffset() {
// L-3 = third-to-last day of the month
CronExpression expression = CronExpression.parse("0 0 0 L-3 * *");
LocalDateTime last = LocalDateTime.of(LocalDate.of(2008, 1, 4), LocalTime.now());
LocalDateTime expected = LocalDateTime.of(2008, 1, 28, 0, 0);
LocalDateTime actual = expression.next(last);
assertThat(actual).isEqualTo(expected);
last = actual;
expected = LocalDateTime.of(2008, 2, 26, 0, 0);
actual = expression.next(last);
assertThat(actual).isEqualTo(expected);
last = actual;
expected = LocalDateTime.of(2008, 3, 28, 0, 0);
actual = expression.next(last);
assertThat(actual).isEqualTo(expected);
last = actual;
expected = LocalDateTime.of(2008, 4, 27, 0, 0);
assertThat(expression.next(last)).isEqualTo(expected);
}
@Test
void quartzLastWeekdayOfMonth() {
CronExpression expression = CronExpression.parse("0 0 0 LW * *");
LocalDateTime last = LocalDateTime.of(LocalDate.of(2008, 1, 4), LocalTime.now());
LocalDateTime expected = LocalDateTime.of(2008, 1, 31, 0, 0);
LocalDateTime actual = expression.next(last);
assertThat(actual).isNotNull();
assertThat(actual).isEqualTo(expected);
assertThat(actual).is(weekday);
last = actual;
expected = LocalDateTime.of(2008, 2, 29, 0, 0);
actual = expression.next(last);
assertThat(actual).isNotNull();
assertThat(actual).isEqualTo(expected);
assertThat(actual).is(weekday);
last = actual;
expected = LocalDateTime.of(2008, 3, 31, 0, 0);
actual = expression.next(last);
assertThat(actual).isNotNull();
assertThat(actual).isEqualTo(expected);
assertThat(actual).is(weekday);
last = actual;
expected = LocalDateTime.of(2008, 4, 30, 0, 0);
actual = expression.next(last);
assertThat(actual).isNotNull();
assertThat(actual).isEqualTo(expected);
assertThat(actual).is(weekday);
last = actual;
expected = LocalDateTime.of(2008, 5, 30, 0, 0);
actual = expression.next(last);
assertThat(actual).isNotNull();
assertThat(actual).isEqualTo(expected);
assertThat(actual).is(weekday);
last = actual;
expected = LocalDateTime.of(2008, 6, 30, 0, 0);
actual = expression.next(last);
assertThat(actual).isNotNull();
assertThat(actual).isEqualTo(expected);
assertThat(actual).is(weekday);
last = actual;
expected = LocalDateTime.of(2008, 7, 31, 0, 0);
actual = expression.next(last);
assertThat(actual).isNotNull();
assertThat(actual).isEqualTo(expected);
assertThat(actual).is(weekday);
last = actual;
expected = LocalDateTime.of(2008, 8, 29, 0, 0);
actual = expression.next(last);
assertThat(actual).isNotNull();
assertThat(actual).isEqualTo(expected);
assertThat(actual).is(weekday);
last = actual;
expected = LocalDateTime.of(2008, 9, 30, 0, 0);
actual = expression.next(last);
assertThat(actual).isNotNull();
assertThat(actual).isEqualTo(expected);
assertThat(actual).is(weekday);
last = actual;
expected = LocalDateTime.of(2008, 10, 31, 0, 0);
actual = expression.next(last);
assertThat(actual).isNotNull();
assertThat(actual).isEqualTo(expected);
assertThat(actual).is(weekday);
last = actual;
expected = LocalDateTime.of(2008, 11, 28, 0, 0);
actual = expression.next(last);
assertThat(actual).isNotNull();
assertThat(actual).isEqualTo(expected);
assertThat(actual).is(weekday);
last = actual;
expected = LocalDateTime.of(2008, 12, 31, 0, 0);
actual = expression.next(last);
assertThat(actual).isNotNull();
assertThat(actual).isEqualTo(expected);
assertThat(actual).is(weekday);
}
@Test
public void quartzLastDayOfWeekFirstDayMonday() {
Locale defaultLocale = Locale.getDefault();
try {
Locale.setDefault(Locale.UK);
CronExpression expression = CronExpression.parse("0 0 0 * * L");
LocalDateTime last = LocalDateTime.of(LocalDate.of(2008, 1, 4), LocalTime.now());
LocalDateTime expected = LocalDateTime.of(2008, 1, 6, 0, 0);
LocalDateTime actual = expression.next(last);
assertThat(actual).isNotNull();
assertThat(actual).isEqualTo(expected);
assertThat(actual.getDayOfWeek()).isEqualTo(SUNDAY);
last = actual;
expected = expected.plusWeeks(1);
actual = expression.next(last);
assertThat(actual).isNotNull();
assertThat(actual).isEqualTo(expected);
assertThat(actual.getDayOfWeek()).isEqualTo(SUNDAY);
}
finally {
Locale.setDefault(defaultLocale);
}
}
@Test
public void quartzLastDayOfWeekFirstDaySunday() {
Locale defaultLocale = Locale.getDefault();
try {
Locale.setDefault(Locale.US);
CronExpression expression = CronExpression.parse("0 0 0 * * L");
LocalDateTime last = LocalDateTime.of(LocalDate.of(2008, 1, 4), LocalTime.now());
LocalDateTime expected = LocalDateTime.of(2008, 1, 5, 0, 0);
LocalDateTime actual = expression.next(last);
assertThat(actual).isNotNull();
assertThat(actual).isEqualTo(expected);
assertThat(actual.getDayOfWeek()).isEqualTo(SATURDAY);
last = actual;
expected = expected.plusWeeks(1);
actual = expression.next(last);
assertThat(actual).isNotNull();
assertThat(actual).isEqualTo(expected);
assertThat(actual.getDayOfWeek()).isEqualTo(SATURDAY);
}
finally {
Locale.setDefault(defaultLocale);
}
}
@Test
public void quartzLastDayOfWeekOffset() {
// last Friday (5) of the month
CronExpression expression = CronExpression.parse("0 0 0 * * 5L");
LocalDateTime last = LocalDateTime.of(LocalDate.of(2008, 1, 4), LocalTime.now());
LocalDateTime expected = LocalDateTime.of(2008, 1, 25, 0, 0);
LocalDateTime actual = expression.next(last);
assertThat(actual).isNotNull();
assertThat(actual).isEqualTo(expected);
assertThat(actual.getDayOfWeek()).isEqualTo(FRIDAY);
last = actual;
expected = LocalDateTime.of(2008, 2, 29, 0, 0);
actual = expression.next(last);
assertThat(actual).isNotNull();
assertThat(actual).isEqualTo(expected);
assertThat(actual.getDayOfWeek()).isEqualTo(FRIDAY);
last = actual;
expected = LocalDateTime.of(2008, 3, 28, 0, 0);
actual = expression.next(last);
assertThat(actual).isNotNull();
assertThat(actual).isEqualTo(expected);
assertThat(actual.getDayOfWeek()).isEqualTo(FRIDAY);
last = actual;
expected = LocalDateTime.of(2008, 4, 25, 0, 0);
actual = expression.next(last);
assertThat(actual).isNotNull();
assertThat(actual).isEqualTo(expected);
assertThat(actual.getDayOfWeek()).isEqualTo(FRIDAY);
last = actual;
expected = LocalDateTime.of(2008, 5, 30, 0, 0);
actual = expression.next(last);
assertThat(actual).isNotNull();
assertThat(actual).isEqualTo(expected);
assertThat(actual.getDayOfWeek()).isEqualTo(FRIDAY);
last = actual;
expected = LocalDateTime.of(2008, 6, 27, 0, 0);
actual = expression.next(last);
assertThat(actual).isNotNull();
assertThat(actual).isEqualTo(expected);
assertThat(actual.getDayOfWeek()).isEqualTo(FRIDAY);
last = actual;
expected = LocalDateTime.of(2008, 7, 25, 0, 0);
actual = expression.next(last);
assertThat(actual).isNotNull();
assertThat(actual).isEqualTo(expected);
assertThat(actual.getDayOfWeek()).isEqualTo(FRIDAY);
last = actual;
expected = LocalDateTime.of(2008, 8, 29, 0, 0);
actual = expression.next(last);
assertThat(actual).isNotNull();
assertThat(actual).isEqualTo(expected);
assertThat(actual.getDayOfWeek()).isEqualTo(FRIDAY);
last = actual;
expected = LocalDateTime.of(2008, 9, 26, 0, 0);
actual = expression.next(last);
assertThat(actual).isNotNull();
assertThat(actual).isEqualTo(expected);
assertThat(actual.getDayOfWeek()).isEqualTo(FRIDAY);
last = actual;
expected = LocalDateTime.of(2008, 10, 31, 0, 0);
actual = expression.next(last);
assertThat(actual).isNotNull();
assertThat(actual).isEqualTo(expected);
assertThat(actual.getDayOfWeek()).isEqualTo(FRIDAY);
last = actual;
expected = LocalDateTime.of(2008, 11, 28, 0, 0);
actual = expression.next(last);
assertThat(actual).isNotNull();
assertThat(actual).isEqualTo(expected);
assertThat(actual.getDayOfWeek()).isEqualTo(FRIDAY);
last = actual;
expected = LocalDateTime.of(2008, 12, 26, 0, 0);
actual = expression.next(last);
assertThat(actual).isNotNull();
assertThat(actual).isEqualTo(expected);
assertThat(actual.getDayOfWeek()).isEqualTo(FRIDAY);
}
@Test
void quartzWeekdayNearestTo15() {
CronExpression expression = CronExpression.parse("0 0 0 15W * ?");
LocalDateTime last = LocalDateTime.of(2020, 1, 1, 0, 0);
LocalDateTime expected = LocalDateTime.of(2020, 1, 15, 0, 0);
LocalDateTime actual = expression.next(last);
assertThat(actual).isNotNull();
assertThat(actual).isEqualTo(expected);
assertThat(actual).is(weekday);
last = actual;
expected = LocalDateTime.of(2020, 2, 14, 0, 0);
actual = expression.next(last);
assertThat(actual).isNotNull();
assertThat(actual).isEqualTo(expected);
assertThat(actual).is(weekday);
last = actual;
expected = LocalDateTime.of(2020, 3, 16, 0, 0);
actual = expression.next(last);
assertThat(actual).isNotNull();
assertThat(actual).isEqualTo(expected);
assertThat(actual).is(weekday);
last = actual;
expected = LocalDateTime.of(2020, 4, 15, 0, 0);
actual = expression.next(last);
assertThat(actual).isNotNull();
assertThat(actual).isEqualTo(expected);
assertThat(actual).is(weekday);
}
@Test
void quartzWeekdayNearestTo1() {
CronExpression expression = CronExpression.parse("0 0 0 1W * ?");
LocalDateTime last = LocalDateTime.of(2019, 12, 31, 0, 0);
LocalDateTime expected = LocalDateTime.of(2020, 1, 1, 0, 0);
LocalDateTime actual = expression.next(last);
assertThat(actual).isNotNull();
assertThat(actual).isEqualTo(expected);
assertThat(actual).is(weekday);
last = actual;
expected = LocalDateTime.of(2020, 2, 3, 0, 0);
actual = expression.next(last);
assertThat(actual).isNotNull();
assertThat(actual).isEqualTo(expected);
assertThat(actual).is(weekday);
last = actual;
expected = LocalDateTime.of(2020, 3, 2, 0, 0);
actual = expression.next(last);
assertThat(actual).isNotNull();
assertThat(actual).isEqualTo(expected);
assertThat(actual).is(weekday);
last = actual;
expected = LocalDateTime.of(2020, 4, 1, 0, 0);
actual = expression.next(last);
assertThat(actual).isNotNull();
assertThat(actual).isEqualTo(expected);
assertThat(actual).is(weekday);
}
@Test
void quartzWeekdayNearestTo31() {
CronExpression expression = CronExpression.parse("0 0 0 31W * ?");
LocalDateTime last = LocalDateTime.of(2020, 1, 1, 0, 0);
LocalDateTime expected = LocalDateTime.of(2020, 1, 31, 0, 0);
LocalDateTime actual = expression.next(last);
assertThat(actual).isNotNull();
assertThat(actual).isEqualTo(expected);
assertThat(actual).is(weekday);
last = actual;
expected = LocalDateTime.of(2020, 3, 31, 0, 0);
actual = expression.next(last);
assertThat(actual).isNotNull();
assertThat(actual).isEqualTo(expected);
assertThat(actual).is(weekday);
last = actual;
expected = LocalDateTime.of(2020, 7, 31, 0, 0);
actual = expression.next(last);
assertThat(actual).isNotNull();
assertThat(actual).isEqualTo(expected);
assertThat(actual).is(weekday);
last = actual;
expected = LocalDateTime.of(2020, 8, 31, 0, 0);
actual = expression.next(last);
assertThat(actual).isNotNull();
assertThat(actual).isEqualTo(expected);
assertThat(actual).is(weekday);
last = actual;
expected = LocalDateTime.of(2020, 10, 30, 0, 0);
actual = expression.next(last);
assertThat(actual).isNotNull();
assertThat(actual).isEqualTo(expected);
assertThat(actual).is(weekday);
last = actual;
expected = LocalDateTime.of(2020, 12, 31, 0, 0);
actual = expression.next(last);
assertThat(actual).isNotNull();
assertThat(actual).isEqualTo(expected);
assertThat(actual).is(weekday);
}
@Test
void quartz2ndFridayOfTheMonth() {
CronExpression expression = CronExpression.parse("0 0 0 ? * 5#2");
LocalDateTime last = LocalDateTime.of(2020, 1, 1, 0, 0);
LocalDateTime expected = LocalDateTime.of(2020, 1, 10, 0, 0);
LocalDateTime actual = expression.next(last);
assertThat(actual).isNotNull();
assertThat(actual).isEqualTo(expected);
assertThat(actual.getDayOfWeek()).isEqualTo(FRIDAY);
last = actual;
expected = LocalDateTime.of(2020, 2, 14, 0, 0);
actual = expression.next(last);
assertThat(actual).isNotNull();
assertThat(actual).isEqualTo(expected);
assertThat(actual.getDayOfWeek()).isEqualTo(FRIDAY);
last = actual;
expected = LocalDateTime.of(2020, 3, 13, 0, 0);
actual = expression.next(last);
assertThat(actual).isNotNull();
assertThat(actual).isEqualTo(expected);
assertThat(actual.getDayOfWeek()).isEqualTo(FRIDAY);
last = actual;
expected = LocalDateTime.of(2020, 4, 10, 0, 0);
actual = expression.next(last);
assertThat(actual).isNotNull();
assertThat(actual).isEqualTo(expected);
assertThat(actual.getDayOfWeek()).isEqualTo(FRIDAY);
}
@Test
void quartz2ndFridayOfTheMonthDayName() {
CronExpression expression = CronExpression.parse("0 0 0 ? * FRI#2");
LocalDateTime last = LocalDateTime.of(2020, 1, 1, 0, 0);
LocalDateTime expected = LocalDateTime.of(2020, 1, 10, 0, 0);
LocalDateTime actual = expression.next(last);
assertThat(actual).isNotNull();
assertThat(actual).isEqualTo(expected);
assertThat(actual.getDayOfWeek()).isEqualTo(FRIDAY);
last = actual;
expected = LocalDateTime.of(2020, 2, 14, 0, 0);
actual = expression.next(last);
assertThat(actual).isNotNull();
assertThat(actual).isEqualTo(expected);
assertThat(actual.getDayOfWeek()).isEqualTo(FRIDAY);
last = actual;
expected = LocalDateTime.of(2020, 3, 13, 0, 0);
actual = expression.next(last);
assertThat(actual).isNotNull();
assertThat(actual).isEqualTo(expected);
assertThat(actual.getDayOfWeek()).isEqualTo(FRIDAY);
last = actual;
expected = LocalDateTime.of(2020, 4, 10, 0, 0);
actual = expression.next(last);
assertThat(actual).isNotNull();
assertThat(actual).isEqualTo(expected);
assertThat(actual.getDayOfWeek()).isEqualTo(FRIDAY);
}
@Test
void quartzFifthWednesdayOfTheMonth() {
CronExpression expression = CronExpression.parse("0 0 0 ? * 3#5");
LocalDateTime last = LocalDateTime.of(2020, 1, 1, 0, 0);
LocalDateTime expected = LocalDateTime.of(2020, 1, 29, 0, 0);
LocalDateTime actual = expression.next(last);
assertThat(actual).isNotNull();
assertThat(actual).isEqualTo(expected);
assertThat(actual.getDayOfWeek()).isEqualTo(WEDNESDAY);
last = actual;
expected = LocalDateTime.of(2020, 4, 29, 0, 0);
actual = expression.next(last);
assertThat(actual).isNotNull();
assertThat(actual).isEqualTo(expected);
assertThat(actual.getDayOfWeek()).isEqualTo(WEDNESDAY);
last = actual;
expected = LocalDateTime.of(2020, 7, 29, 0, 0);
actual = expression.next(last);
assertThat(actual).isNotNull();
assertThat(actual).isEqualTo(expected);
assertThat(actual.getDayOfWeek()).isEqualTo(WEDNESDAY);
last = actual;
expected = LocalDateTime.of(2020, 9, 30, 0, 0);
actual = expression.next(last);
assertThat(actual).isNotNull();
assertThat(actual).isEqualTo(expected);
assertThat(actual.getDayOfWeek()).isEqualTo(WEDNESDAY);
last = actual;
expected = LocalDateTime.of(2020, 12, 30, 0, 0);
actual = expression.next(last);
assertThat(actual).isNotNull();
assertThat(actual).isEqualTo(expected);
assertThat(actual.getDayOfWeek()).isEqualTo(WEDNESDAY);
}
}

73
spring-context/src/test/java/org/springframework/scheduling/support/CronFieldTests.java

@ -1,73 +0,0 @@ @@ -1,73 +0,0 @@
/*
* 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);
}
}

144
spring-context/src/test/java/org/springframework/scheduling/support/QuartzCronFieldTests.java

@ -0,0 +1,144 @@ @@ -0,0 +1,144 @@
/*
* 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.DayOfWeek;
import java.time.LocalDate;
import java.util.Locale;
import org.junit.jupiter.api.Test;
import static java.time.DayOfWeek.SATURDAY;
import static java.time.DayOfWeek.SUNDAY;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
/**
* @author Arjen Poutsma
*/
class QuartzCronFieldTests {
@Test
void lastDayOfMonth() {
QuartzCronField field = QuartzCronField.parseDaysOfMonth("L");
LocalDate last = LocalDate.of(2020, 6, 16);
LocalDate expected = LocalDate.of(2020, 6, 30);
assertThat(field.nextOrSame(last)).isEqualTo(expected);
}
@Test
void lastDayOfMonthOffset() {
QuartzCronField field = QuartzCronField.parseDaysOfMonth("L-3");
LocalDate last = LocalDate.of(2020, 6, 16);
LocalDate expected = LocalDate.of(2020, 6, 27);
assertThat(field.nextOrSame(last)).isEqualTo(expected);
}
@Test
void lastWeekdayOfMonth() {
QuartzCronField field = QuartzCronField.parseDaysOfMonth("LW");
LocalDate last = LocalDate.of(2020, 6, 16);
LocalDate expected = LocalDate.of(2020, 6, 30);
LocalDate actual = field.nextOrSame(last);
assertThat(actual).isNotNull();
assertThat(actual.getDayOfWeek()).isEqualTo(DayOfWeek.TUESDAY);
assertThat(actual).isEqualTo(expected);
}
@Test
public void lastDayOfWeekFirstDayMonday() {
Locale defaultLocale = Locale.getDefault();
try {
Locale.setDefault(Locale.UK);
QuartzCronField field = QuartzCronField.parseDaysOfWeek("L");
LocalDate last = LocalDate.of(2020, 6, 16);
LocalDate expected = LocalDate.of(2020, 6, 21);
assertThat(field.nextOrSame(last)).isEqualTo(expected);
LocalDate actual = field.nextOrSame(last);
assertThat(actual).isNotNull();
assertThat(actual).isEqualTo(expected);
assertThat(actual.getDayOfWeek()).isEqualTo(SUNDAY);
}
finally {
Locale.setDefault(defaultLocale);
}
}
@Test
public void lastDayOfWeekFirstDaySunday() {
Locale defaultLocale = Locale.getDefault();
try {
Locale.setDefault(Locale.US);
QuartzCronField field = QuartzCronField.parseDaysOfWeek("L");
LocalDate last = LocalDate.of(2020, 6, 16);
LocalDate expected = LocalDate.of(2020, 6, 20);
assertThat(field.nextOrSame(last)).isEqualTo(expected);
LocalDate actual = field.nextOrSame(last);
assertThat(actual).isNotNull();
assertThat(actual).isEqualTo(expected);
assertThat(actual.getDayOfWeek()).isEqualTo(SATURDAY);
}
finally {
Locale.setDefault(defaultLocale);
}
}
@Test
void lastDayOfWeekOffset() {
// last Thursday (4) of the month
QuartzCronField field = QuartzCronField.parseDaysOfWeek("4L");
LocalDate last = LocalDate.of(2020, 6, 16);
LocalDate expected = LocalDate.of(2020, 6, 25);
assertThat(field.nextOrSame(last)).isEqualTo(expected);
}
@Test
void invalidValues() {
assertThatIllegalArgumentException().isThrownBy(() -> QuartzCronField.parseDaysOfMonth(""));
assertThatIllegalArgumentException().isThrownBy(() -> QuartzCronField.parseDaysOfMonth("1"));
assertThatIllegalArgumentException().isThrownBy(() -> QuartzCronField.parseDaysOfMonth("1L"));
assertThatIllegalArgumentException().isThrownBy(() -> QuartzCronField.parseDaysOfMonth("LL"));
assertThatIllegalArgumentException().isThrownBy(() -> QuartzCronField.parseDaysOfMonth("4L"));
assertThatIllegalArgumentException().isThrownBy(() -> QuartzCronField.parseDaysOfMonth("0L"));
assertThatIllegalArgumentException().isThrownBy(() -> QuartzCronField.parseDaysOfMonth("W"));
assertThatIllegalArgumentException().isThrownBy(() -> QuartzCronField.parseDaysOfMonth("W1"));
assertThatIllegalArgumentException().isThrownBy(() -> QuartzCronField.parseDaysOfMonth("WW"));
assertThatIllegalArgumentException().isThrownBy(() -> QuartzCronField.parseDaysOfMonth("32W"));
assertThatIllegalArgumentException().isThrownBy(() -> QuartzCronField.parseDaysOfWeek(""));
assertThatIllegalArgumentException().isThrownBy(() -> QuartzCronField.parseDaysOfWeek("1"));
assertThatIllegalArgumentException().isThrownBy(() -> QuartzCronField.parseDaysOfWeek("L1"));
assertThatIllegalArgumentException().isThrownBy(() -> QuartzCronField.parseDaysOfWeek("LL"));
assertThatIllegalArgumentException().isThrownBy(() -> QuartzCronField.parseDaysOfWeek("-4L"));
assertThatIllegalArgumentException().isThrownBy(() -> QuartzCronField.parseDaysOfWeek("8L"));
assertThatIllegalArgumentException().isThrownBy(() -> QuartzCronField.parseDaysOfWeek("#"));
assertThatIllegalArgumentException().isThrownBy(() -> QuartzCronField.parseDaysOfWeek("1#"));
assertThatIllegalArgumentException().isThrownBy(() -> QuartzCronField.parseDaysOfWeek("#1"));
assertThatIllegalArgumentException().isThrownBy(() -> QuartzCronField.parseDaysOfWeek("1#L"));
assertThatIllegalArgumentException().isThrownBy(() -> QuartzCronField.parseDaysOfWeek("L#1"));
assertThatIllegalArgumentException().isThrownBy(() -> QuartzCronField.parseDaysOfWeek("8#1"));
}
}
Loading…
Cancel
Save