001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements. See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache license, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License. You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the license for the specific language governing permissions and
015 * limitations under the license.
016 */
017package org.apache.logging.log4j.core.util.datetime;
018
019import java.io.IOException;
020import java.io.ObjectInputStream;
021import java.io.Serializable;
022import java.text.DateFormatSymbols;
023import java.text.ParseException;
024import java.text.ParsePosition;
025import java.util.ArrayList;
026import java.util.Calendar;
027import java.util.Comparator;
028import java.util.Date;
029import java.util.HashMap;
030import java.util.List;
031import java.util.ListIterator;
032import java.util.Locale;
033import java.util.Map;
034import java.util.Set;
035import java.util.TimeZone;
036import java.util.TreeSet;
037import java.util.concurrent.ConcurrentHashMap;
038import java.util.concurrent.ConcurrentMap;
039import java.util.regex.Matcher;
040import java.util.regex.Pattern;
041
042/**
043 * <p>FastDateParser is a fast and thread-safe version of
044 * {@link java.text.SimpleDateFormat}.</p>
045 *
046 * <p>To obtain a proxy to a FastDateParser, use {@link FastDateFormat#getInstance(String, TimeZone, Locale)}
047 * or another variation of the factory methods of {@link FastDateFormat}.</p>
048 *
049 * <p>Since FastDateParser is thread safe, you can use a static member instance:</p>
050 * <code>
051 *     private static final DateParser DATE_PARSER = FastDateFormat.getInstance("yyyy-MM-dd");
052 * </code>
053 *
054 * <p>This class can be used as a direct replacement for
055 * <code>SimpleDateFormat</code> in most parsing situations.
056 * This class is especially useful in multi-threaded server environments.
057 * <code>SimpleDateFormat</code> is not thread-safe in any JDK version,
058 * nor will it be as Sun has closed the
059 * <a href="http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4228335">bug</a>/RFE.
060 * </p>
061 *
062 * <p>Only parsing is supported by this class, but all patterns are compatible with
063 * SimpleDateFormat.</p>
064 *
065 * <p>The class operates in lenient mode, so for example a time of 90 minutes is treated as 1 hour 30 minutes.</p>
066 *
067 * <p>Timing tests indicate this class is as about as fast as SimpleDateFormat
068 * in single thread applications and about 25% faster in multi-thread applications.</p>
069 *
070 * <p>
071 * Copied and modified from <a href="https://commons.apache.org/proper/commons-lang/">Apache Commons Lang</a>.
072 * </p>
073 *
074 * @since Apache Commons Lang 3.2
075 * @see FastDatePrinter
076 */
077public class FastDateParser implements DateParser, Serializable {
078
079    /**
080     * Required for serialization support.
081     *
082     * @see java.io.Serializable
083     */
084    private static final long serialVersionUID = 3L;
085
086    static final Locale JAPANESE_IMPERIAL = new Locale("ja","JP","JP");
087
088    // defining fields
089    private final String pattern;
090    private final TimeZone timeZone;
091    private final Locale locale;
092    private final int century;
093    private final int startYear;
094
095    // derived fields
096    private transient List<StrategyAndWidth> patterns;
097
098    // comparator used to sort regex alternatives
099    // alternatives should be ordered longer first, and shorter last. ('february' before 'feb')
100    // all entries must be lowercase by locale.
101    private static final Comparator<String> LONGER_FIRST_LOWERCASE = new Comparator<String>() {
102        @Override
103        public int compare(final String left, final String right) {
104            return right.compareTo(left);
105        }
106    };
107
108    /**
109     * <p>Constructs a new FastDateParser.</p>
110     *
111     * Use {@link FastDateFormat#getInstance(String, TimeZone, Locale)} or another variation of the
112     * factory methods of {@link FastDateFormat} to get a cached FastDateParser instance.
113     *
114     * @param pattern non-null {@link java.text.SimpleDateFormat} compatible
115     *  pattern
116     * @param timeZone non-null time zone to use
117     * @param locale non-null locale
118     */
119    protected FastDateParser(final String pattern, final TimeZone timeZone, final Locale locale) {
120        this(pattern, timeZone, locale, null);
121    }
122
123    /**
124     * <p>Constructs a new FastDateParser.</p>
125     *
126     * @param pattern non-null {@link java.text.SimpleDateFormat} compatible
127     *  pattern
128     * @param timeZone non-null time zone to use
129     * @param locale non-null locale
130     * @param centuryStart The start of the century for 2 digit year parsing
131     *
132     * @since 3.5
133     */
134    protected FastDateParser(final String pattern, final TimeZone timeZone, final Locale locale, final Date centuryStart) {
135        this.pattern = pattern;
136        this.timeZone = timeZone;
137        this.locale = locale;
138
139        final Calendar definingCalendar = Calendar.getInstance(timeZone, locale);
140
141        int centuryStartYear;
142        if(centuryStart!=null) {
143            definingCalendar.setTime(centuryStart);
144            centuryStartYear= definingCalendar.get(Calendar.YEAR);
145        }
146        else if(locale.equals(JAPANESE_IMPERIAL)) {
147            centuryStartYear= 0;
148        }
149        else {
150            // from 80 years ago to 20 years from now
151            definingCalendar.setTime(new Date());
152            centuryStartYear= definingCalendar.get(Calendar.YEAR)-80;
153        }
154        century= centuryStartYear / 100 * 100;
155        startYear= centuryStartYear - century;
156
157        init(definingCalendar);
158    }
159
160    /**
161     * Initialize derived fields from defining fields.
162     * This is called from constructor and from readObject (de-serialization)
163     *
164     * @param definingCalendar the {@link java.util.Calendar} instance used to initialize this FastDateParser
165     */
166    private void init(final Calendar definingCalendar) {
167        patterns = new ArrayList<>();
168
169        final StrategyParser fm = new StrategyParser(definingCalendar);
170        for(;;) {
171            final StrategyAndWidth field = fm.getNextStrategy();
172            if(field==null) {
173                break;
174            }
175            patterns.add(field);
176        }
177    }
178
179    // helper classes to parse the format string
180    //-----------------------------------------------------------------------
181
182    /**
183     * Holds strategy and field width
184     */
185    private static class StrategyAndWidth {
186        final Strategy strategy;
187        final int width;
188
189        StrategyAndWidth(final Strategy strategy, final int width) {
190            this.strategy = strategy;
191            this.width = width;
192        }
193
194        int getMaxWidth(final ListIterator<StrategyAndWidth> lt) {
195            if(!strategy.isNumber() || !lt.hasNext()) {
196                return 0;
197            }
198            final Strategy nextStrategy = lt.next().strategy;
199            lt.previous();
200            return nextStrategy.isNumber() ?width :0;
201       }
202    }
203
204    /**
205     * Parse format into Strategies
206     */
207    private class StrategyParser {
208        final private Calendar definingCalendar;
209        private int currentIdx;
210
211        StrategyParser(final Calendar definingCalendar) {
212            this.definingCalendar = definingCalendar;
213        }
214
215        StrategyAndWidth getNextStrategy() {
216            if (currentIdx >= pattern.length()) {
217                return null;
218            }
219
220            final char c = pattern.charAt(currentIdx);
221            if (isFormatLetter(c)) {
222                return letterPattern(c);
223            }
224            return literal();
225        }
226
227        private StrategyAndWidth letterPattern(final char c) {
228            final int begin = currentIdx;
229            while (++currentIdx < pattern.length()) {
230                if (pattern.charAt(currentIdx) != c) {
231                    break;
232                }
233            }
234
235            final int width = currentIdx - begin;
236            return new StrategyAndWidth(getStrategy(c, width, definingCalendar), width);
237        }
238
239        private StrategyAndWidth literal() {
240            boolean activeQuote = false;
241
242            final StringBuilder sb = new StringBuilder();
243            while (currentIdx < pattern.length()) {
244                final char c = pattern.charAt(currentIdx);
245                if (!activeQuote && isFormatLetter(c)) {
246                    break;
247                } else if (c == '\'' && (++currentIdx == pattern.length() || pattern.charAt(currentIdx) != '\'')) {
248                    activeQuote = !activeQuote;
249                    continue;
250                }
251                ++currentIdx;
252                sb.append(c);
253            }
254
255            if (activeQuote) {
256                throw new IllegalArgumentException("Unterminated quote");
257            }
258
259            final String formatField = sb.toString();
260            return new StrategyAndWidth(new CopyQuotedStrategy(formatField), formatField.length());
261        }
262    }
263
264    private static boolean isFormatLetter(final char c) {
265        return c >= 'A' && c <= 'Z' || c >= 'a' && c <= 'z';
266    }
267
268    // Accessors
269    //-----------------------------------------------------------------------
270    /* (non-Javadoc)
271     * @see org.apache.commons.lang3.time.DateParser#getPattern()
272     */
273    @Override
274    public String getPattern() {
275        return pattern;
276    }
277
278    /* (non-Javadoc)
279     * @see org.apache.commons.lang3.time.DateParser#getTimeZone()
280     */
281    @Override
282    public TimeZone getTimeZone() {
283        return timeZone;
284    }
285
286    /* (non-Javadoc)
287     * @see org.apache.commons.lang3.time.DateParser#getLocale()
288     */
289    @Override
290    public Locale getLocale() {
291        return locale;
292    }
293
294
295    // Basics
296    //-----------------------------------------------------------------------
297    /**
298     * <p>Compare another object for equality with this object.</p>
299     *
300     * @param obj  the object to compare to
301     * @return <code>true</code>if equal to this instance
302     */
303    @Override
304    public boolean equals(final Object obj) {
305        if (!(obj instanceof FastDateParser)) {
306            return false;
307        }
308        final FastDateParser other = (FastDateParser) obj;
309        return pattern.equals(other.pattern)
310            && timeZone.equals(other.timeZone)
311            && locale.equals(other.locale);
312    }
313
314    /**
315     * <p>Return a hash code compatible with equals.</p>
316     *
317     * @return a hash code compatible with equals
318     */
319    @Override
320    public int hashCode() {
321        return pattern.hashCode() + 13 * (timeZone.hashCode() + 13 * locale.hashCode());
322    }
323
324    /**
325     * <p>Get a string version of this formatter.</p>
326     *
327     * @return a debugging string
328     */
329    @Override
330    public String toString() {
331        return "FastDateParser[" + pattern + "," + locale + "," + timeZone.getID() + "]";
332    }
333
334    // Serializing
335    //-----------------------------------------------------------------------
336    /**
337     * Create the object after serialization. This implementation reinitializes the
338     * transient properties.
339     *
340     * @param in ObjectInputStream from which the object is being deserialized.
341     * @throws IOException if there is an IO issue.
342     * @throws ClassNotFoundException if a class cannot be found.
343     */
344    private void readObject(final ObjectInputStream in) throws IOException, ClassNotFoundException {
345        in.defaultReadObject();
346
347        final Calendar definingCalendar = Calendar.getInstance(timeZone, locale);
348        init(definingCalendar);
349    }
350
351    /* (non-Javadoc)
352     * @see org.apache.commons.lang3.time.DateParser#parseObject(java.lang.String)
353     */
354    @Override
355    public Object parseObject(final String source) throws ParseException {
356        return parse(source);
357    }
358
359    /* (non-Javadoc)
360     * @see org.apache.commons.lang3.time.DateParser#parse(java.lang.String)
361     */
362    @Override
363    public Date parse(final String source) throws ParseException {
364        final ParsePosition pp = new ParsePosition(0);
365        final Date date= parse(source, pp);
366        if (date == null) {
367            // Add a note re supported date range
368            if (locale.equals(JAPANESE_IMPERIAL)) {
369                throw new ParseException(
370                        "(The " +locale + " locale does not support dates before 1868 AD)\n" +
371                                "Unparseable date: \""+source, pp.getErrorIndex());
372            }
373            throw new ParseException("Unparseable date: "+source, pp.getErrorIndex());
374        }
375        return date;
376    }
377
378    /* (non-Javadoc)
379     * @see org.apache.commons.lang3.time.DateParser#parseObject(java.lang.String, java.text.ParsePosition)
380     */
381    @Override
382    public Object parseObject(final String source, final ParsePosition pos) {
383        return parse(source, pos);
384    }
385
386    /**
387     * This implementation updates the ParsePosition if the parse succeeds.
388     * However, it sets the error index to the position before the failed field unlike
389     * the method {@link java.text.SimpleDateFormat#parse(String, ParsePosition)} which sets
390     * the error index to after the failed field.
391     * <p>
392     * To determine if the parse has succeeded, the caller must check if the current parse position
393     * given by {@link ParsePosition#getIndex()} has been updated. If the input buffer has been fully
394     * parsed, then the index will point to just after the end of the input buffer.
395     *
396     * @see org.apache.commons.lang3.time.DateParser#parse(java.lang.String, java.text.ParsePosition)
397     */
398    @Override
399    public Date parse(final String source, final ParsePosition pos) {
400        // timing tests indicate getting new instance is 19% faster than cloning
401        final Calendar cal= Calendar.getInstance(timeZone, locale);
402        cal.clear();
403
404        return parse(source, pos, cal) ? cal.getTime() : null;
405    }
406
407    /**
408     * Parse a formatted date string according to the format.  Updates the Calendar with parsed fields.
409     * Upon success, the ParsePosition index is updated to indicate how much of the source text was consumed.
410     * Not all source text needs to be consumed.  Upon parse failure, ParsePosition error index is updated to
411     * the offset of the source text which does not match the supplied format.
412     *
413     * @param source The text to parse.
414     * @param pos On input, the position in the source to start parsing, on output, updated position.
415     * @param calendar The calendar into which to set parsed fields.
416     * @return true, if source has been parsed (pos parsePosition is updated); otherwise false (and pos errorIndex is updated)
417     * @throws IllegalArgumentException when Calendar has been set to be not lenient, and a parsed field is
418     * out of range.
419     */
420    @Override
421    public boolean parse(final String source, final ParsePosition pos, final Calendar calendar) {
422        final ListIterator<StrategyAndWidth> lt = patterns.listIterator();
423        while (lt.hasNext()) {
424            final StrategyAndWidth strategyAndWidth = lt.next();
425            final int maxWidth = strategyAndWidth.getMaxWidth(lt);
426            if (!strategyAndWidth.strategy.parse(this, calendar, source, pos, maxWidth)) {
427                return false;
428            }
429        }
430        return true;
431    }
432
433    // Support for strategies
434    //-----------------------------------------------------------------------
435
436    private static StringBuilder simpleQuote(final StringBuilder sb, final String value) {
437        for (int i = 0; i < value.length(); ++i) {
438            final char c = value.charAt(i);
439            switch (c) {
440            case '\\':
441            case '^':
442            case '$':
443            case '.':
444            case '|':
445            case '?':
446            case '*':
447            case '+':
448            case '(':
449            case ')':
450            case '[':
451            case '{':
452                sb.append('\\');
453            default:
454                sb.append(c);
455            }
456        }
457        return sb;
458    }
459
460    /**
461     * Get the short and long values displayed for a field
462     * @param cal The calendar to obtain the short and long values
463     * @param locale The locale of display names
464     * @param field The field of interest
465     * @param regex The regular expression to build
466     * @return The map of string display names to field values
467     */
468    private static Map<String, Integer> appendDisplayNames(final Calendar cal, final Locale locale, final int field, final StringBuilder regex) {
469        final Map<String, Integer> values = new HashMap<>();
470
471        final Map<String, Integer> displayNames = cal.getDisplayNames(field, Calendar.ALL_STYLES, locale);
472        final TreeSet<String> sorted = new TreeSet<>(LONGER_FIRST_LOWERCASE);
473        for (final Map.Entry<String, Integer> displayName : displayNames.entrySet()) {
474            final String key = displayName.getKey().toLowerCase(locale);
475            if (sorted.add(key)) {
476                values.put(key, displayName.getValue());
477            }
478        }
479        for (final String symbol : sorted) {
480            simpleQuote(regex, symbol).append('|');
481        }
482        return values;
483    }
484
485    /**
486     * Adjust dates to be within appropriate century
487     * @param twoDigitYear The year to adjust
488     * @return A value between centuryStart(inclusive) to centuryStart+100(exclusive)
489     */
490    private int adjustYear(final int twoDigitYear) {
491        final int trial = century + twoDigitYear;
492        return twoDigitYear >= startYear ? trial : trial + 100;
493    }
494
495    /**
496     * A strategy to parse a single field from the parsing pattern
497     */
498    private static abstract class Strategy {
499        /**
500         * Is this field a number?
501         * The default implementation returns false.
502         *
503         * @return true, if field is a number
504         */
505        boolean isNumber() {
506            return false;
507        }
508
509        abstract boolean parse(FastDateParser parser, Calendar calendar, String source, ParsePosition pos, int maxWidth);
510    }
511
512    /**
513     * A strategy to parse a single field from the parsing pattern
514     */
515    private static abstract class PatternStrategy extends Strategy {
516
517        private Pattern pattern;
518
519        void createPattern(final StringBuilder regex) {
520            createPattern(regex.toString());
521        }
522
523        void createPattern(final String regex) {
524            this.pattern = Pattern.compile(regex);
525        }
526
527        /**
528         * Is this field a number?
529         * The default implementation returns false.
530         *
531         * @return true, if field is a number
532         */
533        @Override
534        boolean isNumber() {
535            return false;
536        }
537
538        @Override
539        boolean parse(final FastDateParser parser, final Calendar calendar, final String source, final ParsePosition pos, final int maxWidth) {
540            final Matcher matcher = pattern.matcher(source.substring(pos.getIndex()));
541            if (!matcher.lookingAt()) {
542                pos.setErrorIndex(pos.getIndex());
543                return false;
544            }
545            pos.setIndex(pos.getIndex() + matcher.end(1));
546            setCalendar(parser, calendar, matcher.group(1));
547            return true;
548        }
549
550        abstract void setCalendar(FastDateParser parser, Calendar cal, String value);
551    }
552
553    /**
554     * Obtain a Strategy given a field from a SimpleDateFormat pattern
555     * @param formatField A sub-sequence of the SimpleDateFormat pattern
556     * @param definingCalendar The calendar to obtain the short and long values
557     * @return The Strategy that will handle parsing for the field
558     */
559    private Strategy getStrategy(final char f, final int width, final Calendar definingCalendar) {
560        switch(f) {
561        default:
562            throw new IllegalArgumentException("Format '"+f+"' not supported");
563        case 'D':
564            return DAY_OF_YEAR_STRATEGY;
565        case 'E':
566            return getLocaleSpecificStrategy(Calendar.DAY_OF_WEEK, definingCalendar);
567        case 'F':
568            return DAY_OF_WEEK_IN_MONTH_STRATEGY;
569        case 'G':
570            return getLocaleSpecificStrategy(Calendar.ERA, definingCalendar);
571        case 'H':  // Hour in day (0-23)
572            return HOUR_OF_DAY_STRATEGY;
573        case 'K':  // Hour in am/pm (0-11)
574            return HOUR_STRATEGY;
575        case 'M':
576            return width >= 3 ? getLocaleSpecificStrategy(Calendar.MONTH, definingCalendar) : NUMBER_MONTH_STRATEGY;
577        case 'S':
578            return MILLISECOND_STRATEGY;
579        case 'W':
580            return WEEK_OF_MONTH_STRATEGY;
581        case 'a':
582            return getLocaleSpecificStrategy(Calendar.AM_PM, definingCalendar);
583        case 'd':
584            return DAY_OF_MONTH_STRATEGY;
585        case 'h':  // Hour in am/pm (1-12), i.e. midday/midnight is 12, not 0
586            return HOUR12_STRATEGY;
587        case 'k':  // Hour in day (1-24), i.e. midnight is 24, not 0
588            return HOUR24_OF_DAY_STRATEGY;
589        case 'm':
590            return MINUTE_STRATEGY;
591        case 's':
592            return SECOND_STRATEGY;
593        case 'u':
594            return DAY_OF_WEEK_STRATEGY;
595        case 'w':
596            return WEEK_OF_YEAR_STRATEGY;
597        case 'y':
598        case 'Y':
599            return width > 2 ? LITERAL_YEAR_STRATEGY : ABBREVIATED_YEAR_STRATEGY;
600        case 'X':
601            return ISO8601TimeZoneStrategy.getStrategy(width);
602        case 'Z':
603            if (width==2) {
604                return ISO8601TimeZoneStrategy.ISO_8601_3_STRATEGY;
605            }
606            //$FALL-THROUGH$
607        case 'z':
608            return getLocaleSpecificStrategy(Calendar.ZONE_OFFSET, definingCalendar);
609        }
610    }
611
612    @SuppressWarnings("unchecked") // OK because we are creating an array with no entries
613    private static final ConcurrentMap<Locale, Strategy>[] caches = new ConcurrentMap[Calendar.FIELD_COUNT];
614
615    /**
616     * Get a cache of Strategies for a particular field
617     * @param field The Calendar field
618     * @return a cache of Locale to Strategy
619     */
620    private static ConcurrentMap<Locale, Strategy> getCache(final int field) {
621        synchronized (caches) {
622            if (caches[field] == null) {
623                caches[field] = new ConcurrentHashMap<>(3);
624            }
625            return caches[field];
626        }
627    }
628
629    /**
630     * Construct a Strategy that parses a Text field
631     * @param field The Calendar field
632     * @param definingCalendar The calendar to obtain the short and long values
633     * @return a TextStrategy for the field and Locale
634     */
635    private Strategy getLocaleSpecificStrategy(final int field, final Calendar definingCalendar) {
636        final ConcurrentMap<Locale, Strategy> cache = getCache(field);
637        Strategy strategy = cache.get(locale);
638        if (strategy == null) {
639            strategy = field == Calendar.ZONE_OFFSET
640                    ? new TimeZoneStrategy(locale)
641                    : new CaseInsensitiveTextStrategy(field, definingCalendar, locale);
642            final Strategy inCache = cache.putIfAbsent(locale, strategy);
643            if (inCache != null) {
644                return inCache;
645            }
646        }
647        return strategy;
648    }
649
650    /**
651     * A strategy that copies the static or quoted field in the parsing pattern
652     */
653    private static class CopyQuotedStrategy extends Strategy {
654
655        final private String formatField;
656
657        /**
658         * Construct a Strategy that ensures the formatField has literal text
659         * @param formatField The literal text to match
660         */
661        CopyQuotedStrategy(final String formatField) {
662            this.formatField = formatField;
663        }
664
665        /**
666         * {@inheritDoc}
667         */
668        @Override
669        boolean isNumber() {
670            return false;
671        }
672
673        @Override
674        boolean parse(final FastDateParser parser, final Calendar calendar, final String source, final ParsePosition pos, final int maxWidth) {
675            for (int idx = 0; idx < formatField.length(); ++idx) {
676                final int sIdx = idx + pos.getIndex();
677                if (sIdx == source.length()) {
678                    pos.setErrorIndex(sIdx);
679                    return false;
680                }
681                if (formatField.charAt(idx) != source.charAt(sIdx)) {
682                    pos.setErrorIndex(sIdx);
683                    return false;
684                }
685            }
686            pos.setIndex(formatField.length() + pos.getIndex());
687            return true;
688        }
689    }
690
691    /**
692     * A strategy that handles a text field in the parsing pattern
693     */
694     private static class CaseInsensitiveTextStrategy extends PatternStrategy {
695        private final int field;
696        final Locale locale;
697        private final Map<String, Integer> lKeyValues;
698
699        /**
700         * Construct a Strategy that parses a Text field
701         * @param field  The Calendar field
702         * @param definingCalendar  The Calendar to use
703         * @param locale  The Locale to use
704         */
705        CaseInsensitiveTextStrategy(final int field, final Calendar definingCalendar, final Locale locale) {
706            this.field = field;
707            this.locale = locale;
708
709            final StringBuilder regex = new StringBuilder();
710            regex.append("((?iu)");
711            lKeyValues = appendDisplayNames(definingCalendar, locale, field, regex);
712            regex.setLength(regex.length()-1);
713            regex.append(")");
714            createPattern(regex);
715        }
716
717        /**
718         * {@inheritDoc}
719         */
720        @Override
721        void setCalendar(final FastDateParser parser, final Calendar cal, final String value) {
722            final Integer iVal = lKeyValues.get(value.toLowerCase(locale));
723            cal.set(field, iVal.intValue());
724        }
725    }
726
727
728    /**
729     * A strategy that handles a number field in the parsing pattern
730     */
731    private static class NumberStrategy extends Strategy {
732        private final int field;
733
734        /**
735         * Construct a Strategy that parses a Number field
736         * @param field The Calendar field
737         */
738        NumberStrategy(final int field) {
739             this.field= field;
740        }
741
742        /**
743         * {@inheritDoc}
744         */
745        @Override
746        boolean isNumber() {
747            return true;
748        }
749
750        @Override
751        boolean parse(final FastDateParser parser, final Calendar calendar, final String source, final ParsePosition pos, final int maxWidth) {
752            int idx = pos.getIndex();
753            int last = source.length();
754
755            if (maxWidth == 0) {
756                // if no maxWidth, strip leading white space
757                for (; idx < last; ++idx) {
758                    final char c = source.charAt(idx);
759                    if (!Character.isWhitespace(c)) {
760                        break;
761                    }
762                }
763                pos.setIndex(idx);
764            } else {
765                final int end = idx + maxWidth;
766                if (last > end) {
767                    last = end;
768                }
769            }
770
771            for (; idx < last; ++idx) {
772                final char c = source.charAt(idx);
773                if (!Character.isDigit(c)) {
774                    break;
775                }
776            }
777
778            if (pos.getIndex() == idx) {
779                pos.setErrorIndex(idx);
780                return false;
781            }
782
783            final int value = Integer.parseInt(source.substring(pos.getIndex(), idx));
784            pos.setIndex(idx);
785
786            calendar.set(field, modify(parser, value));
787            return true;
788        }
789
790        /**
791         * Make any modifications to parsed integer
792         * @param parser The parser
793         * @param iValue The parsed integer
794         * @return The modified value
795         */
796        int modify(final FastDateParser parser, final int iValue) {
797            return iValue;
798        }
799
800    }
801
802    private static final Strategy ABBREVIATED_YEAR_STRATEGY = new NumberStrategy(Calendar.YEAR) {
803        /**
804         * {@inheritDoc}
805         */
806        @Override
807        int modify(final FastDateParser parser, final int iValue) {
808            return iValue < 100 ? parser.adjustYear(iValue) : iValue;
809        }
810    };
811
812    /**
813     * A strategy that handles a timezone field in the parsing pattern
814     */
815    static class TimeZoneStrategy extends PatternStrategy {
816        private static final String RFC_822_TIME_ZONE = "[+-]\\d{4}";
817        private static final String GMT_OPTION= "GMT[+-]\\d{1,2}:\\d{2}";
818
819        private final Locale locale;
820        private final Map<String, TzInfo> tzNames= new HashMap<>();
821
822        private static class TzInfo {
823            TimeZone zone;
824            int dstOffset;
825
826            TzInfo(final TimeZone tz, final boolean useDst) {
827                zone = tz;
828                dstOffset = useDst ?tz.getDSTSavings() :0;
829            }
830        }
831
832        /**
833         * Index of zone id
834         */
835        private static final int ID = 0;
836
837        /**
838         * Construct a Strategy that parses a TimeZone
839         * @param locale The Locale
840         */
841        TimeZoneStrategy(final Locale locale) {
842            this.locale = locale;
843
844            final StringBuilder sb = new StringBuilder();
845            sb.append("((?iu)" + RFC_822_TIME_ZONE + "|" + GMT_OPTION );
846
847            final Set<String> sorted = new TreeSet<>(LONGER_FIRST_LOWERCASE);
848
849            final String[][] zones = DateFormatSymbols.getInstance(locale).getZoneStrings();
850            for (final String[] zoneNames : zones) {
851                // offset 0 is the time zone ID and is not localized
852                final String tzId = zoneNames[ID];
853                if (tzId.equalsIgnoreCase("GMT")) {
854                    continue;
855                }
856                final TimeZone tz = TimeZone.getTimeZone(tzId);
857                // offset 1 is long standard name
858                // offset 2 is short standard name
859                final TzInfo standard = new TzInfo(tz, false);
860                TzInfo tzInfo = standard;
861                for (int i = 1; i < zoneNames.length; ++i) {
862                    switch (i) {
863                    case 3: // offset 3 is long daylight savings (or summertime) name
864                            // offset 4 is the short summertime name
865                        tzInfo = new TzInfo(tz, true);
866                        break;
867                    case 5: // offset 5 starts additional names, probably standard time
868                        tzInfo = standard;
869                        break;
870                    }
871                    if (zoneNames[i] != null) {
872                        final String key = zoneNames[i].toLowerCase(locale);
873                        // ignore the data associated with duplicates supplied in
874                        // the additional names
875                        if (sorted.add(key)) {
876                            tzNames.put(key, tzInfo);
877                        }
878                    }
879                }
880            }
881            // order the regex alternatives with longer strings first, greedy
882            // match will ensure longest string will be consumed
883            for (final String zoneName : sorted) {
884                simpleQuote(sb.append('|'), zoneName);
885            }
886            sb.append(")");
887            createPattern(sb);
888        }
889
890        /**
891         * {@inheritDoc}
892         */
893        @Override
894        void setCalendar(final FastDateParser parser, final Calendar cal, final String value) {
895            if (value.charAt(0) == '+' || value.charAt(0) == '-') {
896                final TimeZone tz = TimeZone.getTimeZone("GMT" + value);
897                cal.setTimeZone(tz);
898            } else if (value.regionMatches(true, 0, "GMT", 0, 3)) {
899                final TimeZone tz = TimeZone.getTimeZone(value.toUpperCase());
900                cal.setTimeZone(tz);
901            } else {
902                final TzInfo tzInfo = tzNames.get(value.toLowerCase(locale));
903                cal.set(Calendar.DST_OFFSET, tzInfo.dstOffset);
904                cal.set(Calendar.ZONE_OFFSET, tzInfo.zone.getRawOffset());
905            }
906        }
907    }
908
909    private static class ISO8601TimeZoneStrategy extends PatternStrategy {
910        // Z, +hh, -hh, +hhmm, -hhmm, +hh:mm or -hh:mm
911
912        /**
913         * Construct a Strategy that parses a TimeZone
914         * @param pattern The Pattern
915         */
916        ISO8601TimeZoneStrategy(final String pattern) {
917            createPattern(pattern);
918        }
919
920        /**
921         * {@inheritDoc}
922         */
923        @Override
924        void setCalendar(final FastDateParser parser, final Calendar cal, final String value) {
925            if (value.equals("Z")) {
926                cal.setTimeZone(TimeZone.getTimeZone("UTC"));
927            } else {
928                cal.setTimeZone(TimeZone.getTimeZone("GMT" + value));
929            }
930        }
931
932        private static final Strategy ISO_8601_1_STRATEGY = new ISO8601TimeZoneStrategy("(Z|(?:[+-]\\d{2}))");
933        private static final Strategy ISO_8601_2_STRATEGY = new ISO8601TimeZoneStrategy("(Z|(?:[+-]\\d{2}\\d{2}))");
934        private static final Strategy ISO_8601_3_STRATEGY = new ISO8601TimeZoneStrategy("(Z|(?:[+-]\\d{2}(?::)\\d{2}))");
935
936        /**
937         * Factory method for ISO8601TimeZoneStrategies.
938         *
939         * @param tokenLen a token indicating the length of the TimeZone String to be formatted.
940         * @return a ISO8601TimeZoneStrategy that can format TimeZone String of length {@code tokenLen}. If no such
941         *          strategy exists, an IllegalArgumentException will be thrown.
942         */
943        static Strategy getStrategy(final int tokenLen) {
944            switch(tokenLen) {
945            case 1:
946                return ISO_8601_1_STRATEGY;
947            case 2:
948                return ISO_8601_2_STRATEGY;
949            case 3:
950                return ISO_8601_3_STRATEGY;
951            default:
952                throw new IllegalArgumentException("invalid number of X");
953            }
954        }
955    }
956
957    private static final Strategy NUMBER_MONTH_STRATEGY = new NumberStrategy(Calendar.MONTH) {
958        @Override
959        int modify(final FastDateParser parser, final int iValue) {
960            return iValue-1;
961        }
962    };
963    private static final Strategy LITERAL_YEAR_STRATEGY = new NumberStrategy(Calendar.YEAR);
964    private static final Strategy WEEK_OF_YEAR_STRATEGY = new NumberStrategy(Calendar.WEEK_OF_YEAR);
965    private static final Strategy WEEK_OF_MONTH_STRATEGY = new NumberStrategy(Calendar.WEEK_OF_MONTH);
966    private static final Strategy DAY_OF_YEAR_STRATEGY = new NumberStrategy(Calendar.DAY_OF_YEAR);
967    private static final Strategy DAY_OF_MONTH_STRATEGY = new NumberStrategy(Calendar.DAY_OF_MONTH);
968    private static final Strategy DAY_OF_WEEK_STRATEGY = new NumberStrategy(Calendar.DAY_OF_WEEK) {
969        @Override
970        int modify(final FastDateParser parser, final int iValue) {
971            return iValue != 7 ? iValue + 1 : Calendar.SUNDAY;
972        }
973    };
974    private static final Strategy DAY_OF_WEEK_IN_MONTH_STRATEGY = new NumberStrategy(Calendar.DAY_OF_WEEK_IN_MONTH);
975    private static final Strategy HOUR_OF_DAY_STRATEGY = new NumberStrategy(Calendar.HOUR_OF_DAY);
976    private static final Strategy HOUR24_OF_DAY_STRATEGY = new NumberStrategy(Calendar.HOUR_OF_DAY) {
977        @Override
978        int modify(final FastDateParser parser, final int iValue) {
979            return iValue == 24 ? 0 : iValue;
980        }
981    };
982    private static final Strategy HOUR12_STRATEGY = new NumberStrategy(Calendar.HOUR) {
983        @Override
984        int modify(final FastDateParser parser, final int iValue) {
985            return iValue == 12 ? 0 : iValue;
986        }
987    };
988    private static final Strategy HOUR_STRATEGY = new NumberStrategy(Calendar.HOUR);
989    private static final Strategy MINUTE_STRATEGY = new NumberStrategy(Calendar.MINUTE);
990    private static final Strategy SECOND_STRATEGY = new NumberStrategy(Calendar.SECOND);
991    private static final Strategy MILLISECOND_STRATEGY = new NumberStrategy(Calendar.MILLISECOND);
992}