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

ninja.scheduler.cron.CronExpression Maven / Gradle / Ivy

There is a newer version: 7.0.0
Show newest version
/**
 * Copyright (C) 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
 *
 *     http://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 ninja.scheduler.cron;

import org.apache.commons.lang3.StringUtils;

import java.time.DayOfWeek;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.temporal.ChronoUnit;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

/**
 * Represent a CRON expression.
 */
public class CronExpression {

    private static final Pattern REGEXP_PATTERN_RANGE = Pattern.compile("^([^-]+)-([^-/]+)(/(\\d+))?$");
    private static final Pattern REGEXP_PATTERN_LIST = Pattern.compile("^([^-/]+)(/(\\d+))?$");
    private static final Pattern REGEXP_PATTERN_SINGLE = Pattern.compile("^(\\d+)(/(\\d+))?$");

    private static final List WILDCARD_REPLACEMENT = Arrays.asList(
            "0-59",  // Second
            "0-59",  // Minute
            "0-23",  // Hour
            "1-31",  // Day of Month
            "1-12",  // Month
            "0-6");  // Day of Week

    private static final List> CRON_EXPRESSION_PART_VALIDATOR = Arrays.asList(
            (c) -> c.assertViolation(1, 60, 0, 59),  // Second
            (c) -> c.assertViolation(1, 60, 0, 59),  // Minute
            (c) -> c.assertViolation(1, 24, 0, 23),  // Hour
            (c) -> c.assertViolation(1, 32, 0, 31),  // Day of Month
            (c) -> c.assertViolation(1, 13, 0, 12),  // Month
            (c) -> c.assertViolation(1, 7, 0, 6));   // Day of Week

    private static final int IDX_SECOND = 0;
    private static final int IDX_MINUTE = 1;
    private static final int IDX_HOUR = 2;
    private static final int IDX_DAY_OF_MONTH = 3;
    private static final int IDX_MONTH = 4;
    private static final int IDX_DAY_OF_WEEK = 5;

    final Map DAY_OF_WEEK_CRON_VALUE = new HashMap() {{
        put(DayOfWeek.SUNDAY, 0);
        put(DayOfWeek.MONDAY, 1);
        put(DayOfWeek.TUESDAY, 2);
        put(DayOfWeek.WEDNESDAY, 3);
        put(DayOfWeek.THURSDAY, 4);
        put(DayOfWeek.FRIDAY, 5);
        put(DayOfWeek.SATURDAY, 6);
    }};

    private final CronExpressionPart[] cronExpressionPartArray;

    /**
     * Build a new instance.
     *
     * @param cron The CRON expression to parse
     */
    public CronExpression(final String cron) {
        // ┌───────────── second (0 - 59)
        // │ ┌───────────── minute (0 - 59)
        // │ │ ┌───────────── hour (0 - 23)
        // │ │ │ ┌───────────── day of the month (1 - 31)
        // │ │ │ │ ┌───────────── month (1 - 12)
        // │ │ │ │ │ ┌───────────── day of the week (0 - 6) (Sunday to Saturday)
        // │ │ │ │ │ │
        // │ │ │ │ │ │
        // * * * * * *


        final String[] cronArray = cron.split(StringUtils.SPACE);
        if (cronArray.length < 5) {
            throw new BadCronExpressionException("CRON expression is invalid '%s'", cron);
        }

        cronExpressionPartArray = new CronExpressionPart[6];
        for (int idx = 0; idx < cronExpressionPartArray.length; ++idx) {
            // Standardize CRON expression
            final String standardizedPart = cronArray[idx]
                    .replace("?", "*")
                    .replace("*", WILDCARD_REPLACEMENT.get(idx));

            // Parse and validate each part
            try {
                cronExpressionPartArray[idx] = parseCronExpressionPart(standardizedPart);
                if (idx < CRON_EXPRESSION_PART_VALIDATOR.size()) {
                    CRON_EXPRESSION_PART_VALIDATOR.get(idx).accept(cronExpressionPartArray[idx]);
                }
            } catch (final BadCronExpressionException ex) {
                throw new BadCronExpressionException(
                        ex,
                        "Can't use CRON '%s', error with the part #%d '%s'",
                        cron,
                        idx + 1,
                        cronArray[idx]);
            }
        }
    }

