/** * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You 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 org.apache.activemq.broker.scheduler; import java.util.ArrayList; import java.util.Calendar; import java.util.Collections; import java.util.List; import java.util.StringTokenizer; import javax.jms.MessageFormatException; public class CronParser { private static final int NUMBER_TOKENS = 5; private static final int MINUTES = 0; private static final int HOURS = 1; private static final int DAY_OF_MONTH = 2; private static final int MONTH = 3; private static final int DAY_OF_WEEK = 4; public static long getNextScheduledTime(final String cronEntry, long currentTime) throws MessageFormatException { long result = 0; if (cronEntry == null || cronEntry.length() == 0) { return result; } // Handle the once per minute case "* * * * *" // starting the next event at the top of the minute. if (cronEntry.equals("* * * * *")) { result = currentTime + 60 * 1000; result = result / 60000 * 60000; return result; } List<String> list = tokenize(cronEntry); List<CronEntry> entries = buildCronEntries(list); Calendar working = Calendar.getInstance(); working.setTimeInMillis(currentTime); working.set(Calendar.SECOND, 0); CronEntry minutes = entries.get(MINUTES); CronEntry hours = entries.get(HOURS); CronEntry dayOfMonth = entries.get(DAY_OF_MONTH); CronEntry month = entries.get(MONTH); CronEntry dayOfWeek = entries.get(DAY_OF_WEEK); // Start at the top of the next minute, cron is only guaranteed to be // run on the minute. int timeToNextMinute = 60 - working.get(Calendar.SECOND); working.add(Calendar.SECOND, timeToNextMinute); // If its already to late in the day this will roll us over to tomorrow // so we'll need to check again when done updating month and day. int currentMinutes = working.get(Calendar.MINUTE); if (!isCurrent(minutes, currentMinutes)) { int nextMinutes = getNext(minutes, currentMinutes); working.add(Calendar.MINUTE, nextMinutes); } int currentHours = working.get(Calendar.HOUR_OF_DAY); if (!isCurrent(hours, currentHours)) { int nextHour = getNext(hours, currentHours); working.add(Calendar.HOUR_OF_DAY, nextHour); } // We can roll into the next month here which might violate the cron setting // rules so we check once then recheck again after applying the month settings. doUpdateCurrentDay(working, dayOfMonth, dayOfWeek); // Start by checking if we are in the right month, if not then calculations // need to start from the beginning of the month to ensure that we don't end // up on the wrong day. (Can happen when DAY_OF_WEEK is set and current time // is ahead of the day of the week to execute on). doUpdateCurrentMonth(working, month); // Now Check day of week and day of month together since they can be specified // together in one entry, if both "day of month" and "day of week" are restricted // (not "*"), then either the "day of month" field (3) or the "day of week" field // (5) must match the current day or the Calenday must be advanced. doUpdateCurrentDay(working, dayOfMonth, dayOfWeek); // Now we can chose the correct hour and minute of the day in question. currentHours = working.get(Calendar.HOUR_OF_DAY); if (!isCurrent(hours, currentHours)) { int nextHour = getNext(hours, currentHours); working.add(Calendar.HOUR_OF_DAY, nextHour); } currentMinutes = working.get(Calendar.MINUTE); if (!isCurrent(minutes, currentMinutes)) { int nextMinutes = getNext(minutes, currentMinutes); working.add(Calendar.MINUTE, nextMinutes); } result = working.getTimeInMillis(); if (result <= currentTime) { throw new ArithmeticException("Unable to compute next scheduled exection time."); } return result; } protected static long doUpdateCurrentMonth(Calendar working, CronEntry month) throws MessageFormatException { int currentMonth = working.get(Calendar.MONTH) + 1; if (!isCurrent(month, currentMonth)) { int nextMonth = getNext(month, currentMonth); working.add(Calendar.MONTH, nextMonth); // Reset to start of month. resetToStartOfDay(working, 1); return working.getTimeInMillis(); } return 0L; } protected static long doUpdateCurrentDay(Calendar working, CronEntry dayOfMonth, CronEntry dayOfWeek) throws MessageFormatException { int currentDayOfWeek = working.get(Calendar.DAY_OF_WEEK) - 1; int currentDayOfMonth = working.get(Calendar.DAY_OF_MONTH); // Simplest case, both are unrestricted or both match today otherwise // result must be the closer of the two if both are set, or the next // match to the one that is. if (!isCurrent(dayOfWeek, currentDayOfWeek) || !isCurrent(dayOfMonth, currentDayOfMonth) ) { int nextWeekDay = Integer.MAX_VALUE; int nextCalendarDay = Integer.MAX_VALUE; if (!isCurrent(dayOfWeek, currentDayOfWeek)) { nextWeekDay = getNext(dayOfWeek, currentDayOfWeek); } if (!isCurrent(dayOfMonth, currentDayOfMonth)) { nextCalendarDay = getNext(dayOfMonth, currentDayOfMonth); } if( nextWeekDay < nextCalendarDay ) { working.add(Calendar.DAY_OF_WEEK, nextWeekDay); } else { working.add(Calendar.DAY_OF_MONTH, nextCalendarDay); } // Since the day changed, we restart the clock at the start of the day // so that the next time will either be at 12am + value of hours and // minutes pattern. resetToStartOfDay(working, working.get(Calendar.DAY_OF_MONTH)); return working.getTimeInMillis(); } return 0L; } public static void validate(final String cronEntry) throws MessageFormatException { List<String> list = tokenize(cronEntry); List<CronEntry> entries = buildCronEntries(list); for (CronEntry e : entries) { validate(e); } } static void validate(final CronEntry entry) throws MessageFormatException { List<Integer> list = entry.currentWhen; if (list.isEmpty() || list.get(0).intValue() < entry.start || list.get(list.size() - 1).intValue() > entry.end) { throw new MessageFormatException("Invalid token: " + entry); } } static int getNext(final CronEntry entry, final int current) throws MessageFormatException { int result = 0; if (entry.currentWhen == null) { entry.currentWhen = calculateValues(entry); } List<Integer> list = entry.currentWhen; int next = -1; for (Integer i : list) { if (i.intValue() > current) { next = i.intValue(); break; } } if (next != -1) { result = next - current; } else { int first = list.get(0).intValue(); result = entry.end + first - entry.start - current; // Account for difference of one vs zero based indices. if (entry.name.equals("DayOfWeek") || entry.name.equals("Month")) { result++; } } return result; } static boolean isCurrent(final CronEntry entry, final int current) throws MessageFormatException { boolean result = entry.currentWhen.contains(new Integer(current)); return result; } protected static void resetToStartOfDay(Calendar target, int day) { target.set(Calendar.DAY_OF_MONTH, day); target.set(Calendar.HOUR_OF_DAY, 0); target.set(Calendar.MINUTE, 0); target.set(Calendar.SECOND, 0); } static List<String> tokenize(String cron) throws IllegalArgumentException { StringTokenizer tokenize = new StringTokenizer(cron); List<String> result = new ArrayList<String>(); while (tokenize.hasMoreTokens()) { result.add(tokenize.nextToken()); } if (result.size() != NUMBER_TOKENS) { throw new IllegalArgumentException("Not a valid cron entry - wrong number of tokens(" + result.size() + "): " + cron); } return result; } protected static List<Integer> calculateValues(final CronEntry entry) { List<Integer> result = new ArrayList<Integer>(); if (isAll(entry.token)) { for (int i = entry.start; i <= entry.end; i++) { result.add(i); } } else if (isAStep(entry.token)) { int denominator = getDenominator(entry.token); String numerator = getNumerator(entry.token); CronEntry ce = new CronEntry(entry.name, numerator, entry.start, entry.end); List<Integer> list = calculateValues(ce); for (Integer i : list) { if (i.intValue() % denominator == 0) { result.add(i); } } } else if (isAList(entry.token)) { StringTokenizer tokenizer = new StringTokenizer(entry.token, ","); while (tokenizer.hasMoreTokens()) { String str = tokenizer.nextToken(); CronEntry ce = new CronEntry(entry.name, str, entry.start, entry.end); List<Integer> list = calculateValues(ce); result.addAll(list); } } else if (isARange(entry.token)) { int index = entry.token.indexOf('-'); int first = Integer.parseInt(entry.token.substring(0, index)); int last = Integer.parseInt(entry.token.substring(index + 1)); for (int i = first; i <= last; i++) { result.add(i); } } else { int value = Integer.parseInt(entry.token); result.add(value); } Collections.sort(result); return result; } protected static boolean isARange(String token) { return token != null && token.indexOf('-') >= 0; } protected static boolean isAStep(String token) { return token != null && token.indexOf('/') >= 0; } protected static boolean isAList(String token) { return token != null && token.indexOf(',') >= 0; } protected static boolean isAll(String token) { return token != null && token.length() == 1 && (token.charAt(0) == '*' || token.charAt(0) == '?'); } protected static int getDenominator(final String token) { int result = 0; int index = token.indexOf('/'); String str = token.substring(index + 1); result = Integer.parseInt(str); return result; } protected static String getNumerator(final String token) { int index = token.indexOf('/'); String str = token.substring(0, index); return str; } static List<CronEntry> buildCronEntries(List<String> tokens) { List<CronEntry> result = new ArrayList<CronEntry>(); CronEntry minutes = new CronEntry("Minutes", tokens.get(MINUTES), 0, 60); minutes.currentWhen = calculateValues(minutes); result.add(minutes); CronEntry hours = new CronEntry("Hours", tokens.get(HOURS), 0, 24); hours.currentWhen = calculateValues(hours); result.add(hours); CronEntry dayOfMonth = new CronEntry("DayOfMonth", tokens.get(DAY_OF_MONTH), 1, 32); dayOfMonth.currentWhen = calculateValues(dayOfMonth); result.add(dayOfMonth); CronEntry month = new CronEntry("Month", tokens.get(MONTH), 1, 12); month.currentWhen = calculateValues(month); result.add(month); CronEntry dayOfWeek = new CronEntry("DayOfWeek", tokens.get(DAY_OF_WEEK), 0, 6); dayOfWeek.currentWhen = calculateValues(dayOfWeek); result.add(dayOfWeek); return result; } static class CronEntry { final String name; final String token; final int start; final int end; List<Integer> currentWhen; CronEntry(String name, String token, int start, int end) { this.name = name; this.token = token; this.start = start; this.end = end; } @Override public String toString() { return this.name + ":" + token; } } }