/*
* Copyright 2002-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.scheduling.support;
import java.time.temporal.ChronoUnit;
import java.time.temporal.Temporal;
import java.util.Arrays;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
Representation of a
crontab expression
that can calculate the next time it matches.
CronExpression
instances are created through parse(String)
; the next match is determined with next(T)
.
Author: Arjen Poutsma See Also: Since: 5.3
/**
* Representation of a
* <a href="https://www.manpagez.com/man/5/crontab/">crontab expression</a>
* that can calculate the next time it matches.
*
* <p>{@code CronExpression} instances are created through
* {@link #parse(String)}; the next match is determined with
* {@link #next(Temporal)}.
*
* @author Arjen Poutsma
* @since 5.3
* @see CronTrigger
*/
public final class CronExpression {
static final int MAX_ATTEMPTS = 366;
private static final String[] MACROS = new String[] {
"@yearly", "0 0 0 1 1 *",
"@annually", "0 0 0 1 1 *",
"@monthly", "0 0 0 1 * *",
"@weekly", "0 0 0 * * 0",
"@daily", "0 0 0 * * *",
"@midnight", "0 0 0 * * *",
"@hourly", "0 0 * * * *"
};
private final CronField[] fields;
private final String expression;
private CronExpression(
CronField seconds,
CronField minutes,
CronField hours,
CronField daysOfMonth,
CronField months,
CronField daysOfWeek,
String expression) {
// to make sure we end up at 0 nanos, we add an extra field
this.fields = new CronField[]{CronField.zeroNanos(), seconds, minutes, hours, daysOfMonth, months, daysOfWeek};
this.expression = expression;
}
Parse the given
crontab expression string into a CronExpression
. The string has six single space-separated time and date fields: ┌───────────── second (0-59)
│ ┌───────────── minute (0 - 59)
│ │ ┌───────────── hour (0 - 23)
│ │ │ ┌───────────── day of the month (1 - 31)
│ │ │ │ ┌───────────── month (1 - 12) (or JAN-DEC)
│ │ │ │ │ ┌───────────── day of the week (0 - 7)
│ │ │ │ │ │ (0 or 7 is Sunday, or MON-SUN)
│ │ │ │ │ │
* * * * * *
The following rules apply:
- A field may be an asterisk (
*
), which always stands for "first-last". For the "day of the month" or "day of the week" fields, a question mark (?
) may be used instead of an asterisk.
- Ranges of numbers are expressed by two numbers separated with a hyphen (
-
). The specified range is inclusive.
- Following a range (or
*
) with /n
specifies the interval of the number's value through the range.
-
English names can also be used for the "month" and "day of week" fields.
Use the first three letters of the particular day or month (case does not
matter).
- The "day of month" and "day of week" fields can contain a
L
-character, which stands for "last", and has a different meaning in each field:
- In the "day of month" field,
L
stands for "the last day of the month". If followed by an negative offset (i.e. L-n
), it means "n
th-to-last day of the month". If followed by W
(i.e. LW
), it means "the last weekday of the month".
- In the "day of week" field,
L
stands for "the last day of the week". If prefixed by a number or three-letter name (i.e. dL
or DDDL
), it means "the last day of week d
(or DDD
) in the month".
- The "day of month" field can be
nW
, which stands for "the nearest weekday to day of the month n
". If n
falls on Saturday, this yields the Friday before it. If n
falls on Sunday, this yields the Monday after, which also happens if n
is 1
and falls on a Saturday (i.e. 1W
stands for "the first weekday of the month").
- The "day of week" field can be
d#n
(or DDD#n
), which stands for "the n
-th day of week d
(or DDD
) in the month".
Example expressions:
"0 0 * * * *"
= the top of every hour of every day.
"*/10 * * * * *"
= every ten seconds.
"0 0 8-10 * * *"
= 8, 9 and 10 o'clock of every day.
"0 0 6,19 * * *"
= 6:00 AM and 7:00 PM every day.
"0 0/30 8-10 * * *"
= 8:00, 8:30, 9:00, 9:30, 10:00 and 10:30 every day.
"0 0 9-17 * * MON-FRI"
= on the hour nine-to-five weekdays
"0 0 0 25 12 ?"
= every Christmas Day at midnight
"0 0 0 L * *"
= last day of the month at midnight
"0 0 0 L-3 * *"
= third-to-last day of the month at midnight
"0 0 0 1W * *"
= first weekday of the month at midnight
"0 0 0 LW * *"
= last weekday of the month at midnight
"0 0 0 * * 5L"
= last Friday of the month at midnight
"0 0 0 * * THUL"
= last Thursday of the month at midnight
"0 0 0 ? * 5#2"
= the second Friday in the month at midnight
"0 0 0 ? * MON#1"
= the first Monday in the month at midnight
The following macros are also supported:
"@yearly"
(or "@annually"
) to run un once a year, i.e. "0 0 0 1 1 *"
,
"@monthly"
to run once a month, i.e. "0 0 0 1 * *"
,
"@weekly"
to run once a week, i.e. "0 0 0 * * 0"
,
"@daily"
(or "@midnight"
) to run once a day, i.e. "0 0 0 * * *"
,
"@hourly"
to run once an hour, i.e. "0 0 * * * *"
.
Params: - expression – the expression string to parse
Throws: - IllegalArgumentException – in the expression does not conform to
the cron format
Returns: the parsed CronExpression
object
/**
* Parse the given
* <a href="https://www.manpagez.com/man/5/crontab/">crontab expression</a>
* string into a {@code CronExpression}.
* The string has six single space-separated time and date fields:
* <pre>
* ┌───────────── second (0-59)
* │ ┌───────────── minute (0 - 59)
* │ │ ┌───────────── hour (0 - 23)
* │ │ │ ┌───────────── day of the month (1 - 31)
* │ │ │ │ ┌───────────── month (1 - 12) (or JAN-DEC)
* │ │ │ │ │ ┌───────────── day of the week (0 - 7)
* │ │ │ │ │ │ (0 or 7 is Sunday, or MON-SUN)
* │ │ │ │ │ │
* * * * * * *
* </pre>
*
* <p>The following rules apply:
* <ul>
* <li>
* A field may be an asterisk ({@code *}), which always stands for
* "first-last". For the "day of the month" or "day of the week" fields, a
* question mark ({@code ?}) may be used instead of an asterisk.
* </li>
* <li>
* Ranges of numbers are expressed by two numbers separated with a hyphen
* ({@code -}). The specified range is inclusive.
* </li>
* <li>Following a range (or {@code *}) with {@code /n} specifies
* the interval of the number's value through the range.
* </li>
* <li>
* English names can also be used for the "month" and "day of week" fields.
* Use the first three letters of the particular day or month (case does not
* matter).
* </li>
* <li>
* The "day of month" and "day of week" fields can contain a
* {@code L}-character, which stands for "last", and has a different meaning
* in each field:
* <ul>
* <li>
* In the "day of month" field, {@code L} stands for "the last day of the
* month". If followed by an negative offset (i.e. {@code L-n}), it means
* "{@code n}th-to-last day of the month". If followed by {@code W} (i.e.
* {@code LW}), it means "the last weekday of the month".
* </li>
* <li>
* In the "day of week" field, {@code L} stands for "the last day of the
* week".
* If prefixed by a number or three-letter name (i.e. {@code dL} or
* {@code DDDL}), it means "the last day of week {@code d} (or {@code DDD})
* in the month".
* </li>
* </ul>
* </li>
* <li>
* The "day of month" field can be {@code nW}, which stands for "the nearest
* weekday to day of the month {@code n}".
* If {@code n} falls on Saturday, this yields the Friday before it.
* If {@code n} falls on Sunday, this yields the Monday after,
* which also happens if {@code n} is {@code 1} and falls on a Saturday
* (i.e. {@code 1W} stands for "the first weekday of the month").
* </li>
* <li>
* The "day of week" field can be {@code d#n} (or {@code DDD#n}), which
* stands for "the {@code n}-th day of week {@code d} (or {@code DDD}) in
* the month".
* </li>
* </ul>
*
* <p>Example expressions:
* <ul>
* <li>{@code "0 0 * * * *"} = the top of every hour of every day.</li>
* <li><code>"*/10 * * * * *"</code> = every ten seconds.</li>
* <li>{@code "0 0 8-10 * * *"} = 8, 9 and 10 o'clock of every day.</li>
* <li>{@code "0 0 6,19 * * *"} = 6:00 AM and 7:00 PM every day.</li>
* <li>{@code "0 0/30 8-10 * * *"} = 8:00, 8:30, 9:00, 9:30, 10:00 and 10:30 every day.</li>
* <li>{@code "0 0 9-17 * * MON-FRI"} = on the hour nine-to-five weekdays</li>
* <li>{@code "0 0 0 25 12 ?"} = every Christmas Day at midnight</li>
* <li>{@code "0 0 0 L * *"} = last day of the month at midnight</li>
* <li>{@code "0 0 0 L-3 * *"} = third-to-last day of the month at midnight</li>
* <li>{@code "0 0 0 1W * *"} = first weekday of the month at midnight</li>
* <li>{@code "0 0 0 LW * *"} = last weekday of the month at midnight</li>
* <li>{@code "0 0 0 * * 5L"} = last Friday of the month at midnight</li>
* <li>{@code "0 0 0 * * THUL"} = last Thursday of the month at midnight</li>
* <li>{@code "0 0 0 ? * 5#2"} = the second Friday in the month at midnight</li>
* <li>{@code "0 0 0 ? * MON#1"} = the first Monday in the month at midnight</li>
* </ul>
*
* <p>The following macros are also supported:
* <ul>
* <li>{@code "@yearly"} (or {@code "@annually"}) to run un once a year, i.e. {@code "0 0 0 1 1 *"},</li>
* <li>{@code "@monthly"} to run once a month, i.e. {@code "0 0 0 1 * *"},</li>
* <li>{@code "@weekly"} to run once a week, i.e. {@code "0 0 0 * * 0"},</li>
* <li>{@code "@daily"} (or {@code "@midnight"}) to run once a day, i.e. {@code "0 0 0 * * *"},</li>
* <li>{@code "@hourly"} to run once an hour, i.e. {@code "0 0 * * * *"}.</li>
* </ul>
*
* @param expression the expression string to parse
* @return the parsed {@code CronExpression} object
* @throws IllegalArgumentException in the expression does not conform to
* the cron format
*/
public static CronExpression parse(String expression) {
Assert.hasLength(expression, "Expression string must not be empty");
expression = resolveMacros(expression);
String[] fields = StringUtils.tokenizeToStringArray(expression, " ");
if (fields.length != 6) {
throw new IllegalArgumentException(String.format(
"Cron expression must consist of 6 fields (found %d in \"%s\")", fields.length, expression));
}
try {
CronField seconds = CronField.parseSeconds(fields[0]);
CronField minutes = CronField.parseMinutes(fields[1]);
CronField hours = CronField.parseHours(fields[2]);
CronField daysOfMonth = CronField.parseDaysOfMonth(fields[3]);
CronField months = CronField.parseMonth(fields[4]);
CronField daysOfWeek = CronField.parseDaysOfWeek(fields[5]);
return new CronExpression(seconds, minutes, hours, daysOfMonth, months, daysOfWeek, expression);
}
catch (IllegalArgumentException ex) {
String msg = ex.getMessage() + " in cron expression \"" + expression + "\"";
throw new IllegalArgumentException(msg, ex);
}
}
private static String resolveMacros(String expression) {
expression = expression.trim();
for (int i = 0; i < MACROS.length; i = i + 2) {
if (MACROS[i].equalsIgnoreCase(expression)) {
return MACROS[i + 1];
}
}
return expression;
}
Calculate the next Temporal
that matches this expression. Params: - temporal – the seed value
Type parameters: - <T> – the type of temporal
Returns: the next temporal that matches this expression, or null
if no such temporal can be found
/**
* Calculate the next {@link Temporal} that matches this expression.
* @param temporal the seed value
* @param <T> the type of temporal
* @return the next temporal that matches this expression, or {@code null}
* if no such temporal can be found
*/
@Nullable
public <T extends Temporal & Comparable<? super T>> T next(T temporal) {
return nextOrSame(ChronoUnit.NANOS.addTo(temporal, 1));
}
@Nullable
private <T extends Temporal & Comparable<? super T>> T nextOrSame(T temporal) {
for (int i = 0; i < MAX_ATTEMPTS; i++) {
T result = nextOrSameInternal(temporal);
if (result == null || result.equals(temporal)) {
return result;
}
temporal = result;
}
return null;
}
@Nullable
private <T extends Temporal & Comparable<? super T>> T nextOrSameInternal(T temporal) {
for (CronField field : this.fields) {
temporal = field.nextOrSame(temporal);
if (temporal == null) {
return null;
}
}
return temporal;
}
@Override
public int hashCode() {
return Arrays.hashCode(this.fields);
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o instanceof CronExpression) {
CronExpression other = (CronExpression) o;
return Arrays.equals(this.fields, other.fields);
}
else {
return false;
}
}
Return the expression string used to create this CronExpression
. Returns: the expression string
/**
* Return the expression string used to create this {@code CronExpression}.
* @return the expression string
*/
@Override
public String toString() {
return this.expression;
}
}