001 /** 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017 package org.apache.activemq.broker.scheduler; 018 019 import java.util.ArrayList; 020 import java.util.Calendar; 021 import java.util.Collections; 022 import java.util.List; 023 import java.util.StringTokenizer; 024 import javax.jms.MessageFormatException; 025 026 public class CronParser { 027 028 private static final int NUMBER_TOKENS = 5; 029 private static final int MINUTES = 0; 030 private static final int HOURS = 1; 031 private static final int DAY_OF_MONTH = 2; 032 private static final int MONTH = 3; 033 private static final int DAY_OF_WEEK = 4; 034 035 public static long getNextScheduledTime(final String cronEntry, long currentTime) throws MessageFormatException { 036 037 long result = 0; 038 039 if (cronEntry == null || cronEntry.length() == 0) { 040 return result; 041 } 042 043 // Handle the once per minute case "* * * * *" 044 // starting the next event at the top of the minute. 045 if (cronEntry.startsWith("* * * * *")) { 046 result = currentTime + 60 * 1000; 047 result = result / 1000 * 1000; 048 return result; 049 } 050 051 List<String> list = tokenize(cronEntry); 052 List<CronEntry> entries = buildCronEntries(list); 053 Calendar working = Calendar.getInstance(); 054 working.setTimeInMillis(currentTime); 055 working.set(Calendar.SECOND, 0); 056 057 CronEntry minutes = entries.get(MINUTES); 058 CronEntry hours = entries.get(HOURS); 059 CronEntry dayOfMonth = entries.get(DAY_OF_MONTH); 060 CronEntry month = entries.get(MONTH); 061 CronEntry dayOfWeek = entries.get(DAY_OF_WEEK); 062 063 // Start at the top of the next minute, cron is only guaranteed to be 064 // run on the minute. 065 int timeToNextMinute = 60 - working.get(Calendar.SECOND); 066 working.add(Calendar.SECOND, timeToNextMinute); 067 068 // If its already to late in the day this will roll us over to tomorrow 069 // so we'll need to check again when done updating month and day. 070 int currentMinutes = working.get(Calendar.MINUTE); 071 if (!isCurrent(minutes, currentMinutes)) { 072 int nextMinutes = getNext(minutes, currentMinutes); 073 working.add(Calendar.MINUTE, nextMinutes); 074 } 075 076 int currentHours = working.get(Calendar.HOUR_OF_DAY); 077 if (!isCurrent(hours, currentHours)) { 078 int nextHour = getNext(hours, currentHours); 079 working.add(Calendar.HOUR_OF_DAY, nextHour); 080 } 081 082 // We can roll into the next month here which might violate the cron setting 083 // rules so we check once then recheck again after applying the month settings. 084 doUpdateCurrentDay(working, dayOfMonth, dayOfWeek); 085 086 // Start by checking if we are in the right month, if not then calculations 087 // need to start from the beginning of the month to ensure that we don't end 088 // up on the wrong day. (Can happen when DAY_OF_WEEK is set and current time 089 // is ahead of the day of the week to execute on). 090 doUpdateCurrentMonth(working, month); 091 092 // Now Check day of week and day of month together since they can be specified 093 // together in one entry, if both "day of month" and "day of week" are restricted 094 // (not "*"), then either the "day of month" field (3) or the "day of week" field 095 // (5) must match the current day or the Calenday must be advanced. 096 doUpdateCurrentDay(working, dayOfMonth, dayOfWeek); 097 098 // Now we can chose the correct hour and minute of the day in question. 099 100 currentHours = working.get(Calendar.HOUR_OF_DAY); 101 if (!isCurrent(hours, currentHours)) { 102 int nextHour = getNext(hours, currentHours); 103 working.add(Calendar.HOUR_OF_DAY, nextHour); 104 } 105 106 currentMinutes = working.get(Calendar.MINUTE); 107 if (!isCurrent(minutes, currentMinutes)) { 108 int nextMinutes = getNext(minutes, currentMinutes); 109 working.add(Calendar.MINUTE, nextMinutes); 110 } 111 112 result = working.getTimeInMillis(); 113 114 if (result <= currentTime) { 115 throw new ArithmeticException("Unable to compute next scheduled exection time."); 116 } 117 118 return result; 119 } 120 121 protected static long doUpdateCurrentMonth(Calendar working, CronEntry month) throws MessageFormatException { 122 123 int currentMonth = working.get(Calendar.MONTH) + 1; 124 if (!isCurrent(month, currentMonth)) { 125 int nextMonth = getNext(month, currentMonth); 126 working.add(Calendar.MONTH, nextMonth); 127 128 // Reset to start of month. 129 resetToStartOfDay(working, 1); 130 131 return working.getTimeInMillis(); 132 } 133 134 return 0L; 135 } 136 137 protected static long doUpdateCurrentDay(Calendar working, CronEntry dayOfMonth, CronEntry dayOfWeek) throws MessageFormatException { 138 139 int currentDayOfWeek = working.get(Calendar.DAY_OF_WEEK) - 1; 140 int currentDayOfMonth = working.get(Calendar.DAY_OF_MONTH); 141 142 // Simplest case, both are unrestricted or both match today otherwise 143 // result must be the closer of the two if both are set, or the next 144 // match to the one that is. 145 if (!isCurrent(dayOfWeek, currentDayOfWeek) || 146 !isCurrent(dayOfMonth, currentDayOfMonth) ) { 147 148 int nextWeekDay = Integer.MAX_VALUE; 149 int nextCalendarDay = Integer.MAX_VALUE; 150 151 if (!isCurrent(dayOfWeek, currentDayOfWeek)) { 152 nextWeekDay = getNext(dayOfWeek, currentDayOfWeek); 153 } 154 155 if (!isCurrent(dayOfMonth, currentDayOfMonth)) { 156 nextCalendarDay = getNext(dayOfMonth, currentDayOfMonth); 157 } 158 159 if( nextWeekDay < nextCalendarDay ) { 160 working.add(Calendar.DAY_OF_WEEK, nextWeekDay); 161 } else { 162 working.add(Calendar.DAY_OF_MONTH, nextCalendarDay); 163 } 164 165 // Since the day changed, we restart the clock at the start of the day 166 // so that the next time will either be at 12am + value of hours and 167 // minutes pattern. 168 resetToStartOfDay(working, working.get(Calendar.DAY_OF_MONTH)); 169 170 return working.getTimeInMillis(); 171 } 172 173 return 0L; 174 } 175 176 public static void validate(final String cronEntry) throws MessageFormatException { 177 List<String> list = tokenize(cronEntry); 178 List<CronEntry> entries = buildCronEntries(list); 179 for (CronEntry e : entries) { 180 validate(e); 181 } 182 } 183 184 static void validate(final CronEntry entry) throws MessageFormatException { 185 186 List<Integer> list = entry.currentWhen; 187 if (list.isEmpty() || list.get(0).intValue() < entry.start || list.get(list.size() - 1).intValue() > entry.end) { 188 throw new MessageFormatException("Invalid token: " + entry); 189 } 190 } 191 192 static int getNext(final CronEntry entry, final int current) throws MessageFormatException { 193 int result = 0; 194 195 if (entry.currentWhen == null) { 196 entry.currentWhen = calculateValues(entry); 197 } 198 199 List<Integer> list = entry.currentWhen; 200 int next = -1; 201 for (Integer i : list) { 202 if (i.intValue() > current) { 203 next = i.intValue(); 204 break; 205 } 206 } 207 if (next != -1) { 208 result = next - current; 209 } else { 210 int first = list.get(0).intValue(); 211 result = entry.end + first - entry.start - current; 212 213 // Account for difference of one vs zero based indices. 214 if (entry.name.equals("DayOfWeek") || entry.name.equals("Month")) { 215 result++; 216 } 217 } 218 219 return result; 220 } 221 222 static boolean isCurrent(final CronEntry entry, final int current) throws MessageFormatException { 223 boolean result = entry.currentWhen.contains(new Integer(current)); 224 return result; 225 } 226 227 protected static void resetToStartOfDay(Calendar target, int day) { 228 target.set(Calendar.DAY_OF_MONTH, day); 229 target.set(Calendar.HOUR_OF_DAY, 0); 230 target.set(Calendar.MINUTE, 0); 231 target.set(Calendar.SECOND, 0); 232 } 233 234 static List<String> tokenize(String cron) throws IllegalArgumentException { 235 StringTokenizer tokenize = new StringTokenizer(cron); 236 List<String> result = new ArrayList<String>(); 237 while (tokenize.hasMoreTokens()) { 238 result.add(tokenize.nextToken()); 239 } 240 if (result.size() != NUMBER_TOKENS) { 241 throw new IllegalArgumentException("Not a valid cron entry - wrong number of tokens(" + result.size() 242 + "): " + cron); 243 } 244 return result; 245 } 246 247 protected static List<Integer> calculateValues(final CronEntry entry) { 248 List<Integer> result = new ArrayList<Integer>(); 249 if (isAll(entry.token)) { 250 for (int i = entry.start; i <= entry.end; i++) { 251 result.add(i); 252 } 253 } else if (isAStep(entry.token)) { 254 int denominator = getDenominator(entry.token); 255 String numerator = getNumerator(entry.token); 256 CronEntry ce = new CronEntry(entry.name, numerator, entry.start, entry.end); 257 List<Integer> list = calculateValues(ce); 258 for (Integer i : list) { 259 if (i.intValue() % denominator == 0) { 260 result.add(i); 261 } 262 } 263 } else if (isAList(entry.token)) { 264 StringTokenizer tokenizer = new StringTokenizer(entry.token, ","); 265 while (tokenizer.hasMoreTokens()) { 266 String str = tokenizer.nextToken(); 267 CronEntry ce = new CronEntry(entry.name, str, entry.start, entry.end); 268 List<Integer> list = calculateValues(ce); 269 result.addAll(list); 270 } 271 } else if (isARange(entry.token)) { 272 int index = entry.token.indexOf('-'); 273 int first = Integer.parseInt(entry.token.substring(0, index)); 274 int last = Integer.parseInt(entry.token.substring(index + 1)); 275 for (int i = first; i <= last; i++) { 276 result.add(i); 277 } 278 } else { 279 int value = Integer.parseInt(entry.token); 280 result.add(value); 281 } 282 Collections.sort(result); 283 return result; 284 } 285 286 protected static boolean isARange(String token) { 287 return token != null && token.indexOf('-') >= 0; 288 } 289 290 protected static boolean isAStep(String token) { 291 return token != null && token.indexOf('/') >= 0; 292 } 293 294 protected static boolean isAList(String token) { 295 return token != null && token.indexOf(',') >= 0; 296 } 297 298 protected static boolean isAll(String token) { 299 return token != null && token.length() == 1 && token.charAt(0) == '*'; 300 } 301 302 protected static int getDenominator(final String token) { 303 int result = 0; 304 int index = token.indexOf('/'); 305 String str = token.substring(index + 1); 306 result = Integer.parseInt(str); 307 return result; 308 } 309 310 protected static String getNumerator(final String token) { 311 int index = token.indexOf('/'); 312 String str = token.substring(0, index); 313 return str; 314 } 315 316 static List<CronEntry> buildCronEntries(List<String> tokens) { 317 318 List<CronEntry> result = new ArrayList<CronEntry>(); 319 320 CronEntry minutes = new CronEntry("Minutes", tokens.get(MINUTES), 0, 60); 321 minutes.currentWhen = calculateValues(minutes); 322 result.add(minutes); 323 CronEntry hours = new CronEntry("Hours", tokens.get(HOURS), 0, 24); 324 hours.currentWhen = calculateValues(hours); 325 result.add(hours); 326 CronEntry dayOfMonth = new CronEntry("DayOfMonth", tokens.get(DAY_OF_MONTH), 1, 31); 327 dayOfMonth.currentWhen = calculateValues(dayOfMonth); 328 result.add(dayOfMonth); 329 CronEntry month = new CronEntry("Month", tokens.get(MONTH), 1, 12); 330 month.currentWhen = calculateValues(month); 331 result.add(month); 332 CronEntry dayOfWeek = new CronEntry("DayOfWeek", tokens.get(DAY_OF_WEEK), 0, 6); 333 dayOfWeek.currentWhen = calculateValues(dayOfWeek); 334 result.add(dayOfWeek); 335 336 return result; 337 } 338 339 static class CronEntry { 340 341 final String name; 342 final String token; 343 final int start; 344 final int end; 345 346 List<Integer> currentWhen; 347 348 CronEntry(String name, String token, int start, int end) { 349 this.name = name; 350 this.token = token; 351 this.start = start; 352 this.end = end; 353 } 354 355 @Override 356 public String toString() { 357 return this.name + ":" + token; 358 } 359 } 360 361 }