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 }