jakarta.enterprise.concurrent.CronTrigger Maven / Gradle / Ivy
Show all versions of jakarta.enterprise.concurrent-api Show documentation
/*
* Copyright (c) 2021,2023 Contributors to the Eclipse Foundation
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0, which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* This Source Code may also be made available under the following Secondary
* Licenses when the conditions for such availability set forth in the
* Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
* version 2 with the GNU Classpath Exception, which is available at
* https://www.gnu.org/software/classpath/license.html.
*
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
*/
package jakarta.enterprise.concurrent;
import java.time.DateTimeException;
import java.time.DayOfWeek;
import java.time.Month;
import java.time.Year;
import java.time.ZonedDateTime;
import java.time.ZoneId;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.function.Function;
/**
* Cron-based {@link Trigger} implementation, which supports 5 or 6 fields
* delimited by a single space character, plus a {@link ZoneId}.
* Basic cron syntax is supported. For more advanced scenarios, you can
* subclass this implementation or combine multiple CronTrigger
* instances in a Trigger
implementation of your own.
*
*
* Cron Expression Fields
* seconds (optional) 0-59, *. When absent, 0 is assumed
* minutes 0-59, *
* hours 0-23, *
* dayOfMonth 0-31, *, L
* month 1-12, JAN-DEC, January-December, *
* dayOfWeek SUN-SAT, Sunday-Saturday, 0-7, *.
* 0 and 7 both represent Sunday: 0 to designate the first day of the week,
* and 7 for consistency with {@link java.time.DayOfWeek}.
*
*
*
* Cron Expression Syntax
* ,
* delimits lists for all fields. For example, MON,WED,FRI
or MAY,SEP
* -
* delimits ranges for all fields. For example, MON-FRI
or 9-17
* /
* specifies a repeating increment for all fields except dayOfWeek.
* For example, 6/7
for the hours
field equates to 6,13,20
.
* #
* specifies an ordinal day of week. For example,
* FRI#1,SAT#L
is the first Friday and last Saturday of the month.
*
#
cannot be used within ranges (-
) and increments (/
).
* *
* indicates any value is permitted.
* L
* indicates the last day of the month.
* 2L
indicates the second-to-last day, and so forth.
*
*
*
* Cron Expression Examples
* 0 * * * *
* every hour at the top of the hour
* 0 9-17 * * MON-FRI
* weekdays from 9am to 5pm, at the top of each hour
* 0 13/3 * MAY-SEP SAT,SUN
* weekends from May to September, every 3 hours, starting at 1pm
* 30 10 * APR,AUG TUE#2,TUE#L
* second and last Tuesdays of April and August at 10:30am
* 15 22 4 10,20,L * *
* 4:22:15 AM on the 10th, 20th, and last day of every month
* 0 8-11,13-16 2L JAN-MAR *
* 8AM-11AM and 1PM-4PM on the second-to-last day of January, February, and March
*
* A constructor is provided that accepts a cron expression such as the above and a timezone id. For example,
*
* trigger = new CronTrigger("0 7 * SEP-MAY MON-FRI", ZoneId.of("America/New_York"));
*
* Another constructor allows cron fields to be specified in a fluent manner, in any order. For example,
*
* trigger = new CronTrigger(ZoneId.of("America/Los_Angeles"))
* .months(Month.DECEMBER)
* .daysOfMonth(24)
* .hours(16, 18);
*
*
* The {@link #getNextRunTime(LastExecution, ZonedDateTime) getNextRunTime} method of this trigger
* determines the next run time based on the cron schedule.
* The {@link #skipRun(LastExecution, ZonedDateTime) skipRun} method always returns false
* unless overridden by a subclass.
*
* Methods of this class that configure the cron expression fields are not thread safe. It is the
* responsibility of the caller to ensure that initialization of the CronTrigger
* happens before it is supplied to a {@link ManagedScheduledExecutorService} and that the
* CronTrigger
is not subsequently modified.
*
* You can subclass CronTrigger
to provide for more complex logic, such as in the following
* example of combining two triggers to schedule twice-a-month payroll on the 15th and last day of month
* or the prior Fridays when the former fall on a weekend:
*
* public class PayrollTrigger extends CronTrigger {
* private final CronTrigger fridaysBeforeWeekendPayrollDay;
*
* PayrollTrigger() {
* // Every 15th and last day of the month that is a weekday,
* super("0 10 15,L * MON-FRI", ZoneId.of("America/Chicago"));
*
* // Every 13th, 14th, third-to-last, and second-to-last day of the month that is a Friday,
* fridaysBeforeWeekendPayrollDay = new CronTrigger(
* "0 10 13,14,3L,2L * FRI", getZoneId());
* }
*
* public ZonedDateTime getNextRunTime(LastExecution lastExec, ZonedDateTime scheduledAt) {
* ZonedDateTime time1 = super.getNextRunTime(lastExec, scheduledAt);
* ZonedDateTime time2 = fridaysBeforeWeekendPayrollDay.getNextRunTime(lastExec, scheduledAt);
* return time1.isBefore(time2) ? time1 : time2;
* }
* }
*
*
* @since 3.0
*/
public class CronTrigger implements ZonedTrigger {
private static final Map DAYS_OF_WEEK = new HashMap(7);
private static final Map MONTHS = new HashMap(12);
static {
for (DayOfWeek day : DayOfWeek.values()) {
DAYS_OF_WEEK.put(day.name().substring(0, 3), day.getValue());
}
for (Month month : Month.values()) {
MONTHS.put(month.name().substring(0, 3), month.getValue());
}
}
private static final int[] ALL_DAYS_OF_MONTH = new int[] {
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20,
21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31
};
private static final int[] ALL_DAYS_OF_WEEK = new int[] {
1, 2, 3, 4, 5, 6, 7
};
private static final int[] ALL_MONTHS = new int[] {
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12
};
private static final int LAST = -1;
private static final int[] ZERO = new int[] {
0
};
// cron expression fields are parsed into lists
private int[] daysOfMonth = ALL_DAYS_OF_MONTH;
private int[] daysOfWeek = ALL_DAYS_OF_WEEK;
private int[] hours = ZERO;
private int[] minutes = ZERO;
private int[] months = ALL_MONTHS;
private int[] seconds = ZERO;
private final ZoneId zone;
/**
* Constructor that accepts a cron expression.
*
* @param cron cron expression.
* @param zone timezone ID to use for {@link java.time.ZonedDateTime} that is supplied to
* {@link #getNextRunTime(LastExecution, ZonedDateTime) getNextRunTime} and
* {@link #skipRun(LastExecution, ZonedDateTime) skipRun} methods.
* Null indicates to use the system default.
*/
public CronTrigger(final String cron, final ZoneId zone) {
this(zone);
String[] c = cron.split(" ");
if (c.length == 5) {
minutes(c[0]).hours(c[1]).daysOfMonth(c[2]).months(c[3]).daysOfWeek(c[4]);
} else if (c.length == 6) {
seconds(c[0]).minutes(c[1]).hours(c[2]).daysOfMonth(c[3]).months(c[4]).daysOfWeek(c[5]);
} else {
throw new IllegalArgumentException(cron);
}
}
/**
* Constructor for the fluent configuration pattern.
* Seconds, minutes, and hours default to 0. The remaining fields default to *.
*
* @param zone timezone ID to use for {@link java.time.ZonedDateTime} that is supplied to
* {@link #getNextRunTime(LastExecution, ZonedDateTime) getNextRunTime} and
* {@link #skipRun(LastExecution, ZonedDateTime) skipRun} methods.
* Null indicates to use the system default.
*/
public CronTrigger(final ZoneId zone) {
this.zone = zone == null ? ZoneId.systemDefault() : zone;
}
/**
* Using the cron schedule, and based on the end of the most recent execution
* (or absent that, the initial scheduling time), retrieve the next time
* that the task should run after.
*
* @param lastExecutionInfo information about the last execution of the task.
* This value will be null if the task has not yet run.
* @param taskScheduledTime the date/time at which the
* {@code ManagedScheduledExecutorService.schedule}
* method was invoked to schedule the task.
* @return the date/time after which the next execution of the task should start.
* @throws DateTimeException if a next time cannot be determined from the cron expression.
*/
@Override
public ZonedDateTime getNextRunTime(final LastExecution lastExecutionInfo, final ZonedDateTime taskScheduledTime) {
return next(lastExecutionInfo == null ? taskScheduledTime : lastExecutionInfo.getRunEnd(zone));
}
/**
* Returns the timezone to use for
* {@link java.time.ZonedDateTime ZonedDateTime} that is supplied to the
* {@link #getNextRunTime(LastExecution, java.time.ZonedDateTime) getNextRunTime} and
* {@link #skipRun(LastExecution, java.time.ZonedDateTime) skipRun} methods.
*
* @return timezone to use for operations on this trigger.
*/
@Override
public final ZoneId getZoneId() {
return zone;
}
/**
* Configure the day-of-month cron field, overwriting any previous value for day-of-month.
*
* @param d one or more day numbers ranging from 1 to 31.
* @return this instance.
*/
public CronTrigger daysOfMonth(final int... d) {
daysOfMonth = parse("daysOfMonth", 1, 31, d);
return this;
}
/**
* Configure the day-of-month cron field, overwriting any previous value for day-of-month.
*
* @param d dayOfMonth cron field. For example, 15,L
.
* @return this instance.
*/
public CronTrigger daysOfMonth(final String d) {
daysOfMonth = parse("daysOfMonth", 1, 31, LAST, d, CronTrigger::parseDayOfMonth);
return this;
}
/**
* Configure the day-of-week cron field, overwriting any previous value for day-of-week.
*
* @param d one or more days of the week.
* @return this instance.
*/
public CronTrigger daysOfWeek(final DayOfWeek... d) {
if (d.length == 0) {
throw new IllegalArgumentException("daysOfWeek: []");
}
daysOfWeek = Arrays.stream(d).map(DayOfWeek::getValue).sorted().distinct()
.mapToInt(Integer::intValue).toArray();
return this;
}
/**
* Configure the day-of-week cron field, overwriting any previous value for day-of-week.
*
* @param d dayOfWeek cron field. For example, MON-FRI,SAT#L
.
* @return this instance.
*/
public CronTrigger daysOfWeek(final String d) {
daysOfWeek = parse("daysOfWeek", 1, 7, 49, d, CronTrigger::parseDayOfWeek);
return this;
}
/**
* Configure the hours cron field, overwriting any previous value for hours.
*
* @param h one or more hour values ranging from 0 to 23.
* @return this instance.
*/
public CronTrigger hours(final int... h) {
hours = parse("hours", 0, 23, h);
return this;
}
/**
* Configure the hours cron field, overwriting any previous value for hours.
*
* @param h hours cron field. For example, 9-17
for 9am to 5pm.
* @return this instance.
*/
public CronTrigger hours(final String h) {
hours = parse("hours", 0, 23, 23, h, Integer::parseInt);
return this;
}
/**
* Configure the minutes cron field, overwriting any previous value for minutes.
*
* @param m one or more minute values ranging from 0 to 59.
* @return this instance.
*/
public CronTrigger minutes(final int... m) {
minutes = parse("minutes", 0, 59, m);
return this;
}
/**
* Configure the minutes cron field, overwriting any previous value for minutes.
*
* @param m minutes cron field. For example, 5/10
for 10 minute intervals
* starting at 5 minutes after the hour (:05, :15, :25, :35, :45, :55).
* @return this instance.
*/
public CronTrigger minutes(final String m) {
minutes = parse("minutes", 0, 59, 59, m, Integer::parseInt);
return this;
}
/**
* Configure the month cron field, overwriting any previous value for month.
*
* @param m one or more months.
* @return this instance.
*/
public CronTrigger months(final Month... m) {
if (m.length == 0) {
throw new IllegalArgumentException("months: []");
}
months = Arrays.stream(m).map(Month::getValue).sorted().distinct().mapToInt(Integer::intValue).toArray();
return this;
}
/**
* Configure the months cron field, overwriting any previous value for months.
*
* @param m months cron field. For example, SEP-NOV,FEB-MAY
.
* @return this instance.
*/
public CronTrigger months(final String m) {
months = parse("months", 1, 12, 12, m, CronTrigger::parseMonth);
return this;
}
/**
* Configure the seconds cron field, overwriting any previous value for seconds.
*
* @param s one or more seconds values ranging from 0 to 59.
* @return this instance.
*/
public CronTrigger seconds(final int... s) {
seconds = parse("seconds", 0, 59, s);
return this;
}
/**
* Configure the seconds cron field, overwriting any previous value for seconds.
*
* @param s seconds cron field. For example, 30
.
* @return this instance.
*/
public CronTrigger seconds(final String s) {
seconds = parse("seconds", 0, 59, 59, s, Integer::parseInt);
return this;
}
/**
* Readable representation of the CronTrigger, which displays fields in list form
* or with the * character for brevity.
*
* For example,
*
CronTrigger@89abcdef seconds 0, minutes 0, hours 9, *, months 3,6,9,12, SAT#2,SAT#4
*
* @return readable representation of the parsed cron expression.
*/
@Override
public String toString() {
StringBuilder s = new StringBuilder("CronTrigger@").append(Integer.toHexString(hashCode()));
toStringBuilder(s, "seconds", seconds, 60);
toStringBuilder(s, "minutes", minutes, 60);
toStringBuilder(s, "hours", hours, 24);
toStringBuilder(s, "days", daysOfMonth, 31);
toStringBuilder(s, "months", months, 12);
if (daysOfWeek.length == 7 && daysOfWeek[6] == 7) {
s.append(" *");
} else {
for (int i = 0; i < daysOfWeek.length; i++) {
int d = ((daysOfWeek[i] - 1) % 7 + 1);
int ord = (daysOfWeek[i] - 1) / 7;
s.append(i == 0 ? ' ' : ',').append(DayOfWeek.of(d).name().substring(0, 3));
if (ord > 0) {
s.append('#').append(ord == 6 ? "L" : ord);
}
}
}
return s.toString();
}
/**
* Utility method for repeated logic in toString.
*
* @param s The string builder
* @param label The cron expression
* @param list Cron expression as list
* @param max Max value of cron expression
*/
private void toStringBuilder(final StringBuilder s, final String label, final int[] list, final int max) {
if (list.length == max) {
s.append(" *");
} else {
s.append(' ').append(label).append(' ');
StringBuilder l = new StringBuilder();
for (int i = 0; i < list.length; i++) {
if (list[i] < 0) {
l.append(list[i] == LAST ? "L" : (-list[i] + "L")).append(',');
} else {
s.append(list[i]).append(',');
}
}
s.append(l.toString());
}
}
/**
* Advance to the next date/time according to the cron schedule.
*
* @param from the date/time from which to compute the next time.
* @return next date/time according to the cron schedule, or the original time if it matches.
*/
protected ZonedDateTime next(final ZonedDateTime from) {
ZonedDateTime time = from.getNano() == 0 ? from : from.plusSeconds(1).withNano(0);
for (int i = 0; i < 1000 /** just in case expression never matches */ && time != null; ++i) {
int year = time.getYear();
int monthIndex = Arrays.binarySearch(months, time.getMonthValue());
if (monthIndex < 0) {
time = nextMonth(-monthIndex - 2, year);
} else {
int dayOfMonth = time.getDayOfMonth();
int lastDayOfMonth = time.getMonth().length(Year.isLeap(year));
int dayIndex = Arrays.binarySearch(daysOfMonth, dayOfMonth);
int lastDayIndex = Arrays.binarySearch(daysOfMonth, dayOfMonth - lastDayOfMonth - 1);
if (dayIndex < 0 && lastDayIndex < 0) {
time = nextDayOfMonth(-dayIndex - 2, -lastDayIndex - 2, monthIndex, year, time);
} else {
dayIndex = dayIndex < 0 ? (-dayIndex - 2) : dayIndex;
lastDayIndex = lastDayIndex < 0 ? (-lastDayIndex - 2) : lastDayIndex;
int dayOfWeek = time.getDayOfWeek().getValue();
int ordinalDayOfWeek = (((dayOfMonth - 1) / 7) + 1) * 7 + dayOfWeek;
int dayOfLastWeek = lastDayOfMonth - dayOfMonth >= 7 ? -1 : (6 * 7 + dayOfWeek);
if (Arrays.binarySearch(daysOfWeek, dayOfWeek) < 0 // (TUE)
&& Arrays.binarySearch(daysOfWeek, ordinalDayOfWeek) < 0 // (WED#3)
&& Arrays.binarySearch(daysOfWeek, dayOfLastWeek) < 0) { // (THU#L)
time = nextDayOfMonth(dayIndex, lastDayIndex, monthIndex, year, time);
} else {
int hourIndex = Arrays.binarySearch(hours, time.getHour());
if (hourIndex < 0) {
time = nextHour(-hourIndex - 2, dayIndex, lastDayIndex, dayOfMonth, monthIndex, year, time);
} else {
int minuteIndex = Arrays.binarySearch(minutes, time.getMinute());
if (minuteIndex < 0) {
time = nextMinute(-minuteIndex - 2, hourIndex, dayIndex, lastDayIndex, dayOfMonth, monthIndex, year, time);
} else {
int secondIndex = Arrays.binarySearch(seconds, time.getSecond());
if (secondIndex < 0) {
time = nextSecond(-secondIndex - 2, minuteIndex, hourIndex, dayIndex, lastDayIndex, dayOfMonth, monthIndex, year, time);
} else {
return time;
}
}
}
}
}
}
}
throw new DateTimeException("Unable to determine next time after " + from + " with " + this);
}
/**
* Advance to next day of month.
*
* @param dayIndex index of the day in daysOfMonth[]
* @param lastDayIndex index of the last day of the month in daysOfMonth[]
* @param monthIndex index of the month in months[]
* @param year year
* @param time Date/Time
* @return ZonedDateTime for next month
*/
private ZonedDateTime nextDayOfMonth(final int dayIndex, final int lastDayIndex, final int monthIndex,
final int year, final ZonedDateTime time) {
int lastDayOfMonth = Month.of(months[monthIndex]).length(Year.isLeap(year));
int dd = dayIndex + 1 < daysOfMonth.length ? daysOfMonth[dayIndex + 1] : 32;
int ld = lastDayIndex + 1 < daysOfMonth.length && daysOfMonth[lastDayIndex + 1] < 0 ? (1 + lastDayOfMonth + daysOfMonth[lastDayIndex + 1]) : 32;
int dayOfMonth = Math.min(dd, ld);
if (dayOfMonth > lastDayOfMonth) {
return nextMonth(monthIndex, year);
}
return ZonedDateTime.of(year, months[monthIndex], dayOfMonth, hours[0], minutes[0], seconds[0], 0, time.getZone());
}
/**
* Advance to next hour.
*
* @param hourIndex index of the hour in hours[]
* @param dayIndex index of the day in daysOfMonth[]
* @param lastDayIndex index of the last day of the month in daysOfMonth[]
* @param dayOfMonth day of month
* @param monthIndex index of the month in months[]
* @param year year
* @param time Date/Time
* @return ZonedDateTime for next hour
*/
private ZonedDateTime nextHour(final int hourIndex, final int dayIndex, final int lastDayIndex, final int dayOfMonth,
final int monthIndex, final int year, final ZonedDateTime time) {
// Determine if the same hour can be kept due to transition from Daylight Saving Time to Standard Time:
if (hourIndex >= 0) {
ZonedDateTime dst = ZonedDateTime.of(year, months[monthIndex], dayOfMonth,
hours[hourIndex], minutes[0], seconds[0], 0, time.getZone());
ZonedDateTime std = dst.plusHours(1);
if (dst.getHour() == std.getHour() && time.isAfter(dst) && time.isBefore(std)) {
return std; // Daylight Saving Time --> Standard Time
}
}
if (hourIndex + 1 < hours.length) {
return ZonedDateTime.of(year, months[monthIndex], dayOfMonth,
hours[hourIndex + 1], minutes[0], seconds[0], 0, time.getZone());
} else {
return nextDayOfMonth(dayIndex, lastDayIndex, monthIndex, year, time);
}
}
/**
* Advance to next minute.
*
* @param minuteIndex index of the minute in minutes[]
* @param hourIndex index of the hour in hours[]
* @param dayIndex index of the day in daysOfMonth[]
* @param lastDayIndex index of the last day of the month in daysOfMonth[]
* @param dayOfMonth day of month
* @param monthIndex index of the month in months[]
* @param year year
* @param time Date/Time
* @return ZonedDateTime for next second
*/
private ZonedDateTime nextMinute(final int minuteIndex, final int hourIndex, final int dayIndex, final int lastDayIndex, final int dayOfMonth,
final int monthIndex, final int year, final ZonedDateTime time) {
if (minuteIndex + 1 < minutes.length) {
return time.withMinute(minutes[minuteIndex + 1]).withSecond(seconds[0]);
} else {
return nextHour(hourIndex, dayIndex, lastDayIndex, dayOfMonth, monthIndex, year, time);
}
}
/**
* Advance to next month.
*
* @param month the month
* @param year the year
* @return ZonedDateTime for next month
*/
private ZonedDateTime nextMonth(final int month, final int year) {
int dayOfMonth;
int lastDayOfMonth;
int cycles = 0;
int m = month;
int y = year;
do {
if (++m >= months.length) {
m = 0;
y++;
}
int d = 0;
for (int i = 0; i < daysOfMonth.length && daysOfMonth[i] < 0;) {
d = ++i;
}
lastDayOfMonth = Month.of(months[m]).length(Year.isLeap(y));
int dd = d < daysOfMonth.length && daysOfMonth[d] > 0 ? daysOfMonth[d] : 32;
int ld = daysOfMonth[0] < 0 ? (1 + lastDayOfMonth + daysOfMonth[0]) : 32;
dayOfMonth = Math.min(dd, ld);
} while ((dayOfMonth < 1 || dayOfMonth > lastDayOfMonth) && (++cycles < 1000));
return cycles < 1000
? ZonedDateTime.of(y, months[m], dayOfMonth, hours[0], minutes[0], seconds[0], 0, zone)
: null; // expression never matched, for example 0 0 30 FEB *
}
/**
* Advance to next second.
*
* @param secondIndex index of the second in seconds[]
* @param minuteIndex index of the minute in minutes[]
* @param hourIndex index of the hour in hours[]
* @param dayIndex index of the day in daysOfMonth[]
* @param lastDayIndex index of the last day of the month in daysOfMonth[]
* @param dayOfMonth day of month
* @param monthIndex index of the month in months[]
* @param year year
* @param time Date/Time
* @return ZonedDateTime for next second
*/
private ZonedDateTime nextSecond(final int secondIndex, final int minuteIndex, final int hourIndex,
final int dayIndex, final int lastDayIndex, final int dayOfMonth,
final int monthIndex, final int year, final ZonedDateTime time) {
if (secondIndex + 1 < seconds.length) {
return time.withSecond(seconds[secondIndex + 1]);
} else {
return nextMinute(minuteIndex, hourIndex, dayIndex, lastDayIndex, dayOfMonth, monthIndex, year, time);
}
}
private static final void add(final SortedSet vals, final int start, final int end, final int increment) {
for (int val = start; val <= end; val += increment) {
vals.add(val);
}
}
/**
* Validate that the supplied list values are within the allowed range for the cron field type.
*
* @param type cron field type, such as months or hours.
* @param min minimum allowed value
* @param max maximum allowed value
* @param list supplied list of values
* @return sorted list of values with any duplicates removed.
*/
private int[] parse(final String type, final int min, final int max, final int[] list) {
if (list.length == 0) {
throw new IllegalArgumentException(type + ": []");
}
SortedSet vals = new TreeSet();
for (int i = 0; i < list.length; i++) {
if (list[i] < min || list[i] > max) {
throw new IllegalArgumentException(type + ": " + list[i]);
} else {
vals.add(list[i]);
}
}
return vals.stream().mapToInt(Integer::intValue).toArray();
}
/**
* Validate that the supplied list values are within the allowed range for the cron field type.
*
* @param name cron field type, such as months or hours.
* @param min minimum allowed normal value
* @param max maximum allowed normal value
* @param maxExt maximum allowed special value (L or SUN#L), or max if no special values allowed for this field.
* @param field the field's cron expression
* @param parser parser function, such as Integer::parseInt or CronTrigger::parseMonth
* @return sorted list of values with any duplicates removed.
*/
private int[] parse(final String name, final int min, final int max, final int maxExt,
final String field, final Function parser) {
if (field == null || field.length() == 0) {
throw new IllegalArgumentException(name + ": []");
}
SortedSet vals = new TreeSet();
for (String f : field.split(",")) {
try {
if ("*".equals(f) || "?".equals(f)) { // all values
add(vals, min, max, 1);
} else {
int slash = f.indexOf('/', 1);
if (slash > 0 && slash < f.length() - 1) { // increment
int val1 = slash == 1 && f.charAt(0) == '*' ? min : parser.apply(f.substring(0, slash));
int increment = parser.apply(f.substring(slash + 1));
if (val1 < min || val1 > max || increment < 1 || maxExt > max /* dayOfWeek */) {
throw new IllegalArgumentException(name + ": " + f);
}
add(vals, val1, max, increment);
} else {
int dash = f.indexOf('-', 1);
if (dash > 0 && dash < f.length() - 1) { // range
int val1 = parser.apply(f.substring(0, dash));
String end = f.substring(dash + 1);
int val2 = "L".equals(end) ? max : parser.apply(end);
if (val1 < min || val1 > max || val2 < min || val2 > max) {
throw new IllegalArgumentException(name + ": " + f);
}
if (val2 >= val1) {
add(vals, val1, val2, 1);
} else { // wrap around (eg. OCT-MAY)
add(vals, val1, max, 1);
add(vals, min, val2, 1);
}
} else { // single value
int val = parser.apply(f);
if ((val < min || val > maxExt) && maxExt != LAST) {
throw new IllegalArgumentException(name + ": " + f);
}
vals.add(val);
}
}
}
} catch (NumberFormatException x) {
throw new IllegalArgumentException(name + ": " + f, x);
}
}
return vals.stream().mapToInt(Integer::intValue).toArray();
}
/**
* Convert dayOfMonth value to 1-31, or negative for days from the end of the month
* For example, L is the last day (-1) and 2L is the second to last day (-2).
* @param day name of day of month
* @return integer representation of day of month
* @throws IllegalArgumentException
*/
private static int parseDayOfMonth(final String day) throws IllegalArgumentException {
try {
if (day.charAt(day.length() - 1) == 'L') {
int d = day.length() == 1 ? LAST : -Integer.parseInt(day.substring(0, day.length() - 1));
if (d > -1 || d < -31) {
throw new IllegalArgumentException("dayOfMonth: " + day);
}
return d;
} else {
int d = Integer.parseInt(day);
if (d < 1 || d > 31) {
throw new IllegalArgumentException("dayOfMonth: " + day);
}
return d;
}
} catch (NumberFormatException x) {
throw new IllegalArgumentException("dayOfMonth: " + day, x);
}
}
/**
* Convert dayOfWeek value to 1-49 where first 7 are standard week days,
* next 35 are ordinal 1st-5th of each day, and final 7 are ordinal last for each day.
*
* @param dayName name of day of the week
* @return integer representation of day of week
* @throws IllegalArgumentException
*/
private static int parseDayOfWeek(final String dayName) throws IllegalArgumentException {
String day = dayName;
int ordinal = 0;
int n = day.indexOf('#'); // ordinal day of week within month (TUE#2 for second Tuesday)
try {
if (n > 0) {
ordinal = day.charAt(n + 1) == 'L' ? 6 : Integer.parseInt(day.substring(n + 1));
if (ordinal < 1 || ordinal > 6) {
throw new IllegalArgumentException("dayOfWeek: " + day);
}
day = day.substring(0, n);
}
if (day.length() < 3) {
int d = Integer.parseInt(day);
return d == 0 ? 7 : d;
}
} catch (NumberFormatException x) {
throw new IllegalArgumentException("dayOfWeek: " + day, x);
}
day = day.toUpperCase();
Integer d = DAYS_OF_WEEK.get(day);
if (d == null) {
d = DayOfWeek.valueOf(day).getValue();
}
return 7 * ordinal + d;
}
/**
* Convert month value to 1-12.
*
* @param monthName name of month (January)
* @return integer representation of month
* @throws IllegalArgumentException
*/
private static int parseMonth(final String monthName) throws IllegalArgumentException {
String month = monthName;
if (month.length() < 3) {
try {
return Integer.parseInt(month);
} catch (NumberFormatException x) {
throw new IllegalArgumentException("month: " + month, x);
}
}
month = month.toUpperCase();
Integer m = MONTHS.get(month);
return m == null ? Month.valueOf(month).getValue() : m;
}
}