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