Browse Source
This commit introduces support for Quartz-specific features in CronExpression. This includes support for "L", "W", and "#". Closes gh-20106 Closes gh-22436pull/25528/head
8 changed files with 1350 additions and 231 deletions
@ -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; |
||||
} |
||||
|
||||
} |
||||
@ -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 + "'"; |
||||
|
||||
} |
||||
|
||||
} |
||||
@ -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); |
||||
} |
||||
|
||||
} |
||||
@ -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); |
||||
} |
||||
|
||||
} |
||||
@ -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…
Reference in new issue