Skip to content

Commit d387d9a

Browse files
committed
Allow quartz expression in cron expression lists
This commit introduces support for lists of quartz cron fields, such as "1L, LW" or "TUE#1, TUE#3, TUE#5". Closes gh-26289
1 parent 138f6bf commit d387d9a

File tree

5 files changed

+238
-8
lines changed

5 files changed

+238
-8
lines changed

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

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,13 @@
1919
import java.time.DateTimeException;
2020
import java.time.temporal.Temporal;
2121
import java.time.temporal.ValueRange;
22-
import java.util.BitSet;
2322

2423
import org.springframework.lang.Nullable;
2524
import org.springframework.util.Assert;
2625
import org.springframework.util.StringUtils;
2726

2827
/**
29-
* Efficient {@link BitSet}-based extension of {@link CronField}.
28+
* Efficient bitwise-operator extension of {@link CronField}.
3029
* Created using the {@code parse*} methods.
3130
*
3231
* @author Arjen Poutsma
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
/*
2+
* Copyright 2002-2021 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.temporal.Temporal;
20+
21+
import org.springframework.lang.Nullable;
22+
import org.springframework.util.Assert;
23+
24+
/**
25+
* Extension of {@link CronField} that wraps an array of cron fields.
26+
*
27+
* @author Arjen Poutsma
28+
* @since 5.3.3
29+
*/
30+
final class CompositeCronField extends CronField {
31+
32+
private final CronField[] fields;
33+
34+
private final String value;
35+
36+
37+
private CompositeCronField(Type type, CronField[] fields, String value) {
38+
super(type);
39+
this.fields = fields;
40+
this.value = value;
41+
}
42+
43+
/**
44+
* Composes the given fields into a {@link CronField}.
45+
*/
46+
public static CronField compose(CronField[] fields, Type type, String value) {
47+
Assert.notEmpty(fields, "Fields must not be empty");
48+
Assert.hasLength(value, "Value must not be empty");
49+
50+
if (fields.length == 1) {
51+
return fields[0];
52+
}
53+
else {
54+
return new CompositeCronField(type, fields, value);
55+
}
56+
}
57+
58+
59+
@Nullable
60+
@Override
61+
public <T extends Temporal & Comparable<? super T>> T nextOrSame(T temporal) {
62+
T result = null;
63+
for (CronField field : this.fields) {
64+
T candidate = field.nextOrSame(temporal);
65+
if (result == null ||
66+
candidate != null && candidate.compareTo(result) < 0) {
67+
result = candidate;
68+
}
69+
}
70+
return result;
71+
}
72+
73+
74+
@Override
75+
public int hashCode() {
76+
return this.value.hashCode();
77+
}
78+
79+
@Override
80+
public boolean equals(Object o) {
81+
if (this == o) {
82+
return true;
83+
}
84+
if (!(o instanceof CompositeCronField)) {
85+
return false;
86+
}
87+
CompositeCronField other = (CompositeCronField) o;
88+
return type() == other.type() &&
89+
this.value.equals(other.value);
90+
}
91+
92+
@Override
93+
public String toString() {
94+
return type() + " '" + this.value + "'";
95+
96+
}
97+
}

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

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,10 @@
2020
import java.time.temporal.ChronoField;
2121
import java.time.temporal.Temporal;
2222
import java.time.temporal.ValueRange;
23+
import java.util.function.BiFunction;
2324

2425
import org.springframework.lang.Nullable;
26+
import org.springframework.util.Assert;
2527
import org.springframework.util.StringUtils;
2628

2729
/**
@@ -77,11 +79,18 @@ public static CronField parseHours(String value) {
7779
* Parse the given value into a days of months {@code CronField}, the fourth entry of a cron expression.
7880
*/
7981
public static CronField parseDaysOfMonth(String value) {
80-
if (value.contains("L") || value.contains("W")) {
81-
return QuartzCronField.parseDaysOfMonth(value);
82+
if (!QuartzCronField.isQuartzDaysOfMonthField(value)) {
83+
return BitsCronField.parseDaysOfMonth(value);
8284
}
8385
else {
84-
return BitsCronField.parseDaysOfMonth(value);
86+
return parseList(value, Type.DAY_OF_MONTH, (field, type) -> {
87+
if (QuartzCronField.isQuartzDaysOfMonthField(field)) {
88+
return QuartzCronField.parseDaysOfMonth(field);
89+
}
90+
else {
91+
return BitsCronField.parseDaysOfMonth(field);
92+
}
93+
});
8594
}
8695
}
8796

@@ -98,15 +107,32 @@ public static CronField parseMonth(String value) {
98107
*/
99108
public static CronField parseDaysOfWeek(String value) {
100109
value = replaceOrdinals(value, DAYS);
101-
if (value.contains("L") || value.contains("#")) {
102-
return QuartzCronField.parseDaysOfWeek(value);
110+
if (!QuartzCronField.isQuartzDaysOfWeekField(value)) {
111+
return BitsCronField.parseDaysOfWeek(value);
103112
}
104113
else {
105-
return BitsCronField.parseDaysOfWeek(value);
114+
return parseList(value, Type.DAY_OF_WEEK, (field, type) -> {
115+
if (QuartzCronField.isQuartzDaysOfWeekField(field)) {
116+
return QuartzCronField.parseDaysOfWeek(field);
117+
}
118+
else {
119+
return BitsCronField.parseDaysOfWeek(field);
120+
}
121+
});
106122
}
107123
}
108124

109125

126+
private static CronField parseList(String value, Type type, BiFunction<String, Type, CronField> parseFieldFunction) {
127+
Assert.hasLength(value, "Value must not be empty");
128+
String[] fields = StringUtils.delimitedListToStringArray(value, ",");
129+
CronField[] cronFields = new CronField[fields.length];
130+
for (int i = 0; i < fields.length; i++) {
131+
cronFields[i] = parseFieldFunction.apply(fields[i], type);
132+
}
133+
return CompositeCronField.compose(cronFields, type, value);
134+
}
135+
110136
private static String replaceOrdinals(String value, String[] list) {
111137
value = value.toUpperCase();
112138
for (int i = 0; i < list.length; i++) {

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,12 @@ private QuartzCronField(Type type, Type rollForwardType, TemporalAdjuster adjust
7878
this.rollForwardType = rollForwardType;
7979
}
8080

81+
/**
82+
* Returns whether the given value is a Quartz day-of-month field.
83+
*/
84+
public static boolean isQuartzDaysOfMonthField(String value) {
85+
return value.contains("L") || value.contains("W");
86+
}
8187

8288
/**
8389
* Parse the given value into a days of months {@code QuartzCronField}, the fourth entry of a cron expression.
@@ -125,6 +131,13 @@ else if (idx != value.length() - 1) {
125131
throw new IllegalArgumentException("No 'L' or 'W' found in '" + value + "'");
126132
}
127133

134+
/**
135+
* Returns whether the given value is a Quartz day-of-week field.
136+
*/
137+
public static boolean isQuartzDaysOfWeekField(String value) {
138+
return value.contains("L") || value.contains("#");
139+
}
140+
128141
/**
129142
* Parse the given value into a days of week {@code QuartzCronField}, the sixth entry of a cron expression.
130143
* Expects a "L" or "#" in the given value.

0 commit comments

Comments
 (0)