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