001/*
002 * Copyright (C) 2014 Jörg Prante
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy 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,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016package org.xbib.elasticsearch.plugin.jdbc.cron;
017
018import java.text.ParseException;
019import java.util.Calendar;
020import java.util.Date;
021import java.util.HashMap;
022import java.util.Iterator;
023import java.util.List;
024import java.util.Locale;
025import java.util.Map;
026import java.util.Set;
027import java.util.SortedSet;
028import java.util.StringTokenizer;
029import java.util.TimeZone;
030import java.util.TreeSet;
031
032public final class CronExpression implements Cloneable {
033
034    protected static final int SECOND = 0;
035    protected static final int MINUTE = 1;
036    protected static final int HOUR = 2;
037    protected static final int DAY_OF_MONTH = 3;
038    protected static final int MONTH = 4;
039    protected static final int DAY_OF_WEEK = 5;
040    protected static final int YEAR = 6;
041    protected static final int ALL_SPEC_INT = 99; // '*'
042    protected static final int NO_SPEC_INT = 98; // '?'
043    protected static final Integer ALL_SPEC = ALL_SPEC_INT;
044    protected static final Integer NO_SPEC = NO_SPEC_INT;
045
046    protected static final Map<String, Integer> monthMap = new HashMap<String, Integer>(20);
047    protected static final Map<String, Integer> dayMap = new HashMap<String, Integer>(60);
048
049    static {
050        monthMap.put("JAN", 0);
051        monthMap.put("FEB", 1);
052        monthMap.put("MAR", 2);
053        monthMap.put("APR", 3);
054        monthMap.put("MAY", 4);
055        monthMap.put("JUN", 5);
056        monthMap.put("JUL", 6);
057        monthMap.put("AUG", 7);
058        monthMap.put("SEP", 8);
059        monthMap.put("OCT", 9);
060        monthMap.put("NOV", 10);
061        monthMap.put("DEC", 11);
062
063        dayMap.put("SUN", 1);
064        dayMap.put("MON", 2);
065        dayMap.put("TUE", 3);
066        dayMap.put("WED", 4);
067        dayMap.put("THU", 5);
068        dayMap.put("FRI", 6);
069        dayMap.put("SAT", 7);
070    }
071
072    private final String cronExpression;
073
074    private TimeZone timeZone = null;
075
076    protected transient TreeSet<Integer> seconds;
077
078    protected transient TreeSet<Integer> minutes;
079
080    protected transient TreeSet<Integer> hours;
081
082    protected transient TreeSet<Integer> daysOfMonth;
083
084    protected transient TreeSet<Integer> months;
085
086    protected transient TreeSet<Integer> daysOfWeek;
087
088    protected transient TreeSet<Integer> years;
089
090    protected transient boolean lastdayOfWeek = false;
091
092    protected transient int nthdayOfWeek = 0;
093
094    protected transient boolean lastdayOfMonth = false;
095
096    protected transient boolean nearestWeekday = false;
097
098    protected transient int lastdayOffset = 0;
099
100    protected transient boolean expressionParsed = false;
101
102    public static final int MAX_YEAR = Calendar.getInstance().get(Calendar.YEAR) + 100;
103
104    /**
105     * Constructs a new <code>CronExpression</code> based on the specified
106     * parameter.
107     *
108     * @param cronExpression String representation of the cron expression the
109     *                       new object should represent
110     */
111    public CronExpression(String cronExpression) {
112        if (cronExpression == null) {
113            throw new IllegalArgumentException("cron expression cannot be null");
114        }
115        this.cronExpression = cronExpression.toUpperCase(Locale.US);
116        buildExpression(this.cronExpression);
117    }
118
119    /**
120     * Constructs a new {@code CronExpression} as a copy of an existing
121     * instance.
122     *
123     * @param expression The existing cron expression to be copied
124     */
125    public CronExpression(CronExpression expression) {
126        if (expression == null) {
127            throw new IllegalArgumentException("cron expression cannot be null");
128        }
129        this.cronExpression = expression.getCronExpression();
130        buildExpression(cronExpression);
131        if (expression.getTimeZone() != null) {
132            setTimeZone((TimeZone) expression.getTimeZone().clone());
133        }
134    }
135
136    /**
137     * Indicates whether the given date satisfies the cron expression. Note that
138     * milliseconds are ignored, so two Dates falling on different milliseconds
139     * of the same second will always have the same result here.
140     *
141     * @param date the date to evaluate
142     * @return a boolean indicating whether the given date satisfies the cron
143     * expression
144     */
145    public boolean isSatisfiedBy(Date date) {
146        Calendar testDateCal = Calendar.getInstance(getTimeZone());
147        testDateCal.setTime(date);
148        testDateCal.set(Calendar.MILLISECOND, 0);
149        Date originalDate = testDateCal.getTime();
150        testDateCal.add(Calendar.SECOND, -1);
151        Date timeAfter = getTimeAfter(testDateCal.getTime());
152        return ((timeAfter != null) && (timeAfter.equals(originalDate)));
153    }
154
155    /**
156     * Returns the next date/time <I>after</I> the given date/time which
157     * satisfies the cron expression.
158     *
159     * @param date the date/time at which to begin the search for the next valid
160     *             date/time
161     * @return the next valid date/time
162     */
163    public Date getNextValidTimeAfter(Date date) {
164        return getTimeAfter(date);
165    }
166
167    /**
168     * Returns the next date/time <I>after</I> the given date/time which does
169     * <I>not</I> satisfy the expression
170     *
171     * @param date the date/time at which to begin the search for the next
172     *             invalid date/time
173     * @return the next valid date/time
174     */
175    public Date getNextInvalidTimeAfter(Date date) {
176        long difference = 1000;
177        Calendar adjustCal = Calendar.getInstance(getTimeZone());
178        adjustCal.setTime(date);
179        adjustCal.set(Calendar.MILLISECOND, 0);
180        Date lastDate = adjustCal.getTime();
181        Date newDate;
182        while (difference == 1000) {
183            newDate = getTimeAfter(lastDate);
184            if (newDate == null) {
185                break;
186            }
187            difference = newDate.getTime() - lastDate.getTime();
188            if (difference == 1000) {
189                lastDate = newDate;
190            }
191        }
192        return new Date(lastDate.getTime() + 1000);
193    }
194
195    /**
196     * Returns the time zone for which this <code>CronExpression</code>
197     * will be resolved.
198     *
199     * @return the time zone
200     */
201    public TimeZone getTimeZone() {
202        if (timeZone == null) {
203            timeZone = TimeZone.getDefault();
204        }
205        return timeZone;
206    }
207
208    /**
209     * Sets the time zone for which  this <code>CronExpression</code>
210     * will be resolved.
211     *
212     * @param timeZone the time zone
213     */
214    public void setTimeZone(TimeZone timeZone) {
215        this.timeZone = timeZone;
216    }
217
218    /**
219     * Returns the string representation of the <code>CronExpression</code>
220     *
221     * @return a string representation of the <code>CronExpression</code>
222     */
223    @Override
224    public String toString() {
225        return cronExpression;
226    }
227
228    /**
229     * Indicates whether the specified cron expression can be parsed into a
230     * valid cron expression
231     *
232     * @param cronExpression the expression to evaluate
233     * @return a boolean indicating whether the given expression is a valid cron
234     * expression
235     */
236    public static boolean isValidExpression(String cronExpression) {
237        new CronExpression(cronExpression);
238        return true;
239    }
240
241    protected void buildExpression(String expression) {
242        expressionParsed = true;
243        try {
244            if (seconds == null) {
245                seconds = new TreeSet<Integer>();
246            }
247            if (minutes == null) {
248                minutes = new TreeSet<Integer>();
249            }
250            if (hours == null) {
251                hours = new TreeSet<Integer>();
252            }
253            if (daysOfMonth == null) {
254                daysOfMonth = new TreeSet<Integer>();
255            }
256            if (months == null) {
257                months = new TreeSet<Integer>();
258            }
259            if (daysOfWeek == null) {
260                daysOfWeek = new TreeSet<Integer>();
261            }
262            if (years == null) {
263                years = new TreeSet<Integer>();
264            }
265            int exprOn = SECOND;
266            StringTokenizer exprsTok = new StringTokenizer(expression, " \t", false);
267            while (exprsTok.hasMoreTokens() && exprOn <= YEAR) {
268                String expr = exprsTok.nextToken().trim();
269                if (exprOn == DAY_OF_MONTH && expr.indexOf('L') != -1 && expr.length() > 1 && expr.contains(",")) {
270                    throw new ParseException("Support for specifying 'L' and 'LW' with other days of the month is not implemented", -1);
271                }
272                if (exprOn == DAY_OF_WEEK && expr.indexOf('L') != -1 && expr.length() > 1 && expr.contains(",")) {
273                    throw new ParseException("Support for specifying 'L' with other days of the week is not implemented", -1);
274                }
275                if (exprOn == DAY_OF_WEEK && expr.indexOf('#') != -1 && expr.indexOf('#', expr.indexOf('#') + 1) != -1) {
276                    throw new ParseException("Support for specifying multiple \"nth\" days is not implemented.", -1);
277                }
278                StringTokenizer vTok = new StringTokenizer(expr, ",");
279                while (vTok.hasMoreTokens()) {
280                    String v = vTok.nextToken();
281                    storeExpressionVals(0, v, exprOn);
282                }
283                exprOn++;
284            }
285            if (exprOn <= DAY_OF_WEEK) {
286                throw new ParseException("Unexpected end of expression.",
287                        expression.length());
288            }
289            if (exprOn <= YEAR) {
290                storeExpressionVals(0, "*", YEAR);
291            }
292            TreeSet<Integer> dow = getSet(DAY_OF_WEEK);
293            TreeSet<Integer> dom = getSet(DAY_OF_MONTH);
294            boolean dayOfMSpec = !dom.contains(NO_SPEC);
295            boolean dayOfWSpec = !dow.contains(NO_SPEC);
296            if (!dayOfMSpec || dayOfWSpec) {
297                if (!dayOfWSpec || dayOfMSpec) {
298                    throw new ParseException("support for specifying both a day-of-week AND a day-of-month parameter is not implemented.", 0);
299                }
300            }
301        } catch (Exception e) {
302            throw new IllegalArgumentException("invalid cron expression format: " + e.getMessage());
303        }
304    }
305
306    protected int storeExpressionVals(int pos, String s, int type)
307            throws ParseException {
308        int incr = 0;
309        int i = skipWhiteSpace(pos, s);
310        if (i >= s.length()) {
311            return i;
312        }
313        char c = s.charAt(i);
314        if ((c >= 'A') && (c <= 'Z') && (!s.equals("L")) && (!s.equals("LW")) && (!s.matches("^L-[0-9]*[W]?"))) {
315            String sub = s.substring(i, i + 3);
316            int sval = -1;
317            int eval = -1;
318            if (type == MONTH) {
319                sval = getMonthNumber(sub) + 1;
320                if (sval <= 0) {
321                    throw new ParseException("invalid Month value: '" + sub + "'", i);
322                }
323                if (s.length() > i + 3) {
324                    c = s.charAt(i + 3);
325                    if (c == '-') {
326                        i += 4;
327                        sub = s.substring(i, i + 3);
328                        eval = getMonthNumber(sub) + 1;
329                        if (eval <= 0) {
330                            throw new ParseException("invalid Mmnth value: '" + sub + "'", i);
331                        }
332                    }
333                }
334            } else if (type == DAY_OF_WEEK) {
335                sval = getDayOfWeekNumber(sub);
336                if (sval < 0) {
337                    throw new ParseException("invalid day-of-week value: '"
338                            + sub + "'", i);
339                }
340                if (s.length() > i + 3) {
341                    c = s.charAt(i + 3);
342                    if (c == '-') {
343                        i += 4;
344                        sub = s.substring(i, i + 3);
345                        eval = getDayOfWeekNumber(sub);
346                        if (eval < 0) {
347                            throw new ParseException("invalid day-of-Week value: '" + sub + "'", i);
348                        }
349                    } else if (c == '#') {
350                        try {
351                            i += 4;
352                            nthdayOfWeek = Integer.parseInt(s.substring(i));
353                            if (nthdayOfWeek < 1 || nthdayOfWeek > 5) {
354                                throw new Exception();
355                            }
356                        } catch (Exception e) {
357                            throw new ParseException("numeric value between 1 and 5 must follow the '#' option", i);
358                        }
359                    } else if (c == 'L') {
360                        lastdayOfWeek = true;
361                        i++;
362                    }
363                }
364
365            } else {
366                throw new ParseException("illegal characters for this position: '" + sub + "'", i);
367            }
368            if (eval != -1) {
369                incr = 1;
370            }
371            addToSet(sval, eval, incr, type);
372            return (i + 3);
373        }
374
375        if (c == '?') {
376            i++;
377            if ((i + 1) < s.length()
378                    && (s.charAt(i) != ' ' && s.charAt(i + 1) != '\t')) {
379                throw new ParseException("illegal character after '?': " + s.charAt(i), i);
380            }
381            if (type != DAY_OF_WEEK && type != DAY_OF_MONTH) {
382                throw new ParseException("'?' can only be specfied for day-of-month or day-of-week", i);
383            }
384            if (type == DAY_OF_WEEK && !lastdayOfMonth) {
385                int val = daysOfMonth.last();
386                if (val == NO_SPEC_INT) {
387                    throw new ParseException("'?' can only be specfied for day-of-month or day-of-week", i);
388                }
389            }
390
391            addToSet(NO_SPEC_INT, -1, 0, type);
392            return i;
393        }
394        if (c == '*' || c == '/') {
395            if (c == '*' && (i + 1) >= s.length()) {
396                addToSet(ALL_SPEC_INT, -1, incr, type);
397                return i + 1;
398            } else if (c == '/'
399                    && ((i + 1) >= s.length() || s.charAt(i + 1) == ' ' || s
400                    .charAt(i + 1) == '\t')) {
401                throw new ParseException("'/' must be followed by an integer", i);
402            } else if (c == '*') {
403                i++;
404            }
405            c = s.charAt(i);
406            if (c == '/') {
407                i++;
408                if (i >= s.length()) {
409                    throw new ParseException("unexpected end of string", i);
410                }
411
412                incr = getNumericValue(s, i);
413
414                i++;
415                if (incr > 10) {
416                    i++;
417                }
418                if (incr > 59 && (type == SECOND || type == MINUTE)) {
419                    throw new ParseException("increment > 60 : " + incr, i);
420                } else if (incr > 23 && (type == HOUR)) {
421                    throw new ParseException("increment > 24 : " + incr, i);
422                } else if (incr > 31 && (type == DAY_OF_MONTH)) {
423                    throw new ParseException("increment > 31 : " + incr, i);
424                } else if (incr > 7 && (type == DAY_OF_WEEK)) {
425                    throw new ParseException("increment > 7 : " + incr, i);
426                } else if (incr > 12 && (type == MONTH)) {
427                    throw new ParseException("increment > 12 : " + incr, i);
428                }
429            } else {
430                incr = 1;
431            }
432            addToSet(ALL_SPEC_INT, -1, incr, type);
433            return i;
434        } else if (c == 'L') {
435            i++;
436            if (type == DAY_OF_MONTH) {
437                lastdayOfMonth = true;
438            }
439            if (type == DAY_OF_WEEK) {
440                addToSet(7, 7, 0, type);
441            }
442            if (type == DAY_OF_MONTH && s.length() > i) {
443                c = s.charAt(i);
444                if (c == '-') {
445                    ValueSet vs = getValue(0, s, i + 1);
446                    lastdayOffset = vs.value;
447                    if (lastdayOffset > 30) {
448                        throw new ParseException("offset from last day must be <= 30", i + 1);
449                    }
450                    i = vs.pos;
451                }
452                if (s.length() > i) {
453                    c = s.charAt(i);
454                    if (c == 'W') {
455                        nearestWeekday = true;
456                        i++;
457                    }
458                }
459            }
460            return i;
461        } else if (c >= '0' && c <= '9') {
462            int val = Integer.parseInt(String.valueOf(c));
463            i++;
464            if (i >= s.length()) {
465                addToSet(val, -1, -1, type);
466            } else {
467                c = s.charAt(i);
468                if (c >= '0' && c <= '9') {
469                    ValueSet vs = getValue(val, s, i);
470                    val = vs.value;
471                    i = vs.pos;
472                }
473                i = checkNext(i, s, val, type);
474                return i;
475            }
476        } else {
477            throw new ParseException("unexpected character: " + c, i);
478        }
479        return i;
480    }
481
482    protected int checkNext(int pos, String s, int val, int type)
483            throws ParseException {
484        int end = -1;
485        int i = pos;
486        if (i >= s.length()) {
487            addToSet(val, end, -1, type);
488            return i;
489        }
490        char c = s.charAt(pos);
491        if (c == 'L') {
492            if (type == DAY_OF_WEEK) {
493                if (val < 1 || val > 7) {
494                    throw new ParseException("day-of-week values must be between 1 and 7", -1);
495                }
496                lastdayOfWeek = true;
497            } else {
498                throw new ParseException("'L' option is not valid here (pos=" + i + ")", i);
499            }
500            TreeSet<Integer> set = getSet(type);
501            set.add(val);
502            i++;
503            return i;
504        }
505
506        if (c == 'W') {
507            if (type == DAY_OF_MONTH) {
508                nearestWeekday = true;
509            } else {
510                throw new ParseException("'W' option is not valid here (pos=" + i + ")", i);
511            }
512            if (val > 31) {
513                throw new ParseException("'W' option does not make sense with values larger than 31 (max number of days in a month)", i);
514            }
515            TreeSet<Integer> set = getSet(type);
516            set.add(val);
517            i++;
518            return i;
519        }
520        if (c == '#') {
521            if (type != DAY_OF_WEEK) {
522                throw new ParseException("'#' option is not valid here. (pos=" + i + ")", i);
523            }
524            i++;
525            try {
526                nthdayOfWeek = Integer.parseInt(s.substring(i));
527                if (nthdayOfWeek < 1 || nthdayOfWeek > 5) {
528                    throw new Exception();
529                }
530            } catch (Exception e) {
531                throw new ParseException("numeric value between 1 and 5 must follow the '#' option", i);
532            }
533            TreeSet<Integer> set = getSet(type);
534            set.add(val);
535            i++;
536            return i;
537        }
538
539        if (c == '-') {
540            i++;
541            c = s.charAt(i);
542            int v = Integer.parseInt(String.valueOf(c));
543            end = v;
544            i++;
545            if (i >= s.length()) {
546                addToSet(val, end, 1, type);
547                return i;
548            }
549            c = s.charAt(i);
550            if (c >= '0' && c <= '9') {
551                ValueSet vs = getValue(v, s, i);
552                end = vs.value;
553                i = vs.pos;
554            }
555            if (i < s.length() && ((c = s.charAt(i)) == '/')) {
556                i++;
557                c = s.charAt(i);
558                int v2 = Integer.parseInt(String.valueOf(c));
559                i++;
560                if (i >= s.length()) {
561                    addToSet(val, end, v2, type);
562                    return i;
563                }
564                c = s.charAt(i);
565                if (c >= '0' && c <= '9') {
566                    ValueSet vs = getValue(v2, s, i);
567                    int v3 = vs.value;
568                    addToSet(val, end, v3, type);
569                    i = vs.pos;
570                    return i;
571                } else {
572                    addToSet(val, end, v2, type);
573                    return i;
574                }
575            } else {
576                addToSet(val, end, 1, type);
577                return i;
578            }
579        }
580        if (c == '/') {
581            i++;
582            c = s.charAt(i);
583            int v2 = Integer.parseInt(String.valueOf(c));
584            i++;
585            if (i >= s.length()) {
586                addToSet(val, end, v2, type);
587                return i;
588            }
589            c = s.charAt(i);
590            if (c >= '0' && c <= '9') {
591                ValueSet vs = getValue(v2, s, i);
592                int v3 = vs.value;
593                addToSet(val, end, v3, type);
594                i = vs.pos;
595                return i;
596            } else {
597                throw new ParseException("unexpected character '" + c + "' after '/'", i);
598            }
599        }
600        addToSet(val, end, 0, type);
601        i++;
602        return i;
603    }
604
605    public String getCronExpression() {
606        return cronExpression;
607    }
608
609    public String getExpressionSummary() {
610        StringBuilder buf = new StringBuilder();
611        buf.append("seconds: ");
612        buf.append(getExpressionSetSummary(seconds));
613        buf.append("\n");
614        buf.append("minutes: ");
615        buf.append(getExpressionSetSummary(minutes));
616        buf.append("\n");
617        buf.append("hours: ");
618        buf.append(getExpressionSetSummary(hours));
619        buf.append("\n");
620        buf.append("daysOfMonth: ");
621        buf.append(getExpressionSetSummary(daysOfMonth));
622        buf.append("\n");
623        buf.append("months: ");
624        buf.append(getExpressionSetSummary(months));
625        buf.append("\n");
626        buf.append("daysOfWeek: ");
627        buf.append(getExpressionSetSummary(daysOfWeek));
628        buf.append("\n");
629        buf.append("lastdayOfWeek: ");
630        buf.append(lastdayOfWeek);
631        buf.append("\n");
632        buf.append("nearestWeekday: ");
633        buf.append(nearestWeekday);
634        buf.append("\n");
635        buf.append("NthDayOfWeek: ");
636        buf.append(nthdayOfWeek);
637        buf.append("\n");
638        buf.append("lastdayOfMonth: ");
639        buf.append(lastdayOfMonth);
640        buf.append("\n");
641        buf.append("years: ");
642        buf.append(getExpressionSetSummary(years));
643        buf.append("\n");
644        return buf.toString();
645    }
646
647    protected String getExpressionSetSummary(Set<Integer> set) {
648        if (set.contains(NO_SPEC)) {
649            return "?";
650        }
651        if (set.contains(ALL_SPEC)) {
652            return "*";
653        }
654        StringBuilder buf = new StringBuilder();
655        Iterator<Integer> itr = set.iterator();
656        boolean first = true;
657        while (itr.hasNext()) {
658            Integer iVal = itr.next();
659            String val = iVal.toString();
660            if (!first) {
661                buf.append(",");
662            }
663            buf.append(val);
664            first = false;
665        }
666
667        return buf.toString();
668    }
669
670    protected String getExpressionSetSummary(List<Integer> list) {
671        if (list.contains(NO_SPEC)) {
672            return "?";
673        }
674        if (list.contains(ALL_SPEC)) {
675            return "*";
676        }
677        StringBuilder buf = new StringBuilder();
678        Iterator<Integer> itr = list.iterator();
679        boolean first = true;
680        while (itr.hasNext()) {
681            Integer iVal = itr.next();
682            String val = iVal.toString();
683            if (!first) {
684                buf.append(",");
685            }
686            buf.append(val);
687            first = false;
688        }
689        return buf.toString();
690    }
691
692    protected int skipWhiteSpace(int i, String s) {
693        for (; i < s.length() && (s.charAt(i) == ' ' || s.charAt(i) == '\t'); i++) {
694            ;
695        }
696        return i;
697    }
698
699    protected int findNextWhiteSpace(int i, String s) {
700        for (; i < s.length() && (s.charAt(i) != ' ' || s.charAt(i) != '\t'); i++) {
701            ;
702        }
703        return i;
704    }
705
706    protected void addToSet(int val, int end, int incr, int type)
707            throws ParseException {
708        TreeSet<Integer> set = getSet(type);
709        if (type == SECOND || type == MINUTE) {
710            if ((val < 0 || val > 59 || end > 59) && (val != ALL_SPEC_INT)) {
711                throw new ParseException(
712                        "Minute and Second values must be between 0 and 59",
713                        -1);
714            }
715        } else if (type == HOUR) {
716            if ((val < 0 || val > 23 || end > 23) && (val != ALL_SPEC_INT)) {
717                throw new ParseException(
718                        "Hour values must be between 0 and 23", -1);
719            }
720        } else if (type == DAY_OF_MONTH) {
721            if ((val < 1 || val > 31 || end > 31) && (val != ALL_SPEC_INT)
722                    && (val != NO_SPEC_INT)) {
723                throw new ParseException(
724                        "Day of month values must be between 1 and 31", -1);
725            }
726        } else if (type == MONTH) {
727            if ((val < 1 || val > 12 || end > 12) && (val != ALL_SPEC_INT)) {
728                throw new ParseException(
729                        "Month values must be between 1 and 12", -1);
730            }
731        } else if (type == DAY_OF_WEEK) {
732            if ((val == 0 || val > 7 || end > 7) && (val != ALL_SPEC_INT)
733                    && (val != NO_SPEC_INT)) {
734                throw new ParseException(
735                        "Day-of-Week values must be between 1 and 7", -1);
736            }
737        }
738        if ((incr == 0 || incr == -1) && val != ALL_SPEC_INT) {
739            if (val != -1) {
740                set.add(val);
741            } else {
742                set.add(NO_SPEC);
743            }
744
745            return;
746        }
747        int startAt = val;
748        int stopAt = end;
749
750        if (val == ALL_SPEC_INT && incr <= 0) {
751            incr = 1;
752            set.add(ALL_SPEC);
753        }
754        if (type == SECOND || type == MINUTE) {
755            if (stopAt == -1) {
756                stopAt = 59;
757            }
758            if (startAt == -1 || startAt == ALL_SPEC_INT) {
759                startAt = 0;
760            }
761        } else if (type == HOUR) {
762            if (stopAt == -1) {
763                stopAt = 23;
764            }
765            if (startAt == -1 || startAt == ALL_SPEC_INT) {
766                startAt = 0;
767            }
768        } else if (type == DAY_OF_MONTH) {
769            if (stopAt == -1) {
770                stopAt = 31;
771            }
772            if (startAt == -1 || startAt == ALL_SPEC_INT) {
773                startAt = 1;
774            }
775        } else if (type == MONTH) {
776            if (stopAt == -1) {
777                stopAt = 12;
778            }
779            if (startAt == -1 || startAt == ALL_SPEC_INT) {
780                startAt = 1;
781            }
782        } else if (type == DAY_OF_WEEK) {
783            if (stopAt == -1) {
784                stopAt = 7;
785            }
786            if (startAt == -1 || startAt == ALL_SPEC_INT) {
787                startAt = 1;
788            }
789        } else if (type == YEAR) {
790            if (stopAt == -1) {
791                stopAt = MAX_YEAR;
792            }
793            if (startAt == -1 || startAt == ALL_SPEC_INT) {
794                startAt = 1970;
795            }
796        }
797        int max = -1;
798        if (stopAt < startAt) {
799            switch (type) {
800                case SECOND:
801                    max = 60;
802                    break;
803                case MINUTE:
804                    max = 60;
805                    break;
806                case HOUR:
807                    max = 24;
808                    break;
809                case MONTH:
810                    max = 12;
811                    break;
812                case DAY_OF_WEEK:
813                    max = 7;
814                    break;
815                case DAY_OF_MONTH:
816                    max = 31;
817                    break;
818                case YEAR:
819                    throw new IllegalArgumentException("Start year must be less than stop year");
820                default:
821                    throw new IllegalArgumentException("Unexpected type encountered");
822            }
823            stopAt += max;
824        }
825
826        for (int i = startAt; i <= stopAt; i += incr) {
827            if (max == -1) {
828                set.add(i);
829            } else {
830                int i2 = i % max;
831                if (i2 == 0 && (type == MONTH || type == DAY_OF_WEEK || type == DAY_OF_MONTH)) {
832                    i2 = max;
833                }
834                set.add(i2);
835            }
836        }
837    }
838
839    private TreeSet<Integer> getSet(int type) {
840        switch (type) {
841            case SECOND:
842                return seconds;
843            case MINUTE:
844                return minutes;
845            case HOUR:
846                return hours;
847            case DAY_OF_MONTH:
848                return daysOfMonth;
849            case MONTH:
850                return months;
851            case DAY_OF_WEEK:
852                return daysOfWeek;
853            case YEAR:
854                return years;
855            default:
856                return null;
857        }
858    }
859
860    protected ValueSet getValue(int v, String s, int i) {
861        char c = s.charAt(i);
862        StringBuilder s1 = new StringBuilder(String.valueOf(v));
863        while (c >= '0' && c <= '9') {
864            s1.append(c);
865            i++;
866            if (i >= s.length()) {
867                break;
868            }
869            c = s.charAt(i);
870        }
871        ValueSet val = new ValueSet();
872
873        val.pos = (i < s.length()) ? i : i + 1;
874        val.value = Integer.parseInt(s1.toString());
875        return val;
876    }
877
878    protected int getNumericValue(String s, int i) {
879        int endOfVal = findNextWhiteSpace(i, s);
880        String val = s.substring(i, endOfVal);
881        return Integer.parseInt(val);
882    }
883
884    protected int getMonthNumber(String s) {
885        Integer integer = monthMap.get(s);
886
887        if (integer == null) {
888            return -1;
889        }
890
891        return integer;
892    }
893
894    protected int getDayOfWeekNumber(String s) {
895        Integer integer = dayMap.get(s);
896
897        if (integer == null) {
898            return -1;
899        }
900
901        return integer;
902    }
903
904    public Date getTimeAfter(Date afterTime) {
905        Calendar cl = new java.util.GregorianCalendar(getTimeZone());
906        afterTime = new Date(afterTime.getTime() + 1000);
907        cl.setTime(afterTime);
908        cl.set(Calendar.MILLISECOND, 0);
909        boolean gotOne = false;
910        while (!gotOne) {
911            if (cl.get(Calendar.YEAR) > 2999) {
912                return null;
913            }
914            SortedSet<Integer> st;
915            int t;
916            int sec = cl.get(Calendar.SECOND);
917            int min = cl.get(Calendar.MINUTE);
918            st = seconds.tailSet(sec);
919            if (st.size() != 0) {
920                sec = st.first();
921            } else {
922                sec = seconds.first();
923                min++;
924                cl.set(Calendar.MINUTE, min);
925            }
926            cl.set(Calendar.SECOND, sec);
927            min = cl.get(Calendar.MINUTE);
928            int hr = cl.get(Calendar.HOUR_OF_DAY);
929            t = -1;
930            st = minutes.tailSet(min);
931            if (st.size() != 0) {
932                t = min;
933                min = st.first();
934            } else {
935                min = minutes.first();
936                hr++;
937            }
938            if (min != t) {
939                cl.set(Calendar.SECOND, 0);
940                cl.set(Calendar.MINUTE, min);
941                setCalendarHour(cl, hr);
942                continue;
943            }
944            cl.set(Calendar.MINUTE, min);
945            hr = cl.get(Calendar.HOUR_OF_DAY);
946            int day = cl.get(Calendar.DAY_OF_MONTH);
947            t = -1;
948            st = hours.tailSet(hr);
949            if (st.size() != 0) {
950                t = hr;
951                hr = st.first();
952            } else {
953                hr = hours.first();
954                day++;
955            }
956            if (hr != t) {
957                cl.set(Calendar.SECOND, 0);
958                cl.set(Calendar.MINUTE, 0);
959                cl.set(Calendar.DAY_OF_MONTH, day);
960                setCalendarHour(cl, hr);
961                continue;
962            }
963            cl.set(Calendar.HOUR_OF_DAY, hr);
964            day = cl.get(Calendar.DAY_OF_MONTH);
965            int mon = cl.get(Calendar.MONTH) + 1;
966            t = -1;
967            int tmon = mon;
968            boolean dayOfMSpec = !daysOfMonth.contains(NO_SPEC);
969            boolean dayOfWSpec = !daysOfWeek.contains(NO_SPEC);
970            if (dayOfMSpec && !dayOfWSpec) {
971                st = daysOfMonth.tailSet(day);
972                if (lastdayOfMonth) {
973                    if (!nearestWeekday) {
974                        t = day;
975                        day = getLastDayOfMonth(mon, cl.get(Calendar.YEAR));
976                        day -= lastdayOffset;
977                        if (t > day) {
978                            mon++;
979                            if (mon > 12) {
980                                mon = 1;
981                                tmon = 3333;
982                                cl.add(Calendar.YEAR, 1);
983                            }
984                            day = 1;
985                        }
986                    } else {
987                        t = day;
988                        day = getLastDayOfMonth(mon, cl.get(Calendar.YEAR));
989                        day -= lastdayOffset;
990                        Calendar tcal = Calendar.getInstance(getTimeZone());
991                        tcal.set(Calendar.SECOND, 0);
992                        tcal.set(Calendar.MINUTE, 0);
993                        tcal.set(Calendar.HOUR_OF_DAY, 0);
994                        tcal.set(Calendar.DAY_OF_MONTH, day);
995                        tcal.set(Calendar.MONTH, mon - 1);
996                        tcal.set(Calendar.YEAR, cl.get(Calendar.YEAR));
997                        int ldom = getLastDayOfMonth(mon, cl.get(Calendar.YEAR));
998                        int dow = tcal.get(Calendar.DAY_OF_WEEK);
999                        if (dow == Calendar.SATURDAY && day == 1) {
1000                            day += 2;
1001                        } else if (dow == Calendar.SATURDAY) {
1002                            day -= 1;
1003                        } else if (dow == Calendar.SUNDAY && day == ldom) {
1004                            day -= 2;
1005                        } else if (dow == Calendar.SUNDAY) {
1006                            day += 1;
1007                        }
1008                        tcal.set(Calendar.SECOND, sec);
1009                        tcal.set(Calendar.MINUTE, min);
1010                        tcal.set(Calendar.HOUR_OF_DAY, hr);
1011                        tcal.set(Calendar.DAY_OF_MONTH, day);
1012                        tcal.set(Calendar.MONTH, mon - 1);
1013                        Date nTime = tcal.getTime();
1014                        if (nTime.before(afterTime)) {
1015                            day = 1;
1016                            mon++;
1017                        }
1018                    }
1019                } else if (nearestWeekday) {
1020                    t = day;
1021                    day = daysOfMonth.first();
1022                    Calendar tcal = Calendar.getInstance(getTimeZone());
1023                    tcal.set(Calendar.SECOND, 0);
1024                    tcal.set(Calendar.MINUTE, 0);
1025                    tcal.set(Calendar.HOUR_OF_DAY, 0);
1026                    tcal.set(Calendar.DAY_OF_MONTH, day);
1027                    tcal.set(Calendar.MONTH, mon - 1);
1028                    tcal.set(Calendar.YEAR, cl.get(Calendar.YEAR));
1029                    int ldom = getLastDayOfMonth(mon, cl.get(Calendar.YEAR));
1030                    int dow = tcal.get(Calendar.DAY_OF_WEEK);
1031                    if (dow == Calendar.SATURDAY && day == 1) {
1032                        day += 2;
1033                    } else if (dow == Calendar.SATURDAY) {
1034                        day -= 1;
1035                    } else if (dow == Calendar.SUNDAY && day == ldom) {
1036                        day -= 2;
1037                    } else if (dow == Calendar.SUNDAY) {
1038                        day += 1;
1039                    }
1040                    tcal.set(Calendar.SECOND, sec);
1041                    tcal.set(Calendar.MINUTE, min);
1042                    tcal.set(Calendar.HOUR_OF_DAY, hr);
1043                    tcal.set(Calendar.DAY_OF_MONTH, day);
1044                    tcal.set(Calendar.MONTH, mon - 1);
1045                    Date nTime = tcal.getTime();
1046                    if (nTime.before(afterTime)) {
1047                        day = daysOfMonth.first();
1048                        mon++;
1049                    }
1050                } else if (st.size() != 0) {
1051                    t = day;
1052                    day = st.first();
1053                    int lastDay = getLastDayOfMonth(mon, cl.get(Calendar.YEAR));
1054                    if (day > lastDay) {
1055                        day = daysOfMonth.first();
1056                        mon++;
1057                    }
1058                } else {
1059                    day = daysOfMonth.first();
1060                    mon++;
1061                }
1062                if (day != t || mon != tmon) {
1063                    cl.set(Calendar.SECOND, 0);
1064                    cl.set(Calendar.MINUTE, 0);
1065                    cl.set(Calendar.HOUR_OF_DAY, 0);
1066                    cl.set(Calendar.DAY_OF_MONTH, day);
1067                    cl.set(Calendar.MONTH, mon - 1);
1068                    continue;
1069                }
1070            } else if (dayOfWSpec && !dayOfMSpec) {
1071                if (lastdayOfWeek) {
1072                    int dow = daysOfWeek.first();
1073                    int cDow = cl.get(Calendar.DAY_OF_WEEK);
1074                    int daysToAdd = 0;
1075                    if (cDow < dow) {
1076                        daysToAdd = dow - cDow;
1077                    }
1078                    if (cDow > dow) {
1079                        daysToAdd = dow + (7 - cDow);
1080                    }
1081                    int lDay = getLastDayOfMonth(mon, cl.get(Calendar.YEAR));
1082                    if (day + daysToAdd > lDay) {
1083                        cl.set(Calendar.SECOND, 0);
1084                        cl.set(Calendar.MINUTE, 0);
1085                        cl.set(Calendar.HOUR_OF_DAY, 0);
1086                        cl.set(Calendar.DAY_OF_MONTH, 1);
1087                        cl.set(Calendar.MONTH, mon);
1088                        continue;
1089                    }
1090                    while ((day + daysToAdd + 7) <= lDay) {
1091                        daysToAdd += 7;
1092                    }
1093                    day += daysToAdd;
1094                    if (daysToAdd > 0) {
1095                        cl.set(Calendar.SECOND, 0);
1096                        cl.set(Calendar.MINUTE, 0);
1097                        cl.set(Calendar.HOUR_OF_DAY, 0);
1098                        cl.set(Calendar.DAY_OF_MONTH, day);
1099                        cl.set(Calendar.MONTH, mon - 1);
1100                        continue;
1101                    }
1102                } else if (nthdayOfWeek != 0) {
1103                    int dow = daysOfWeek.first();
1104                    int cDow = cl.get(Calendar.DAY_OF_WEEK);
1105                    int daysToAdd = 0;
1106                    if (cDow < dow) {
1107                        daysToAdd = dow - cDow;
1108                    } else if (cDow > dow) {
1109                        daysToAdd = dow + (7 - cDow);
1110                    }
1111                    boolean dayShifted = false;
1112                    if (daysToAdd > 0) {
1113                        dayShifted = true;
1114                    }
1115                    day += daysToAdd;
1116                    int weekOfMonth = day / 7;
1117                    if (day % 7 > 0) {
1118                        weekOfMonth++;
1119                    }
1120                    daysToAdd = (nthdayOfWeek - weekOfMonth) * 7;
1121                    day += daysToAdd;
1122                    if (daysToAdd < 0
1123                            || day > getLastDayOfMonth(mon, cl
1124                            .get(Calendar.YEAR))) {
1125                        cl.set(Calendar.SECOND, 0);
1126                        cl.set(Calendar.MINUTE, 0);
1127                        cl.set(Calendar.HOUR_OF_DAY, 0);
1128                        cl.set(Calendar.DAY_OF_MONTH, 1);
1129                        cl.set(Calendar.MONTH, mon);
1130                        // no '- 1' here because we are promoting the month
1131                        continue;
1132                    } else if (daysToAdd > 0 || dayShifted) {
1133                        cl.set(Calendar.SECOND, 0);
1134                        cl.set(Calendar.MINUTE, 0);
1135                        cl.set(Calendar.HOUR_OF_DAY, 0);
1136                        cl.set(Calendar.DAY_OF_MONTH, day);
1137                        cl.set(Calendar.MONTH, mon - 1);
1138                        // '- 1' here because we are NOT promoting the month
1139                        continue;
1140                    }
1141                } else {
1142                    int cDow = cl.get(Calendar.DAY_OF_WEEK); // current d-o-w
1143                    int dow = daysOfWeek.first(); // desired
1144                    // d-o-w
1145                    st = daysOfWeek.tailSet(cDow);
1146                    if (st.size() > 0) {
1147                        dow = st.first();
1148                    }
1149
1150                    int daysToAdd = 0;
1151                    if (cDow < dow) {
1152                        daysToAdd = dow - cDow;
1153                    }
1154                    if (cDow > dow) {
1155                        daysToAdd = dow + (7 - cDow);
1156                    }
1157
1158                    int lDay = getLastDayOfMonth(mon, cl.get(Calendar.YEAR));
1159
1160                    if (day + daysToAdd > lDay) { // will we pass the end of
1161                        // the month?
1162                        cl.set(Calendar.SECOND, 0);
1163                        cl.set(Calendar.MINUTE, 0);
1164                        cl.set(Calendar.HOUR_OF_DAY, 0);
1165                        cl.set(Calendar.DAY_OF_MONTH, 1);
1166                        cl.set(Calendar.MONTH, mon);
1167                        continue;
1168                    } else if (daysToAdd > 0) { // are we swithing days?
1169                        cl.set(Calendar.SECOND, 0);
1170                        cl.set(Calendar.MINUTE, 0);
1171                        cl.set(Calendar.HOUR_OF_DAY, 0);
1172                        cl.set(Calendar.DAY_OF_MONTH, day + daysToAdd);
1173                        cl.set(Calendar.MONTH, mon - 1);
1174                        continue;
1175                    }
1176                }
1177            } else {
1178                throw new UnsupportedOperationException(
1179                        "Support for specifying both a day-of-week AND a day-of-month parameter is not implemented.");
1180            }
1181            cl.set(Calendar.DAY_OF_MONTH, day);
1182
1183            mon = cl.get(Calendar.MONTH) + 1;
1184            int year = cl.get(Calendar.YEAR);
1185            t = -1;
1186            if (year > MAX_YEAR) {
1187                return null;
1188            }
1189            st = months.tailSet(mon);
1190            if (st.size() != 0) {
1191                t = mon;
1192                mon = st.first();
1193            } else {
1194                mon = months.first();
1195                year++;
1196            }
1197            if (mon != t) {
1198                cl.set(Calendar.SECOND, 0);
1199                cl.set(Calendar.MINUTE, 0);
1200                cl.set(Calendar.HOUR_OF_DAY, 0);
1201                cl.set(Calendar.DAY_OF_MONTH, 1);
1202                cl.set(Calendar.MONTH, mon - 1);
1203                cl.set(Calendar.YEAR, year);
1204                continue;
1205            }
1206            cl.set(Calendar.MONTH, mon - 1);
1207            year = cl.get(Calendar.YEAR);
1208            st = years.tailSet(year);
1209            if (st.size() != 0) {
1210                t = year;
1211                year = st.first();
1212            } else {
1213                return null;
1214            }
1215            if (year != t) {
1216                cl.set(Calendar.SECOND, 0);
1217                cl.set(Calendar.MINUTE, 0);
1218                cl.set(Calendar.HOUR_OF_DAY, 0);
1219                cl.set(Calendar.DAY_OF_MONTH, 1);
1220                cl.set(Calendar.MONTH, 0);
1221                cl.set(Calendar.YEAR, year);
1222                continue;
1223            }
1224            cl.set(Calendar.YEAR, year);
1225            gotOne = true;
1226        }
1227        return cl.getTime();
1228    }
1229
1230    /**
1231     * Advance the calendar to the particular hour paying particular attention
1232     * to daylight saving problems.
1233     *
1234     * @param cal  the calendar to operate on
1235     * @param hour the hour to set
1236     */
1237    protected void setCalendarHour(Calendar cal, int hour) {
1238        cal.set(Calendar.HOUR_OF_DAY, hour);
1239        if (cal.get(Calendar.HOUR_OF_DAY) != hour && hour != 24) {
1240            cal.set(Calendar.HOUR_OF_DAY, hour + 1);
1241        }
1242    }
1243
1244    protected boolean isLeapYear(int year) {
1245        return ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0));
1246    }
1247
1248    protected int getLastDayOfMonth(int monthNum, int year) {
1249
1250        switch (monthNum) {
1251            case 1:
1252                return 31;
1253            case 2:
1254                return (isLeapYear(year)) ? 29 : 28;
1255            case 3:
1256                return 31;
1257            case 4:
1258                return 30;
1259            case 5:
1260                return 31;
1261            case 6:
1262                return 30;
1263            case 7:
1264                return 31;
1265            case 8:
1266                return 31;
1267            case 9:
1268                return 30;
1269            case 10:
1270                return 31;
1271            case 11:
1272                return 30;
1273            case 12:
1274                return 31;
1275            default:
1276                throw new IllegalArgumentException("Illegal month number: "
1277                        + monthNum);
1278        }
1279    }
1280
1281    private static class ValueSet {
1282        int value;
1283        int pos;
1284    }
1285
1286}
1287