[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); + } + } +}
