Skip to content

Commit 93b53da

Browse files
committed
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
1 parent 1a8906b commit 93b53da

File tree

8 files changed

+1350
-231
lines changed

8 files changed

+1350
-231
lines changed
Lines changed: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,235 @@
1+
/*
2+
* Copyright 2002-2020 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.scheduling.support;
18+
19+
import java.time.DateTimeException;
20+
import java.time.temporal.Temporal;
21+
import java.time.temporal.ValueRange;
22+
import java.util.BitSet;
23+
24+
import org.springframework.lang.Nullable;
25+
import org.springframework.util.Assert;
26+
import org.springframework.util.StringUtils;
27+
28+
/**
29+
* Efficient {@link BitSet}-based extension of {@link CronField}.
30+
* Created using the {@code parse*} methods.
31+
*
32+
* @author Arjen Poutsma
33+
* @since 5.3
34+
*/
35+
final class BitsCronField extends CronField {
36+
37+
private static final BitsCronField ZERO_NANOS;
38+
39+
40+
static {
41+
ZERO_NANOS = new BitsCronField(Type.NANO);
42+
ZERO_NANOS.bits.set(0);
43+
}
44+
45+
private final BitSet bits;
46+
47+
48+
49+
private BitsCronField(Type type) {
50+
super(type);
51+
this.bits = new BitSet((int) type.range().getMaximum());
52+
}
53+
54+
/**
55+
* Return a {@code BitsCronField} enabled for 0 nano seconds.
56+
*/
57+
public static BitsCronField zeroNanos() {
58+
return BitsCronField.ZERO_NANOS;
59+
}
60+
61+
/**
62+
* Parse the given value into a seconds {@code BitsCronField}, the first entry of a cron expression.
63+
*/
64+
public static BitsCronField parseSeconds(String value) {
65+
return parseField(value, Type.SECOND);
66+
}
67+
68+
/**
69+
* Parse the given value into a minutes {@code BitsCronField}, the second entry of a cron expression.
70+
*/
71+
public static BitsCronField parseMinutes(String value) {
72+
return BitsCronField.parseField(value, Type.MINUTE);
73+
}
74+
75+
/**
76+
* Parse the given value into a hours {@code BitsCronField}, the third entry of a cron expression.
77+
*/
78+
public static BitsCronField parseHours(String value) {
79+
return BitsCronField.parseField(value, Type.HOUR);
80+
}
81+
82+
/**
83+
* Parse the given value into a days of months {@code BitsCronField}, the fourth entry of a cron expression.
84+
*/
85+
public static BitsCronField parseDaysOfMonth(String value) {
86+
return parseDate(value, Type.DAY_OF_MONTH);
87+
}
88+
89+
/**
90+
* Parse the given value into a month {@code BitsCronField}, the fifth entry of a cron expression.
91+
*/
92+
public static BitsCronField parseMonth(String value) {
93+
return BitsCronField.parseField(value, Type.MONTH);
94+
}
95+
96+
/**
97+
* Parse the given value into a days of week {@code BitsCronField}, the sixth entry of a cron expression.
98+
*/
99+
public static BitsCronField parseDaysOfWeek(String value) {
100+
BitsCronField result = parseDate(value, Type.DAY_OF_WEEK);
101+
BitSet bits = result.bits;
102+
if (bits.get(0)) {
103+
// cron supports 0 for Sunday; we use 7 like java.time
104+
bits.set(7);
105+
bits.clear(0);
106+
}
107+
return result;
108+
}
109+
110+
111+
private static BitsCronField parseDate(String value, BitsCronField.Type type) {
112+
if (value.indexOf('?') != -1) {
113+
value = "*";
114+
}
115+
return BitsCronField.parseField(value, type);
116+
}
117+
118+
private static BitsCronField parseField(String value, Type type) {
119+
Assert.hasLength(value, "Value must not be empty");
120+
Assert.notNull(type, "Type must not be null");
121+
try {
122+
BitsCronField result = new BitsCronField(type);
123+
String[] fields = StringUtils.delimitedListToStringArray(value, ",");
124+
for (String field : fields) {
125+
int slashPos = field.indexOf('/');
126+
if (slashPos == -1) {
127+
ValueRange range = parseRange(field, type);
128+
result.setBits(range);
129+
}
130+
else {
131+
String rangeStr = value.substring(0, slashPos);
132+
String deltaStr = value.substring(slashPos + 1);
133+
ValueRange range = parseRange(rangeStr, type);
134+
if (rangeStr.indexOf('-') == -1) {
135+
range = ValueRange.of(range.getMinimum(), type.range().getMaximum());
136+
}
137+
int delta = Integer.parseInt(deltaStr);
138+
if (delta <= 0) {
139+
throw new IllegalArgumentException("Incrementer delta must be 1 or higher");
140+
}
141+
result.setBits(range, delta);
142+
}
143+
}
144+
return result;
145+
}
146+
catch (DateTimeException | IllegalArgumentException ex) {
147+
String msg = ex.getMessage() + " '" + value + "'";
148+
throw new IllegalArgumentException(msg, ex);
149+
}
150+
}
151+
152+
private static ValueRange parseRange(String value, Type type) {
153+
if (value.indexOf('*') != -1) {
154+
return type.range();
155+
}
156+
else {
157+
int hyphenPos = value.indexOf('-');
158+
if (hyphenPos == -1) {
159+
int result = type.checkValidValue(Integer.parseInt(value));
160+
return ValueRange.of(result, result);
161+
}
162+
else {
163+
int min = Integer.parseInt(value.substring(0, hyphenPos));
164+
int max = Integer.parseInt(value.substring(hyphenPos + 1));
165+
min = type.checkValidValue(min);
166+
max = type.checkValidValue(max);
167+
return ValueRange.of(min, max);
168+
}
169+
}
170+
}
171+
172+
@Nullable
173+
@Override
174+
public <T extends Temporal & Comparable<? super T>> T nextOrSame(T temporal) {
175+
int current = type().get(temporal);
176+
int next = this.bits.nextSetBit(current);
177+
if (next == -1) {
178+
temporal = type().rollForward(temporal);
179+
next = this.bits.nextSetBit(0);
180+
}
181+
if (next == current) {
182+
return temporal;
183+
}
184+
else {
185+
int count = 0;
186+
current = type().get(temporal);
187+
while (current != next && count++ < CronExpression.MAX_ATTEMPTS) {
188+
temporal = type().elapseUntil(temporal, next);
189+
current = type().get(temporal);
190+
}
191+
if (count >= CronExpression.MAX_ATTEMPTS) {
192+
return null;
193+
}
194+
return type().reset(temporal);
195+
}
196+
}
197+
198+
BitSet bits() {
199+
return this.bits;
200+
}
201+
202+
private void setBits(ValueRange range) {
203+
this.bits.set((int) range.getMinimum(), (int) range.getMaximum() + 1);
204+
}
205+
206+
private void setBits(ValueRange range, int delta) {
207+
for (int i = (int) range.getMinimum(); i <= range.getMaximum(); i += delta) {
208+
this.bits.set(i);
209+
}
210+
}
211+
212+
@Override
213+
public int hashCode() {
214+
return this.bits.hashCode();
215+
}
216+
217+
@Override
218+
public boolean equals(Object o) {
219+
if (this == o) {
220+
return true;
221+
}
222+
if (!(o instanceof BitsCronField)) {
223+
return false;
224+
}
225+
BitsCronField other = (BitsCronField) o;
226+
return type() == other.type() &&
227+
this.bits.equals(other.bits);
228+
}
229+
230+
@Override
231+
public String toString() {
232+
return type() + " " + this.bits;
233+
}
234+
235+
}

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

