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 f189ce63..cde87cac 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 @@ -38,6 +38,7 @@ import java.util.Objects; import java.util.function.BiFunction; import java.util.function.Function; +import java.util.regex.Matcher; import java.util.regex.Pattern; /** @@ -59,6 +60,13 @@ public class InstantDeserializer */ private static final Pattern ISO8601_UTC_ZERO_OFFSET_SUFFIX_REGEX = Pattern.compile("\\+00:?(00)?$"); + /** + * Constants used to check if ISO 8601 time string is colonless. See [jackson-modules-java8#131] + * + * @since 2.13 + */ + protected static final Pattern ISO8601_COLONLESS_OFFSET_REGEX = Pattern.compile("[+-][0-9]{4}(?=\\[|$)"); + public static final InstantDeserializer INSTANT = new InstantDeserializer<>( Instant.class, DateTimeFormatter.ISO_INSTANT, Instant::from, @@ -277,6 +285,17 @@ protected T _fromString(JsonParser p, DeserializationContext ctxt, string = replaceZeroOffsetAsZIfNecessary(string); } + // For some reason DateTimeFormatter.ISO_INSTANT only supports UTC ISO 8601 strings, so it have to be excluded + if (_formatter == DateTimeFormatter.ISO_OFFSET_DATE_TIME || + _formatter == DateTimeFormatter.ISO_ZONED_DATE_TIME) { + + // 21-March-2021, Oeystein: Work-around to support basic iso 8601 format (colon-less). + // As per JSR-310; Only extended 8601 formats (with colon) are supported for + // ZonedDateTime.parse() and OffsetDateTime.parse(). + // https://github.com/FasterXML/jackson-modules-java8/issues/131 + string = addInColonToOffsetIfMissing(string); + } + T value; try { TemporalAccessor acc = _formatter.parse(string); @@ -323,6 +342,20 @@ private String replaceZeroOffsetAsZIfNecessary(String text) return text; } + private String addInColonToOffsetIfMissing(String text) + { + Matcher matcher = ISO8601_COLONLESS_OFFSET_REGEX.matcher(text); + + if (matcher.find()){ + StringBuilder sb = new StringBuilder(matcher.group(0)); + sb.insert(3, ":"); + + return matcher.replaceFirst(sb.toString()); + } + + return text; + } + public static class FromIntegerArguments // since 2.8.3 { public final long value; diff --git a/datetime/src/test/java/com/fasterxml/jackson/datatype/jsr310/deser/InstantDeserTest.java b/datetime/src/test/java/com/fasterxml/jackson/datatype/jsr310/deser/InstantDeserTest.java index 2254cb70..070d89f5 100644 --- a/datetime/src/test/java/com/fasterxml/jackson/datatype/jsr310/deser/InstantDeserTest.java +++ b/datetime/src/test/java/com/fasterxml/jackson/datatype/jsr310/deser/InstantDeserTest.java @@ -5,6 +5,7 @@ import java.time.temporal.ChronoUnit; import java.time.temporal.Temporal; import java.util.Map; +import java.util.regex.Matcher; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.exc.MismatchedInputException; @@ -19,6 +20,7 @@ import com.fasterxml.jackson.datatype.jsr310.MockObjectConfiguration; import com.fasterxml.jackson.datatype.jsr310.ModuleTestBase; +import static com.fasterxml.jackson.datatype.jsr310.deser.InstantDeserializer.ISO8601_COLONLESS_OFFSET_REGEX; import static org.junit.Assert.*; import static org.junit.Assert.assertNull; @@ -522,4 +524,40 @@ public void testStrictDeserializeFromEmptyString() throws Exception { String valueFromEmptyStr = mapper.writeValueAsString(asMap(key, "")); objectReader.readValue(valueFromEmptyStr); } + + /* + /************************************************************************ + /* Tests for InstantDeserializer.ISO8601_COLONLESS_OFFSET_REGEX + /************************************************************************ + */ + @Test + public void testISO8601ColonlessRegexFindsOffset() { + Matcher matcher = ISO8601_COLONLESS_OFFSET_REGEX.matcher("2000-01-01T12:00+0100"); + + assertTrue("Matcher finds +0100 as an colonless offset", matcher.find()); + assertEquals("Matcher groups +0100 as an colonless offset", matcher.group(), "+0100"); + } + + @Test + public void testISO8601ColonlessRegexFindsOffsetWithTZ() { + Matcher matcher = ISO8601_COLONLESS_OFFSET_REGEX.matcher("2000-01-01T12:00+0100[Europe/Paris]"); + + assertTrue("Matcher finds +0100 as an colonless offset", matcher.find()); + assertEquals("Matcher groups +0100 as an colonless offset", matcher.group(), "+0100"); + } + + @Test + public void testISO8601ColonlessRegexDoesNotAffectNegativeYears() { + Matcher matcher = ISO8601_COLONLESS_OFFSET_REGEX.matcher("-2000-01-01T12:00+01:00[Europe/Paris]"); + + assertFalse("Matcher does not find -2000 (years) as an offset without colon", matcher.find()); + } + + @Test + public void testISO8601ColonlessRegexDoesNotAffectNegativeYearsWithColonless() { + Matcher matcher = ISO8601_COLONLESS_OFFSET_REGEX.matcher("-2000-01-01T12:00+0100[Europe/Paris]"); + + assertTrue("Matcher finds +0100 as an colonless offset", matcher.find()); + assertEquals("Matcher groups +0100 as an colonless offset", matcher.group(), "+0100"); + } } 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 2da74be7..ce559362 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 @@ -343,6 +343,22 @@ public void testDeserializationAsString01WithTimeZoneTurnedOff() throws Exceptio assertEquals("The time zone is not correct.", getOffset(value, Z1), value.getOffset()); } + @Test + public void testDeserializationAsString01WithTimeZoneColonless() throws Exception + { + OffsetDateTime date = OffsetDateTime.ofInstant(Instant.ofEpochSecond(0L), Z1); + ObjectMapper m = newMapper() + .configure(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE, false); + + String sDate = offsetWithoutColon(FORMATTER.format(date)); + + OffsetDateTime value = m.readValue('"' + sDate + '"', OffsetDateTime.class); + + assertNotNull("The value should not be null.", value); + assertIsEqual(date, value); + assertEquals("The time zone is not correct.", getOffset(value, Z1), value.getOffset()); + } + @Test public void testDeserializationAsString02WithoutTimeZone() throws Exception { @@ -384,6 +400,22 @@ public void testDeserializationAsString02WithTimeZoneTurnedOff() throws Exceptio assertEquals("The time zone is not correct.", getOffset(value, Z2), value.getOffset()); } + @Test + public void testDeserializationAsString02WithTimeZoneColonless() throws Exception + { + OffsetDateTime date = OffsetDateTime.ofInstant(Instant.ofEpochSecond(123456789L, 183917322), Z2); + ObjectMapper m = newMapper() + .configure(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE, false); + + String sDate = offsetWithoutColon(FORMATTER.format(date)); + + OffsetDateTime value = m.readValue('"' + sDate + '"', OffsetDateTime.class); + + assertNotNull("The value should not be null.", value); + assertIsEqual(date, value); + assertEquals("The time zone is not correct.", getOffset(value, Z2), value.getOffset()); + } + @Test public void testDeserializationAsString03WithoutTimeZone() throws Exception { @@ -425,6 +457,23 @@ public void testDeserializationAsString03WithTimeZoneTurnedOff() throws Exceptio assertEquals("The time zone is not correct.", getOffset(value, Z3), value.getOffset()); } + + @Test + public void testDeserializationAsString03WithTimeZoneColonless() throws Exception + { + OffsetDateTime date = OffsetDateTime.now(Z3); + ObjectMapper m = newMapper() + .configure(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE, false); + + String sDate = offsetWithoutColon(FORMATTER.format(date)); + + OffsetDateTime value = m.readValue('"' + sDate + '"', OffsetDateTime.class); + + assertNotNull("The value should not be null.", value); + assertIsEqual(date, value); + assertEquals("The time zone is not correct.", getOffset(value, Z3), value.getOffset()); + } + @Test public void testDeserializationWithTypeInfo01WithoutTimeZone() throws Exception { @@ -709,4 +758,8 @@ private static ZoneOffset getOffset(OffsetDateTime date, ZoneId zone) { return zone.getRules().getOffset(date.toLocalDateTime()); } + + private static String offsetWithoutColon(String string){ + return new StringBuilder(string).deleteCharAt(string.lastIndexOf(":")).toString(); + } } 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 a3f49c9b..a668045f 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 @@ -161,6 +161,36 @@ public void testStrictDeserializeFromEmptyString() throws Exception { objectReader.readValue(valueFromEmptyStr); } + /* + /********************************************************** + / Tests for Iso 8601s ZonedDateTimes that are colonless + /********************************************************** + */ + + @Test + public void testDeserializationWithoutColonInOffset() throws Throwable + { + WrapperWithFeatures wrapper = newMapper() + .readerFor(WrapperWithFeatures.class) + .readValue("{\"value\":\"2000-01-01T12:00+0100\"}"); + + assertEquals("Value parses as if it were with colon", + ZonedDateTime.of(2000, 1, 1, 12, 0, 0 ,0, ZoneOffset.ofHours(1)), + wrapper.value); + } + + @Test + public void testDeserializationWithoutColonInTimeZoneWithTZDB() throws Throwable + { + WrapperWithFeatures wrapper = newMapper() + .readerFor(WrapperWithFeatures.class) + .readValue("{\"value\":\"2000-01-01T12:00+0100[Europe/Paris]\"}"); + assertEquals("Timezone should be preserved.", + ZonedDateTime.of(2000, 1, 1, 12, 0, 0 ,0, ZoneId.of("Europe/Paris")), + wrapper.value); + } + + private void expectFailure(String json) throws Throwable { try { READER.readValue(a2q(json));