[LOG4J2-1712] Pick up bug fixes from Apache Commons Lang's
org.apache.commons.lang3.time package.

Project: http://git-wip-us.apache.org/repos/asf/logging-log4j2/repo
Commit: http://git-wip-us.apache.org/repos/asf/logging-log4j2/commit/8af687f4
Tree: http://git-wip-us.apache.org/repos/asf/logging-log4j2/tree/8af687f4
Diff: http://git-wip-us.apache.org/repos/asf/logging-log4j2/diff/8af687f4

Branch: refs/heads/master
Commit: 8af687f48297950be8d29bcd143a4ba622732b7d
Parents: 220aeb7
Author: Gary Gregory <[email protected]>
Authored: Thu Nov 17 09:43:14 2016 -0800
Committer: Gary Gregory <[email protected]>
Committed: Thu Nov 17 09:43:14 2016 -0800

----------------------------------------------------------------------
 .../log4j/core/util/datetime/DateParser.java    |  25 +-
 .../core/util/datetime/FastDateFormat.java      |   5 +
 .../core/util/datetime/FastDateParser.java      | 898 ++++++++++---------
 .../util/datetime/FastDateParserSDFTest.java    | 232 +++++
 .../core/util/datetime/FastDateParserTest.java  | 720 +++++++++++++++
 .../datetime/FastDateParser_MoreOrLessTest.java | 115 +++
 .../FastDateParser_TimeZoneStrategyTest.java    |  69 ++
 src/changes/changes.xml                         |   3 +
 8 files changed, 1627 insertions(+), 440 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/logging-log4j2/blob/8af687f4/log4j-core/src/main/java/org/apache/logging/log4j/core/util/datetime/DateParser.java
----------------------------------------------------------------------
diff --git 
a/log4j-core/src/main/java/org/apache/logging/log4j/core/util/datetime/DateParser.java
 
b/log4j-core/src/main/java/org/apache/logging/log4j/core/util/datetime/DateParser.java
index 1823bab..f21a527 100644
--- 
a/log4j-core/src/main/java/org/apache/logging/log4j/core/util/datetime/DateParser.java
+++ 
b/log4j-core/src/main/java/org/apache/logging/log4j/core/util/datetime/DateParser.java
@@ -18,6 +18,7 @@ package org.apache.logging.log4j.core.util.datetime;
 
 import java.text.ParseException;
 import java.text.ParsePosition;
+import java.util.Calendar;
 import java.util.Date;
 import java.util.Locale;
 import java.util.TimeZone;
@@ -50,17 +51,32 @@ public interface DateParser {
      */
     Date parse(String source, ParsePosition pos);
 
+    /**
+     * Parses a formatted date string according to the format.  Updates the 
Calendar with parsed fields.
+     * Upon success, the ParsePosition index is updated to indicate how much 
of the source text was consumed.
+     * Not all source text needs to be consumed.  Upon parse failure, 
ParsePosition error index is updated to
+     * the offset of the source text which does not match the supplied format.
+     *
+     * @param source The text to parse.
+     * @param pos On input, the position in the source to start parsing, on 
output, updated position.
+     * @param calendar The calendar into which to set parsed fields.
+     * @return true, if source has been parsed (pos parsePosition is updated); 
otherwise false (and pos errorIndex is updated)
+     * @throws IllegalArgumentException when Calendar has been set to be not 
lenient, and a parsed field is
+     * out of range.
+     */
+    boolean parse(String source, ParsePosition pos, Calendar calendar);
+
     // Accessors
     // -----------------------------------------------------------------------
     /**
-     * Get the pattern used by this parser.
+     * Gets the pattern used by this parser.
      * 
      * @return the pattern, {@link java.text.SimpleDateFormat} compatible
      */
     String getPattern();
 
     /**
-     * Get the time zone used by this parser.
+     * Gets the time zone used by this parser.
      * 
      * <p>
      * The default {@link TimeZone} used to create a {@link Date} when the 
{@link TimeZone} is not specified by the
@@ -72,7 +88,7 @@ public interface DateParser {
     TimeZone getTimeZone();
 
     /**
-     * Get the locale used by this parser.
+     * Gets the locale used by this parser.
      * 
      * @return the locale
      */
@@ -89,7 +105,7 @@ public interface DateParser {
     Object parseObject(String source) throws ParseException;
 
     /**
-     * Parse a date/time string according to the given parse position.
+     * Parses a date/time string according to the given parse position.
      * 
      * @param source A <code>String</code> whose beginning should be parsed.
      * @param pos the parse position
@@ -97,4 +113,5 @@ public interface DateParser {
      * @see java.text.DateFormat#parseObject(String, ParsePosition)
      */
     Object parseObject(String source, ParsePosition pos);
+
 }

http://git-wip-us.apache.org/repos/asf/logging-log4j2/blob/8af687f4/log4j-core/src/main/java/org/apache/logging/log4j/core/util/datetime/FastDateFormat.java
----------------------------------------------------------------------
diff --git 
a/log4j-core/src/main/java/org/apache/logging/log4j/core/util/datetime/FastDateFormat.java
 
b/log4j-core/src/main/java/org/apache/logging/log4j/core/util/datetime/FastDateFormat.java
index 848715e..b6ff2c1 100644
--- 
a/log4j-core/src/main/java/org/apache/logging/log4j/core/util/datetime/FastDateFormat.java
+++ 
b/log4j-core/src/main/java/org/apache/logging/log4j/core/util/datetime/FastDateFormat.java
@@ -607,4 +607,9 @@ public class FastDateFormat extends Format implements 
DatePrinter, DateParser, S
         return printer.applyRules(calendar, buf);
     }
 
+    @Override
+    public boolean parse(String source, ParsePosition pos, Calendar calendar) {
+        return parser.parse(source, pos, calendar);
+    }
+
 }

http://git-wip-us.apache.org/repos/asf/logging-log4j2/blob/8af687f4/log4j-core/src/main/java/org/apache/logging/log4j/core/util/datetime/FastDateParser.java
----------------------------------------------------------------------
diff --git 
a/log4j-core/src/main/java/org/apache/logging/log4j/core/util/datetime/FastDateParser.java
 
b/log4j-core/src/main/java/org/apache/logging/log4j/core/util/datetime/FastDateParser.java
index 7c21aed..13b00fa 100644
--- 
a/log4j-core/src/main/java/org/apache/logging/log4j/core/util/datetime/FastDateParser.java
+++ 
b/log4j-core/src/main/java/org/apache/logging/log4j/core/util/datetime/FastDateParser.java
@@ -24,80 +24,62 @@ import java.text.ParseException;
 import java.text.ParsePosition;
 import java.util.ArrayList;
 import java.util.Calendar;
+import java.util.Comparator;
 import java.util.Date;
 import java.util.HashMap;
 import java.util.List;
+import java.util.ListIterator;
 import java.util.Locale;
 import java.util.Map;
+import java.util.Set;
 import java.util.TimeZone;
