001/* 002 * All content copyright Terracotta, Inc., unless otherwise indicated. All rights reserved. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); you may not 005 * use this file except in compliance with the License. You may obtain a copy 006 * of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 012 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 013 * License for the specific language governing permissions and limitations 014 * under the License. 015 * 016 */ 017// Copied from org.quartz 018package net.sf.hajdbc.util.concurrent.cron; 019 020import java.io.Serializable; 021import java.text.ParseException; 022import java.util.Calendar; 023import java.util.Date; 024import java.util.HashMap; 025import java.util.Iterator; 026import java.util.Locale; 027import java.util.Map; 028import java.util.SortedSet; 029import java.util.StringTokenizer; 030import java.util.TimeZone; 031import java.util.TreeSet; 032 033/** 034 * Provides a parser and evaluator for unix-like cron expressions. Cron 035 * expressions provide the ability to specify complex time combinations such as 036 * "At 8:00am every Monday through Friday" or "At 1:30am every 037 * last Friday of the month". 038 * <P> 039 * Cron expressions are comprised of 6 required fields and one optional field 040 * separated by white space. The fields respectively are described as follows: 041 * 042 * <table cellspacing="8"> 043 * <tr> 044 * <th align="left">Field Name</th> 045 * <th align="left"> </th> 046 * <th align="left">Allowed Values</th> 047 * <th align="left"> </th> 048 * <th align="left">Allowed Special Characters</th> 049 * </tr> 050 * <tr> 051 * <td align="left"><code>Seconds</code></td> 052 * <td align="left"> </th> 053 * <td align="left"><code>0-59</code></td> 054 * <td align="left"> </th> 055 * <td align="left"><code>, - * /</code></td> 056 * </tr> 057 * <tr> 058 * <td align="left"><code>Minutes</code></td> 059 * <td align="left"> </th> 060 * <td align="left"><code>0-59</code></td> 061 * <td align="left"> </th> 062 * <td align="left"><code>, - * /</code></td> 063 * </tr> 064 * <tr> 065 * <td align="left"><code>Hours</code></td> 066 * <td align="left"> </th> 067 * <td align="left"><code>0-23</code></td> 068 * <td align="left"> </th> 069 * <td align="left"><code>, - * /</code></td> 070 * </tr> 071 * <tr> 072 * <td align="left"><code>Day-of-month</code></td> 073 * <td align="left"> </th> 074 * <td align="left"><code>1-31</code></td> 075 * <td align="left"> </th> 076 * <td align="left"><code>, - * ? / L W</code></td> 077 * </tr> 078 * <tr> 079 * <td align="left"><code>Month</code></td> 080 * <td align="left"> </th> 081 * <td align="left"><code>0-11 or JAN-DEC</code></td> 082 * <td align="left"> </th> 083 * <td align="left"><code>, - * /</code></td> 084 * </tr> 085 * <tr> 086 * <td align="left"><code>Day-of-Week</code></td> 087 * <td align="left"> </th> 088 * <td align="left"><code>1-7 or SUN-SAT</code></td> 089 * <td align="left"> </th> 090 * <td align="left"><code>, - * ? / L #</code></td> 091 * </tr> 092 * <tr> 093 * <td align="left"><code>Year (Optional)</code></td> 094 * <td align="left"> </th> 095 * <td align="left"><code>empty, 1970-2199</code></td> 096 * <td align="left"> </th> 097 * <td align="left"><code>, - * /</code></td> 098 * </tr> 099 * </table> 100 * <P> 101 * The '*' character is used to specify all values. For example, "*" 102 * in the minute field means "every minute". 103 * <P> 104 * The '?' character is allowed for the day-of-month and day-of-week fields. It 105 * is used to specify 'no specific value'. This is useful when you need to 106 * specify something in one of the two fields, but not the other. 107 * <P> 108 * The '-' character is used to specify ranges For example "10-12" in 109 * the hour field means "the hours 10, 11 and 12". 110 * <P> 111 * The ',' character is used to specify additional values. For example 112 * "MON,WED,FRI" in the day-of-week field means "the days Monday, 113 * Wednesday, and Friday". 114 * <P> 115 * The '/' character is used to specify increments. For example "0/15" 116 * in the seconds field means "the seconds 0, 15, 30, and 45". And 117 * "5/15" in the seconds field means "the seconds 5, 20, 35, and 118 * 50". Specifying '*' before the '/' is equivalent to specifying 0 is 119 * the value to start with. Essentially, for each field in the expression, there 120 * is a set of numbers that can be turned on or off. For seconds and minutes, 121 * the numbers range from 0 to 59. For hours 0 to 23, for days of the month 0 to 122 * 31, and for months 0 to 11 (JAN to DEC). The "/" character simply helps you turn 123 * on every "nth" value in the given set. Thus "7/6" in the 124 * month field only turns on month "7", it does NOT mean every 6th 125 * month, please note that subtlety. 126 * <P> 127 * The 'L' character is allowed for the day-of-month and day-of-week fields. 128 * This character is short-hand for "last", but it has different 129 * meaning in each of the two fields. For example, the value "L" in 130 * the day-of-month field means "the last day of the month" - day 31 131 * for January, day 28 for February on non-leap years. If used in the 132 * day-of-week field by itself, it simply means "7" or 133 * "SAT". But if used in the day-of-week field after another value, it 134 * means "the last xxx day of the month" - for example "6L" 135 * means "the last friday of the month". You can also specify an offset 136 * from the last day of the month, such as "L-3" which would mean the third-to-last 137 * day of the calendar month. <i>When using the 'L' option, it is important not to 138 * specify lists, or ranges of values, as you'll get confusing/unexpected results.</i> 139 * <P> 140 * The 'W' character is allowed for the day-of-month field. This character 141 * is used to specify the weekday (Monday-Friday) nearest the given day. As an 142 * example, if you were to specify "15W" as the value for the 143 * day-of-month field, the meaning is: "the nearest weekday to the 15th of 144 * the month". So if the 15th is a Saturday, the trigger will fire on 145 * Friday the 14th. If the 15th is a Sunday, the trigger will fire on Monday the 146 * 16th. If the 15th is a Tuesday, then it will fire on Tuesday the 15th. 147 * However if you specify "1W" as the value for day-of-month, and the 148 * 1st is a Saturday, the trigger will fire on Monday the 3rd, as it will not 149 * 'jump' over the boundary of a month's days. The 'W' character can only be 150 * specified when the day-of-month is a single day, not a range or list of days. 151 * <P> 152 * The 'L' and 'W' characters can also be combined for the day-of-month 153 * expression to yield 'LW', which translates to "last weekday of the 154 * month". 155 * <P> 156 * The '#' character is allowed for the day-of-week field. This character is 157 * used to specify "the nth" XXX day of the month. For example, the 158 * value of "6#3" in the day-of-week field means the third Friday of 159 * the month (day 6 = Friday and "#3" = the 3rd one in the month). 160 * Other examples: "2#1" = the first Monday of the month and 161 * "4#5" = the fifth Wednesday of the month. Note that if you specify 162 * "#5" and there is not 5 of the given day-of-week in the month, then 163 * no firing will occur that month. If the '#' character is used, there can 164 * only be one expression in the day-of-week field ("3#1,6#3" is 165 * not valid, since there are two expressions). 166 * <P> 167 * <!--The 'C' character is allowed for the day-of-month and day-of-week fields. 168 * This character is short-hand for "calendar". This means values are 169 * calculated against the associated calendar, if any. If no calendar is 170 * associated, then it is equivalent to having an all-inclusive calendar. A 171 * value of "5C" in the day-of-month field means "the first day included by the 172 * calendar on or after the 5th". A value of "1C" in the day-of-week field 173 * means "the first day included by the calendar on or after Sunday".--> 174 * <P> 175 * The legal characters and the names of months and days of the week are not 176 * case sensitive. 177 * 178 * <p> 179 * <b>NOTES:</b> 180 * <ul> 181 * <li>Support for specifying both a day-of-week and a day-of-month value is 182 * not complete (you'll need to use the '?' character in one of these fields). 183 * </li> 184 * <li>Overflowing ranges is supported - that is, having a larger number on 185 * the left hand side than the right. You might do 22-2 to catch 10 o'clock 186 * at night until 2 o'clock in the morning, or you might have NOV-FEB. It is 187 * very important to note that overuse of overflowing ranges creates ranges 188 * that don't make sense and no effort has been made to determine which 189 * interpretation CronExpression chooses. An example would be 190 * "0 0 14-6 ? * FRI-MON". </li> 191 * </ul> 192 * </p> 193 * 194 * 195 * @author Sharada Jambula, James House 196 * @author Contributions from Mads Henderson 197 * @author Refactoring from CronTrigger to CronExpression by Aaron Craven 198 */ 199@SuppressWarnings({ "static-method", "unqualified-field-access", "unused" }) 200public final class CronExpression implements Serializable, Cloneable { 201 202 private static final long serialVersionUID = 12423409423L; 203 204 protected static final int SECOND = 0; 205 protected static final int MINUTE = 1; 206 protected static final int HOUR = 2; 207 protected static final int DAY_OF_MONTH = 3; 208 protected static final int MONTH = 4; 209 protected static final int DAY_OF_WEEK = 5; 210 protected static final int YEAR = 6; 211 protected static final int ALL_SPEC_INT = 99; // '*' 212 protected static final int NO_SPEC_INT = 98; // '?' 213 protected static final Integer ALL_SPEC = ALL_SPEC_INT; 214 protected static final Integer NO_SPEC = NO_SPEC_INT; 215 216 protected static final Map<String, Integer> monthMap = new HashMap<String, Integer>(20); 217 protected static final Map<String, Integer> dayMap = new HashMap<String, Integer>(60); 218 static { 219 monthMap.put("JAN", 0); 220 monthMap.put("FEB", 1); 221 monthMap.put("MAR", 2); 222 monthMap.put("APR", 3); 223 monthMap.put("MAY", 4); 224 monthMap.put("JUN", 5); 225 monthMap.put("JUL", 6); 226 monthMap.put("AUG", 7); 227 monthMap.put("SEP", 8); 228 monthMap.put("OCT", 9); 229 monthMap.put("NOV", 10); 230 monthMap.put("DEC", 11); 231 232 dayMap.put("SUN", 1); 233 dayMap.put("MON", 2); 234 dayMap.put("TUE", 3); 235 dayMap.put("WED", 4); 236 dayMap.put("THU", 5); 237 dayMap.put("FRI", 6); 238 dayMap.put("SAT", 7); 239 } 240 241 private final String cronExpression; 242 private TimeZone timeZone = null; 243 protected transient TreeSet<Integer> seconds; 244 protected transient TreeSet<Integer> minutes; 245 protected transient TreeSet<Integer> hours; 246 protected transient TreeSet<Integer> daysOfMonth; 247 protected transient TreeSet<Integer> months; 248 protected transient TreeSet<Integer> daysOfWeek; 249 protected transient TreeSet<Integer> years; 250 251 protected transient boolean lastdayOfWeek = false; 252 protected transient int nthdayOfWeek = 0; 253 protected transient boolean lastdayOfMonth = false; 254 protected transient boolean nearestWeekday = false; 255 protected transient int lastdayOffset = 0; 256 protected transient boolean expressionParsed = false; 257 258 public static final int MAX_YEAR = Calendar.getInstance().get(Calendar.YEAR) + 100; 259 260 /** 261 * Constructs a new <CODE>CronExpression</CODE> based on the specified 262 * parameter. 263 * 264 * @param cronExpression String representation of the cron expression the 265 * new object should represent 266 * @throws java.text.ParseException 267 * if the string expression cannot be parsed into a valid 268 * <CODE>CronExpression</CODE> 269 */ 270 public CronExpression(String cronExpression) throws ParseException { 271 if (cronExpression == null) { 272 throw new IllegalArgumentException("cronExpression cannot be null"); 273 } 274 275 this.cronExpression = cronExpression.toUpperCase(Locale.US); 276 277 buildExpression(this.cronExpression); 278 } 279 280 /** 281 * Constructs a new {@code CronExpression} as a copy of an existing 282 * instance. 283 * 284 * @param expression 285 * The existing cron expression to be copied 286 */ 287 public CronExpression(CronExpression expression) { 288 /* 289 * We don't call the other constructor here since we need to swallow the 290 * ParseException. We also elide some of the sanity checking as it is 291 * not logically trippable. 292 */ 293 this.cronExpression = expression.getCronExpression(); 294 try { 295 buildExpression(cronExpression); 296 } catch (ParseException ex) { 297 throw new AssertionError(); 298 } 299 if (expression.getTimeZone() != null) { 300 setTimeZone((TimeZone) expression.getTimeZone().clone()); 301 } 302 } 303 304 /** 305 * Indicates whether the given date satisfies the cron expression. Note that 306 * milliseconds are ignored, so two Dates falling on different milliseconds 307 * of the same second will always have the same result here. 308 * 309 * @param date the date to evaluate 310 * @return a boolean indicating whether the given date satisfies the cron 311 * expression 312 */ 313 public boolean isSatisfiedBy(Date date) { 314 Calendar testDateCal = Calendar.getInstance(getTimeZone()); 315 testDateCal.setTime(date); 316 testDateCal.set(Calendar.MILLISECOND, 0); 317 Date originalDate = testDateCal.getTime(); 318 319 testDateCal.add(Calendar.SECOND, -1); 320 321 Date timeAfter = getTimeAfter(testDateCal.getTime()); 322 323 return ((timeAfter != null) && (timeAfter.equals(originalDate))); 324 } 325 326 /** 327 * Returns the next date/time <I>after</I> the given date/time which 328 * satisfies the cron expression. 329 * 330 * @param date the date/time at which to begin the search for the next valid 331 * date/time 332 * @return the next valid date/time 333 */ 334 public Date getNextValidTimeAfter(Date date) { 335 return getTimeAfter(date); 336 } 337 338 /** 339 * Returns the next date/time <I>after</I> the given date/time which does 340 * <I>not</I> satisfy the expression 341 * 342 * @param date the date/time at which to begin the search for the next 343 * invalid date/time 344 * @return the next valid date/time 345 */ 346 public Date getNextInvalidTimeAfter(Date date) { 347 long difference = 1000; 348 349 //move back to the nearest second so differences will be accurate 350 Calendar adjustCal = Calendar.getInstance(getTimeZone()); 351 adjustCal.setTime(date); 352 adjustCal.set(Calendar.MILLISECOND, 0); 353 Date lastDate = adjustCal.getTime(); 354 355 Date newDate; 356 357 //FUTURE_TODO: (QUARTZ-481) IMPROVE THIS! The following is a BAD solution to this problem. Performance will be very bad here, depending on the cron expression. It is, however A solution. 358 359 //keep getting the next included time until it's farther than one second 360 // apart. At that point, lastDate is the last valid fire time. We return 361 // the second immediately following it. 362 while (difference == 1000) { 363 newDate = getTimeAfter(lastDate); 364 if(newDate == null) 365 break; 366 367 difference = newDate.getTime() - lastDate.getTime(); 368 369 if (difference == 1000) { 370 lastDate = newDate; 371 } 372 } 373 374 return new Date(lastDate.getTime() + 1000); 375 } 376 377 /** 378 * Returns the time zone for which this <code>CronExpression</code> 379 * will be resolved. 380 */ 381 public TimeZone getTimeZone() { 382 if (timeZone == null) { 383 timeZone = TimeZone.getDefault(); 384 } 385 386 return timeZone; 387 } 388 389 /** 390 * Sets the time zone for which this <code>CronExpression</code> 391 * will be resolved. 392 */ 393 public void setTimeZone(TimeZone timeZone) { 394 this.timeZone = timeZone; 395 } 396 397 /** 398 * Returns the string representation of the <CODE>CronExpression</CODE> 399 * 400 * @return a string representation of the <CODE>CronExpression</CODE> 401 */ 402 @Override 403 public String toString() { 404 return cronExpression; 405 } 406 407 /** 408 * Indicates whether the specified cron expression can be parsed into a 409 * valid cron expression 410 * 411 * @param cronExpression the expression to evaluate 412 * @return a boolean indicating whether the given expression is a valid cron 413 * expression 414 */ 415 public static boolean isValidExpression(String cronExpression) { 416 417 try { 418 new CronExpression(cronExpression); 419 } catch (ParseException pe) { 420 return false; 421 } 422 423 return true; 424 } 425 426 public static void validateExpression(String cronExpression) throws ParseException { 427 428 new CronExpression(cronExpression); 429 } 430 431 432 //////////////////////////////////////////////////////////////////////////// 433 // 434 // Expression Parsing Functions 435 // 436 //////////////////////////////////////////////////////////////////////////// 437 438 protected void buildExpression(String expression) throws ParseException { 439 expressionParsed = true; 440 441 try { 442 443 if (seconds == null) { 444 seconds = new TreeSet<Integer>(); 445 } 446 if (minutes == null) { 447 minutes = new TreeSet<Integer>(); 448 } 449 if (hours == null) { 450 hours = new TreeSet<Integer>(); 451 } 452 if (daysOfMonth == null) { 453 daysOfMonth = new TreeSet<Integer>(); 454 } 455 if (months == null) { 456 months = new TreeSet<Integer>(); 457 } 458 if (daysOfWeek == null) { 459 daysOfWeek = new TreeSet<Integer>(); 460 } 461 if (years == null) { 462 years = new TreeSet<Integer>(); 463 } 464 465 int exprOn = SECOND; 466 467 StringTokenizer exprsTok = new StringTokenizer(expression, " \t", 468 false); 469 470 while (exprsTok.hasMoreTokens() && exprOn <= YEAR) { 471 String expr = exprsTok.nextToken().trim(); 472 473 // throw an exception if L is used with other days of the month 474 if(exprOn == DAY_OF_MONTH && expr.indexOf('L') != -1 && expr.length() > 1 && expr.contains(",")) { 475 throw new ParseException("Support for specifying 'L' and 'LW' with other days of the month is not implemented", -1); 476 } 477 // throw an exception if L is used with other days of the week 478 if(exprOn == DAY_OF_WEEK && expr.indexOf('L') != -1 && expr.length() > 1 && expr.contains(",")) { 479 throw new ParseException("Support for specifying 'L' with other days of the week is not implemented", -1); 480 } 481 if(exprOn == DAY_OF_WEEK && expr.indexOf('#') != -1 && expr.indexOf('#', expr.indexOf('#') +1) != -1) { 482 throw new ParseException("Support for specifying multiple \"nth\" days is not implemented.", -1); 483 } 484 485 StringTokenizer vTok = new StringTokenizer(expr, ","); 486 while (vTok.hasMoreTokens()) { 487 String v = vTok.nextToken(); 488 storeExpressionVals(0, v, exprOn); 489 } 490 491 exprOn++; 492 } 493 494 if (exprOn <= DAY_OF_WEEK) { 495 throw new ParseException("Unexpected end of expression.", 496 expression.length()); 497 } 498 499 if (exprOn <= YEAR) { 500 storeExpressionVals(0, "*", YEAR); 501 } 502 503 TreeSet<Integer> dow = getSet(DAY_OF_WEEK); 504 TreeSet<Integer> dom = getSet(DAY_OF_MONTH); 505 506 // Copying the logic from the UnsupportedOperationException below 507 boolean dayOfMSpec = !dom.contains(NO_SPEC); 508 boolean dayOfWSpec = !dow.contains(NO_SPEC); 509 510 if (!dayOfMSpec || dayOfWSpec) { 511 if (!dayOfWSpec || dayOfMSpec) { 512 throw new ParseException( 513 "Support for specifying both a day-of-week AND a day-of-month parameter is not implemented.", 0); 514 } 515 } 516 } catch (ParseException pe) { 517 throw pe; 518 } catch (Exception e) { 519 throw new ParseException("Illegal cron expression format (" 520 + e.toString() + ")", 0); 521 } 522 } 523 524 protected int storeExpressionVals(int pos, String s, int type) 525 throws ParseException { 526 527 int incr = 0; 528 int i = skipWhiteSpace(pos, s); 529 if (i >= s.length()) { 530 return i; 531 } 532 char c = s.charAt(i); 533 if ((c >= 'A') && (c <= 'Z') && (!s.equals("L")) && (!s.equals("LW")) && (!s.matches("^L-[0-9]*[W]?"))) { 534 String sub = s.substring(i, i + 3); 535 int sval = -1; 536 int eval = -1; 537 if (type == MONTH) { 538 sval = getMonthNumber(sub) + 1; 539 if (sval <= 0) { 540 throw new ParseException("Invalid Month value: '" + sub + "'", i); 541 } 542 if (s.length() > i + 3) { 543 c = s.charAt(i + 3); 544 if (c == '-') { 545 i += 4; 546 sub = s.substring(i, i + 3); 547 eval = getMonthNumber(sub) + 1; 548 if (eval <= 0) { 549 throw new ParseException("Invalid Month value: '" + sub + "'", i); 550 } 551 } 552 } 553 } else if (type == DAY_OF_WEEK) { 554 sval = getDayOfWeekNumber(sub); 555 if (sval < 0) { 556 throw new ParseException("Invalid Day-of-Week value: '" 557 + sub + "'", i); 558 } 559 if (s.length() > i + 3) { 560 c = s.charAt(i + 3); 561 if (c == '-') { 562 i += 4; 563 sub = s.substring(i, i + 3); 564 eval = getDayOfWeekNumber(sub); 565 if (eval < 0) { 566 throw new ParseException( 567 "Invalid Day-of-Week value: '" + sub 568 + "'", i); 569 } 570 } else if (c == '#') { 571 try { 572 i += 4; 573 nthdayOfWeek = Integer.parseInt(s.substring(i)); 574 if (nthdayOfWeek < 1 || nthdayOfWeek > 5) { 575 throw new Exception(); 576 } 577 } catch (Exception e) { 578 throw new ParseException( 579 "A numeric value between 1 and 5 must follow the '#' option", 580 i); 581 } 582 } else if (c == 'L') { 583 lastdayOfWeek = true; 584 i++; 585 } 586 } 587 588 } else { 589 throw new ParseException( 590 "Illegal characters for this position: '" + sub + "'", 591 i); 592 } 593 if (eval != -1) { 594 incr = 1; 595 } 596 addToSet(sval, eval, incr, type); 597 return (i + 3); 598 } 599 600 if (c == '?') { 601 i++; 602 if ((i + 1) < s.length() 603 && (s.charAt(i) != ' ' && s.charAt(i + 1) != '\t')) { 604 throw new ParseException("Illegal character after '?': " 605 + s.charAt(i), i); 606 } 607 if (type != DAY_OF_WEEK && type != DAY_OF_MONTH) { 608 throw new ParseException( 609 "'?' can only be specfied for Day-of-Month or Day-of-Week.", 610 i); 611 } 612 if (type == DAY_OF_WEEK && !lastdayOfMonth) { 613 int val = daysOfMonth.last(); 614 if (val == NO_SPEC_INT) { 615 throw new ParseException( 616 "'?' can only be specfied for Day-of-Month -OR- Day-of-Week.", 617 i); 618 } 619 } 620 621 addToSet(NO_SPEC_INT, -1, 0, type); 622 return i; 623 } 624 625 if (c == '*' || c == '/') { 626 if (c == '*' && (i + 1) >= s.length()) { 627 addToSet(ALL_SPEC_INT, -1, incr, type); 628 return i + 1; 629 } else if (c == '/' 630 && ((i + 1) >= s.length() || s.charAt(i + 1) == ' ' || s 631 .charAt(i + 1) == '\t')) { 632 throw new ParseException("'/' must be followed by an integer.", i); 633 } else if (c == '*') { 634 i++; 635 } 636 c = s.charAt(i); 637 if (c == '/') { // is an increment specified? 638 i++; 639 if (i >= s.length()) { 640 throw new ParseException("Unexpected end of string.", i); 641 } 642 643 incr = getNumericValue(s, i); 644 645 i++; 646 if (incr > 10) { 647 i++; 648 } 649 if (incr > 59 && (type == SECOND || type == MINUTE)) { 650 throw new ParseException("Increment > 60 : " + incr, i); 651 } else if (incr > 23 && (type == HOUR)) { 652 throw new ParseException("Increment > 24 : " + incr, i); 653 } else if (incr > 31 && (type == DAY_OF_MONTH)) { 654 throw new ParseException("Increment > 31 : " + incr, i); 655 } else if (incr > 7 && (type == DAY_OF_WEEK)) { 656 throw new ParseException("Increment > 7 : " + incr, i); 657 } else if (incr > 12 && (type == MONTH)) { 658 throw new ParseException("Increment > 12 : " + incr, i); 659 } 660 } else { 661 incr = 1; 662 } 663 664 addToSet(ALL_SPEC_INT, -1, incr, type); 665 return i; 666 } else if (c == 'L') { 667 i++; 668 if (type == DAY_OF_MONTH) { 669 lastdayOfMonth = true; 670 } 671 if (type == DAY_OF_WEEK) { 672 addToSet(7, 7, 0, type); 673 } 674 if(type == DAY_OF_MONTH && s.length() > i) { 675 c = s.charAt(i); 676 if(c == '-') { 677 ValueSet vs = getValue(0, s, i+1); 678 lastdayOffset = vs.value; 679 if(lastdayOffset > 30) 680 throw new ParseException("Offset from last day must be <= 30", i+1); 681 i = vs.pos; 682 } 683 if(s.length() > i) { 684 c = s.charAt(i); 685 if(c == 'W') { 686 nearestWeekday = true; 687 i++; 688 } 689 } 690 } 691 return i; 692 } else if (c >= '0' && c <= '9') { 693 int val = Integer.parseInt(String.valueOf(c)); 694 i++; 695 if (i >= s.length()) { 696 addToSet(val, -1, -1, type); 697 } else { 698 c = s.charAt(i); 699 if (c >= '0' && c <= '9') { 700 ValueSet vs = getValue(val, s, i); 701 val = vs.value; 702 i = vs.pos; 703 } 704 i = checkNext(i, s, val, type); 705 return i; 706 } 707 } else { 708 throw new ParseException("Unexpected character: " + c, i); 709 } 710 711 return i; 712 } 713 714 protected int checkNext(int pos, String s, int val, int type) 715 throws ParseException { 716 717 int end = -1; 718 int i = pos; 719 720 if (i >= s.length()) { 721 addToSet(val, end, -1, type); 722 return i; 723 } 724 725 char c = s.charAt(pos); 726 727 if (c == 'L') { 728 if (type == DAY_OF_WEEK) { 729 if(val < 1 || val > 7) 730 throw new ParseException("Day-of-Week values must be between 1 and 7", -1); 731 lastdayOfWeek = true; 732 } else { 733 throw new ParseException("'L' option is not valid here. (pos=" + i + ")", i); 734 } 735 TreeSet<Integer> set = getSet(type); 736 set.add(val); 737 i++; 738 return i; 739 } 740 741 if (c == 'W') { 742 if (type == DAY_OF_MONTH) { 743 nearestWeekday = true; 744 } else { 745 throw new ParseException("'W' option is not valid here. (pos=" + i + ")", i); 746 } 747 if(val > 31) 748 throw new ParseException("The 'W' option does not make sense with values larger than 31 (max number of days in a month)", i); 749 TreeSet<Integer> set = getSet(type); 750 set.add(val); 751 i++; 752 return i; 753 } 754 755 if (c == '#') { 756 if (type != DAY_OF_WEEK) { 757 throw new ParseException("'#' option is not valid here. (pos=" + i + ")", i); 758 } 759 i++; 760 try { 761 nthdayOfWeek = Integer.parseInt(s.substring(i)); 762 if (nthdayOfWeek < 1 || nthdayOfWeek > 5) { 763 throw new Exception(); 764 } 765 } catch (Exception e) { 766 throw new ParseException( 767 "A numeric value between 1 and 5 must follow the '#' option", 768 i); 769 } 770 771 TreeSet<Integer> set = getSet(type); 772 set.add(val); 773 i++; 774 return i; 775 } 776 777 if (c == '-') { 778 i++; 779 c = s.charAt(i); 780 int v = Integer.parseInt(String.valueOf(c)); 781 end = v; 782 i++; 783 if (i >= s.length()) { 784 addToSet(val, end, 1, type); 785 return i; 786 } 787 c = s.charAt(i); 788 if (c >= '0' && c <= '9') { 789 ValueSet vs = getValue(v, s, i); 790 end = vs.value; 791 i = vs.pos; 792 } 793 if (i < s.length() && ((c = s.charAt(i)) == '/')) { 794 i++; 795 c = s.charAt(i); 796 int v2 = Integer.parseInt(String.valueOf(c)); 797 i++; 798 if (i >= s.length()) { 799 addToSet(val, end, v2, type); 800 return i; 801 } 802 c = s.charAt(i); 803 if (c >= '0' && c <= '9') { 804 ValueSet vs = getValue(v2, s, i); 805 int v3 = vs.value; 806 addToSet(val, end, v3, type); 807 i = vs.pos; 808 return i; 809 } else { 810 addToSet(val, end, v2, type); 811 return i; 812 } 813 } else { 814 addToSet(val, end, 1, type); 815 return i; 816 } 817 } 818 819 if (c == '/') { 820 i++; 821 c = s.charAt(i); 822 int v2 = Integer.parseInt(String.valueOf(c)); 823 i++; 824 if (i >= s.length()) { 825 addToSet(val, end, v2, type); 826 return i; 827 } 828 c = s.charAt(i); 829 if (c >= '0' && c <= '9') { 830 ValueSet vs = getValue(v2, s, i); 831 int v3 = vs.value; 832 addToSet(val, end, v3, type); 833 i = vs.pos; 834 return i; 835 } else { 836 throw new ParseException("Unexpected character '" + c + "' after '/'", i); 837 } 838 } 839 840 addToSet(val, end, 0, type); 841 i++; 842 return i; 843 } 844 845 public String getCronExpression() { 846 return cronExpression; 847 } 848 849 public String getExpressionSummary() { 850 StringBuilder buf = new StringBuilder(); 851 852 buf.append("seconds: "); 853 buf.append(getExpressionSetSummary(seconds)); 854 buf.append("\n"); 855 buf.append("minutes: "); 856 buf.append(getExpressionSetSummary(minutes)); 857 buf.append("\n"); 858 buf.append("hours: "); 859 buf.append(getExpressionSetSummary(hours)); 860 buf.append("\n"); 861 buf.append("daysOfMonth: "); 862 buf.append(getExpressionSetSummary(daysOfMonth)); 863 buf.append("\n"); 864 buf.append("months: "); 865 buf.append(getExpressionSetSummary(months)); 866 buf.append("\n"); 867 buf.append("daysOfWeek: "); 868 buf.append(getExpressionSetSummary(daysOfWeek)); 869 buf.append("\n"); 870 buf.append("lastdayOfWeek: "); 871 buf.append(lastdayOfWeek); 872 buf.append("\n"); 873 buf.append("nearestWeekday: "); 874 buf.append(nearestWeekday); 875 buf.append("\n"); 876 buf.append("NthDayOfWeek: "); 877 buf.append(nthdayOfWeek); 878 buf.append("\n"); 879 buf.append("lastdayOfMonth: "); 880 buf.append(lastdayOfMonth); 881 buf.append("\n"); 882 buf.append("years: "); 883 buf.append(getExpressionSetSummary(years)); 884 buf.append("\n"); 885 886 return buf.toString(); 887 } 888 889 protected String getExpressionSetSummary(java.util.Set<Integer> set) { 890 891 if (set.contains(NO_SPEC)) { 892 return "?"; 893 } 894 if (set.contains(ALL_SPEC)) { 895 return "*"; 896 } 897 898 StringBuilder buf = new StringBuilder(); 899 900 Iterator<Integer> itr = set.iterator(); 901 boolean first = true; 902 while (itr.hasNext()) { 903 Integer iVal = itr.next(); 904 String val = iVal.toString(); 905 if (!first) { 906 buf.append(","); 907 } 908 buf.append(val); 909 first = false; 910 } 911 912 return buf.toString(); 913 } 914 915 protected String getExpressionSetSummary(java.util.ArrayList<Integer> list) { 916 917 if (list.contains(NO_SPEC)) { 918 return "?"; 919 } 920 if (list.contains(ALL_SPEC)) { 921 return "*"; 922 } 923 924 StringBuilder buf = new StringBuilder(); 925 926 Iterator<Integer> itr = list.iterator(); 927 boolean first = true; 928 while (itr.hasNext()) { 929 Integer iVal = itr.next(); 930 String val = iVal.toString(); 931 if (!first) { 932 buf.append(","); 933 } 934 buf.append(val); 935 first = false; 936 } 937 938 return buf.toString(); 939 } 940 941 protected int skipWhiteSpace(int i, String s) { 942 for (; i < s.length() && (s.charAt(i) == ' ' || s.charAt(i) == '\t'); i++) { 943 ; 944 } 945 946 return i; 947 } 948 949 protected int findNextWhiteSpace(int i, String s) { 950 for (; i < s.length() && (s.charAt(i) != ' ' || s.charAt(i) != '\t'); i++) { 951 ; 952 } 953 954 return i; 955 } 956 957 protected void addToSet(int val, int end, int incr, int type) 958 throws ParseException { 959 960 TreeSet<Integer> set = getSet(type); 961 962 if (type == SECOND || type == MINUTE) { 963 if ((val < 0 || val > 59 || end > 59) && (val != ALL_SPEC_INT)) { 964 throw new ParseException( 965 "Minute and Second values must be between 0 and 59", 966 -1); 967 } 968 } else if (type == HOUR) { 969 if ((val < 0 || val > 23 || end > 23) && (val != ALL_SPEC_INT)) { 970 throw new ParseException( 971 "Hour values must be between 0 and 23", -1); 972 } 973 } else if (type == DAY_OF_MONTH) { 974 if ((val < 1 || val > 31 || end > 31) && (val != ALL_SPEC_INT) 975 && (val != NO_SPEC_INT)) { 976 throw new ParseException( 977 "Day of month values must be between 1 and 31", -1); 978 } 979 } else if (type == MONTH) { 980 if ((val < 1 || val > 12 || end > 12) && (val != ALL_SPEC_INT)) { 981 throw new ParseException( 982 "Month values must be between 1 and 12", -1); 983 } 984 } else if (type == DAY_OF_WEEK) { 985 if ((val == 0 || val > 7 || end > 7) && (val != ALL_SPEC_INT) 986 && (val != NO_SPEC_INT)) { 987 throw new ParseException( 988 "Day-of-Week values must be between 1 and 7", -1); 989 } 990 } 991 992 if ((incr == 0 || incr == -1) && val != ALL_SPEC_INT) { 993 if (val != -1) { 994 set.add(val); 995 } else { 996 set.add(NO_SPEC); 997 } 998 999 return; 1000 } 1001 1002 int startAt = val; 1003 int stopAt = end; 1004 1005 if (val == ALL_SPEC_INT && incr <= 0) { 1006 incr = 1; 1007 set.add(ALL_SPEC); // put in a marker, but also fill values 1008 } 1009 1010 if (type == SECOND || type == MINUTE) { 1011 if (stopAt == -1) { 1012 stopAt = 59; 1013 } 1014 if (startAt == -1 || startAt == ALL_SPEC_INT) { 1015 startAt = 0; 1016 } 1017 } else if (type == HOUR) { 1018 if (stopAt == -1) { 1019 stopAt = 23; 1020 } 1021 if (startAt == -1 || startAt == ALL_SPEC_INT) { 1022 startAt = 0; 1023 } 1024 } else if (type == DAY_OF_MONTH) { 1025 if (stopAt == -1) { 1026 stopAt = 31; 1027 } 1028 if (startAt == -1 || startAt == ALL_SPEC_INT) { 1029 startAt = 1; 1030 } 1031 } else if (type == MONTH) { 1032 if (stopAt == -1) { 1033 stopAt = 12; 1034 } 1035 if (startAt == -1 || startAt == ALL_SPEC_INT) { 1036 startAt = 1; 1037 } 1038 } else if (type == DAY_OF_WEEK) { 1039 if (stopAt == -1) { 1040 stopAt = 7; 1041 } 1042 if (startAt == -1 || startAt == ALL_SPEC_INT) { 1043 startAt = 1; 1044 } 1045 } else if (type == YEAR) { 1046 if (stopAt == -1) { 1047 stopAt = MAX_YEAR; 1048 } 1049 if (startAt == -1 || startAt == ALL_SPEC_INT) { 1050 startAt = 1970; 1051 } 1052 } 1053 1054 // if the end of the range is before the start, then we need to overflow into 1055 // the next day, month etc. This is done by adding the maximum amount for that 1056 // type, and using modulus max to determine the value being added. 1057 int max = -1; 1058 if (stopAt < startAt) { 1059 switch (type) { 1060 case SECOND : max = 60; break; 1061 case MINUTE : max = 60; break; 1062 case HOUR : max = 24; break; 1063 case MONTH : max = 12; break; 1064 case DAY_OF_WEEK : max = 7; break; 1065 case DAY_OF_MONTH : max = 31; break; 1066 case YEAR : throw new IllegalArgumentException("Start year must be less than stop year"); 1067 default : throw new IllegalArgumentException("Unexpected type encountered"); 1068 } 1069 stopAt += max; 1070 } 1071 1072 for (int i = startAt; i <= stopAt; i += incr) { 1073 if (max == -1) { 1074 // ie: there's no max to overflow over 1075 set.add(i); 1076 } else { 1077 // take the modulus to get the real value 1078 int i2 = i % max; 1079 1080 // 1-indexed ranges should not include 0, and should include their max 1081 if (i2 == 0 && (type == MONTH || type == DAY_OF_WEEK || type == DAY_OF_MONTH) ) { 1082 i2 = max; 1083 } 1084 1085 set.add(i2); 1086 } 1087 } 1088 } 1089 1090 TreeSet<Integer> getSet(int type) { 1091 switch (type) { 1092 case SECOND: 1093 return seconds; 1094 case MINUTE: 1095 return minutes; 1096 case HOUR: 1097 return hours; 1098 case DAY_OF_MONTH: 1099 return daysOfMonth; 1100 case MONTH: 1101 return months; 1102 case DAY_OF_WEEK: 1103 return daysOfWeek; 1104 case YEAR: 1105 return years; 1106 default: 1107 return null; 1108 } 1109 } 1110 1111 protected ValueSet getValue(int v, String s, int i) { 1112 char c = s.charAt(i); 1113 StringBuilder s1 = new StringBuilder(String.valueOf(v)); 1114 while (c >= '0' && c <= '9') { 1115 s1.append(c); 1116 i++; 1117 if (i >= s.length()) { 1118 break; 1119 } 1120 c = s.charAt(i); 1121 } 1122 ValueSet val = new ValueSet(); 1123 1124 val.pos = (i < s.length()) ? i : i + 1; 1125 val.value = Integer.parseInt(s1.toString()); 1126 return val; 1127 } 1128 1129 protected int getNumericValue(String s, int i) { 1130 int endOfVal = findNextWhiteSpace(i, s); 1131 String val = s.substring(i, endOfVal); 1132 return Integer.parseInt(val); 1133 } 1134 1135 protected int getMonthNumber(String s) { 1136 Integer integer = monthMap.get(s); 1137 1138 if (integer == null) { 1139 return -1; 1140 } 1141 1142 return integer; 1143 } 1144 1145 protected int getDayOfWeekNumber(String s) { 1146 Integer integer = dayMap.get(s); 1147 1148 if (integer == null) { 1149 return -1; 1150 } 1151 1152 return integer; 1153 } 1154 1155 //////////////////////////////////////////////////////////////////////////// 1156 // 1157 // Computation Functions 1158 // 1159 //////////////////////////////////////////////////////////////////////////// 1160 1161 public Date getTimeAfter(Date afterTime) { 1162 1163 // Computation is based on Gregorian year only. 1164 Calendar cl = new java.util.GregorianCalendar(getTimeZone()); 1165 1166 // move ahead one second, since we're computing the time *after* the 1167 // given time 1168 afterTime = new Date(afterTime.getTime() + 1000); 1169 // CronTrigger does not deal with milliseconds 1170 cl.setTime(afterTime); 1171 cl.set(Calendar.MILLISECOND, 0); 1172 1173 boolean gotOne = false; 1174 // loop until we've computed the next time, or we've past the endTime 1175 while (!gotOne) { 1176 1177 //if (endTime != null && cl.getTime().after(endTime)) return null; 1178 if(cl.get(Calendar.YEAR) > 2999) { // prevent endless loop... 1179 return null; 1180 } 1181 1182 SortedSet<Integer> st = null; 1183 int t = 0; 1184 1185 int sec = cl.get(Calendar.SECOND); 1186 int min = cl.get(Calendar.MINUTE); 1187 1188 // get second................................................. 1189 st = seconds.tailSet(sec); 1190 if (st != null && st.size() != 0) { 1191 sec = st.first(); 1192 } else { 1193 sec = seconds.first(); 1194 min++; 1195 cl.set(Calendar.MINUTE, min); 1196 } 1197 cl.set(Calendar.SECOND, sec); 1198 1199 min = cl.get(Calendar.MINUTE); 1200 int hr = cl.get(Calendar.HOUR_OF_DAY); 1201 t = -1; 1202 1203 // get minute................................................. 1204 st = minutes.tailSet(min); 1205 if (st != null && st.size() != 0) { 1206 t = min; 1207 min = st.first(); 1208 } else { 1209 min = minutes.first(); 1210 hr++; 1211 } 1212 if (min != t) { 1213 cl.set(Calendar.SECOND, 0); 1214 cl.set(Calendar.MINUTE, min); 1215 setCalendarHour(cl, hr); 1216 continue; 1217 } 1218 cl.set(Calendar.MINUTE, min); 1219 1220 hr = cl.get(Calendar.HOUR_OF_DAY); 1221 int day = cl.get(Calendar.DAY_OF_MONTH); 1222 t = -1; 1223 1224 // get hour................................................... 1225 st = hours.tailSet(hr); 1226 if (st != null && st.size() != 0) { 1227 t = hr; 1228 hr = st.first(); 1229 } else { 1230 hr = hours.first(); 1231 day++; 1232 } 1233 if (hr != t) { 1234 cl.set(Calendar.SECOND, 0); 1235 cl.set(Calendar.MINUTE, 0); 1236 cl.set(Calendar.DAY_OF_MONTH, day); 1237 setCalendarHour(cl, hr); 1238 continue; 1239 } 1240 cl.set(Calendar.HOUR_OF_DAY, hr); 1241 1242 day = cl.get(Calendar.DAY_OF_MONTH); 1243 int mon = cl.get(Calendar.MONTH) + 1; 1244 // '+ 1' because calendar is 0-based for this field, and we are 1245 // 1-based 1246 t = -1; 1247 int tmon = mon; 1248 1249 // get day................................................... 1250 boolean dayOfMSpec = !daysOfMonth.contains(NO_SPEC); 1251 boolean dayOfWSpec = !daysOfWeek.contains(NO_SPEC); 1252 if (dayOfMSpec && !dayOfWSpec) { // get day by day of month rule 1253 st = daysOfMonth.tailSet(day); 1254 if (lastdayOfMonth) { 1255 if(!nearestWeekday) { 1256 t = day; 1257 day = getLastDayOfMonth(mon, cl.get(Calendar.YEAR)); 1258 day -= lastdayOffset; 1259 if(t > day) { 1260 mon++; 1261 if(mon > 12) { 1262 mon = 1; 1263 tmon = 3333; // ensure test of mon != tmon further below fails 1264 cl.add(Calendar.YEAR, 1); 1265 } 1266 day = 1; 1267 } 1268 } else { 1269 t = day; 1270 day = getLastDayOfMonth(mon, cl.get(Calendar.YEAR)); 1271 day -= lastdayOffset; 1272 1273 java.util.Calendar tcal = java.util.Calendar.getInstance(getTimeZone()); 1274 tcal.set(Calendar.SECOND, 0); 1275 tcal.set(Calendar.MINUTE, 0); 1276 tcal.set(Calendar.HOUR_OF_DAY, 0); 1277 tcal.set(Calendar.DAY_OF_MONTH, day); 1278 tcal.set(Calendar.MONTH, mon - 1); 1279 tcal.set(Calendar.YEAR, cl.get(Calendar.YEAR)); 1280 1281 int ldom = getLastDayOfMonth(mon, cl.get(Calendar.YEAR)); 1282 int dow = tcal.get(Calendar.DAY_OF_WEEK); 1283 1284 if(dow == Calendar.SATURDAY && day == 1) { 1285 day += 2; 1286 } else if(dow == Calendar.SATURDAY) { 1287 day -= 1; 1288 } else if(dow == Calendar.SUNDAY && day == ldom) { 1289 day -= 2; 1290 } else if(dow == Calendar.SUNDAY) { 1291 day += 1; 1292 } 1293 1294 tcal.set(Calendar.SECOND, sec); 1295 tcal.set(Calendar.MINUTE, min); 1296 tcal.set(Calendar.HOUR_OF_DAY, hr); 1297 tcal.set(Calendar.DAY_OF_MONTH, day); 1298 tcal.set(Calendar.MONTH, mon - 1); 1299 Date nTime = tcal.getTime(); 1300 if(nTime.before(afterTime)) { 1301 day = 1; 1302 mon++; 1303 } 1304 } 1305 } else if(nearestWeekday) { 1306 t = day; 1307 day = daysOfMonth.first(); 1308 1309 java.util.Calendar tcal = java.util.Calendar.getInstance(getTimeZone()); 1310 tcal.set(Calendar.SECOND, 0); 1311 tcal.set(Calendar.MINUTE, 0); 1312 tcal.set(Calendar.HOUR_OF_DAY, 0); 1313 tcal.set(Calendar.DAY_OF_MONTH, day); 1314 tcal.set(Calendar.MONTH, mon - 1); 1315 tcal.set(Calendar.YEAR, cl.get(Calendar.YEAR)); 1316 1317 int ldom = getLastDayOfMonth(mon, cl.get(Calendar.YEAR)); 1318 int dow = tcal.get(Calendar.DAY_OF_WEEK); 1319 1320 if(dow == Calendar.SATURDAY && day == 1) { 1321 day += 2; 1322 } else if(dow == Calendar.SATURDAY) { 1323 day -= 1; 1324 } else if(dow == Calendar.SUNDAY && day == ldom) { 1325 day -= 2; 1326 } else if(dow == Calendar.SUNDAY) { 1327 day += 1; 1328 } 1329 1330 1331 tcal.set(Calendar.SECOND, sec); 1332 tcal.set(Calendar.MINUTE, min); 1333 tcal.set(Calendar.HOUR_OF_DAY, hr); 1334 tcal.set(Calendar.DAY_OF_MONTH, day); 1335 tcal.set(Calendar.MONTH, mon - 1); 1336 Date nTime = tcal.getTime(); 1337 if(nTime.before(afterTime)) { 1338 day = daysOfMonth.first(); 1339 mon++; 1340 } 1341 } else if (st != null && st.size() != 0) { 1342 t = day; 1343 day = st.first(); 1344 // make sure we don't over-run a short month, such as february 1345 int lastDay = getLastDayOfMonth(mon, cl.get(Calendar.YEAR)); 1346 if (day > lastDay) { 1347 day = daysOfMonth.first(); 1348 mon++; 1349 } 1350 } else { 1351 day = daysOfMonth.first(); 1352 mon++; 1353 } 1354 1355 if (day != t || mon != tmon) { 1356 cl.set(Calendar.SECOND, 0); 1357 cl.set(Calendar.MINUTE, 0); 1358 cl.set(Calendar.HOUR_OF_DAY, 0); 1359 cl.set(Calendar.DAY_OF_MONTH, day); 1360 cl.set(Calendar.MONTH, mon - 1); 1361 // '- 1' because calendar is 0-based for this field, and we 1362 // are 1-based 1363 continue; 1364 } 1365 } else if (dayOfWSpec && !dayOfMSpec) { // get day by day of week rule 1366 if (lastdayOfWeek) { // are we looking for the last XXX day of 1367 // the month? 1368 int dow = daysOfWeek.first(); // desired 1369 // d-o-w 1370 int cDow = cl.get(Calendar.DAY_OF_WEEK); // current d-o-w 1371 int daysToAdd = 0; 1372 if (cDow < dow) { 1373 daysToAdd = dow - cDow; 1374 } 1375 if (cDow > dow) { 1376 daysToAdd = dow + (7 - cDow); 1377 } 1378 1379 int lDay = getLastDayOfMonth(mon, cl.get(Calendar.YEAR)); 1380 1381 if (day + daysToAdd > lDay) { // did we already miss the 1382 // last one? 1383 cl.set(Calendar.SECOND, 0); 1384 cl.set(Calendar.MINUTE, 0); 1385 cl.set(Calendar.HOUR_OF_DAY, 0); 1386 cl.set(Calendar.DAY_OF_MONTH, 1); 1387 cl.set(Calendar.MONTH, mon); 1388 // no '- 1' here because we are promoting the month 1389 continue; 1390 } 1391 1392 // find date of last occurrence of this day in this month... 1393 while ((day + daysToAdd + 7) <= lDay) { 1394 daysToAdd += 7; 1395 } 1396 1397 day += daysToAdd; 1398 1399 if (daysToAdd > 0) { 1400 cl.set(Calendar.SECOND, 0); 1401 cl.set(Calendar.MINUTE, 0); 1402 cl.set(Calendar.HOUR_OF_DAY, 0); 1403 cl.set(Calendar.DAY_OF_MONTH, day); 1404 cl.set(Calendar.MONTH, mon - 1); 1405 // '- 1' here because we are not promoting the month 1406 continue; 1407 } 1408 1409 } else if (nthdayOfWeek != 0) { 1410 // are we looking for the Nth XXX day in the month? 1411 int dow = daysOfWeek.first(); // desired 1412 // d-o-w 1413 int cDow = cl.get(Calendar.DAY_OF_WEEK); // current d-o-w 1414 int daysToAdd = 0; 1415 if (cDow < dow) { 1416 daysToAdd = dow - cDow; 1417 } else if (cDow > dow) { 1418 daysToAdd = dow + (7 - cDow); 1419 } 1420 1421 boolean dayShifted = false; 1422 if (daysToAdd > 0) { 1423 dayShifted = true; 1424 } 1425 1426 day += daysToAdd; 1427 int weekOfMonth = day / 7; 1428 if (day % 7 > 0) { 1429 weekOfMonth++; 1430 } 1431 1432 daysToAdd = (nthdayOfWeek - weekOfMonth) * 7; 1433 day += daysToAdd; 1434 if (daysToAdd < 0 1435 || day > getLastDayOfMonth(mon, cl 1436 .get(Calendar.YEAR))) { 1437 cl.set(Calendar.SECOND, 0); 1438 cl.set(Calendar.MINUTE, 0); 1439 cl.set(Calendar.HOUR_OF_DAY, 0); 1440 cl.set(Calendar.DAY_OF_MONTH, 1); 1441 cl.set(Calendar.MONTH, mon); 1442 // no '- 1' here because we are promoting the month 1443 continue; 1444 } else if (daysToAdd > 0 || dayShifted) { 1445 cl.set(Calendar.SECOND, 0); 1446 cl.set(Calendar.MINUTE, 0); 1447 cl.set(Calendar.HOUR_OF_DAY, 0); 1448 cl.set(Calendar.DAY_OF_MONTH, day); 1449 cl.set(Calendar.MONTH, mon - 1); 1450 // '- 1' here because we are NOT promoting the month 1451 continue; 1452 } 1453 } else { 1454 int cDow = cl.get(Calendar.DAY_OF_WEEK); // current d-o-w 1455 int dow = daysOfWeek.first(); // desired 1456 // d-o-w 1457 st = daysOfWeek.tailSet(cDow); 1458 if (st != null && st.size() > 0) { 1459 dow = st.first(); 1460 } 1461 1462 int daysToAdd = 0; 1463 if (cDow < dow) { 1464 daysToAdd = dow - cDow; 1465 } 1466 if (cDow > dow) { 1467 daysToAdd = dow + (7 - cDow); 1468 } 1469 1470 int lDay = getLastDayOfMonth(mon, cl.get(Calendar.YEAR)); 1471 1472 if (day + daysToAdd > lDay) { // will we pass the end of 1473 // the month? 1474 cl.set(Calendar.SECOND, 0); 1475 cl.set(Calendar.MINUTE, 0); 1476 cl.set(Calendar.HOUR_OF_DAY, 0); 1477 cl.set(Calendar.DAY_OF_MONTH, 1); 1478 cl.set(Calendar.MONTH, mon); 1479 // no '- 1' here because we are promoting the month 1480 continue; 1481 } else if (daysToAdd > 0) { // are we swithing days? 1482 cl.set(Calendar.SECOND, 0); 1483 cl.set(Calendar.MINUTE, 0); 1484 cl.set(Calendar.HOUR_OF_DAY, 0); 1485 cl.set(Calendar.DAY_OF_MONTH, day + daysToAdd); 1486 cl.set(Calendar.MONTH, mon - 1); 1487 // '- 1' because calendar is 0-based for this field, 1488 // and we are 1-based 1489 continue; 1490 } 1491 } 1492 } else { // dayOfWSpec && !dayOfMSpec 1493 throw new UnsupportedOperationException( 1494 "Support for specifying both a day-of-week AND a day-of-month parameter is not implemented."); 1495 } 1496 cl.set(Calendar.DAY_OF_MONTH, day); 1497 1498 mon = cl.get(Calendar.MONTH) + 1; 1499 // '+ 1' because calendar is 0-based for this field, and we are 1500 // 1-based 1501 int year = cl.get(Calendar.YEAR); 1502 t = -1; 1503 1504 // test for expressions that never generate a valid fire date, 1505 // but keep looping... 1506 if (year > MAX_YEAR) { 1507 return null; 1508 } 1509 1510 // get month................................................... 1511 st = months.tailSet(mon); 1512 if (st != null && st.size() != 0) { 1513 t = mon; 1514 mon = st.first(); 1515 } else { 1516 mon = months.first(); 1517 year++; 1518 } 1519 if (mon != t) { 1520 cl.set(Calendar.SECOND, 0); 1521 cl.set(Calendar.MINUTE, 0); 1522 cl.set(Calendar.HOUR_OF_DAY, 0); 1523 cl.set(Calendar.DAY_OF_MONTH, 1); 1524 cl.set(Calendar.MONTH, mon - 1); 1525 // '- 1' because calendar is 0-based for this field, and we are 1526 // 1-based 1527 cl.set(Calendar.YEAR, year); 1528 continue; 1529 } 1530 cl.set(Calendar.MONTH, mon - 1); 1531 // '- 1' because calendar is 0-based for this field, and we are 1532 // 1-based 1533 1534 year = cl.get(Calendar.YEAR); 1535 t = -1; 1536 1537 // get year................................................... 1538 st = years.tailSet(year); 1539 if (st != null && st.size() != 0) { 1540 t = year; 1541 year = st.first(); 1542 } else { 1543 return null; // ran out of years... 1544 } 1545 1546 if (year != t) { 1547 cl.set(Calendar.SECOND, 0); 1548 cl.set(Calendar.MINUTE, 0); 1549 cl.set(Calendar.HOUR_OF_DAY, 0); 1550 cl.set(Calendar.DAY_OF_MONTH, 1); 1551 cl.set(Calendar.MONTH, 0); 1552 // '- 1' because calendar is 0-based for this field, and we are 1553 // 1-based 1554 cl.set(Calendar.YEAR, year); 1555 continue; 1556 } 1557 cl.set(Calendar.YEAR, year); 1558 1559 gotOne = true; 1560 } // while( !done ) 1561 1562 return cl.getTime(); 1563 } 1564 1565 /** 1566 * Advance the calendar to the particular hour paying particular attention 1567 * to daylight saving problems. 1568 * 1569 * @param cal the calendar to operate on 1570 * @param hour the hour to set 1571 */ 1572 protected void setCalendarHour(Calendar cal, int hour) { 1573 cal.set(java.util.Calendar.HOUR_OF_DAY, hour); 1574 if (cal.get(java.util.Calendar.HOUR_OF_DAY) != hour && hour != 24) { 1575 cal.set(java.util.Calendar.HOUR_OF_DAY, hour + 1); 1576 } 1577 } 1578 1579 /** 1580 * NOT YET IMPLEMENTED: Returns the time before the given time 1581 * that the <code>CronExpression</code> matches. 1582 */ 1583 public Date getTimeBefore(Date endTime) { 1584 // FUTURE_TODO: implement QUARTZ-423 1585 return null; 1586 } 1587 1588 /** 1589 * NOT YET IMPLEMENTED: Returns the final time that the 1590 * <code>CronExpression</code> will match. 1591 */ 1592 public Date getFinalFireTime() { 1593 // FUTURE_TODO: implement QUARTZ-423 1594 return null; 1595 } 1596 1597 protected boolean isLeapYear(int year) { 1598 return ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)); 1599 } 1600 1601 protected int getLastDayOfMonth(int monthNum, int year) { 1602 1603 switch (monthNum) { 1604 case 1: 1605 return 31; 1606 case 2: 1607 return (isLeapYear(year)) ? 29 : 28; 1608 case 3: 1609 return 31; 1610 case 4: 1611 return 30; 1612 case 5: 1613 return 31; 1614 case 6: 1615 return 30; 1616 case 7: 1617 return 31; 1618 case 8: 1619 return 31; 1620 case 9: 1621 return 30; 1622 case 10: 1623 return 31; 1624 case 11: 1625 return 30; 1626 case 12: 1627 return 31; 1628 default: 1629 throw new IllegalArgumentException("Illegal month number: " 1630 + monthNum); 1631 } 1632 } 1633 1634 1635 private void readObject(java.io.ObjectInputStream stream) 1636 throws java.io.IOException, ClassNotFoundException { 1637 1638 stream.defaultReadObject(); 1639 try { 1640 buildExpression(cronExpression); 1641 } catch (Exception ignore) { 1642 } // never happens 1643 } 1644 1645 @Override 1646 @Deprecated 1647 public Object clone() { 1648 return new CronExpression(this); 1649 } 1650} 1651 1652class ValueSet { 1653 public int value; 1654 1655 public int pos; 1656}