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