    /**
     * Retrieves the delay from now to the next match.
     *
     * @param zoneId The Zone to use for manipulating datetime
     * @return The next delay in milliseconds
     */
    public long getNextDelayMilliseconds(final ZoneId zoneId) {
        return getNextDelayMilliseconds(LocalDateTime.now(zoneId));
    }

    /**
     * Retrieves the delay from given datetime to the next match.
     *
     * @param from The datetime
     * @return The next delay in milliseconds
     */
    protected long getNextDelayMilliseconds(final LocalDateTime from) {
        LocalDateTime nextTrigger = from.withNano(0).plusSeconds(1);

        CronExpressionPart cronExpressionPart = cronExpressionPartArray[IDX_SECOND];
        while (cronExpressionPart.isNotCompliant(nextTrigger.getSecond())) {
            nextTrigger = nextTrigger.plusSeconds(1);
        }

        cronExpressionPart = cronExpressionPartArray[IDX_MINUTE];
        while (cronExpressionPart.isNotCompliant(nextTrigger.getMinute())) {
            nextTrigger = nextTrigger.plusMinutes(1);
        }

        cronExpressionPart = cronExpressionPartArray[IDX_HOUR];
        while (cronExpressionPart.isNotCompliant(nextTrigger.getHour())) {
            nextTrigger = nextTrigger.plusHours(1);
        }

        cronExpressionPart = cronExpressionPartArray[IDX_DAY_OF_MONTH];
        while (cronExpressionPart.isNotCompliant(nextTrigger.getDayOfMonth())) {
            nextTrigger = nextTrigger.plusDays(1);
        }

        cronExpressionPart = cronExpressionPartArray[IDX_MONTH];
        while (cronExpressionPart.isNotCompliant(nextTrigger.getMonthValue())) {
            nextTrigger = nextTrigger.plusMonths(1);
        }

        cronExpressionPart = cronExpressionPartArray[IDX_DAY_OF_WEEK];
        while (cronExpressionPart.isNotCompliant(DAY_OF_WEEK_CRON_VALUE.get(nextTrigger.getDayOfWeek()))) {
            nextTrigger = nextTrigger.plusDays(1);
        }

        return ChronoUnit.MILLIS.between(from, nextTrigger);
    }

    private CronExpressionPart parseCronExpressionPart(final String str) {
        CronExpressionPart cronExpressionPart = null;

        if (str.contains("-")) {
            final Matcher matcher = REGEXP_PATTERN_RANGE.matcher(str);
            if (matcher.find()) {
                final int minRange = matcher.group(1) != null ? Integer.parseInt(matcher.group(1)) : 0;
                final int maxRange = matcher.group(2) != null ? Integer.parseInt(matcher.group(2)) : minRange;
                final int stepValue = matcher.group(4) != null ? Integer.parseInt(matcher.group(4)) : -1;

                cronExpressionPart = new CronExpressionPartRange(stepValue, minRange, maxRange);
            }
        } else if (str.contains(",")) {
            final Matcher matcher = REGEXP_PATTERN_LIST.matcher(str);
            if (matcher.find()) {
                final String listValue = matcher.group(1);
                final int stepValue = matcher.group(3) != null ? Integer.parseInt(matcher.group(3)) : -1;

                cronExpressionPart = new CronExpressionPartList(
                        stepValue,
                        Arrays.stream(listValue.split(",")).map(Integer::parseInt).collect(Collectors.toList()));
            }
        } else {
            final Matcher matcher = REGEXP_PATTERN_SINGLE.matcher(str);
            if (matcher.find()) {
                final int singleValue = Integer.parseInt(matcher.group(1));
                final int stepValue = matcher.group(3) != null ? Integer.parseInt(matcher.group(3)) : -1;

                cronExpressionPart = new CronExpressionPartRange(stepValue, singleValue, singleValue);
            }
        }

        if (cronExpressionPart == null) {
            throw new BadCronExpressionException("Can't parse CRON expression part: %s", str);
        }

        return cronExpressionPart;
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy