Explicit documentation note on cron-vs-quartz parsing convention
Closes gh-32128
This commit is contained in:
parent
9c4b4ab81e
commit
a738e4d5fd
|
@ -29,9 +29,14 @@ import org.springframework.util.StringUtils;
|
||||||
* <a href="https://www.manpagez.com/man/5/crontab/">crontab expression</a>
|
* <a href="https://www.manpagez.com/man/5/crontab/">crontab expression</a>
|
||||||
* that can calculate the next time it matches.
|
* that can calculate the next time it matches.
|
||||||
*
|
*
|
||||||
* <p>{@code CronExpression} instances are created through
|
* <p>{@code CronExpression} instances are created through {@link #parse(String)};
|
||||||
* {@link #parse(String)}; the next match is determined with
|
* the next match is determined with {@link #next(Temporal)}.
|
||||||
* {@link #next(Temporal)}.
|
*
|
||||||
|
* <p>Supports a Quartz day-of-month/week field with an L/# expression. Follows
|
||||||
|
* common cron conventions in every other respect, including 0-6 for SUN-SAT
|
||||||
|
* (plus 7 for SUN as well). Note that Quartz deviates from the day-of-week
|
||||||
|
* convention in cron through 1-7 for SUN-SAT whereas Spring strictly follows
|
||||||
|
* cron even in combination with the optional Quartz-specific L/# expressions.
|
||||||
*
|
*
|
||||||
* @author Arjen Poutsma
|
* @author Arjen Poutsma
|
||||||
* @since 5.3
|
* @since 5.3
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright 2002-2021 the original author or authors.
|
* Copyright 2002-2024 the original author or authors.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@ -31,15 +31,22 @@ import org.springframework.util.StringUtils;
|
||||||
* 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)}.
|
* main and only entry point is {@link #nextOrSame(Temporal)}.
|
||||||
*
|
*
|
||||||
|
* <p>Supports a Quartz day-of-month/week field with an L/# expression. Follows
|
||||||
|
* common cron conventions in every other respect, including 0-6 for SUN-SAT
|
||||||
|
* (plus 7 for SUN as well). Note that Quartz deviates from the day-of-week
|
||||||
|
* convention in cron through 1-7 for SUN-SAT whereas Spring strictly follows
|
||||||
|
* cron even in combination with the optional Quartz-specific L/# expressions.
|
||||||
|
*
|
||||||
* @author Arjen Poutsma
|
* @author Arjen Poutsma
|
||||||
* @since 5.3
|
* @since 5.3
|
||||||
*/
|
*/
|
||||||
abstract class CronField {
|
abstract class CronField {
|
||||||
|
|
||||||
private static final String[] MONTHS = new String[]{"JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP",
|
private static final String[] MONTHS = new String[]
|
||||||
"OCT", "NOV", "DEC"};
|
{"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 String[] DAYS = new String[]
|
||||||
|
{"MON", "TUE", "WED", "THU", "FRI", "SAT", "SUN"};
|
||||||
|
|
||||||
private final Type type;
|
private final Type type;
|
||||||
|
|
||||||
|
@ -48,6 +55,7 @@ abstract class CronField {
|
||||||
this.type = type;
|
this.type = type;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return a {@code CronField} enabled for 0 nanoseconds.
|
* Return a {@code CronField} enabled for 0 nanoseconds.
|
||||||
*/
|
*/
|
||||||
|
@ -169,6 +177,7 @@ abstract class CronField {
|
||||||
* day-of-month, month, day-of-week.
|
* day-of-month, month, day-of-week.
|
||||||
*/
|
*/
|
||||||
protected enum Type {
|
protected enum Type {
|
||||||
|
|
||||||
NANO(ChronoField.NANO_OF_SECOND, ChronoUnit.SECONDS),
|
NANO(ChronoField.NANO_OF_SECOND, ChronoUnit.SECONDS),
|
||||||
SECOND(ChronoField.SECOND_OF_MINUTE, ChronoUnit.MINUTES, ChronoField.NANO_OF_SECOND),
|
SECOND(ChronoField.SECOND_OF_MINUTE, ChronoUnit.MINUTES, ChronoField.NANO_OF_SECOND),
|
||||||
MINUTE(ChronoField.MINUTE_OF_HOUR, ChronoUnit.HOURS, ChronoField.SECOND_OF_MINUTE, ChronoField.NANO_OF_SECOND),
|
MINUTE(ChronoField.MINUTE_OF_HOUR, ChronoUnit.HOURS, ChronoField.SECOND_OF_MINUTE, ChronoField.NANO_OF_SECOND),
|
||||||
|
@ -184,14 +193,12 @@ abstract class CronField {
|
||||||
|
|
||||||
private final ChronoField[] lowerOrders;
|
private final ChronoField[] lowerOrders;
|
||||||
|
|
||||||
|
|
||||||
Type(ChronoField field, ChronoUnit higherOrder, ChronoField... lowerOrders) {
|
Type(ChronoField field, ChronoUnit higherOrder, ChronoField... lowerOrders) {
|
||||||
this.field = field;
|
this.field = field;
|
||||||
this.higherOrder = higherOrder;
|
this.higherOrder = higherOrder;
|
||||||
this.lowerOrders = lowerOrders;
|
this.lowerOrders = lowerOrders;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return the value of this type for the given temporal.
|
* Return the value of this type for the given temporal.
|
||||||
* @return the value of this type
|
* @return the value of this type
|
||||||
|
|
|
@ -27,8 +27,14 @@ import org.springframework.scheduling.TriggerContext;
|
||||||
import org.springframework.util.Assert;
|
import org.springframework.util.Assert;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* {@link Trigger} implementation for cron expressions.
|
* {@link Trigger} implementation for cron expressions. Wraps a
|
||||||
* Wraps a {@link CronExpression}.
|
* {@link CronExpression} which parses according to common crontab conventions.
|
||||||
|
*
|
||||||
|
* <p>Supports a Quartz day-of-month/week field with an L/# expression. Follows
|
||||||
|
* common cron conventions in every other respect, including 0-6 for SUN-SAT
|
||||||
|
* (plus 7 for SUN as well). Note that Quartz deviates from the day-of-week
|
||||||
|
* convention in cron through 1-7 for SUN-SAT whereas Spring strictly follows
|
||||||
|
* cron even in combination with the optional Quartz-specific L/# expressions.
|
||||||
*
|
*
|
||||||
* @author Juergen Hoeller
|
* @author Juergen Hoeller
|
||||||
* @author Arjen Poutsma
|
* @author Arjen Poutsma
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright 2002-2023 the original author or authors.
|
* Copyright 2002-2024 the original author or authors.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@ -30,10 +30,15 @@ import org.springframework.util.Assert;
|
||||||
/**
|
/**
|
||||||
* Extension of {@link CronField} for
|
* Extension of {@link CronField} for
|
||||||
* <a href="https://www.quartz-scheduler.org">Quartz</a>-specific fields.
|
* <a href="https://www.quartz-scheduler.org">Quartz</a>-specific fields.
|
||||||
*
|
* Created using the {@code parse*} methods, uses a {@link TemporalAdjuster}
|
||||||
* <p>Created using the {@code parse*} methods, uses a {@link TemporalAdjuster}
|
|
||||||
* internally.
|
* internally.
|
||||||
*
|
*
|
||||||
|
* <p>Supports a Quartz day-of-month/week field with an L/# expression. Follows
|
||||||
|
* common cron conventions in every other respect, including 0-6 for SUN-SAT
|
||||||
|
* (plus 7 for SUN as well). Note that Quartz deviates from the day-of-week
|
||||||
|
* convention in cron through 1-7 for SUN-SAT whereas Spring strictly follows
|
||||||
|
* cron even in combination with the optional Quartz-specific L/# expressions.
|
||||||
|
*
|
||||||
* @author Arjen Poutsma
|
* @author Arjen Poutsma
|
||||||
* @since 5.3
|
* @since 5.3
|
||||||
*/
|
*/
|
||||||
|
@ -61,8 +66,9 @@ final class QuartzCronField extends CronField {
|
||||||
this.rollForwardType = rollForwardType;
|
this.rollForwardType = rollForwardType;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns whether the given value is a Quartz day-of-month field.
|
* Determine whether the given value is a Quartz day-of-month field.
|
||||||
*/
|
*/
|
||||||
public static boolean isQuartzDaysOfMonthField(String value) {
|
public static boolean isQuartzDaysOfMonthField(String value) {
|
||||||
return value.contains("L") || value.contains("W");
|
return value.contains("L") || value.contains("W");
|
||||||
|
@ -116,7 +122,7 @@ final class QuartzCronField extends CronField {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns whether the given value is a Quartz day-of-week field.
|
* Determine whether the given value is a Quartz day-of-week field.
|
||||||
*/
|
*/
|
||||||
public static boolean isQuartzDaysOfWeekField(String value) {
|
public static boolean isQuartzDaysOfWeekField(String value) {
|
||||||
return value.contains("L") || value.contains("#");
|
return value.contains("L") || value.contains("#");
|
||||||
|
@ -160,7 +166,6 @@ final class QuartzCronField extends CronField {
|
||||||
throw new IllegalArgumentException("Ordinal '" + ordinal + "' in '" + value +
|
throw new IllegalArgumentException("Ordinal '" + ordinal + "' in '" + value +
|
||||||
"' must be positive number ");
|
"' must be positive number ");
|
||||||
}
|
}
|
||||||
|
|
||||||
TemporalAdjuster adjuster = dayOfWeekInMonth(ordinal, dayOfWeek);
|
TemporalAdjuster adjuster = dayOfWeekInMonth(ordinal, dayOfWeek);
|
||||||
return new QuartzCronField(Type.DAY_OF_WEEK, Type.DAY_OF_MONTH, adjuster, value);
|
return new QuartzCronField(Type.DAY_OF_WEEK, Type.DAY_OF_MONTH, adjuster, value);
|
||||||
}
|
}
|
||||||
|
@ -176,8 +181,7 @@ final class QuartzCronField extends CronField {
|
||||||
return DayOfWeek.of(dayOfWeek);
|
return DayOfWeek.of(dayOfWeek);
|
||||||
}
|
}
|
||||||
catch (DateTimeException ex) {
|
catch (DateTimeException ex) {
|
||||||
String msg = ex.getMessage() + " '" + value + "'";
|
throw new IllegalArgumentException(ex.getMessage() + " '" + value + "'", ex);
|
||||||
throw new IllegalArgumentException(msg, ex);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -356,27 +360,20 @@ final class QuartzCronField extends CronField {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(@Nullable Object other) {
|
||||||
|
return (this == other || (other instanceof QuartzCronField that &&
|
||||||
|
type() == that.type() && this.value.equals(that.value)));
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int hashCode() {
|
public int hashCode() {
|
||||||
return this.value.hashCode();
|
return this.value.hashCode();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean equals(@Nullable Object o) {
|
|
||||||
if (this == o) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (!(o instanceof QuartzCronField other)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return type() == other.type() &&
|
|
||||||
this.value.equals(other.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return type() + " '" + this.value + "'";
|
return type() + " '" + this.value + "'";
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,13 +35,16 @@ class BitsCronFieldTests {
|
||||||
@Test
|
@Test
|
||||||
void parse() {
|
void parse() {
|
||||||
assertThat(BitsCronField.parseSeconds("42")).has(clearRange(0, 41)).has(set(42)).has(clearRange(43, 59));
|
assertThat(BitsCronField.parseSeconds("42")).has(clearRange(0, 41)).has(set(42)).has(clearRange(43, 59));
|
||||||
assertThat(BitsCronField.parseSeconds("0-4,8-12")).has(setRange(0, 4)).has(clearRange(5,7)).has(setRange(8, 12)).has(clearRange(13,59));
|
assertThat(BitsCronField.parseSeconds("0-4,8-12")).has(setRange(0, 4)).has(clearRange(5,7))
|
||||||
assertThat(BitsCronField.parseSeconds("57/2")).has(clearRange(0, 56)).has(set(57)).has(clear(58)).has(set(59));
|
.has(setRange(8, 12)).has(clearRange(13,59));
|
||||||
|
assertThat(BitsCronField.parseSeconds("57/2")).has(clearRange(0, 56)).has(set(57))
|
||||||
|
.has(clear(58)).has(set(59));
|
||||||
|
|
||||||
assertThat(BitsCronField.parseMinutes("30")).has(set(30)).has(clearRange(1, 29)).has(clearRange(31, 59));
|
assertThat(BitsCronField.parseMinutes("30")).has(set(30)).has(clearRange(1, 29)).has(clearRange(31, 59));
|
||||||
|
|
||||||
assertThat(BitsCronField.parseHours("23")).has(set(23)).has(clearRange(0, 23));
|
assertThat(BitsCronField.parseHours("23")).has(set(23)).has(clearRange(0, 23));
|
||||||
assertThat(BitsCronField.parseHours("0-23/2")).has(set(0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22)).has(clear(1,3,5,7,9,11,13,15,17,19,21,23));
|
assertThat(BitsCronField.parseHours("0-23/2")).has(set(0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22))
|
||||||
|
.has(clear(1,3,5,7,9,11,13,15,17,19,21,23));
|
||||||
|
|
||||||
assertThat(BitsCronField.parseDaysOfMonth("1")).has(set(1)).has(clearRange(2, 31));
|
assertThat(BitsCronField.parseDaysOfMonth("1")).has(set(1)).has(clearRange(2, 31));
|
||||||
|
|
||||||
|
@ -49,13 +52,16 @@ class BitsCronFieldTests {
|
||||||
|
|
||||||
assertThat(BitsCronField.parseDaysOfWeek("0")).has(set(7, 7)).has(clearRange(0, 6));
|
assertThat(BitsCronField.parseDaysOfWeek("0")).has(set(7, 7)).has(clearRange(0, 6));
|
||||||
|
|
||||||
assertThat(BitsCronField.parseDaysOfWeek("7-5")).has(clear(0)).has(setRange(1, 5)).has(clear(6)).has(set(7));
|
assertThat(BitsCronField.parseDaysOfWeek("7-5")).has(clear(0)).has(setRange(1, 5))
|
||||||
|
.has(clear(6)).has(set(7));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void parseLists() {
|
void parseLists() {
|
||||||
assertThat(BitsCronField.parseSeconds("15,30")).has(set(15, 30)).has(clearRange(1, 15)).has(clearRange(31, 59));
|
assertThat(BitsCronField.parseSeconds("15,30")).has(set(15, 30)).has(clearRange(1, 15))
|
||||||
assertThat(BitsCronField.parseMinutes("1,2,5,9")).has(set(1, 2, 5, 9)).has(clear(0)).has(clearRange(3, 4)).has(clearRange(6, 8)).has(clearRange(10, 59));
|
.has(clearRange(31, 59));
|
||||||
|
assertThat(BitsCronField.parseMinutes("1,2,5,9")).has(set(1, 2, 5, 9)).has(clear(0))
|
||||||
|
.has(clearRange(3, 4)).has(clearRange(6, 8)).has(clearRange(10, 59));
|
||||||
assertThat(BitsCronField.parseHours("1,2,3")).has(set(1, 2, 3)).has(clearRange(4, 23));
|
assertThat(BitsCronField.parseHours("1,2,3")).has(set(1, 2, 3)).has(clearRange(4, 23));
|
||||||
assertThat(BitsCronField.parseDaysOfMonth("1,2,3")).has(set(1, 2, 3)).has(clearRange(4, 31));
|
assertThat(BitsCronField.parseDaysOfMonth("1,2,3")).has(set(1, 2, 3)).has(clearRange(4, 31));
|
||||||
assertThat(BitsCronField.parseMonth("1,2,3")).has(set(1, 2, 3)).has(clearRange(4, 12));
|
assertThat(BitsCronField.parseMonth("1,2,3")).has(set(1, 2, 3)).has(clearRange(4, 12));
|
||||||
|
@ -107,6 +113,7 @@ class BitsCronFieldTests {
|
||||||
.has(clear(0)).has(setRange(1, 7));
|
.has(clear(0)).has(setRange(1, 7));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private static Condition<BitsCronField> set(int... indices) {
|
private static Condition<BitsCronField> set(int... indices) {
|
||||||
return new Condition<>(String.format("set bits %s", Arrays.toString(indices))) {
|
return new Condition<>(String.format("set bits %s", Arrays.toString(indices))) {
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -28,6 +28,7 @@ import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException
|
||||||
* Tests for {@link QuartzCronField}.
|
* Tests for {@link QuartzCronField}.
|
||||||
*
|
*
|
||||||
* @author Arjen Poutsma
|
* @author Arjen Poutsma
|
||||||
|
* @author Juergen Hoeller
|
||||||
*/
|
*/
|
||||||
class QuartzCronFieldTests {
|
class QuartzCronFieldTests {
|
||||||
|
|
||||||
|
@ -71,6 +72,42 @@ class QuartzCronFieldTests {
|
||||||
assertThat(field.nextOrSame(last)).isEqualTo(expected);
|
assertThat(field.nextOrSame(last)).isEqualTo(expected);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void dayOfWeek_0(){
|
||||||
|
QuartzCronField field = QuartzCronField.parseDaysOfWeek("0#3");
|
||||||
|
|
||||||
|
LocalDate last = LocalDate.of(2024, 1, 1);
|
||||||
|
LocalDate expected = LocalDate.of(2024, 1, 21);
|
||||||
|
assertThat(field.nextOrSame(last)).isEqualTo(expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void dayOfWeek_1(){
|
||||||
|
QuartzCronField field = QuartzCronField.parseDaysOfWeek("1#3");
|
||||||
|
|
||||||
|
LocalDate last = LocalDate.of(2024, 1, 1);
|
||||||
|
LocalDate expected = LocalDate.of(2024, 1, 15);
|
||||||
|
assertThat(field.nextOrSame(last)).isEqualTo(expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void dayOfWeek_2(){
|
||||||
|
QuartzCronField field = QuartzCronField.parseDaysOfWeek("2#3");
|
||||||
|
|
||||||
|
LocalDate last = LocalDate.of(2024, 1, 1);
|
||||||
|
LocalDate expected = LocalDate.of(2024, 1, 16);
|
||||||
|
assertThat(field.nextOrSame(last)).isEqualTo(expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void dayOfWeek_7() {
|
||||||
|
QuartzCronField field = QuartzCronField.parseDaysOfWeek("7#3");
|
||||||
|
|
||||||
|
LocalDate last = LocalDate.of(2024, 1, 1);
|
||||||
|
LocalDate expected = LocalDate.of(2024, 1, 21);
|
||||||
|
assertThat(field.nextOrSame(last)).isEqualTo(expected);
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void invalidValues() {
|
void invalidValues() {
|
||||||
assertThatIllegalArgumentException().isThrownBy(() -> QuartzCronField.parseDaysOfMonth(""));
|
assertThatIllegalArgumentException().isThrownBy(() -> QuartzCronField.parseDaysOfMonth(""));
|
||||||
|
|
Loading…
Reference in New Issue