From d8faf082bbb5fedc6d19fc3ec89499123b5679d2 Mon Sep 17 00:00:00 2001 From: Gary Gregory Date: Sat, 2 Nov 2024 17:27:35 -0400 Subject: [PATCH] Fix NullPointerException in FastDateParser.TimeZoneStrategy.setCalendar(FastDateParser, Calendar, String) on Java 23 - Tested locally on Java 8, 17, and 23 - Javadoc --- src/changes/changes.xml | 1 + .../commons/lang3/time/FastDateParser.java | 43 +++++++++++-------- 2 files changed, 26 insertions(+), 18 deletions(-) diff --git a/src/changes/changes.xml b/src/changes/changes.xml index b9eec7d57a7..1b1aa4551e7 100644 --- a/src/changes/changes.xml +++ b/src/changes/changes.xml @@ -61,6 +61,7 @@ The type attribute can be add,update,fix,remove. StringUtils.replaceEachRepeatedly regression in 3.11+ #1297. Use simplified JUnit assertion methods #1298. Javadoc and test: Use Strings.CI.startsWithAny method instead #1299. + Fix NullPointerException in FastDateParser.TimeZoneStrategy.setCalendar(FastDateParser, Calendar, String) on Java 23. Add Strings and refactor StringUtils. Add StopWatch.run([Failable]Runnable) and get([Failable]Supplier). diff --git a/src/main/java/org/apache/commons/lang3/time/FastDateParser.java b/src/main/java/org/apache/commons/lang3/time/FastDateParser.java index 8b7830cb030..ff0ad34dc22 100644 --- a/src/main/java/org/apache/commons/lang3/time/FastDateParser.java +++ b/src/main/java/org/apache/commons/lang3/time/FastDateParser.java @@ -24,6 +24,7 @@ import java.text.ParsePosition; import java.text.SimpleDateFormat; import java.util.ArrayList; +import java.util.Arrays; import java.util.Calendar; import java.util.Comparator; import java.util.Date; @@ -35,12 +36,14 @@ import java.util.Objects; import java.util.Set; import java.util.TimeZone; +import java.util.TreeMap; import java.util.TreeSet; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.regex.Matcher; import java.util.regex.Pattern; +import org.apache.commons.lang3.ArraySorter; import org.apache.commons.lang3.LocaleUtils; /** @@ -494,14 +497,19 @@ public String toString() { private static final String RFC_822_TIME_ZONE = "[+-]\\d{4}"; private static final String GMT_OPTION = TimeZones.GMT_ID + "[+-]\\d{1,2}:\\d{2}"; + /** - * Index of zone id + * Index of zone id from {@link DateFormatSymbols#getZoneStrings()}. */ private static final int ID = 0; private final Locale locale; - private final Map tzNames = new HashMap<>(); + /** + * Using lower case only or upper case only will cause problems with some Locales like Turkey, Armenia, Colognian and also depending on the Java + * version. For details, see https://garygregory.wordpress.com/2015/11/03/java-lowercase-conversion-turkey/ + */ + private final Map tzNames = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); /** * Constructs a Strategy that parses a TimeZone @@ -543,30 +551,23 @@ public String toString() { break; } final String zoneName = zoneNames[i]; - if (zoneName != null) { - final String key = zoneName.toLowerCase(locale); - // ignore the data associated with duplicates supplied in - // the additional names - if (sorted.add(key)) { - tzNames.put(key, tzInfo); - } + // ignore the data associated with duplicates supplied in the additional names + if (zoneName != null && sorted.add(zoneName)) { + tzNames.put(zoneName, tzInfo); } } } - // Order is undefined. - for (final String tzId : TimeZone.getAvailableIDs()) { + for (final String tzId : ArraySorter.sort(TimeZone.getAvailableIDs())) { if (tzId.equalsIgnoreCase(TimeZones.GMT_ID)) { continue; } final TimeZone tz = TimeZone.getTimeZone(tzId); final String zoneName = tz.getDisplayName(locale); - final String key = zoneName.toLowerCase(locale); - if (sorted.add(key)) { - tzNames.put(key, new TzInfo(tz, tz.observesDaylightTime())); + if (sorted.add(zoneName)) { + tzNames.put(zoneName, new TzInfo(tz, tz.observesDaylightTime())); } } - // order the regex alternatives with longer strings first, greedy // match will ensure the longest string will be consumed sorted.forEach(zoneName -> simpleQuote(sb.append('|'), zoneName)); @@ -583,11 +584,16 @@ void setCalendar(final FastDateParser parser, final Calendar calendar, final Str if (tz != null) { calendar.setTimeZone(tz); } else { - final String lowerCase = timeZone.toLowerCase(locale); - TzInfo tzInfo = tzNames.get(lowerCase); + TzInfo tzInfo = tzNames.get(timeZone); if (tzInfo == null) { // match missing the optional trailing period - tzInfo = tzNames.get(lowerCase + '.'); + tzInfo = tzNames.get(timeZone + '.'); + if (tzInfo == null) { + // show chars in case this is multiple byte character issue + final char[] charArray = timeZone.toCharArray(); + throw new IllegalStateException(String.format("Can't find time zone '%s' (%d %s) in %s", timeZone, charArray.length, + Arrays.toString(charArray), new TreeSet<>(tzNames.keySet()))); + } } calendar.set(Calendar.DST_OFFSET, tzInfo.dstOffset); calendar.set(Calendar.ZONE_OFFSET, tzInfo.zone.getRawOffset()); @@ -1079,6 +1085,7 @@ private void readObject(final ObjectInputStream in) throws IOException, ClassNot public String toString() { return "FastDateParser[" + pattern + ", " + locale + ", " + timeZone.getID() + "]"; } + /** * Converts all state of this instance to a String handy for debugging. *