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