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