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