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