+import java.util.TreeSet;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.ConcurrentMap;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
 /**
- * Copied from Commons Lang 3.
+ * <p>FastDateParser is a fast and thread-safe version of
+ * {@link java.text.SimpleDateFormat}.</p>
+ *
+ * <p>To obtain a proxy to a FastDateParser, use {@link 
FastDateFormat#getInstance(String, TimeZone, Locale)} 
+ * or another variation of the factory methods of {@link FastDateFormat}.</p>
+ * 
+ * <p>Since FastDateParser is thread safe, you can use a static member 
instance:</p>
+ * <code>
+ *     private static final DateParser DATE_PARSER = 
FastDateFormat.getInstance("yyyy-MM-dd");
+ * </code>
+ * 
+ * <p>This class can be used as a direct replacement for
+ * <code>SimpleDateFormat</code> in most parsing situations.
+ * This class is especially useful in multi-threaded server environments.
+ * <code>SimpleDateFormat</code> is not thread-safe in any JDK version,
+ * nor will it be as Sun has closed the
+ * <a 
href="http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4228335";>bug</a>/RFE.
+ * </p>
+ *
+ * <p>Only parsing is supported by this class, but all patterns are compatible 
with
+ * SimpleDateFormat.</p>
+ *
+ * <p>The class operates in lenient mode, so for example a time of 90 minutes 
is treated as 1 hour 30 minutes.</p>
+ *
+ * <p>Timing tests indicate this class is as about as fast as SimpleDateFormat
+ * in single thread applications and about 25% faster in multi-thread 
applications.</p>
+ *
+ * @since 3.2
+ * @see FastDatePrinter
  */
 public class FastDateParser implements DateParser, Serializable {
 
     /**
-     * Japanese locale support.
-     */
-    static final Locale JAPANESE_IMPERIAL = new Locale("ja", "JP", "JP");
-
-    /**
      * Required for serialization support.
      *
      * @see java.io.Serializable
      */
     private static final long serialVersionUID = 3L;
 
-    private static final Strategy NUMBER_MONTH_STRATEGY = new 
NumberStrategy(Calendar.MONTH) {
-        @Override
-        int modify(final int iValue) {
-            return iValue - 1;
-        }
-    };
-
-    private static final Strategy ABBREVIATED_YEAR_STRATEGY = new 
NumberStrategy(Calendar.YEAR) {
-        /**
-         * {@inheritDoc}
-         */
-        @Override
-        void setCalendar(final FastDateParser parser, final Calendar cal, 
final String value) {
-            int iValue = Integer.parseInt(value);
-            if (iValue < 100) {
-                iValue = parser.adjustYear(iValue);
-            }
-            cal.set(Calendar.YEAR, iValue);
-        }
-    };
-
-    private static final Strategy LITERAL_YEAR_STRATEGY = new 
NumberStrategy(Calendar.YEAR);
-    private static final Strategy WEEK_OF_YEAR_STRATEGY = new 
NumberStrategy(Calendar.WEEK_OF_YEAR);
-    private static final Strategy WEEK_OF_MONTH_STRATEGY = new 
NumberStrategy(Calendar.WEEK_OF_MONTH);
-    private static final Strategy DAY_OF_YEAR_STRATEGY = new 
NumberStrategy(Calendar.DAY_OF_YEAR);
-    private static final Strategy DAY_OF_MONTH_STRATEGY = new 
NumberStrategy(Calendar.DAY_OF_MONTH);
-    private static final Strategy DAY_OF_WEEK_IN_MONTH_STRATEGY = new 
NumberStrategy(Calendar.DAY_OF_WEEK_IN_MONTH);
-    private static final Strategy DAY_OF_WEEK_STRATEGY = new 
NumberStrategy(Calendar.DAY_OF_WEEK);
-    private static final Strategy HOUR_OF_DAY_STRATEGY = new 
NumberStrategy(Calendar.HOUR_OF_DAY);
-    private static final Strategy HOUR24_OF_DAY_STRATEGY = new 
NumberStrategy(Calendar.HOUR_OF_DAY) {
-        @Override
-        int modify(final int iValue) {
-            return iValue == 24 ? 0 : iValue;
-        }
-    };
-    private static final Strategy HOUR12_STRATEGY = new 
NumberStrategy(Calendar.HOUR) {
-        @Override
-        int modify(final int iValue) {
-            return iValue == 12 ? 0 : iValue;
-        }
-    };
-    private static final Strategy HOUR_STRATEGY = new 
NumberStrategy(Calendar.HOUR);
-    private static final Strategy MINUTE_STRATEGY = new 
NumberStrategy(Calendar.MINUTE);
-    private static final Strategy SECOND_STRATEGY = new 
NumberStrategy(Calendar.SECOND);
-    private static final Strategy MILLISECOND_STRATEGY = new 
NumberStrategy(Calendar.MILLISECOND);
-    private static final Strategy ISO_8601_STRATEGY = new 
ISO8601TimeZoneStrategy("(Z|(?:[+-]\\d{2}(?::?\\d{2})?))");
+    static final Locale JAPANESE_IMPERIAL = new Locale("ja","JP","JP");
 
     // defining fields
     private final String pattern;
@@ -105,138 +87,183 @@ public class FastDateParser implements DateParser, 
Serializable {
     private final Locale locale;
     private final int century;
     private final int startYear;
-    private final boolean lenient;
 
     // derived fields
-    private transient Pattern parsePattern;
-    private transient Strategy[] strategies;
+    private transient List<StrategyAndWidth> patterns;
 
-    // dynamic fields to communicate with Strategy
-    private transient String currentFormatField;
-    private transient Strategy nextStrategy;
+    // comparator used to sort regex alternatives
+    // alternatives should be ordered longer first, and shorter last. 
('february' before 'feb')
+    // all entries must be lowercase by locale.
+    private static final Comparator<String> LONGER_FIRST_LOWERCASE = new 
Comparator<String>() {
+        @Override
+        public int compare(final String left, final String right) {
+            return right.compareTo(left);
+        }
+    };
 
     /**
-     * <p>
-     * Constructs a new FastDateParser.
-     * </p>
-     *
-     * Use {@link FastDateFormat#getInstance(String, TimeZone, Locale)} or 
another variation of the factory methods of
-     * {@link FastDateFormat} to get a cached FastDateParser instance.
+     * <p>Constructs a new FastDateParser.</p>
+     * 
+     * Use {@link FastDateFormat#getInstance(String, TimeZone, Locale)} or 
another variation of the 
+     * factory methods of {@link FastDateFormat} to get a cached 
FastDateParser instance.
      *
-     * @param pattern non-null {@link java.text.SimpleDateFormat} compatible 
pattern
+     * @param pattern non-null {@link java.text.SimpleDateFormat} compatible
+     *  pattern
      * @param timeZone non-null time zone to use
      * @param locale non-null locale
      */
     protected FastDateParser(final String pattern, final TimeZone timeZone, 
final Locale locale) {
-        this(pattern, timeZone, locale, null, true);
-    }
-
-    /**
-     * <p>
-     * Constructs a new FastDateParser.
-     * </p>
-     *
-     * @param pattern non-null {@link java.text.SimpleDateFormat} compatible 
pattern
-     * @param timeZone non-null time zone to use
-     * @param locale non-null locale
-     * @param centuryStart The start of the century for 2 digit year parsing
-     *
-     * @since 3.3
-     */
-    protected FastDateParser(final String pattern, final TimeZone timeZone, 
final Locale locale, final Date centuryStart) {
-        this(pattern, timeZone, locale, centuryStart, true);
+        this(pattern, timeZone, locale, null);
     }
 
     /**
-     * <p>
-     * Constructs a new FastDateParser.
-     * </p>
+     * <p>Constructs a new FastDateParser.</p>
      *
-     * @param pattern non-null {@link java.text.SimpleDateFormat} compatible 
pattern
+     * @param pattern non-null {@link java.text.SimpleDateFormat} compatible
+     *  pattern
      * @param timeZone non-null time zone to use
      * @param locale non-null locale
      * @param centuryStart The start of the century for 2 digit year parsing
-     * @param lenient if true, non-standard values for Calendar fields should 
be accepted; if false, non-standard values
-     *            will cause a ParseException to be thrown {@link 
Calendar#setLenient(boolean)}
      *
      * @since 3.5
      */
-    protected FastDateParser(final String pattern, final TimeZone timeZone, 
final Locale locale,
-            final Date centuryStart, final boolean lenient) {
+    protected FastDateParser(final String pattern, final TimeZone timeZone, 
final Locale locale, final Date centuryStart) {
         this.pattern = pattern;
         this.timeZone = timeZone;
         this.locale = locale;
-        this.lenient = lenient;
 
         final Calendar definingCalendar = Calendar.getInstance(timeZone, 
locale);
 
         int centuryStartYear;
-        if (centuryStart != null) {
+        if(centuryStart!=null) {
             definingCalendar.setTime(centuryStart);
-            centuryStartYear = definingCalendar.get(Calendar.YEAR);
-        } else if (locale.equals(JAPANESE_IMPERIAL)) {
-            centuryStartYear = 0;
-        } else {
+            centuryStartYear= definingCalendar.get(Calendar.YEAR);
+        }
+        else if(locale.equals(JAPANESE_IMPERIAL)) {
+            centuryStartYear= 0;
+        }
+        else {
             // from 80 years ago to 20 years from now
             definingCalendar.setTime(new Date());
-            centuryStartYear = definingCalendar.get(Calendar.YEAR) - 80;
+            centuryStartYear= definingCalendar.get(Calendar.YEAR)-80;
         }
-        century = centuryStartYear / 100 * 100;
-        startYear = centuryStartYear - century;
+        century= centuryStartYear / 100 * 100;
+        startYear= centuryStartYear - century;
 
         init(definingCalendar);
     }
 
     /**
-     * Initialize derived fields from defining fields. This is called from 
constructor and from readObject
-     * (de-serialization)
+     * Initialize derived fields from defining fields.
+     * This is called from constructor and from readObject (de-serialization)
      *
      * @param definingCalendar the {@link java.util.Calendar} instance used to 
initialize this FastDateParser
      */
     private void init(final Calendar definingCalendar) {
+        patterns = new ArrayList<>();
 
-        final StringBuilder regex = new StringBuilder();
-        final List<Strategy> collector = new ArrayList<>();
+        final StrategyParser fm = new StrategyParser(definingCalendar);
+        for(;;) {
+            final StrategyAndWidth field = fm.getNextStrategy();
+            if(field==null) {
+                break;
+            }
+            patterns.add(field);
+        }
+    }
 
-        final Matcher patternMatcher = formatPattern.matcher(pattern);
-        if (!patternMatcher.lookingAt()) {
-            throw new IllegalArgumentException("Illegal pattern character '"
-                    + pattern.charAt(patternMatcher.regionStart()) + "'");
+    // helper classes to parse the format string
+    //-----------------------------------------------------------------------
+
+    /**
+     * Holds strategy and field width
+     */
+    private static class StrategyAndWidth {
+        final Strategy strategy;
+        final int width;
+
+        StrategyAndWidth(final Strategy strategy, final int width) {
+            this.strategy = strategy;
+            this.width = width;
         }
 
-        currentFormatField = patternMatcher.group();
-        Strategy currentStrategy = getStrategy(currentFormatField, 
definingCalendar);
-        for (;;) {
-            patternMatcher.region(patternMatcher.end(), 
patternMatcher.regionEnd());
-            if (!patternMatcher.lookingAt()) {
-                nextStrategy = null;
-                break;
+        int getMaxWidth(final ListIterator<StrategyAndWidth> lt) {
+            if(!strategy.isNumber() || !lt.hasNext()) {
+                return 0;
+            }
+            final Strategy nextStrategy = lt.next().strategy;
+            lt.previous();
+            return nextStrategy.isNumber() ?width :0;
+       }
+    }
+
+    /**
+     * Parse format into Strategies
+     */
+    private class StrategyParser {
+        final private Calendar definingCalendar;
+        private int currentIdx;
+
+        StrategyParser(final Calendar definingCalendar) {
+            this.definingCalendar = definingCalendar;
+        }
+
+        StrategyAndWidth getNextStrategy() {
+            if (currentIdx >= pattern.length()) {
+                return null;
             }
-            final String nextFormatField = patternMatcher.group();
-            nextStrategy = getStrategy(nextFormatField, definingCalendar);
-            if (currentStrategy.addRegex(this, regex)) {
-                collector.add(currentStrategy);
+
+            final char c = pattern.charAt(currentIdx);
+            if (isFormatLetter(c)) {
+                return letterPattern(c);
             }
-            currentFormatField = nextFormatField;
-            currentStrategy = nextStrategy;
+            return literal();
         }
-        if (patternMatcher.regionStart() != patternMatcher.regionEnd()) {
-            throw new IllegalArgumentException("Failed to parse \"" + pattern 
+ "\" ; gave up at index "
-                    + patternMatcher.regionStart());
+
+        private StrategyAndWidth letterPattern(final char c) {
+            final int begin = currentIdx;
+            while (++currentIdx < pattern.length()) {
+                if (pattern.charAt(currentIdx) != c) {
+                    break;
+                }
+            }
+
+            final int width = currentIdx - begin;
+            return new StrategyAndWidth(getStrategy(c, width, 
definingCalendar), width);
         }
-        if (currentStrategy.addRegex(this, regex)) {
-            collector.add(currentStrategy);
+
+        private StrategyAndWidth literal() {
+            boolean activeQuote = false;
+
+            final StringBuilder sb = new StringBuilder();
+            while (currentIdx < pattern.length()) {
+                final char c = pattern.charAt(currentIdx);
+                if (!activeQuote && isFormatLetter(c)) {
+                    break;
+                } else if (c == '\'' && (++currentIdx == pattern.length() || 
pattern.charAt(currentIdx) != '\'')) {
+                    activeQuote = !activeQuote;
+                    continue;
+                }
+                ++currentIdx;
+                sb.append(c);
+            }
+
+            if (activeQuote) {
+                throw new IllegalArgumentException("Unterminated quote");
+            }
+
+            final String formatField = sb.toString();
+            return new StrategyAndWidth(new CopyQuotedStrategy(formatField), 
formatField.length());
         }
-        currentFormatField = null;
-        strategies = collector.toArray(new Strategy[collector.size()]);
-        parsePattern = Pattern.compile(regex.toString());
+    }
+
+    private static boolean isFormatLetter(final char c) {
+        return c >= 'A' && c <= 'Z' || c >= 'a' && c <= 'z';
     }
 
     // Accessors
-    // -----------------------------------------------------------------------
-    /*
-     * (non-Javadoc)
-     *
+    //-----------------------------------------------------------------------
+    /* (non-Javadoc)
      * @see org.apache.commons.lang3.time.DateParser#getPattern()
      */
     @Override
@@ -244,9 +271,7 @@ public class FastDateParser implements DateParser, 
Serializable {
         return pattern;
     }
 
-    /*
-     * (non-Javadoc)
-     *
+    /* (non-Javadoc)
      * @see org.apache.commons.lang3.time.DateParser#getTimeZone()
      */
     @Override
@@ -254,9 +279,7 @@ public class FastDateParser implements DateParser, 
Serializable {
         return timeZone;
     }
 
-    /*
-     * (non-Javadoc)
-     *
+    /* (non-Javadoc)
      * @see org.apache.commons.lang3.time.DateParser#getLocale()
      */
     @Override
@@ -264,23 +287,13 @@ public class FastDateParser implements DateParser, 
Serializable {
         return locale;
     }
 
-    /**
-     * Returns the generated pattern (for testing purposes).
-     *
-     * @return the generated pattern
-     */
-    Pattern getParsePattern() {
-        return parsePattern;
-    }
 
     // Basics
-    // -----------------------------------------------------------------------
+    //-----------------------------------------------------------------------
     /**
-     * <p>
-     * Compare another object for equality with this object.
-     * </p>
+     * <p>Compare another object for equality with this object.</p>
      *
-     * @param obj the object to compare to
+     * @param obj  the object to compare to
      * @return <code>true</code>if equal to this instance
      */
     @Override
@@ -289,13 +302,13 @@ public class FastDateParser implements DateParser, 
Serializable {
             return false;
         }
         final FastDateParser other = (FastDateParser) obj;
-        return pattern.equals(other.pattern) && 
timeZone.equals(other.timeZone) && locale.equals(other.locale);
+        return pattern.equals(other.pattern)
+            && timeZone.equals(other.timeZone)
+            && locale.equals(other.locale);
     }
 
     /**
-     * <p>
-     * Return a hashcode compatible with equals.
-     * </p>
+     * <p>Return a hashcode compatible with equals.</p>
      *
      * @return a hashcode compatible with equals
      */
@@ -305,9 +318,7 @@ public class FastDateParser implements DateParser, 
Serializable {
     }
 
     /**
-     * <p>
-     * Get a string version of this formatter.
-     * </p>
+     * <p>Get a string version of this formatter.</p>
      *
      * @return a debugging string
      */
@@ -317,9 +328,10 @@ public class FastDateParser implements DateParser, 
Serializable {
     }
 
     // Serializing
-    // -----------------------------------------------------------------------
+    //-----------------------------------------------------------------------
     /**
-     * Create the object after serialization. This implementation 
reinitializes the transient properties.
+     * Create the object after serialization. This implementation 
reinitializes the
+     * transient properties.
      *
      * @param in ObjectInputStream from which the object is being deserialized.
      * @throws IOException if there is an IO issue.
@@ -332,9 +344,7 @@ public class FastDateParser implements DateParser, 
Serializable {
         init(definingCalendar);
     }
 
-    /*
-     * (non-Javadoc)
-     *
+    /* (non-Javadoc)
      * @see 
org.apache.commons.lang3.time.DateParser#parseObject(java.lang.String)
      */
     @Override
@@ -342,28 +352,26 @@ public class FastDateParser implements DateParser, 
Serializable {
         return parse(source);
     }
 
-    /*
-     * (non-Javadoc)
-     *
+    /* (non-Javadoc)
      * @see org.apache.commons.lang3.time.DateParser#parse(java.lang.String)
      */
     @Override
     public Date parse(final String source) throws ParseException {
-        final Date date = parse(source, new ParsePosition(0));
+        final ParsePosition pp = new ParsePosition(0);
+        final Date date= parse(source, pp);
         if (date == null) {
             // Add a note re supported date range
             if (locale.equals(JAPANESE_IMPERIAL)) {
-                throw new ParseException("(The " + locale + " locale does not 
support dates before 1868 AD)\n"
-                        + "Unparseable date: \"" + source + "\" does not match 
" + parsePattern.pattern(), 0);
+                throw new ParseException(
+                        "(The " +locale + " locale does not support dates 
before 1868 AD)\n" +
+                                "Unparseable date: \""+source, 
pp.getErrorIndex());
             }
-            throw new ParseException("Unparseable date: \"" + source + "\" 
does not match " + parsePattern.pattern(), 0);
+            throw new ParseException("Unparseable date: "+source, 
pp.getErrorIndex());
         }
         return date;
     }
 
-    /*
-     * (non-Javadoc)
-     *
+    /* (non-Javadoc)
      * @see 
org.apache.commons.lang3.time.DateParser#parseObject(java.lang.String, 
java.text.ParsePosition)
      */
     @Override
@@ -372,38 +380,54 @@ public class FastDateParser implements DateParser, 
Serializable {
     }
 
     /**
-     * This implementation updates the ParsePosition if the parse succeeeds. 
However, unlike the method
-     * {@link java.text.SimpleDateFormat#parse(String, ParsePosition)} it is 
not able to set the error Index - i.e.
-     * {@link ParsePosition#getErrorIndex()} - if the parse fails.
+     * This implementation updates the ParsePosition if the parse succeeds.
+     * However, it sets the error index to the position before the failed 
field unlike 
+     * the method {@link java.text.SimpleDateFormat#parse(String, 
ParsePosition)} which sets 
+     * the error index to after the failed field.
      * <p>
-     * To determine if the parse has succeeded, the caller must check if the 
current parse position given by
-     * {@link ParsePosition#getIndex()} has been updated. If the input buffer 
has been fully parsed, then the index will
-     * point to just after the end of the input buffer.
+     * To determine if the parse has succeeded, the caller must check if the 
current parse position
+     * given by {@link ParsePosition#getIndex()} has been updated. If the 
input buffer has been fully
+     * parsed, then the index will point to just after the end of the input 
buffer.
      *
-     * {@inheritDoc}
+     * @see org.apache.commons.lang3.time.DateParser#parse(java.lang.String, 
java.text.ParsePosition)
      */
     @Override
     public Date parse(final String source, final ParsePosition pos) {
-        final int offset = pos.getIndex();
-        final Matcher matcher = parsePattern.matcher(source.substring(offset));
-        if (!matcher.lookingAt()) {
-            return null;
-        }
         // timing tests indicate getting new instance is 19% faster than 
cloning
-        final Calendar cal = Calendar.getInstance(timeZone, locale);
+        final Calendar cal= Calendar.getInstance(timeZone, locale);
         cal.clear();
-        cal.setLenient(lenient);
 
-        for (int i = 0; i < strategies.length;) {
-            final Strategy strategy = strategies[i++];
-            strategy.setCalendar(this, cal, matcher.group(i));
+        return parse(source, pos, cal) ? cal.getTime() : null;
+    }
+
+    /**
+     * Parse a formatted date string according to the format.  Updates the 
Calendar with parsed fields.
+     * Upon success, the ParsePosition index is updated to indicate how much 
of the source text was consumed.
+     * Not all source text needs to be consumed.  Upon parse failure, 
ParsePosition error index is updated to
+     * the offset of the source text which does not match the supplied format.
+     * 
+     * @param source The text to parse.
+     * @param pos On input, the position in the source to start parsing, on 
output, updated position.
+     * @param calendar The calendar into which to set parsed fields.
+     * @return true, if source has been parsed (pos parsePosition is updated); 
otherwise false (and pos errorIndex is updated)
+     * @throws IllegalArgumentException when Calendar has been set to be not 
lenient, and a parsed field is
+     * out of range.
+     */
+    @Override
+    public boolean parse(final String source, final ParsePosition pos, final 
Calendar calendar) {
+        final ListIterator<StrategyAndWidth> lt = patterns.listIterator();
+        while (lt.hasNext()) {
+            final StrategyAndWidth strategyAndWidth = lt.next();
+            final int maxWidth = strategyAndWidth.getMaxWidth(lt);
+            if (!strategyAndWidth.strategy.parse(this, calendar, source, pos, 
maxWidth)) {
+                return false;
+            }
         }
-        pos.setIndex(offset + matcher.end());
-        return cal.getTime();
+        return true;
     }
 
     // Support for strategies
-    // -----------------------------------------------------------------------
+    //-----------------------------------------------------------------------
 
     private static StringBuilder simpleQuote(final StringBuilder sb, final 
String value) {
         for (int i = 0; i < value.length(); ++i) {
@@ -430,69 +454,32 @@ public class FastDateParser implements DateParser, 
Serializable {
     }
 
     /**
-     * Escape constant fields into regular expression
-     *
-     * @param regex The destination regex
-     * @param value The source field
-     * @param unquote If true, replace two success quotes ('') with single 
quote (')
-     * @return The <code>StringBuilder</code>
-     */
-    private static StringBuilder escapeRegex(final StringBuilder regex, final 
String value, final boolean unquote) {
-        regex.append("\\Q");
-        for (int i = 0; i < value.length(); ++i) {
-            char c = value.charAt(i);
-            switch (c) {
-            case '\'':
-                if (unquote) {
-                    if (++i == value.length()) {
-                        return regex;
-                    }
-                    c = value.charAt(i);
-                }
-                break;
-            case '\\':
-                if (++i == value.length()) {
-                    break;
-                }
-                /*
-                 * If we have found \E, we replace it with \E\\E\Q, i.e. we 
stop the quoting, quote the \ in \E, then
-                 * restart the quoting.
-                 *
-                 * Otherwise we just output the two characters. In each case 
the initial \ needs to be output and the
-                 * final char is done at the end
-                 */
-                regex.append(c); // we always want the original \
-                c = value.charAt(i); // Is it followed by E ?
-                if (c == 'E') { // \E detected
-                    regex.append("E\\\\E\\"); // see comment above
-                    c = 'Q'; // appended below
-                }
-                break;
-            default:
-                break;
-            }
-            regex.append(c);
-        }
-        regex.append("\\E");
-        return regex;
-    }
-
-    /**
      * Get the short and long values displayed for a field
-     *
-     * @param field The field of interest
-     * @param definingCalendar The calendar to obtain the short and long values
+     * @param cal The calendar to obtain the short and long values
      * @param locale The locale of display names
-     * @return A Map of the field key / value pairs
-     */
-    private static Map<String, Integer> getDisplayNames(final int field, final 
Calendar definingCalendar,
-            final Locale locale) {
-        return definingCalendar.getDisplayNames(field, Calendar.ALL_STYLES, 
locale);
+     * @param field The field of interest
+     * @param regex The regular expression to build
+     * @return The map of string display names to field values
+     */
+    private static Map<String, Integer> appendDisplayNames(final Calendar cal, 
final Locale locale, final int field, final StringBuilder regex) {
+        final Map<String, Integer> values = new HashMap<>();
+
+        final Map<String, Integer> displayNames = cal.getDisplayNames(field, 
Calendar.ALL_STYLES, locale);
+        final TreeSet<String> sorted = new TreeSet<>(LONGER_FIRST_LOWERCASE);
+        for (final Map.Entry<String, Integer> displayName : 
displayNames.entrySet()) {
+            final String key = displayName.getKey().toLowerCase(locale);
+            if (sorted.add(key)) {
+                values.put(key, displayName.getValue());
+            }
+        }
+        for (final String symbol : sorted) {
+            simpleQuote(regex, symbol).append('|');
+        }
+        return values;
     }
 
     /**
      * Adjust dates to be within appropriate century
-     *
      * @param twoDigitYear The year to adjust
      * @return A value between centuryStart(inclusive) to 
centuryStart+100(exclusive)
      */
@@ -502,30 +489,12 @@ public class FastDateParser implements DateParser, 
Serializable {
     }
 
     /**
-     * Is the next field a number?
-     *
-     * @return true, if next field will be a number
-     */
-    boolean isNextNumber() {
-        return nextStrategy != null && nextStrategy.isNumber();
-    }
-
-    /**
-     * What is the width of the current field?
-     *
-     * @return The number of characters in the current format field
-     */
-    int getFieldWidth() {
-        return currentFormatField.length();
-    }
-
-    /**
      * A strategy to parse a single field from the parsing pattern
      */
     private static abstract class Strategy {
-
         /**
-         * Is this field a number? The default implementation returns false.
+         * Is this field a number?
+         * The default implementation returns false.
          *
          * @return true, if field is a number
          */
@@ -533,53 +502,60 @@ public class FastDateParser implements DateParser, 
Serializable {
             return false;
         }
 
-        /**
-         * Set the Calendar with the parsed field.
-         *
-         * The default implementation does nothing.
-         *
-         * @param parser The parser calling this strategy
-         * @param cal The <code>Calendar</code> to set
-         * @param value The parsed field to translate and set in cal
-         */
-        void setCalendar(final FastDateParser parser, final Calendar cal, 
final String value) {
+        abstract boolean parse(FastDateParser parser, Calendar calendar, 
String source, ParsePosition pos, int maxWidth);
+    }
+
+    /**
+     * A strategy to parse a single field from the parsing pattern
+     */
+    private static abstract class PatternStrategy extends Strategy {
 
+        private Pattern pattern;
+
+        void createPattern(final StringBuilder regex) {
+            createPattern(regex.toString());
+        }
+
+        void createPattern(final String regex) {
+            this.pattern = Pattern.compile(regex);
         }
 
         /**
-         * Generate a <code>Pattern</code> regular expression to the 
<code>StringBuilder</code> which will accept this
-         * field
+         * Is this field a number?
+         * The default implementation returns false.
          *
-         * @param parser The parser calling this strategy
-         * @param regex The <code>StringBuilder</code> to append to
-         * @return true, if this field will set the calendar; false, if this 
field is a constant value
+         * @return true, if field is a number
          */
-        abstract boolean addRegex(FastDateParser parser, StringBuilder regex);
+        @Override
+        boolean isNumber() {
+            return false;
+        }
 
-    }
+        @Override
+        boolean parse(final FastDateParser parser, final Calendar calendar, 
final String source, final ParsePosition pos, final int maxWidth) {
+            final Matcher matcher = 
pattern.matcher(source.substring(pos.getIndex()));
+            if (!matcher.lookingAt()) {
+                pos.setErrorIndex(pos.getIndex());
+                return false;
+            }
+            pos.setIndex(pos.getIndex() + matcher.end(1));
+            setCalendar(parser, calendar, matcher.group(1));
+            return true;
+        }
 
-    /**
-     * A <code>Pattern</code> to parse the user supplied SimpleDateFormat 
pattern
-     */
-    private static final Pattern formatPattern = Pattern
-            
.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]++");
+        abstract void setCalendar(FastDateParser parser, Calendar cal, String 
value);
+    }
 
     /**
      * Obtain a Strategy given a field from a SimpleDateFormat pattern
-     *
      * @param formatField A sub-sequence of the SimpleDateFormat pattern
      * @param definingCalendar The calendar to obtain the short and long values
      * @return The Strategy that will handle parsing for the field
      */
-    private Strategy getStrategy(final String formatField, final Calendar 
definingCalendar) {
-        switch (formatField.charAt(0)) {
-        case '\'':
-            if (formatField.length() > 2) {
-                return new CopyQuotedStrategy(formatField.substring(1, 
formatField.length() - 1));
-            }
-            //$FALL-THROUGH$
+    private Strategy getStrategy(final char f, final int width, final Calendar 
definingCalendar) {
+        switch(f) {
         default:
-            return new CopyQuotedStrategy(formatField);
+            throw new IllegalArgumentException("Format '"+f+"' not supported");
         case 'D':
             return DAY_OF_YEAR_STRATEGY;
         case 'E':
@@ -588,13 +564,12 @@ public class FastDateParser implements DateParser, 
Serializable {
             return DAY_OF_WEEK_IN_MONTH_STRATEGY;
         case 'G':
             return getLocaleSpecificStrategy(Calendar.ERA, definingCalendar);
-        case 'H': // Hour in day (0-23)
+        case 'H':  // Hour in day (0-23)
             return HOUR_OF_DAY_STRATEGY;
-        case 'K': // Hour in am/pm (0-11)
+        case 'K':  // Hour in am/pm (0-11) 
             return HOUR_STRATEGY;
         case 'M':
-            return formatField.length() >= 3 ? 
getLocaleSpecificStrategy(Calendar.MONTH, definingCalendar)
-                    : NUMBER_MONTH_STRATEGY;
+            return width>=3 ?getLocaleSpecificStrategy(Calendar.MONTH, 
definingCalendar) :NUMBER_MONTH_STRATEGY;
         case 'S':
             return MILLISECOND_STRATEGY;
         case 'W':
@@ -603,9 +578,9 @@ public class FastDateParser implements DateParser, 
Serializable {
             return getLocaleSpecificStrategy(Calendar.AM_PM, definingCalendar);
         case 'd':
             return DAY_OF_MONTH_STRATEGY;
-        case 'h': // Hour in am/pm (1-12), i.e. midday/midnight is 12, not 0
+        case 'h':  // Hour in am/pm (1-12), i.e. midday/midnight is 12, not 0
             return HOUR12_STRATEGY;
-        case 'k': // Hour in day (1-24), i.e. midnight is 24, not 0
+        case 'k':  // Hour in day (1-24), i.e. midnight is 24, not 0
             return HOUR24_OF_DAY_STRATEGY;
         case 'm':
             return MINUTE_STRATEGY;
@@ -616,12 +591,13 @@ public class FastDateParser implements DateParser, 
Serializable {
         case 'w':
             return WEEK_OF_YEAR_STRATEGY;
         case 'y':
-            return formatField.length() > 2 ? LITERAL_YEAR_STRATEGY : 
ABBREVIATED_YEAR_STRATEGY;
+        case 'Y':
+            return width>2 ?LITERAL_YEAR_STRATEGY :ABBREVIATED_YEAR_STRATEGY;
         case 'X':
-            return ISO8601TimeZoneStrategy.getStrategy(formatField.length());
+            return ISO8601TimeZoneStrategy.getStrategy(width);
         case 'Z':
-            if (formatField.equals("ZZ")) {
-                return ISO_8601_STRATEGY;
+            if (width==2) {
+                return ISO8601TimeZoneStrategy.ISO_8601_3_STRATEGY;
             }
             //$FALL-THROUGH$
         case 'z':
@@ -629,13 +605,11 @@ public class FastDateParser implements DateParser, 
Serializable {
         }
     }
 
-    @SuppressWarnings("unchecked")
-    // OK because we are creating an array with no entries
+    @SuppressWarnings("unchecked") // OK because we are creating an array with 
no entries
     private static final ConcurrentMap<Locale, Strategy>[] caches = new 
ConcurrentMap[Calendar.FIELD_COUNT];
 
     /**
      * Get a cache of Strategies for a particular field
-     *
      * @param field The Calendar field
      * @return a cache of Locale to Strategy
      */
@@ -650,7 +624,6 @@ public class FastDateParser implements DateParser, 
Serializable {
 
     /**
      * Construct a Strategy that parses a Text field
-     *
      * @param field The Calendar field
      * @param definingCalendar The calendar to obtain the short and long values
      * @return a TextStrategy for the field and Locale
@@ -659,8 +632,9 @@ public class FastDateParser implements DateParser, 
Serializable {
         final ConcurrentMap<Locale, Strategy> cache = getCache(field);
         Strategy strategy = cache.get(locale);
         if (strategy == null) {
-            strategy = field == Calendar.ZONE_OFFSET ? new 
TimeZoneStrategy(locale) : new CaseInsensitiveTextStrategy(
-                    field, definingCalendar, locale);
+            strategy = field == Calendar.ZONE_OFFSET 
+                    ? new TimeZoneStrategy(locale)
+                    : new CaseInsensitiveTextStrategy(field, definingCalendar, 
locale);
             final Strategy inCache = cache.putIfAbsent(locale, strategy);
             if (inCache != null) {
                 return inCache;
@@ -673,11 +647,11 @@ public class FastDateParser implements DateParser, 
Serializable {
      * A strategy that copies the static or quoted field in the parsing pattern
      */
     private static class CopyQuotedStrategy extends Strategy {
-        private final String formatField;
+
+        final private String formatField;
 
         /**
          * Construct a Strategy that ensures the formatField has literal text
-         *
          * @param formatField The literal text to match
          */
         CopyQuotedStrategy(final String formatField) {
@@ -689,60 +663,51 @@ public class FastDateParser implements DateParser, 
Serializable {
          */
         @Override
         boolean isNumber() {
-            char c = formatField.charAt(0);
-            if (c == '\'') {
-                c = formatField.charAt(1);
-            }
-            return Character.isDigit(c);
+            return false;
         }
 
-        /**
-         * {@inheritDoc}
-         */
         @Override
-        boolean addRegex(final FastDateParser parser, final StringBuilder 
regex) {
-            escapeRegex(regex, formatField, true);
-            return false;
+        boolean parse(final FastDateParser parser, final Calendar calendar, 
final String source, final ParsePosition pos, final int maxWidth) {
+            for (int idx = 0; idx < formatField.length(); ++idx) {
+                final int sIdx = idx + pos.getIndex();
+                if (sIdx == source.length()) {
+                    pos.setErrorIndex(sIdx);
+                    return false;
+                }
+                if (formatField.charAt(idx) != source.charAt(sIdx)) {
+                    pos.setErrorIndex(sIdx);
+                    return false;
+                }
+            }
+            pos.setIndex(formatField.length() + pos.getIndex());
+            return true;
         }
     }
 
     /**
      * A strategy that handles a text field in the parsing pattern
      */
-    private static class CaseInsensitiveTextStrategy extends Strategy {
+     private static class CaseInsensitiveTextStrategy extends PatternStrategy {
         private final int field;
-        private final Locale locale;
+        final Locale locale;
         private final Map<String, Integer> lKeyValues;
 
         /**
          * Construct a Strategy that parses a Text field
-         *
-         * @param field The Calendar field
-         * @param definingCalendar The Calendar to use
-         * @param locale The Locale to use
+         * @param field  The Calendar field
+         * @param definingCalendar  The Calendar to use
+         * @param locale  The Locale to use
          */
         CaseInsensitiveTextStrategy(final int field, final Calendar 
definingCalendar, final Locale locale) {
             this.field = field;
             this.locale = locale;
-            final Map<String, Integer> keyValues = getDisplayNames(field, 
definingCalendar, locale);
-            this.lKeyValues = new HashMap<>();
-
-            for (final Map.Entry<String, Integer> entry : 
keyValues.entrySet()) {
-                lKeyValues.put(entry.getKey().toLowerCase(locale), 
entry.getValue());
-            }
-        }
-
-        /**
-         * {@inheritDoc}
-         */
-        @Override
-        boolean addRegex(final FastDateParser parser, final StringBuilder 
regex) {
+            
+            final StringBuilder regex = new StringBuilder();
             regex.append("((?iu)");
-            for (final String textKeyValue : lKeyValues.keySet()) {
-                simpleQuote(regex, textKeyValue).append('|');
-            }
-            regex.setCharAt(regex.length() - 1, ')');
-            return true;
+            lKeyValues = appendDisplayNames(definingCalendar, locale, field, 
regex);
+            regex.setLength(regex.length()-1);
+            regex.append(")");
+            createPattern(regex);
         }
 
         /**
@@ -751,19 +716,11 @@ public class FastDateParser implements DateParser, 
Serializable {
         @Override
         void setCalendar(final FastDateParser parser, final Calendar cal, 
final String value) {
             final Integer iVal = lKeyValues.get(value.toLowerCase(locale));
-            if (iVal == null) {
-                final StringBuilder sb = new StringBuilder(value);
-                sb.append(" not in (");
-                for (final String textKeyValue : lKeyValues.keySet()) {
-                    sb.append(textKeyValue).append(' ');
-                }
-                sb.setCharAt(sb.length() - 1, ')');
-                throw new IllegalArgumentException(sb.toString());
-            }
             cal.set(field, iVal.intValue());
         }
     }
 
+
     /**
      * A strategy that handles a number field in the parsing pattern
      */
@@ -772,11 +729,10 @@ public class FastDateParser implements DateParser, 
Serializable {
 
         /**
          * Construct a Strategy that parses a Number field
-         *
          * @param field The Calendar field
          */
         NumberStrategy(final int field) {
-            this.field = field;
+             this.field= field;
         }
 
         /**
@@ -787,49 +743,87 @@ public class FastDateParser implements DateParser, 
Serializable {
             return true;
         }
 
-        /**
-         * {@inheritDoc}
-         */
         @Override
-        boolean addRegex(final FastDateParser parser, final StringBuilder 
regex) {
-            // See LANG-954: We use {Nd} rather than {IsNd} because Android 
does not support the Is prefix
-            if (parser.isNextNumber()) {
-                
regex.append("(\\p{Nd}{").append(parser.getFieldWidth()).append("}+)");
+        boolean parse(final FastDateParser parser, final Calendar calendar, 
final String source, final ParsePosition pos, final int maxWidth) {
+            int idx = pos.getIndex();
+            int last = source.length();
+
+            if (maxWidth == 0) {
+                // if no maxWidth, strip leading white space
+                for (; idx < last; ++idx) {
+                    final char c = source.charAt(idx);
+                    if (!Character.isWhitespace(c)) {
+                        break;
+                    }
+                }
+                pos.setIndex(idx);
             } else {
-                regex.append("(\\p{Nd}++)");
+                final int end = idx + maxWidth;
+                if (last > end) {
+                    last = end;
+                }
             }
-            return true;
-        }
 
-        /**
-         * {@inheritDoc}
-         */
-        @Override
-        void setCalendar(final FastDateParser parser, final Calendar cal, 
final String value) {
-            cal.set(field, modify(Integer.parseInt(value)));
+            for (; idx < last; ++idx) {
+                final char c = source.charAt(idx);
+                if (!Character.isDigit(c)) {
+                    break;
+                }
+            }
+
+            if (pos.getIndex() == idx) {
+                pos.setErrorIndex(idx);
+                return false;
+            }
+
+            final int value = 
Integer.parseInt(source.substring(pos.getIndex(), idx));
+            pos.setIndex(idx);
+
+            calendar.set(field, modify(parser, value));
+            return true;
         }
 
         /**
          * Make any modifications to parsed integer
-         *
+         * @param parser The parser
          * @param iValue The parsed integer
          * @return The modified value
          */
-        int modify(final int iValue) {
+        int modify(final FastDateParser parser, final int iValue) {
             return iValue;
         }
+
     }
 
+    private static final Strategy ABBREVIATED_YEAR_STRATEGY = new 
NumberStrategy(Calendar.YEAR) {
+        /**
+         * {@inheritDoc}
+         */
+        @Override
+        int modify(final FastDateParser parser, final int iValue) {
+            return iValue < 100 ? parser.adjustYear(iValue) : iValue;
+        }
+    };
+
     /**
      * A strategy that handles a timezone field in the parsing pattern
      */
-    static class TimeZoneStrategy extends Strategy {
+    static class TimeZoneStrategy extends PatternStrategy {
         private static final String RFC_822_TIME_ZONE = "[+-]\\d{4}";
-        private static final String GMT_OPTION = "GMT[+-]\\d{1,2}:\\d{2}";
+        private static final String GMT_OPTION= "GMT[+-]\\d{1,2}:\\d{2}";
 
         private final Locale locale;
-        private final Map<String, TimeZone> tzNames = new HashMap<>();
-        private final String validTimeZoneChars;
+        private final Map<String, TzInfo> tzNames= new HashMap<>();
+
+        private static class TzInfo {
+            TimeZone zone;
+            int dstOffset;
+
+            TzInfo(final TimeZone tz, final boolean useDst) {
+                zone = tz;
+                dstOffset = useDst ?tz.getDSTSavings() :0;
+            }
+        }
 
         /**
          * Index of zone id
@@ -838,46 +832,55 @@ public class FastDateParser implements DateParser, 
Serializable {
 
         /**
          * Construct a Strategy that parses a TimeZone
-         *
          * @param locale The Locale
          */
         TimeZoneStrategy(final Locale locale) {
             this.locale = locale;
 
             final StringBuilder sb = new StringBuilder();
-            sb.append('(' + RFC_822_TIME_ZONE + "|(?iu)" + GMT_OPTION);
+            sb.append("((?iu)" + RFC_822_TIME_ZONE + "|" + GMT_OPTION );
+
+            final Set<String> sorted = new TreeSet<>(LONGER_FIRST_LOWERCASE);
 
             final String[][] zones = 
DateFormatSymbols.getInstance(locale).getZoneStrings();
             for (final String[] zoneNames : zones) {
+                // offset 0 is the time zone ID and is not localized
                 final String tzId = zoneNames[ID];
                 if (tzId.equalsIgnoreCase("GMT")) {
                     continue;
                 }
                 final TimeZone tz = TimeZone.getTimeZone(tzId);
+                // offset 1 is long standard name
+                // offset 2 is short standard name
+                final TzInfo standard = new TzInfo(tz, false);
+                TzInfo tzInfo = standard;
                 for (int i = 1; i < zoneNames.length; ++i) {
-                    final String currentZoneName = zoneNames[i];
-                    if (currentZoneName == null) {
-                        continue;
+                    switch (i) {
+                    case 3: // offset 3 is long daylight savings (or 
summertime) name
+                            // offset 4 is the short summertime name
+                        tzInfo = new TzInfo(tz, true);
+                        break;
+                    case 5: // offset 5 starts additional names, probably 
standard time
+                        tzInfo = standard;
+                        break;
                     }
-                    final String zoneName = 
currentZoneName.toLowerCase(locale);
-                    if (!tzNames.containsKey(zoneName)) {
-                        tzNames.put(zoneName, tz);
-                        simpleQuote(sb.append('|'), zoneName);
+                    if (zoneNames[i] != null) {
+                        final String key = zoneNames[i].toLowerCase(locale);
+                        // ignore the data associated with duplicates supplied 
in
+                        // the additional names
+                        if (sorted.add(key)) {
+                            tzNames.put(key, tzInfo);
+                        }
                     }
                 }
             }
-
-            sb.append(')');
-            validTimeZoneChars = sb.toString();
-        }
-
-        /**
-         * {@inheritDoc}
-         */
-        @Override
-        boolean addRegex(final FastDateParser parser, final StringBuilder 
regex) {
-            regex.append(validTimeZoneChars);
-            return true;
+            // order the regex alternatives with longer strings first, greedy
+            // match will ensure longest string will be consumed
+            for (final String zoneName : sorted) {
+                simpleQuote(sb.append('|'), zoneName);
+            }
+            sb.append(")");
+            createPattern(sb);
         }
 
         /**
@@ -885,43 +888,31 @@ public class FastDateParser implements DateParser, 
Serializable {
          */
         @Override
         void setCalendar(final FastDateParser parser, final Calendar cal, 
final String value) {
-            TimeZone tz;
             if (value.charAt(0) == '+' || value.charAt(0) == '-') {
-                tz = TimeZone.getTimeZone("GMT" + value);
+                final TimeZone tz = TimeZone.getTimeZone("GMT" + value);
+                cal.setTimeZone(tz);
             } else if (value.regionMatches(true, 0, "GMT", 0, 3)) {
-                tz = TimeZone.getTimeZone(value.toUpperCase());
+                final TimeZone tz = TimeZone.getTimeZone(value.toUpperCase());
+                cal.setTimeZone(tz);
             } else {
-                tz = tzNames.get(value.toLowerCase(locale));
-                if (tz == null) {
-                    throw new IllegalArgumentException(value + " is not a 
supported timezone name");
-                }
+                final TzInfo tzInfo = tzNames.get(value.toLowerCase(locale));
+                cal.set(Calendar.DST_OFFSET, tzInfo.dstOffset);
+                cal.set(Calendar.ZONE_OFFSET, tzInfo.zone.getRawOffset());
             }
-            cal.setTimeZone(tz);
         }
     }
-
-    private static class ISO8601TimeZoneStrategy extends Strategy {
-        // Z, +hh, -hh, +hhmm, -hhmm, +hh:mm or -hh:mm
-        private final String pattern;
+    
+    private static class ISO8601TimeZoneStrategy extends PatternStrategy {
+        // Z, +hh, -hh, +hhmm, -hhmm, +hh:mm or -hh:mm 
 
         /**
          * Construct a Strategy that parses a TimeZone
-         *
          * @param pattern The Pattern
          */
         ISO8601TimeZoneStrategy(final String pattern) {
-            this.pattern = pattern;
+            createPattern(pattern);
         }
-
-        /**
-         * {@inheritDoc}
-         */
-        @Override
-        boolean addRegex(final FastDateParser parser, final StringBuilder 
regex) {
-            regex.append(pattern);
-            return true;
-        }
-
+        
         /**
          * {@inheritDoc}
          */
@@ -933,20 +924,20 @@ public class FastDateParser implements DateParser, 
Serializable {
                 cal.setTimeZone(TimeZone.getTimeZone("GMT" + value));
             }
         }
-
+        
         private static final Strategy ISO_8601_1_STRATEGY = new 
ISO8601TimeZoneStrategy("(Z|(?:[+-]\\d{2}))");
         private static final Strategy ISO_8601_2_STRATEGY = new 
ISO8601TimeZoneStrategy("(Z|(?:[+-]\\d{2}\\d{2}))");
         private static final Strategy ISO_8601_3_STRATEGY = new 
ISO8601TimeZoneStrategy("(Z|(?:[+-]\\d{2}(?::)\\d{2}))");
 
         /**
          * Factory method for ISO8601TimeZoneStrategies.
-         *
+         * 
          * @param tokenLen a token indicating the length of the TimeZone 
String to be formatted.
          * @return a ISO8601TimeZoneStrategy that can format TimeZone String 
of length {@code tokenLen}. If no such
-         *         strategy exists, an IllegalArgumentException will be thrown.
+         *          strategy exists, an IllegalArgumentException will be 
thrown.
          */
         static Strategy getStrategy(final int tokenLen) {
-            switch (tokenLen) {
+            switch(tokenLen) {
             case 1:
                 return ISO_8601_1_STRATEGY;
             case 2:
@@ -959,4 +950,39 @@ public class FastDateParser implements DateParser, 
Serializable {
         }
     }
 
+    private static final Strategy NUMBER_MONTH_STRATEGY = new 
NumberStrategy(Calendar.MONTH) {
+        @Override
+        int modify(final FastDateParser parser, final int iValue) {
+            return iValue-1;
+        }
+    };
+    private static final Strategy LITERAL_YEAR_STRATEGY = new 
NumberStrategy(Calendar.YEAR);
+    private static final Strategy WEEK_OF_YEAR_STRATEGY = new 
NumberStrategy(Calendar.WEEK_OF_YEAR);
+    private static final Strategy WEEK_OF_MONTH_STRATEGY = new 
NumberStrategy(Calendar.WEEK_OF_MONTH);
+    private static final Strategy DAY_OF_YEAR_STRATEGY = new 
NumberStrategy(Calendar.DAY_OF_YEAR);
+    private static final Strategy DAY_OF_MONTH_STRATEGY = new 
NumberStrategy(Calendar.DAY_OF_MONTH);
+    private static final Strategy DAY_OF_WEEK_STRATEGY = new 
NumberStrategy(Calendar.DAY_OF_WEEK) {
+        @Override
+        int modify(final FastDateParser parser, final int iValue) {
+            return iValue != 7 ? iValue + 1 : Calendar.SUNDAY;
+        }
+    };
+    private static final Strategy DAY_OF_WEEK_IN_MONTH_STRATEGY = new 
NumberStrategy(Calendar.DAY_OF_WEEK_IN_MONTH);
+    private static final Strategy HOUR_OF_DAY_STRATEGY = new 
NumberStrategy(Calendar.HOUR_OF_DAY);
+    private static final Strategy HOUR24_OF_DAY_STRATEGY = new 
NumberStrategy(Calendar.HOUR_OF_DAY) {
+        @Override
+        int modify(final FastDateParser parser, final int iValue) {
+            return iValue == 24 ? 0 : iValue;
+        }
+    };
+    private static final Strategy HOUR12_STRATEGY = new 
NumberStrategy(Calendar.HOUR) {
+        @Override
+        int modify(final FastDateParser parser, final int iValue) {
+            return iValue == 12 ? 0 : iValue;
+        }
+    };
+    private static final Strategy HOUR_STRATEGY = new 
NumberStrategy(Calendar.HOUR);
+    private static final Strategy MINUTE_STRATEGY = new 
NumberStrategy(Calendar.MINUTE);
+    private static final Strategy SECOND_STRATEGY = new 
NumberStrategy(Calendar.SECOND);
+    private static final Strategy MILLISECOND_STRATEGY = new 
NumberStrategy(Calendar.MILLISECOND);
 }

http://git-wip-us.apache.org/repos/asf/logging-log4j2/blob/8af687f4/log4j-core/src/test/java/org/apache/logging/log4j/core/util/datetime/FastDateParserSDFTest.java
----------------------------------------------------------------------
diff --git 
a/log4j-core/src/test/java/org/apache/logging/log4j/core/util/datetime/FastDateParserSDFTest.java
 
b/log4j-core/src/test/java/org/apache/logging/log4j/core/util/datetime/FastDateParserSDFTest.java
new file mode 100644
index 0000000..247dad8
--- /dev/null
+++ 
b/log4j-core/src/test/java/org/apache/logging/log4j/core/util/datetime/FastDateParserSDFTest.java
@@ -0,0 +1,232 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.logging.log4j.core.util.datetime;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import java.text.ParseException;
+import java.text.ParsePosition;
+import java.text.SimpleDateFormat;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Date;
+import java.util.Locale;
+import java.util.TimeZone;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+/**
+ * Compare FastDateParser with SimpleDateFormat 
+ * 
+ * Copied from Apache Commons Lang 3 on 2016-11-16.
+ */
+@RunWith(Parameterized.class)
+public class FastDateParserSDFTest {
+
+    @Parameters(name= "{index}: {0} {1} {2}")
+    public static Collection<Object[]> data() {
+        return Arrays.asList(new Object [][]{
+                // General Time zone tests
+                {"z yyyy", "GMT 2010",       Locale.UK, true}, // no offset 
specified, but this is allowed as a TimeZone name
+                {"z yyyy", "GMT-123 2010",   Locale.UK, false},
+                {"z yyyy", "GMT-1234 2010",  Locale.UK, false},
+                {"z yyyy", "GMT-12:34 2010", Locale.UK, true},
+                {"z yyyy", "GMT-1:23 2010",  Locale.UK, true},
+                // RFC 822 tests
+                {"z yyyy", "-1234 2010",     Locale.UK, true},
+                {"z yyyy", "-12:34 2010",    Locale.UK, false},
+                {"z yyyy", "-123 2010",      Locale.UK, false},
+                // year tests
+                { "MM/dd/yyyy", "01/11/12",  Locale.UK, true},
+                { "MM/dd/yy", "01/11/12",    Locale.UK, true},
+
+                // LANG-1089
+                { "HH", "00",    Locale.UK, true}, // Hour in day (0-23)
+                { "KK", "00",    Locale.UK, true}, // Hour in am/pm (0-11)
+                { "hh", "00",    Locale.UK, true}, // Hour in am/pm (1-12), 
i.e. midday/midnight is 12, not 0
+                { "kk", "00",    Locale.UK, true}, // Hour in day (1-24), i.e. 
midnight is 24, not 0
+
+                { "HH", "01",    Locale.UK, true}, // Hour in day (0-23)
+                { "KK", "01",    Locale.UK, true}, // Hour in am/pm (0-11)
+                { "hh", "01",    Locale.UK, true}, // Hour in am/pm (1-12), 
i.e. midday/midnight is 12, not 0
+                { "kk", "01",    Locale.UK, true}, // Hour in day (1-24), i.e. 
midnight is 24, not 0
+
+                { "HH", "11",    Locale.UK, true}, // Hour in day (0-23)
+                { "KK", "11",    Locale.UK, true}, // Hour in am/pm (0-11)
+                { "hh", "11",    Locale.UK, true}, // Hour in am/pm (1-12), 
i.e. midday/midnight is 12, not 0
+                { "kk", "11",    Locale.UK, true}, // Hour in day (1-24), i.e. 
midnight is 24, not 0
+
+                { "HH", "12",    Locale.UK, true}, // Hour in day (0-23)
+                { "KK", "12",    Locale.UK, true}, // Hour in am/pm (0-11)
+                { "hh", "12",    Locale.UK, true}, // Hour in am/pm (1-12), 
i.e. midday/midnight is 12, not 0
+                { "kk", "12",    Locale.UK, true}, // Hour in day (1-24), i.e. 
midnight is 24, not 0
+
+                { "HH", "13",    Locale.UK, true}, // Hour in day (0-23)
+                { "KK", "13",    Locale.UK, true}, // Hour in am/pm (0-11)
+                { "hh", "13",    Locale.UK, true}, // Hour in am/pm (1-12), 
i.e. midday/midnight is 12, not 0
+                { "kk", "13",    Locale.UK, true}, // Hour in day (1-24), i.e. 
midnight is 24, not 0
+
+                { "HH", "23",    Locale.UK, true}, // Hour in day (0-23)
+                { "KK", "23",    Locale.UK, true}, // Hour in am/pm (0-11)
+                { "hh", "23",    Locale.UK, true}, // Hour in am/pm (1-12), 
i.e. midday/midnight is 12, not 0
+                { "kk", "23",    Locale.UK, true}, // Hour in day (1-24), i.e. 
midnight is 24, not 0
+
+                { "HH", "24",    Locale.UK, true}, // Hour in day (0-23)
+                { "KK", "24",    Locale.UK, true}, // Hour in am/pm (0-11)
+                { "hh", "24",    Locale.UK, true}, // Hour in am/pm (1-12), 
i.e. midday/midnight is 12, not 0
+                { "kk", "24",    Locale.UK, true}, // Hour in day (1-24), i.e. 
midnight is 24, not 0
+
+                { "HH", "25",    Locale.UK, true}, // Hour in day (0-23)
+                { "KK", "25",    Locale.UK, true}, // Hour in am/pm (0-11)
+                { "hh", "25",    Locale.UK, true}, // Hour in am/pm (1-12), 
i.e. midday/midnight is 12, not 0
+                { "kk", "25",    Locale.UK, true}, // Hour in day (1-24), i.e. 
midnight is 24, not 0
+
+                { "HH", "48",    Locale.UK, true}, // Hour in day (0-23)
+                { "KK", "48",    Locale.UK, true}, // Hour in am/pm (0-11)
+                { "hh", "48",    Locale.UK, true}, // Hour in am/pm (1-12), 
i.e. midday/midnight is 12, not 0
+                { "kk", "48",    Locale.UK, true}, // Hour in day (1-24), i.e. 
midnight is 24, not 0
+                });
+    }
+
+    private final String format;
+    private final String input;
+    private final Locale locale;
+    private final boolean valid;
+    private final TimeZone timeZone = TimeZone.getDefault();
+
+    public FastDateParserSDFTest(final String format, final String input, 
final Locale locale, final boolean valid) {
+        this.format = format;
+        this.input = input;
+        this.locale = locale;
+        this.valid = valid;
+    }
+
+    @Test
+    public void testOriginal() throws Exception {
+        checkParse(input);
+    }
+
+    @Test
+    public void testOriginalPP() throws Exception {
+        checkParsePosition(input);
+    }
+
+    @Test
+    public void testUpperCase() throws Exception {
+        checkParse(input.toUpperCase(locale));
+    }
+
+    @Test
+    public void testUpperCasePP() throws Exception {
+        checkParsePosition(input.toUpperCase(locale));
+    }
+
+    @Test
+    public void testLowerCase() throws Exception {
+        checkParse(input.toLowerCase(locale));
+    }
+
+    @Test
+    public void testLowerCasePP() throws Exception {
+        checkParsePosition(input.toLowerCase(locale));
+    }
+
+    private void checkParse(final String formattedDate) {
+        final SimpleDateFormat sdf = new SimpleDateFormat(format, locale);
+        sdf.setTimeZone(timeZone);
+        final DateParser fdf = new FastDateParser(format, timeZone, locale);
+        Date expectedTime=null;
+        Class<?> sdfE = null;
+        try {
+            expectedTime = sdf.parse(formattedDate);
+            if (!valid) {
+                // Error in test data
+                throw new RuntimeException("Test data error: expected SDF 
parse to fail, but got " + expectedTime);
+            }
+        } catch (final ParseException e) {
+            if (valid) {
+                // Error in test data
+                throw new RuntimeException("Test data error: expected SDF 
parse to succeed, but got " + e);
+            }
+            sdfE = e.getClass();
+        }
+        Date actualTime = null;
+        Class<?> fdfE = null;
+        try {
+            actualTime = fdf.parse(formattedDate);
+            if (!valid) {
+                // failure in test
+                fail("Expected FDP parse to fail, but got " + actualTime);
+            }
+        } catch (final ParseException e) {
+            if (valid) {
+                // failure in test
+                fail("Expected FDP parse to succeed, but got " + e);
+            }
+            fdfE = e.getClass();
+        }
+        if (valid) {
+            assertEquals(locale.toString()+" "+formattedDate 
+"\n",expectedTime, actualTime);            
+        } else {
+            assertEquals(locale.toString()+" "+formattedDate + " expected same 
Exception ", sdfE, fdfE);            
+        }
+    }
+    private void checkParsePosition(final String formattedDate) {
+        final SimpleDateFormat sdf = new SimpleDateFormat(format, locale);
+        sdf.setTimeZone(timeZone);
+        final DateParser fdf = new FastDateParser(format, timeZone, locale);
+
+        final ParsePosition sdfP = new ParsePosition(0);
+        final Date expectedTime = sdf.parse(formattedDate, sdfP);
+        final int sdferrorIndex = sdfP.getErrorIndex();
+        if (valid) {
+            assertEquals("Expected SDF error index -1 ", -1, sdferrorIndex);
+            final int endIndex = sdfP.getIndex();
+            final int length = formattedDate.length();
+            if (endIndex != length) {
+                // Error in test data
+                throw new RuntimeException("Test data error: expected SDF 
parse to consume entire string; endindex " + endIndex + " != " + length);       
         
+            }
+        } else {
+            final int errorIndex = sdfP.getErrorIndex();
+            if (errorIndex == -1) {
+                throw new RuntimeException("Test data error: expected SDF 
parse to fail, but got " + expectedTime);                
+            }
+        }
+
+        final ParsePosition fdfP = new ParsePosition(0);
+        final Date actualTime = fdf.parse(formattedDate, fdfP);
+        final int fdferrorIndex = fdfP.getErrorIndex();
+        if (valid) {
+            assertEquals("Expected FDF error index -1 ", -1, fdferrorIndex);
+            final int endIndex = fdfP.getIndex();
+            final int length = formattedDate.length();
+            assertEquals("Expected FDF to parse full string " + fdfP, length, 
endIndex);
+            assertEquals(locale.toString()+" "+formattedDate +"\n", 
expectedTime, actualTime);
+        } else {
+            assertNotEquals("Test data error: expected FDF parse to fail, but 
got " + actualTime, -1, fdferrorIndex);
+            assertTrue("FDF error index ("+ fdferrorIndex + ") should 
approxiamate SDF index (" + sdferrorIndex + ")",
+                    sdferrorIndex - fdferrorIndex <= 4);
+        }        
+    }
+}

Reply via email to