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