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