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