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
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 {
|
||||
* 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 {
|
||||
* <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 {
|
||||
* 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 {
|
||||
}
|
||||
|
||||
@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) {
|
||||
|
||||
@@ -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 {
|
||||
*/
|
||||
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 {
|
||||
*/
|
||||
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 {
|
||||
* @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 {
|
||||
* 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 {
|
||||
* @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 {
|
||||
* @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;
|
||||
|
||||
@@ -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 + "'";
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user