Skip to content

Commit 70e2e89

Browse files
committed
Explicit documentation note on cron-vs-quartz parsing convention
Closes gh-32128
1 parent a2af34f commit 70e2e89

File tree

7 files changed

+138
-68
lines changed

7 files changed

+138
-68
lines changed

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

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2021 the original author or authors.
2+
* Copyright 2002-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -247,24 +247,25 @@ private void setBit(int index) {
247247
}
248248

249249
private void clearBit(int index) {
250-
this.bits &= ~(1L << index);
250+
this.bits &= ~(1L << index);
251251
}
252252

253-
@Override
254-
public int hashCode() {
255-
return Long.hashCode(this.bits);
256-
}
257253

258254
@Override
259-
public boolean equals(Object o) {
260-
if (this == o) {
255+
public boolean equals(Object other) {
256+
if (this == other) {
261257
return true;
262258
}
263-
if (!(o instanceof BitsCronField)) {
259+
if (!(other instanceof BitsCronField)) {
264260
return false;
265261
}
266-
BitsCronField other = (BitsCronField) o;
267-
return type() == other.type() && this.bits == other.bits;
262+
BitsCronField otherField = (BitsCronField) other;
263+
return (type() == otherField.type() && this.bits == otherField.bits);
264+
}
265+
266+
@Override
267+
public int hashCode() {
268+
return Long.hashCode(this.bits);
268269
}
269270

270271
@Override

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

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,14 @@
2929
* <a href="https://www.manpagez.com/man/5/crontab/">crontab expression</a>
3030
* that can calculate the next time it matches.
3131
*
32-
* <p>{@code CronExpression} instances are created through
33-
* {@link #parse(String)}; the next match is determined with
34-
* {@link #next(Temporal)}.
32+
* <p>{@code CronExpression} instances are created through {@link #parse(String)};
33+
* the next match is determined with {@link #next(Temporal)}.
34+
*
35+
* <p>Supports a Quartz day-of-month/week field with an L/# expression. Follows
36+
* common cron conventions in every other respect, including 0-6 for SUN-SAT
37+
* (plus 7 for SUN as well). Note that Quartz deviates from the day-of-week
38+
* convention in cron through 1-7 for SUN-SAT whereas Spring strictly follows
39+
* cron even in combination with the optional Quartz-specific L/# expressions.
3540
*
3641
* @author Arjen Poutsma
3742
* @since 5.3

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

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2021 the original author or authors.
2+
* Copyright 2002-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -31,15 +31,22 @@
3131
* Single field in a cron pattern. Created using the {@code parse*} methods,
3232
* main and only entry point is {@link #nextOrSame(Temporal)}.
3333
*
34+
* <p>Supports a Quartz day-of-month/week field with an L/# expression. Follows
35+
* common cron conventions in every other respect, including 0-6 for SUN-SAT
36+
* (plus 7 for SUN as well). Note that Quartz deviates from the day-of-week
37+
* convention in cron through 1-7 for SUN-SAT whereas Spring strictly follows
38+
* cron even in combination with the optional Quartz-specific L/# expressions.
39+
*
3440
* @author Arjen Poutsma
3541
* @since 5.3
3642
*/
3743
abstract class CronField {
3844

39-
private static final String[] MONTHS = new String[]{"JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP",
40-
"OCT", "NOV", "DEC"};
45+
private static final String[] MONTHS = new String[]
46+
{"JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC"};
4147

42-
private static final String[] DAYS = new String[]{"MON", "TUE", "WED", "THU", "FRI", "SAT", "SUN"};
48+
private static final String[] DAYS = new String[]
49+
{"MON", "TUE", "WED", "THU", "FRI", "SAT", "SUN"};
4350

4451
private final Type type;
4552

@@ -48,6 +55,7 @@ protected CronField(Type type) {
4855
this.type = type;
4956
}
5057

58+
5159
/**
5260
* Return a {@code CronField} enabled for 0 nanoseconds.
5361
*/
@@ -169,6 +177,7 @@ protected static <T extends Temporal & Comparable<? super T>> T cast(Temporal te
169177
* day-of-month, month, day-of-week.
170178
*/
171179
protected enum Type {
180+
172181
NANO(ChronoField.NANO_OF_SECOND, ChronoUnit.SECONDS),
173182
SECOND(ChronoField.SECOND_OF_MINUTE, ChronoUnit.MINUTES, ChronoField.NANO_OF_SECOND),
174183
MINUTE(ChronoField.MINUTE_OF_HOUR, ChronoUnit.HOURS, ChronoField.SECOND_OF_MINUTE, ChronoField.NANO_OF_SECOND),
@@ -184,14 +193,12 @@ protected enum Type {
184193

185194
private final ChronoField[] lowerOrders;
186195

187-
188196
Type(ChronoField field, ChronoUnit higherOrder, ChronoField... lowerOrders) {
189197
this.field = field;
190198
this.higherOrder = higherOrder;
191199
this.lowerOrders = lowerOrders;
192200
}
193201

194-
195202
/**
196203
* Return the value of this type for the given temporal.
197204
* @return the value of this type

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

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,14 @@
2727
import org.springframework.util.Assert;
2828

2929
/**
30-
* {@link Trigger} implementation for cron expressions.
31-
* Wraps a {@link CronExpression}.
30+
* {@link Trigger} implementation for cron expressions. Wraps a
31+
* {@link CronExpression} which parses according to common crontab conventions.
32+
*
33+
* <p>Supports a Quartz day-of-month/week field with an L/# expression. Follows
34+
* common cron conventions in every other respect, including 0-6 for SUN-SAT
35+
* (plus 7 for SUN as well). Note that Quartz deviates from the day-of-week
36+
* convention in cron through 1-7 for SUN-SAT whereas Spring strictly follows
37+
* cron even in combination with the optional Quartz-specific L/# expressions.
3238
*
3339
* @author Juergen Hoeller
3440
* @author Arjen Poutsma

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

Lines changed: 41 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2021 the original author or authors.
2+
* Copyright 2002-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -29,10 +29,16 @@
2929

3030
/**
3131
* Extension of {@link CronField} for
32-
* <a href="https://www.quartz-scheduler.org>Quartz</a> -specific fields.
32+
* <a href="https://www.quartz-scheduler.org">Quartz</a>-specific fields.
3333
* Created using the {@code parse*} methods, uses a {@link TemporalAdjuster}
3434
* internally.
3535
*
36+
* <p>Supports a Quartz day-of-month/week field with an L/# expression. Follows
37+
* common cron conventions in every other respect, including 0-6 for SUN-SAT
38+
* (plus 7 for SUN as well). Note that Quartz deviates from the day-of-week
39+
* convention in cron through 1-7 for SUN-SAT whereas Spring strictly follows
40+
* cron even in combination with the optional Quartz-specific L/# expressions.
41+
*
3642
* @author Arjen Poutsma
3743
* @since 5.3
3844
*/
@@ -60,8 +66,9 @@ private QuartzCronField(Type type, Type rollForwardType, TemporalAdjuster adjust
6066
this.rollForwardType = rollForwardType;
6167
}
6268

69+
6370
/**
64-
* Returns whether the given value is a Quartz day-of-month field.
71+
* Determine whether the given value is a Quartz day-of-month field.
6572
*/
6673
public static boolean isQuartzDaysOfMonthField(String value) {
6774
return value.contains("L") || value.contains("W");
@@ -78,14 +85,14 @@ public static QuartzCronField parseDaysOfMonth(String value) {
7885
if (idx != 0) {
7986
throw new IllegalArgumentException("Unrecognized characters before 'L' in '" + value + "'");
8087
}
81-
else if (value.length() == 2 && value.charAt(1) == 'W') { // "LW"
88+
else if (value.length() == 2 && value.charAt(1) == 'W') { // "LW"
8289
adjuster = lastWeekdayOfMonth();
8390
}
8491
else {
85-
if (value.length() == 1) { // "L"
92+
if (value.length() == 1) { // "L"
8693
adjuster = lastDayOfMonth();
8794
}
88-
else { // "L-[0-9]+"
95+
else { // "L-[0-9]+"
8996
int offset = Integer.parseInt(value.substring(idx + 1));
9097
if (offset >= 0) {
9198
throw new IllegalArgumentException("Offset '" + offset + " should be < 0 '" + value + "'");
@@ -103,7 +110,7 @@ else if (value.length() == 2 && value.charAt(1) == 'W') { // "LW"
103110
else if (idx != value.length() - 1) {
104111
throw new IllegalArgumentException("Unrecognized characters after 'W' in '" + value + "'");
105112
}
106-
else { // "[0-9]+W"
113+
else { // "[0-9]+W"
107114
int dayOfMonth = Integer.parseInt(value.substring(0, idx));
108115
dayOfMonth = Type.DAY_OF_MONTH.checkValidValue(dayOfMonth);
109116
TemporalAdjuster adjuster = weekdayNearestTo(dayOfMonth);
@@ -114,15 +121,16 @@ else if (idx != value.length() - 1) {
114121
}
115122

116123
/**
117-
* Returns whether the given value is a Quartz day-of-week field.
124+
* Determine whether the given value is a Quartz day-of-week field.
118125
*/
119126
public static boolean isQuartzDaysOfWeekField(String value) {
120127
return value.contains("L") || value.contains("#");
121128
}
122129

123130
/**
124-
* Parse the given value into a days of week {@code QuartzCronField}, the sixth entry of a cron expression.
125-
* Expects a "L" or "#" in the given value.
131+
* Parse the given value into a days of week {@code QuartzCronField},
132+
* the sixth entry of a cron expression.
133+
* <p>Expects a "L" or "#" in the given value.
126134
*/
127135
public static QuartzCronField parseDaysOfWeek(String value) {
128136
int idx = value.lastIndexOf('L');
@@ -135,7 +143,7 @@ public static QuartzCronField parseDaysOfWeek(String value) {
135143
if (idx == 0) {
136144
throw new IllegalArgumentException("No day-of-week before 'L' in '" + value + "'");
137145
}
138-
else { // "[0-7]L"
146+
else { // "[0-7]L"
139147
DayOfWeek dayOfWeek = parseDayOfWeek(value.substring(0, idx));
140148
adjuster = lastInMonth(dayOfWeek);
141149
}
@@ -157,7 +165,6 @@ else if (idx == value.length() - 1) {
157165
throw new IllegalArgumentException("Ordinal '" + ordinal + "' in '" + value +
158166
"' must be positive number ");
159167
}
160-
161168
TemporalAdjuster adjuster = dayOfWeekInMonth(ordinal, dayOfWeek);
162169
return new QuartzCronField(Type.DAY_OF_WEEK, Type.DAY_OF_MONTH, adjuster, value);
163170
}
@@ -167,14 +174,13 @@ else if (idx == value.length() - 1) {
167174
private static DayOfWeek parseDayOfWeek(String value) {
168175
int dayOfWeek = Integer.parseInt(value);
169176
if (dayOfWeek == 0) {
170-
dayOfWeek = 7; // cron is 0 based; java.time 1 based
177+
dayOfWeek = 7; // cron is 0 based; java.time 1 based
171178
}
172179
try {
173180
return DayOfWeek.of(dayOfWeek);
174181
}
175182
catch (DateTimeException ex) {
176-
String msg = ex.getMessage() + " '" + value + "'";
177-
throw new IllegalArgumentException(msg, ex);
183+
throw new IllegalArgumentException(ex.getMessage() + " '" + value + "'", ex);
178184
}
179185
}
180186

@@ -213,10 +219,10 @@ private static TemporalAdjuster lastWeekdayOfMonth() {
213219
Temporal lastDom = adjuster.adjustInto(temporal);
214220
Temporal result;
215221
int dow = lastDom.get(ChronoField.DAY_OF_WEEK);
216-
if (dow == 6) { // Saturday
222+
if (dow == 6) { // Saturday
217223
result = lastDom.minus(1, ChronoUnit.DAYS);
218224
}
219-
else if (dow == 7) { // Sunday
225+
else if (dow == 7) { // Sunday
220226
result = lastDom.minus(2, ChronoUnit.DAYS);
221227
}
222228
else {
@@ -227,7 +233,7 @@ else if (dow == 7) { // Sunday
227233
}
228234

229235
/**
230-
* Return a temporal adjuster that finds the nth-to-last day of the month.
236+
* Returns a temporal adjuster that finds the nth-to-last day of the month.
231237
* @param offset the negative offset, i.e. -3 means third-to-last
232238
* @return a nth-to-last day-of-month adjuster
233239
*/
@@ -241,7 +247,7 @@ private static TemporalAdjuster lastDayWithOffset(int offset) {
241247
}
242248

243249
/**
244-
* Return a temporal adjuster that finds the weekday nearest to the given
250+
* Returns a temporal adjuster that finds the weekday nearest to the given
245251
* day-of-month. If {@code dayOfMonth} falls on a Saturday, the date is
246252
* moved back to Friday; if it falls on a Sunday (or if {@code dayOfMonth}
247253
* is 1 and it falls on a Saturday), it is moved forward to Monday.
@@ -253,10 +259,10 @@ private static TemporalAdjuster weekdayNearestTo(int dayOfMonth) {
253259
int current = Type.DAY_OF_MONTH.get(temporal);
254260
DayOfWeek dayOfWeek = DayOfWeek.from(temporal);
255261

256-
if ((current == dayOfMonth && isWeekday(dayOfWeek)) || // dayOfMonth is a weekday
257-
(dayOfWeek == DayOfWeek.FRIDAY && current == dayOfMonth - 1) || // dayOfMonth is a Saturday, so Friday before
258-
(dayOfWeek == DayOfWeek.MONDAY && current == dayOfMonth + 1) || // dayOfMonth is a Sunday, so Monday after
259-
(dayOfWeek == DayOfWeek.MONDAY && dayOfMonth == 1 && current == 3)) { // dayOfMonth is Saturday 1st, so Monday 3rd
262+
if ((current == dayOfMonth && isWeekday(dayOfWeek)) || // dayOfMonth is a weekday
263+
(dayOfWeek == DayOfWeek.FRIDAY && current == dayOfMonth - 1) || // dayOfMonth is a Saturday, so Friday before
264+
(dayOfWeek == DayOfWeek.MONDAY && current == dayOfMonth + 1) || // dayOfMonth is a Sunday, so Monday after
265+
(dayOfWeek == DayOfWeek.MONDAY && dayOfMonth == 1 && current == 3)) { // dayOfMonth is Saturday 1st, so Monday 3rd
260266
return temporal;
261267
}
262268
int count = 0;
@@ -292,7 +298,7 @@ private static boolean isWeekday(DayOfWeek dayOfWeek) {
292298
}
293299

294300
/**
295-
* Return a temporal adjuster that finds the last of the given doy-of-week
301+
* Returns a temporal adjuster that finds the last of the given day-of-week
296302
* in a month.
297303
*/
298304
private static TemporalAdjuster lastInMonth(DayOfWeek dayOfWeek) {
@@ -329,6 +335,7 @@ private static Temporal rollbackToMidnight(Temporal current, Temporal result) {
329335
}
330336
}
331337

338+
332339
@Override
333340
public <T extends Temporal & Comparable<? super T>> T nextOrSame(T temporal) {
334341
T result = adjust(temporal);
@@ -345,7 +352,6 @@ public <T extends Temporal & Comparable<? super T>> T nextOrSame(T temporal) {
345352
return result;
346353
}
347354

348-
349355
@Nullable
350356
@SuppressWarnings("unchecked")
351357
private <T extends Temporal & Comparable<? super T>> T adjust(T temporal) {
@@ -354,27 +360,25 @@ private <T extends Temporal & Comparable<? super T>> T adjust(T temporal) {
354360

355361

356362
@Override
357-
public int hashCode() {
358-
return this.value.hashCode();
359-
}
360-
361-
@Override
362-
public boolean equals(Object o) {
363-
if (this == o) {
363+
public boolean equals(@Nullable Object other) {
364+
if (this == other) {
364365
return true;
365366
}
366-
if (!(o instanceof QuartzCronField)) {
367+
if (!(other instanceof QuartzCronField)) {
367368
return false;
368369
}
369-
QuartzCronField other = (QuartzCronField) o;
370-
return type() == other.type() &&
371-
this.value.equals(other.value);
370+
QuartzCronField otherField = (QuartzCronField) other;
371+
return (type() == otherField.type() && this.value.equals(otherField.value));
372+
}
373+
374+
@Override
375+
public int hashCode() {
376+
return this.value.hashCode();
372377
}
373378

374379
@Override
375380
public String toString() {
376381
return type() + " '" + this.value + "'";
377-
378382
}
379383

380384
}

0 commit comments

Comments
 (0)