Lines changed: 50 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ private CronExpression(
6767
String expression) {
6868

6969
// to make sure we end up at 0 nanos, we add an extra field
70-
this.fields = new CronField[]{daysOfWeek, months, daysOfMonth, hours, minutes, seconds, CronField.zeroNanos()};
70+
this.fields = new CronField[]{CronField.zeroNanos(), seconds, minutes, hours, daysOfMonth, months, daysOfWeek};
7171
this.expression = expression;
7272
}
7373

@@ -100,14 +100,49 @@ private CronExpression(
100100
* Ranges of numbers are expressed by two numbers separated with a hyphen
101101
* ({@code -}). The specified range is inclusive.
102102
* </li>
103-
* <li>Following a range (or {@code *}) with {@code "/n"} specifies
104-
* skips of the number's value through the range.
103+
* <li>Following a range (or {@code *}) with {@code /n} specifies
104+
* the interval of the number's value through the range.
105105
* </li>
106106
* <li>
107107
* English names can also be used for the "month" and "day of week" fields.
108108
* Use the first three letters of the particular day or month (case does not
109109
* matter).
110110
* </li>
111+
* <li>
112+
* The "day of month" and "day of week" fields can contain a
113+
* {@code L}-character, which stands for "last", and has a different meaning
114+
* in each field:
115+
* <ul>
116+
* <li>
117+
* In the "day of month" field, {@code L} stands for "the last day of the
118+
* month". If followed by an negative offset (i.e. {@code L-n}), it means
119+
* "{@code n}th-to-last day of the month". If followed by {@code W} (i.e.
120+
* {@code LW}), it means "the last weekday of the month".
121+
* </li>
122+
* <li>
123+
* In the "day of week" field, {@code L} stands for "the last day of the
124+
* week", and uses the
125+
* {@linkplain java.util.Locale#getDefault() system default locale}
126+
* to determine which day that is (i.e. Sunday or Saturday).
127+
* If prefixed by a number or three-letter name (i.e. {@code dL} or
128+
* {@code DDDL}), it means "the last day of week {@code d} (or {@code DDD})
129+
* in the month".
130+
* </li>
131+
* </ul>
132+
* </li>
133+
* <li>
134+
* The "day of month" field can be {@code nW}, which stands for "the nearest
135+
* weekday to day of the month {@code n}".
136+
* If {@code n} falls on Saturday, this yields the Friday before it.
137+
* If {@code n} falls on Sunday, this yields the Monday after,
138+
* which also happens if {@code n} is {@code 1} and falls on a Saturday
139+
* (i.e. {@code 1W} stands for "the first weekday of the month").
140+
* </li>
141+
* <li>
142+
* The "day of week" field can be {@code d#n} (or {@code DDD#n}), which
143+
* stands for "the {@code n}-th day of week {@code d} (or {@code DDD}) in
144+
* the month".
145+
* </li>
111146
* </ul>
112147
*
113148
* <p>Example expressions:
@@ -119,6 +154,15 @@ private CronExpression(
119154
* <li>{@code "0 0/30 8-10 * * *"} = 8:00, 8:30, 9:00, 9:30, 10:00 and 10:30 every day.</li>
120155
* <li>{@code "0 0 9-17 * * MON-FRI"} = on the hour nine-to-five weekdays</li>
121156
* <li>{@code "0 0 0 25 12 ?"} = every Christmas Day at midnight</li>
157+
* <li>{@code "0 0 0 L * *"} = last day of the month at midnight</li>
158+
* <li>{@code "0 0 0 L-3 * *"} = third-to-last day of the month at midnight</li>
159+
* <li>{@code "0 0 0 1W * *"} = first weekday of the month at midnight</li>
160+
* <li>{@code "0 0 0 LW * *"} = last weekday of the month at midnight</li>
161+
* <li>{@code "0 0 0 * * L"} = last day of the week at midnight</li>
162+
* <li>{@code "0 0 0 * * 5L"} = last Friday of the month at midnight</li>
163+
* <li>{@code "0 0 0 * * THUL"} = last Thursday of the month at midnight</li>
164+
* <li>{@code "0 0 0 ? * 5#2"} = the second Friday in the month at midnight</li>
165+
* <li>{@code "0 0 0 ? * MON#1"} = the first Monday in the month at midnight</li>
122166
* </ul>
123167
*
124168
* <p>The following macros are also supported:
@@ -181,13 +225,13 @@ private static String resolveMacros(String expression) {
181225
* if no such temporal can be found
182226
*/
183227
@Nullable
184-
public <T extends Temporal> T next(T temporal) {
228+
public <T extends Temporal & Comparable<? super T>> T next(T temporal) {
185229
return nextOrSame(ChronoUnit.NANOS.addTo(temporal, 1));
186230
}
187231

188232

189233
@Nullable
190-
private <T extends Temporal> T nextOrSame(T temporal) {
234+
private <T extends Temporal & Comparable<? super T>> T nextOrSame(T temporal) {
191235
for (int i = 0; i < MAX_ATTEMPTS; i++) {
192236
T result = nextOrSameInternal(temporal);
193237
if (result == null || result.equals(temporal)) {
@@ -199,7 +243,7 @@ private <T extends Temporal> T nextOrSame(T temporal) {
199243
}
200244

201245
@Nullable
202-
private <T extends Temporal> T nextOrSameInternal(T temporal) {
246+
private <T extends Temporal & Comparable<? super T>> T nextOrSameInternal(T temporal) {
203247
for (CronField field : this.fields) {
204248
temporal = field.nextOrSame(temporal);
205249
if (temporal == null) {

0 commit comments

Comments
 (0)