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