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