From 29aa2b8f2cb0092f41f099d6d9d3889827aefc6b Mon Sep 17 00:00:00 2001 From: David Schlosnagle Date: Mon, 16 Dec 2024 21:01:48 -0500 Subject: [PATCH] Optimize InstantDeserializer addInColonToOffsetIfMissing (#336) --- .../jsr310/deser/InstantDeserializer.java | 40 ++++++++++++++++--- .../jsr310/deser/OffsetDateTimeDeserTest.java | 35 ++++++++++++++++ .../jsr310/deser/ZonedDateTimeDeserTest.java | 35 ++++++++++++++++ 3 files changed, 104 insertions(+), 6 deletions(-) diff --git a/datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/deser/InstantDeserializer.java b/datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/deser/InstantDeserializer.java index 2aca24c6..75a870c4 100644 --- a/datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/deser/InstantDeserializer.java +++ b/datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/deser/InstantDeserializer.java @@ -519,14 +519,42 @@ private static String replaceZeroOffsetAsZ(String text) } // @since 2.13 - private String addInColonToOffsetIfMissing(String text) + private static String addInColonToOffsetIfMissing(String text) { - final Matcher matcher = ISO8601_COLONLESS_OFFSET_REGEX.matcher(text); - if (matcher.find()){ - StringBuilder sb = new StringBuilder(matcher.group(0)); - sb.insert(3, ":"); + int timeIndex = text.indexOf('T'); + if (timeIndex < 0 || timeIndex > text.length() - 1) { + return text; + } + + int offsetIndex = text.indexOf('+', timeIndex + 1); + if (offsetIndex < 0) { + offsetIndex = text.indexOf('-', timeIndex + 1); + } + + if (offsetIndex < 0 || offsetIndex > text.length() - 5) { + return text; + } + + int colonIndex = text.indexOf(':', offsetIndex); + if (colonIndex == offsetIndex + 3) { + return text; + } - return matcher.replaceFirst(sb.toString()); + if (Character.isDigit(text.charAt(offsetIndex + 1)) + && Character.isDigit(text.charAt(offsetIndex + 2)) + && Character.isDigit(text.charAt(offsetIndex + 3)) + && Character.isDigit(text.charAt(offsetIndex + 4))) { + String match = text.substring(offsetIndex, offsetIndex + 5); + return text.substring(0, offsetIndex) + + match.substring(0, 3) + ':' + match.substring(3) + + text.substring(offsetIndex + match.length()); + } + + // fallback to slow regex path, should be fully handled by the above + final Matcher matcher = ISO8601_COLONLESS_OFFSET_REGEX.matcher(text); + if (matcher.find()) { + String match = matcher.group(0); + return matcher.replaceFirst(match.substring(0, 3) + ':' + match.substring(3)); } return text; } diff --git a/datetime/src/test/java/com/fasterxml/jackson/datatype/jsr310/deser/OffsetDateTimeDeserTest.java b/datetime/src/test/java/com/fasterxml/jackson/datatype/jsr310/deser/OffsetDateTimeDeserTest.java index e676230d..83e46d0e 100644 --- a/datetime/src/test/java/com/fasterxml/jackson/datatype/jsr310/deser/OffsetDateTimeDeserTest.java +++ b/datetime/src/test/java/com/fasterxml/jackson/datatype/jsr310/deser/OffsetDateTimeDeserTest.java @@ -5,6 +5,7 @@ import java.time.temporal.ChronoField; import java.time.temporal.ChronoUnit; import java.time.temporal.Temporal; +import java.util.Arrays; import java.util.Map; import java.util.TimeZone; @@ -811,6 +812,40 @@ public void testOffsetDateTimeMinOrMax() throws Exception _testOffsetDateTimeMinOrMax(OffsetDateTime.MAX); } + @Test + public void OffsetDateTime_with_offset_can_be_deserialized() throws Exception { + ObjectReader r = MAPPER.readerFor(OffsetDateTime.class) + .without(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE); + + String base = "2015-07-24T12:23:34.184"; + for (String offset : Arrays.asList("+00", "-00")) { + String time = base + offset; + if (!System.getProperty("java.version").startsWith("1.8")) { + // JDK 8 cannot parse hour offsets without minutes + assertIsEqual(OffsetDateTime.parse("2015-07-24T12:23:34.184Z"), r.readValue('"' + time + '"')); + } + assertIsEqual(OffsetDateTime.parse("2015-07-24T12:23:34.184Z"), r.readValue('"' + time + "00" + '"')); + assertIsEqual(OffsetDateTime.parse("2015-07-24T12:23:34.184Z"), r.readValue('"' + time + ":00" + '"')); + assertIsEqual(OffsetDateTime.parse("2015-07-24T12:23:34.184" + offset + ":30"), r.readValue('"' + time + "30" + '"')); + assertIsEqual(OffsetDateTime.parse("2015-07-24T12:23:34.184" + offset + ":30"), r.readValue('"' + time + ":30" + '"')); + } + + for (String prefix : Arrays.asList("-", "+")) { + for (String hours : Arrays.asList("00", "01", "02", "03", "11", "12")) { + String time = base + prefix + hours; + OffsetDateTime expectedHour = OffsetDateTime.parse(time + ":00"); + if (!System.getProperty("java.version").startsWith("1.8")) { + // JDK 8 cannot parse hour offsets without minutes + assertIsEqual(expectedHour, r.readValue('"' + time + '"')); + } + assertIsEqual(expectedHour, r.readValue('"' + time + "00" + '"')); + assertIsEqual(expectedHour, r.readValue('"' + time + ":00" + '"')); + assertIsEqual(OffsetDateTime.parse(time + ":30"), r.readValue('"' + time + "30" + '"')); + assertIsEqual(OffsetDateTime.parse(time + ":30"), r.readValue('"' + time + ":30" + '"')); + } + } + } + private void _testOffsetDateTimeMinOrMax(OffsetDateTime offsetDateTime) throws Exception { diff --git a/datetime/src/test/java/com/fasterxml/jackson/datatype/jsr310/deser/ZonedDateTimeDeserTest.java b/datetime/src/test/java/com/fasterxml/jackson/datatype/jsr310/deser/ZonedDateTimeDeserTest.java index 72dc2206..4f6388cd 100644 --- a/datetime/src/test/java/com/fasterxml/jackson/datatype/jsr310/deser/ZonedDateTimeDeserTest.java +++ b/datetime/src/test/java/com/fasterxml/jackson/datatype/jsr310/deser/ZonedDateTimeDeserTest.java @@ -18,11 +18,13 @@ import java.io.IOException; import java.time.LocalDateTime; +import java.time.OffsetDateTime; import java.time.ZoneId; import java.time.ZoneOffset; import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; import java.time.format.DateTimeParseException; +import java.util.Arrays; import java.util.Map; import java.util.TimeZone; @@ -282,6 +284,39 @@ public void testDeserializationWithoutColonInTimeZoneWithTZDB() throws Throwable wrapper.value); } + @Test + public void ZonedDateTime_with_offset_can_be_deserialized() throws Exception { + ObjectReader r = newMapper().readerFor(ZonedDateTime.class) + .without(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE); + + String base = "2015-07-24T12:23:34.184"; + for (String offset : Arrays.asList("+00", "-00")) { + String time = base + offset; + if (!System.getProperty("java.version").startsWith("1.8")) { + // JDK 8 cannot parse hour offsets without minutes + assertEquals(ZonedDateTime.parse("2015-07-24T12:23:34.184Z"), r.readValue('"' + time + '"')); + } + assertEquals(ZonedDateTime.parse("2015-07-24T12:23:34.184Z"), r.readValue('"' + time + "00" + '"')); + assertEquals(ZonedDateTime.parse("2015-07-24T12:23:34.184Z"), r.readValue('"' + time + ":00" + '"')); + assertEquals(ZonedDateTime.parse("2015-07-24T12:23:34.184" + offset + ":30" ), r.readValue('"' + time + "30" + '"')); + assertEquals(ZonedDateTime.parse("2015-07-24T12:23:34.184" + offset + ":30" ), r.readValue('"' + time + ":30" + '"')); + } + + for (String prefix : Arrays.asList("-", "+")) { + for (String hours : Arrays.asList("00", "01", "02", "03", "11", "12")) { + String time = base + prefix + hours; + ZonedDateTime expectedHour = ZonedDateTime.parse(time + ":00"); + if (!System.getProperty("java.version").startsWith("1.8")) { + // JDK 8 cannot parse hour offsets without minutes + assertEquals(expectedHour, r.readValue('"' + time + '"')); + } + assertEquals(expectedHour, r.readValue('"' + time + "00" + '"')); + assertEquals(expectedHour, r.readValue('"' + time + ":00" + '"')); + assertEquals(ZonedDateTime.parse(time + ":30"), r.readValue('"' + time + "30" + '"')); + assertEquals(ZonedDateTime.parse(time + ":30"), r.readValue('"' + time + ":30" + '"')); + } + } + } private void expectFailure(String json) throws Throwable { try {