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