All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.springframework.scheduling.support.CronExpression Maven / Gradle / Ivy

/*
 * Copyright 2002-2021 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.
 *
 * 

{@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) { // reverse order, to make big changes first // to make sure we end up at 0 nanos, we add an extra field this.fields = new CronField[]{daysOfWeek, months, daysOfMonth, hours, minutes, seconds, CronField.zeroNanos()}; this.expression = expression; } /** * Parse the given * crontab expression * string into a {@code 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 ({@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. *
  • *
  • * Ranges of numbers are expressed by two numbers separated with a hyphen * ({@code -}). The specified range is inclusive. *
  • *
  • Following a range (or {@code *}) with {@code /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 * {@code L}-character, which stands for "last", and has a different meaning * in each field: *
      *
    • * 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". *
    • *
    • * 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". *
    • *
    *
  • *
  • * 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"). *
  • *
  • * 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". *
  • *
* *

Example expressions: *

    *
  • {@code "0 0 * * * *"} = the top of every hour of every day.
  • *
  • "*/10 * * * * *" = every ten seconds.
  • *
  • {@code "0 0 8-10 * * *"} = 8, 9 and 10 o'clock of every day.
  • *
  • {@code "0 0 6,19 * * *"} = 6:00 AM and 7:00 PM every day.
  • *
  • {@code "0 0/30 8-10 * * *"} = 8:00, 8:30, 9:00, 9:30, 10:00 and 10:30 every day.
  • *
  • {@code "0 0 9-17 * * MON-FRI"} = on the hour nine-to-five weekdays
  • *
  • {@code "0 0 0 25 12 ?"} = every Christmas Day at midnight
  • *
  • {@code "0 0 0 L * *"} = last day of the month at midnight
  • *
  • {@code "0 0 0 L-3 * *"} = third-to-last day of the month at midnight
  • *
  • {@code "0 0 0 1W * *"} = first weekday of the month at midnight
  • *
  • {@code "0 0 0 LW * *"} = last weekday of the month at midnight
  • *
  • {@code "0 0 0 * * 5L"} = last Friday of the month at midnight
  • *
  • {@code "0 0 0 * * THUL"} = last Thursday of the month at midnight
  • *
  • {@code "0 0 0 ? * 5#2"} = the second Friday in the month at midnight
  • *
  • {@code "0 0 0 ? * MON#1"} = the first Monday in the month at midnight
  • *
* *

The following macros are also supported: *

    *
  • {@code "@yearly"} (or {@code "@annually"}) to run un once a year, i.e. {@code "0 0 0 1 1 *"},
  • *
  • {@code "@monthly"} to run once a month, i.e. {@code "0 0 0 1 * *"},
  • *
  • {@code "@weekly"} to run once a week, i.e. {@code "0 0 0 * * 0"},
  • *
  • {@code "@daily"} (or {@code "@midnight"}) to run once a day, i.e. {@code "0 0 0 * * *"},
  • *
  • {@code "@hourly"} to run once an hour, i.e. {@code "0 0 * * * *"}.
  • *
* @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); } } /** * Determine whether the given string represents a valid cron expression. * @param expression the expression to evaluate * @return {@code true} if the given expression is a valid cron expression * @since 5.3.8 */ public static boolean isValidExpression(@Nullable String expression) { if (expression == null) { return false; } try { parse(expression); return true; } catch (IllegalArgumentException ex) { return false; } } 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 {@link Temporal} that matches this expression. * @param temporal the seed value * @param 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 next(T temporal) { return nextOrSame(ChronoUnit.NANOS.addTo(temporal, 1)); } @Nullable private > 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 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 other) { return Arrays.equals(this.fields, other.fields); } else { return false; } } /** * Return the expression string used to create this {@code CronExpression}. * @return the expression string */ @Override public String toString() { return this.expression; } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy