com.cronutils.model.time.ExecutionTime.java Source code

Java tutorial

Introduction

Here is the source code for com.cronutils.model.time.ExecutionTime.java

Source

package com.cronutils.model.time;

import com.cronutils.mapper.WeekDay;
import com.cronutils.model.Cron;
import com.cronutils.model.definition.CronDefinition;
import com.cronutils.model.field.CronField;
import com.cronutils.model.field.CronFieldName;
import com.cronutils.model.field.definition.DayOfWeekFieldDefinition;
import com.cronutils.model.field.expression.Always;
import com.cronutils.model.field.expression.QuestionMark;
import com.cronutils.model.time.generator.FieldValueGenerator;
import com.cronutils.model.time.generator.FieldValueGeneratorFactory;
import com.cronutils.model.time.generator.NoSuchValueException;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;

import org.apache.commons.lang3.Validate;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.joda.time.Duration;
import org.joda.time.Interval;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;

import static com.cronutils.model.field.CronFieldName.DAY_OF_WEEK;
import static com.cronutils.model.field.value.SpecialChar.QUESTION_MARK;

/*
 * Copyright 2014 jmrozanec
 * 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.
 */

/**
 * Calculates execution time given a cron pattern
 */
public class ExecutionTime {
    private static final Logger log = LoggerFactory.getLogger(ExecutionTime.class);

    private CronDefinition cronDefinition;
    private FieldValueGenerator yearsValueGenerator;
    private CronField daysOfWeekCronField;
    private CronField daysOfMonthCronField;

    private TimeNode months;
    private TimeNode hours;
    private TimeNode minutes;
    private TimeNode seconds;

    @VisibleForTesting
    ExecutionTime(CronDefinition cronDefinition, FieldValueGenerator yearsValueGenerator,
            CronField daysOfWeekCronField, CronField daysOfMonthCronField, TimeNode months, TimeNode hours,
            TimeNode minutes, TimeNode seconds) {
        this.cronDefinition = Validate.notNull(cronDefinition);
        this.yearsValueGenerator = Validate.notNull(yearsValueGenerator);
        this.daysOfWeekCronField = Validate.notNull(daysOfWeekCronField);
        this.daysOfMonthCronField = Validate.notNull(daysOfMonthCronField);
        this.months = Validate.notNull(months);
        this.hours = Validate.notNull(hours);
        this.minutes = Validate.notNull(minutes);
        this.seconds = Validate.notNull(seconds);
    }

    /**
     * Creates execution time for given Cron
     * @param cron - Cron instance
     * @return ExecutionTime instance
     */
    public static ExecutionTime forCron(Cron cron) {
        Map<CronFieldName, CronField> fields = cron.retrieveFieldsAsMap();
        ExecutionTimeBuilder executionTimeBuilder = new ExecutionTimeBuilder(cron.getCronDefinition());
        for (CronFieldName name : CronFieldName.values()) {
            if (fields.get(name) != null) {
                switch (name) {
                case SECOND:
                    executionTimeBuilder.forSecondsMatching(fields.get(name));
                    break;
                case MINUTE:
                    executionTimeBuilder.forMinutesMatching(fields.get(name));
                    break;
                case HOUR:
                    executionTimeBuilder.forHoursMatching(fields.get(name));
                    break;
                case DAY_OF_WEEK:
                    executionTimeBuilder.forDaysOfWeekMatching(fields.get(name));
                    break;
                case DAY_OF_MONTH:
                    executionTimeBuilder.forDaysOfMonthMatching(fields.get(name));
                    break;
                case MONTH:
                    executionTimeBuilder.forMonthsMatching(fields.get(name));
                    break;
                case YEAR:
                    executionTimeBuilder.forYearsMatching(fields.get(name));
                    break;
                }
            }
        }
        return executionTimeBuilder.build();
    }

    /**
     * Provide nearest date for next execution.
     * @param date - jodatime DateTime instance. If null, a NullPointerException will be raised.
     * @return DateTime instance, never null. Next execution time.
     */
    public DateTime nextExecution(DateTime date) {
        Validate.notNull(date);
        try {
            DateTime nextMatch = nextClosestMatch(date);
            if (nextMatch.equals(date)) {
                nextMatch = nextClosestMatch(date.plusSeconds(1));
            }
            return nextMatch;
        } catch (NoSuchValueException e) {
            throw new IllegalArgumentException(e);
        }
    }

    /**
     * If date is not match, will return next closest match.
     * If date is match, will return this date.
     * @param date - reference DateTime instance - never null;
     * @return DateTime instance, never null. Value obeys logic specified above.
     * @throws NoSuchValueException
     */
    DateTime nextClosestMatch(DateTime date) throws NoSuchValueException {
        List<Integer> year = yearsValueGenerator.generateCandidates(date.getYear(), date.getYear());
        TimeNode days = null;
        int lowestMonth = months.getValues().get(0);
        int lowestHour = hours.getValues().get(0);
        int lowestMinute = minutes.getValues().get(0);
        int lowestSecond = seconds.getValues().get(0);

        NearestValue nearestValue;
        DateTime newDate;
        if (year.isEmpty()) {
            int newYear = yearsValueGenerator.generateNextValue(date.getYear());
            days = generateDays(cronDefinition, new DateTime(newYear, lowestMonth, 1, 0, 0));
            return initDateTime(yearsValueGenerator.generateNextValue(date.getYear()), lowestMonth,
                    days.getValues().get(0), lowestHour, lowestMinute, lowestSecond, date.getZone());
        }
        if (!months.getValues().contains(date.getMonthOfYear())) {
            nearestValue = months.getNextValue(date.getMonthOfYear(), 0);
            int nextMonths = nearestValue.getValue();
            if (nearestValue.getShifts() > 0) {
                newDate = new DateTime(date.getYear(), 1, 1, 0, 0, 0, date.getZone())
                        .plusYears(nearestValue.getShifts());
                return nextClosestMatch(newDate);
            }
            if (nearestValue.getValue() < date.getMonthOfYear()) {
                date = date.plusYears(1);
            }
            days = generateDays(cronDefinition, new DateTime(date.getYear(), nextMonths, 1, 0, 0));
            return initDateTime(date.getYear(), nextMonths, days.getValues().get(0), lowestHour, lowestMinute,
                    lowestSecond, date.getZone());
        }
        days = generateDays(cronDefinition, date);
        if (!days.getValues().contains(date.getDayOfMonth())) {
            nearestValue = days.getNextValue(date.getDayOfMonth(), 0);
            if (nearestValue.getShifts() > 0) {
                newDate = new DateTime(date.getYear(), date.getMonthOfYear(), 1, 0, 0, 0, date.getZone())
                        .plusMonths(nearestValue.getShifts());
                return nextClosestMatch(newDate);
            }
            if (nearestValue.getValue() < date.getDayOfMonth()) {
                date = date.plusMonths(1);
            }
            return initDateTime(date.getYear(), date.getMonthOfYear(), nearestValue.getValue(), lowestHour,
                    lowestMinute, lowestSecond, date.getZone());
        }
        if (!hours.getValues().contains(date.getHourOfDay())) {
            nearestValue = hours.getNextValue(date.getHourOfDay(), 0);
            int nextHours = nearestValue.getValue();
            if (nearestValue.getShifts() > 0) {
                newDate = new DateTime(date.getYear(), date.getMonthOfYear(), date.getDayOfMonth(), 0, 0, 0,
                        date.getZone()).plusDays(nearestValue.getShifts());
                return nextClosestMatch(newDate);
            }
            if (nearestValue.getValue() < date.getHourOfDay()) {
                date = date.plusDays(1);
            }
            return initDateTime(date.getYear(), date.getMonthOfYear(), date.getDayOfMonth(), nextHours,
                    lowestMinute, lowestSecond, date.getZone());
        }
        if (!minutes.getValues().contains(date.getMinuteOfHour())) {
            nearestValue = minutes.getNextValue(date.getMinuteOfHour(), 0);
            int nextMinutes = nearestValue.getValue();
            if (nearestValue.getShifts() > 0) {
                newDate = new DateTime(date.getYear(), date.getMonthOfYear(), date.getDayOfMonth(),
                        date.getHourOfDay(), 0, 0, date.getZone()).plusHours(nearestValue.getShifts());
                return nextClosestMatch(newDate);
            }
            if (nearestValue.getValue() < date.getMinuteOfHour()) {
                date = date.plusHours(1);
            }
            return initDateTime(date.getYear(), date.getMonthOfYear(), date.getDayOfMonth(), date.getHourOfDay(),
                    nextMinutes, lowestSecond, date.getZone());
        }
        if (!seconds.getValues().contains(date.getSecondOfMinute())) {
            nearestValue = seconds.getNextValue(date.getSecondOfMinute(), 0);
            int nextSeconds = nearestValue.getValue();
            if (nearestValue.getShifts() > 0) {
                newDate = new DateTime(date.getYear(), date.getMonthOfYear(), date.getDayOfMonth(),
                        date.getHourOfDay(), date.getMinuteOfHour(), 0, date.getZone())
                                .plusMinutes(nearestValue.getShifts());
                return nextClosestMatch(newDate);
            }
            if (nearestValue.getValue() < date.getSecondOfMinute()) {
                date = date.plusMinutes(1);
            }
            return initDateTime(date.getYear(), date.getMonthOfYear(), date.getDayOfMonth(), date.getHourOfDay(),
                    date.getMinuteOfHour(), nextSeconds, date.getZone());
        }
        return date;
    }

    /**
     * If date is not match, will return previous closest match.
     * If date is match, will return this date.
     * @param date - reference DateTime instance - never null;
     * @return DateTime instance, never null. Value obeys logic specified above.
     * @throws NoSuchValueException
     */
    DateTime previousClosestMatch(DateTime date) throws NoSuchValueException {
        List<Integer> year = yearsValueGenerator.generateCandidates(date.getYear(), date.getYear());
        TimeNode days = generateDays(cronDefinition, date);
        int highestMonth = months.getValues().get(months.getValues().size() - 1);
        int highestDay = days.getValues().get(days.getValues().size() - 1);
        int highestHour = hours.getValues().get(hours.getValues().size() - 1);
        int highestMinute = minutes.getValues().get(minutes.getValues().size() - 1);
        int highestSecond = seconds.getValues().get(seconds.getValues().size() - 1);

        NearestValue nearestValue;
        DateTime newDate;
        if (year.isEmpty()) {
            int previousYear = yearsValueGenerator.generatePreviousValue(date.getYear());
            if (highestDay > 28) {
                int highestDayOfMonth = new DateTime(previousYear, highestMonth, 1, 0, 0).dayOfMonth()
                        .getMaximumValue();
                if (highestDay > highestDayOfMonth) {
                    nearestValue = days.getPreviousValue(highestDay, 1);
                    if (nearestValue.getShifts() > 0) {
                        newDate = new DateTime(previousYear, highestMonth, 1, 23, 59, 59)
                                .minusMonths(nearestValue.getShifts()).dayOfMonth().withMaximumValue();
                        return previousClosestMatch(newDate);
                    } else {
                        highestDay = nearestValue.getValue();
                    }
                }
            }
            return initDateTime(previousYear, highestMonth, highestDay, highestHour, highestMinute, highestSecond,
                    date.getZone());
        }
        if (!months.getValues().contains(date.getMonthOfYear())) {
            nearestValue = months.getPreviousValue(date.getMonthOfYear(), 0);
            int previousMonths = nearestValue.getValue();
            if (nearestValue.getShifts() > 0) {
                newDate = new DateTime(date.getYear(), 12, 31, 23, 59, 59).minusYears(nearestValue.getShifts());
                return previousClosestMatch(newDate);
            }
            return initDateTime(date.getYear(), previousMonths, highestDay, highestHour, highestMinute,
                    highestSecond, date.getZone());
        }
        if (!days.getValues().contains(date.getDayOfMonth())) {
            nearestValue = days.getPreviousValue(date.getDayOfMonth(), 0);
            if (nearestValue.getShifts() > 0) {
                newDate = new DateTime(date.getYear(), date.getMonthOfYear(), 1, 23, 59, 59)
                        .minusMonths(nearestValue.getShifts()).dayOfMonth().withMaximumValue();
                return previousClosestMatch(newDate);
            }
            return initDateTime(date.getYear(), date.getMonthOfYear(), nearestValue.getValue(), highestHour,
                    highestMinute, highestSecond, date.getZone());
        }
        if (!hours.getValues().contains(date.getHourOfDay())) {
            nearestValue = hours.getPreviousValue(date.getHourOfDay(), 0);
            if (nearestValue.getShifts() > 0) {
                newDate = new DateTime(date.getYear(), date.getMonthOfYear(), date.getDayOfMonth(), 23, 59, 59)
                        .minusDays(nearestValue.getShifts());
                return previousClosestMatch(newDate);
            }
            return initDateTime(date.getYear(), date.getMonthOfYear(), date.getDayOfMonth(),
                    nearestValue.getValue(), highestMinute, highestSecond, date.getZone());
        }
        if (!minutes.getValues().contains(date.getMinuteOfHour())) {
            nearestValue = minutes.getPreviousValue(date.getMinuteOfHour(), 0);
            if (nearestValue.getShifts() > 0) {
                newDate = new DateTime(date.getYear(), date.getMonthOfYear(), date.getDayOfMonth(),
                        date.getHourOfDay(), 59, 59).minusHours(nearestValue.getShifts());
                return previousClosestMatch(newDate);
            }
            return initDateTime(date.getYear(), date.getMonthOfYear(), date.getDayOfMonth(), date.getHourOfDay(),
                    nearestValue.getValue(), highestSecond, date.getZone());
        }
        if (!seconds.getValues().contains(date.getSecondOfMinute())) {
            nearestValue = seconds.getPreviousValue(date.getSecondOfMinute(), 0);
            int previousSeconds = nearestValue.getValue();
            if (nearestValue.getShifts() > 0) {
                newDate = new DateTime(date.getYear(), date.getMonthOfYear(), date.getDayOfMonth(),
                        date.getHourOfDay(), date.getMinuteOfHour(), 59).minusMinutes(nearestValue.getShifts());
                return previousClosestMatch(newDate);
            }
            return initDateTime(date.getYear(), date.getMonthOfYear(), date.getDayOfMonth(), date.getHourOfDay(),
                    date.getMinuteOfHour(), previousSeconds, date.getZone());
        }
        return date;
    }

    TimeNode generateDays(CronDefinition cronDefinition, DateTime date) {
        boolean questionMarkSupported = cronDefinition.getFieldDefinition(DAY_OF_WEEK).getConstraints()
                .isSpecialCharAllowed(QUESTION_MARK);
        if (questionMarkSupported) {
            return new TimeNode(generateDayCandidatesQuestionMarkSupported(date.getYear(), date.getMonthOfYear(),
                    ((DayOfWeekFieldDefinition) cronDefinition.getFieldDefinition(DAY_OF_WEEK))
                            .getMondayDoWValue()));
        } else {
            return new TimeNode(generateDayCandidatesQuestionMarkNotSupported(date.getYear(), date.getMonthOfYear(),
                    ((DayOfWeekFieldDefinition) cronDefinition.getFieldDefinition(DAY_OF_WEEK))
                            .getMondayDoWValue()));
        }
    }

    /**
     * Provide nearest time for next execution.
     * @param date - jodatime DateTime instance. If null, a NullPointerException will be raised.
     * @return jodatime Duration instance, never null. Time to next execution.
     */
    public Duration timeToNextExecution(DateTime date) {
        return new Interval(date, nextExecution(date)).toDuration();
    }

    /**
     * Provide nearest date for last execution.
     * @param date - jodatime DateTime instance. If null, a NullPointerException will be raised.
     * @return DateTime instance, never null. Last execution time.
     */
    public DateTime lastExecution(DateTime date) {
        Validate.notNull(date);
        try {
            DateTime previousMatch = previousClosestMatch(date);
            if (previousMatch.equals(date)) {
                previousMatch = previousClosestMatch(date.minusSeconds(1));
            }
            return previousMatch;
        } catch (NoSuchValueException e) {
            throw new IllegalArgumentException(e);
        }
    }

    /**
     * Provide nearest time from last execution.
     * @param date - jodatime DateTime instance. If null, a NullPointerException will be raised.
     * @return jodatime Duration instance, never null. Time from last execution.
     */
    public Duration timeFromLastExecution(DateTime date) {
        return new Interval(lastExecution(date), date).toDuration();
    }

    /**
     * Provide feedback if a given date matches the cron expression.
     * @param date - jodatime DateTime instance. If null, a NullPointerException will be raised.
     * @return true if date matches cron expression requirements, false otherwise.
     */
    public boolean isMatch(DateTime date) {
        return nextExecution(lastExecution(date)).equals(date);
    }

    private List<Integer> generateDayCandidatesQuestionMarkNotSupported(int year, int month,
            WeekDay mondayDoWValue) {
        DateTime date = new DateTime(year, month, 1, 1, 1);
        Set<Integer> candidates = Sets.newHashSet();
        if (daysOfMonthCronField.getExpression() instanceof Always
                && daysOfWeekCronField.getExpression() instanceof Always) {
            candidates.addAll(FieldValueGeneratorFactory
                    .createDayOfMonthValueGeneratorInstance(daysOfMonthCronField, year, month)
                    .generateCandidates(1, date.dayOfMonth().getMaximumValue()));
        } else {
            if (daysOfMonthCronField.getExpression() instanceof Always) {
                candidates.addAll(FieldValueGeneratorFactory
                        .createDayOfWeekValueGeneratorInstance(daysOfWeekCronField, year, month, mondayDoWValue)
                        .generateCandidates(1, date.dayOfMonth().getMaximumValue()));
            } else {
                if (daysOfWeekCronField.getExpression() instanceof Always) {
                    candidates.addAll(FieldValueGeneratorFactory
                            .createDayOfMonthValueGeneratorInstance(daysOfMonthCronField, year, month)
                            .generateCandidates(1, date.dayOfMonth().getMaximumValue()));
                } else {
                    candidates.addAll(FieldValueGeneratorFactory
                            .createDayOfWeekValueGeneratorInstance(daysOfWeekCronField, year, month, mondayDoWValue)
                            .generateCandidates(1, date.dayOfMonth().getMaximumValue()));
                    candidates.addAll(FieldValueGeneratorFactory
                            .createDayOfMonthValueGeneratorInstance(daysOfMonthCronField, year, month)
                            .generateCandidates(1, date.dayOfMonth().getMaximumValue()));
                }
            }
        }
        List<Integer> candidatesList = Lists.newArrayList(candidates);
        Collections.sort(candidatesList);
        return candidatesList;
    }

    private List<Integer> generateDayCandidatesQuestionMarkSupported(int year, int month, WeekDay mondayDoWValue) {
        DateTime date = new DateTime(year, month, 1, 1, 1);
        Set<Integer> candidates = Sets.newHashSet();
        if (daysOfMonthCronField.getExpression() instanceof Always
                && daysOfWeekCronField.getExpression() instanceof Always) {
            candidates.addAll(FieldValueGeneratorFactory
                    .createDayOfMonthValueGeneratorInstance(daysOfMonthCronField, year, month)
                    .generateCandidates(1, date.dayOfMonth().getMaximumValue()));
        } else {
            if (daysOfMonthCronField.getExpression() instanceof QuestionMark) {
                // the day of week calculator must get a -1 value to indicate its generating the first value of the month
                candidates.addAll(FieldValueGeneratorFactory
                        .createDayOfWeekValueGeneratorInstance(daysOfWeekCronField, year, month, mondayDoWValue)
                        .generateCandidates(-1, date.dayOfMonth().getMaximumValue()));
            } else {
                if (daysOfWeekCronField.getExpression() instanceof QuestionMark) {
                    candidates.addAll(FieldValueGeneratorFactory
                            .createDayOfMonthValueGeneratorInstance(daysOfMonthCronField, year, month)
                            .generateCandidates(1, date.dayOfMonth().getMaximumValue()));
                } else {
                    candidates.addAll(FieldValueGeneratorFactory
                            .createDayOfWeekValueGeneratorInstance(daysOfWeekCronField, year, month, mondayDoWValue)
                            .generateCandidates(1, date.dayOfMonth().getMaximumValue()));
                    candidates.addAll(FieldValueGeneratorFactory
                            .createDayOfMonthValueGeneratorInstance(daysOfMonthCronField, year, month)
                            .generateCandidates(1, date.dayOfMonth().getMaximumValue()));
                }
            }
        }
        List<Integer> candidatesList = Lists.newArrayList(candidates);
        Collections.sort(candidatesList);
        return candidatesList;
    }

    private DateTime initDateTime(int years, int monthsOfYear, int dayOfMonth, int hoursOfDay, int minutesOfHour,
            int secondsOfMinute, DateTimeZone timeZone) {
        return new DateTime(0, 1, 1, 0, 0, 0, timeZone).plusYears(years).plusMonths(monthsOfYear - 1)
                .plusDays(dayOfMonth - 1).plusHours(hoursOfDay).plusMinutes(minutesOfHour)
                .plusSeconds(secondsOfMinute);
    }
}