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