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