diff --git a/datetime/src/main/java/tools/jackson/datatype/jsr310/deser/InstantDeserializer.java b/datetime/src/main/java/tools/jackson/datatype/jsr310/deser/InstantDeserializer.java index f30070d8..29709f1b 100644 --- a/datetime/src/main/java/tools/jackson/datatype/jsr310/deser/InstantDeserializer.java +++ b/datetime/src/main/java/tools/jackson/datatype/jsr310/deser/InstantDeserializer.java @@ -493,14 +493,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/tools/jackson/datatype/jsr310/deser/OffsetDateTimeDeserTest.java b/datetime/src/test/java/tools/jackson/datatype/jsr310/deser/OffsetDateTimeDeserTest.java index e4bfda9e..3edf9fef 100644 --- a/datetime/src/test/java/tools/jackson/datatype/jsr310/deser/OffsetDateTimeDeserTest.java +++ b/datetime/src/test/java/tools/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; @@ -740,6 +741,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/tools/jackson/datatype/jsr310/deser/ZonedDateTimeDeserTest.java b/datetime/src/test/java/tools/jackson/datatype/jsr310/deser/ZonedDateTimeDeserTest.java index 50821ed7..a5b512af 100644 --- a/datetime/src/test/java/tools/jackson/datatype/jsr310/deser/ZonedDateTimeDeserTest.java +++ b/datetime/src/test/java/tools/jackson/datatype/jsr310/deser/ZonedDateTimeDeserTest.java @@ -1,16 +1,18 @@ package tools.jackson.datatype.jsr310.deser; -import java.time.LocalDateTime; -import java.time.ZoneId; -import java.time.ZoneOffset; -import java.time.ZonedDateTime; +import java.time.*; import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.util.Arrays; import java.util.Map; import java.util.TimeZone; +import org.junit.Test; + import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.annotation.JsonFormat.Feature; +import tools.jackson.core.JacksonException; import tools.jackson.core.type.TypeReference; import tools.jackson.databind.DeserializationFeature; @@ -22,8 +24,6 @@ import tools.jackson.datatype.jsr310.JavaTimeModule; import tools.jackson.datatype.jsr310.ModuleTestBase; -import org.junit.Test; - import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; import static org.junit.Assert.fail; @@ -278,4 +278,38 @@ public void testDeserializationWithoutColonInTimeZoneWithTZDB() throws Throwable ZonedDateTime.of(2000, 1, 1, 12, 0, 0 ,0, ZoneId.of("Europe/Paris")), 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" + '"')); + } + } + } } diff --git a/release-notes/CREDITS-2.x b/release-notes/CREDITS-2.x index cb733c9e..91dd124d 100644 --- a/release-notes/CREDITS-2.x +++ b/release-notes/CREDITS-2.x @@ -170,6 +170,8 @@ MichaƂ Ostrowski (karbi@github) David Schlosnagle (schlosna@github) * Contributed #266: Optimize `InstantDeserializer` method `replaceZeroOffsetAsZIfNecessary()` (2.15.0) + * Contributed #336: Optimize `InstantDeserializer` `addInColonToOffsetIfMissing()` + (2.19.0) Daniel Scalzi (dscalzi@github) * Contributed #267: Normalize zone id during ZonedDateTime deserialization diff --git a/release-notes/VERSION-2.x b/release-notes/VERSION-2.x index 94acc6e7..4588f980 100644 --- a/release-notes/VERSION-2.x +++ b/release-notes/VERSION-2.x @@ -10,7 +10,8 @@ Modules: 2.19.0 (not yet released) -- +#336: Optimize `InstantDeserializer` `addInColonToOffsetIfMissing()` + (contributed by David S) 2.18.3 (not yet released)