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