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