001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.logging.log4j.core.util.datetime;
018
019import java.io.IOException;
020import java.io.ObjectInputStream;
021import java.io.Serializable;
022import java.text.DateFormatSymbols;
023import java.text.ParseException;
024import java.text.ParsePosition;
025import java.util.ArrayList;
026import java.util.Calendar;
027import java.util.Date;
028import java.util.HashMap;
029import java.util.List;
030import java.util.Locale;
031import java.util.Map;
032import java.util.TimeZone;
033import java.util.concurrent.ConcurrentHashMap;
034import java.util.concurrent.ConcurrentMap;
035import java.util.regex.Matcher;
036import java.util.regex.Pattern;
037
038/**
039 * Copied from Commons Lang 3
040 */
041public class FastDateParser implements DateParser, Serializable {
042    /**
043     * Required for serialization support.
044     *
045     * @see java.io.Serializable
046     */
047    private static final long serialVersionUID = 3L;
048
049    static final Locale JAPANESE_IMPERIAL = new Locale("ja","JP","JP");
050
051    // defining fields
052    private final String pattern;
053    private final TimeZone timeZone;
054    private final Locale locale;
055    private final int century;
056    private final int startYear;
057    private final boolean lenient;
058
059    // derived fields
060    private transient Pattern parsePattern;
061    private transient Strategy[] strategies;
062
063    // dynamic fields to communicate with Strategy
064    private transient String currentFormatField;
065    private transient Strategy nextStrategy;
066
067    /**
068     * <p>Constructs a new FastDateParser.</p>
069     * 
070     * Use {@link FastDateFormat#getInstance(String, TimeZone, Locale)} or another variation of the 
071     * factory methods of {@link FastDateFormat} to get a cached FastDateParser instance.
072     *
073     * @param pattern non-null {@link java.text.SimpleDateFormat} compatible
074     *  pattern
075     * @param timeZone non-null time zone to use
076     * @param locale non-null locale
077     */
078    protected FastDateParser(final String pattern, final TimeZone timeZone, final Locale locale) {
079        this(pattern, timeZone, locale, null, true);
080    }
081
082    /**
083     * <p>Constructs a new FastDateParser.</p>
084     *
085     * @param pattern non-null {@link java.text.SimpleDateFormat} compatible
086     *  pattern
087     * @param timeZone non-null time zone to use
088     * @param locale non-null locale
089     * @param centuryStart The start of the century for 2 digit year parsing
090     *
091     * @since 3.3
092     */
093    protected FastDateParser(final String pattern, final TimeZone timeZone, final Locale locale, final Date centuryStart) {
094        this(pattern, timeZone, locale, centuryStart, true);
095    }
096
097    /**
098     * <p>Constructs a new FastDateParser.</p>
099     *
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}