From 183ae5aad731b4460dee6e6e61a15a2eed02cfcc Mon Sep 17 00:00:00 2001 From: kevinjom Date: Mon, 13 Mar 2017 18:25:05 +0800 Subject: [PATCH 1/3] Supported to parse UTC time with valid zero offset. Offset format with `+00`, `+0000` and `+00:00` are all supported now. --- .../jsr310/deser/InstantDeserializer.java | 45 +++++++++++++------ 1 file changed, 31 insertions(+), 14 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 7e782a01..b8ae636c 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 @@ -51,6 +51,14 @@ public class InstantDeserializer { private static final long serialVersionUID = 1L; + /** + * Constants used to check if the time offset is zero + * + * @since 2.9.0 + */ + private static final String ISO8601_UTC_ZERO_OFFSET_SUFFIX_REGEX = "\\+00:?(00)?$"; + private static final String ISO8601_UTC_ZERO_OFFSET_REGEX = "[^\\+]+" + ISO8601_UTC_ZERO_OFFSET_SUFFIX_REGEX; + public static final InstantDeserializer INSTANT = new InstantDeserializer<>( Instant.class, DateTimeFormatter.ISO_INSTANT, Instant::from, @@ -87,13 +95,13 @@ public class InstantDeserializer protected final BiFunction adjust; /** - * In case of vanilla `Instant` we seem to need to translate "+0000" + * In case of vanilla `Instant` we seem to need to translate "+0000 | +00:00 | +00" * timezone designator into plain "Z" for some reason; see * [datatype-jsr310#79] for more info * - * @since 2.7.5 + * @since 2.9.0 */ - protected final boolean replace0000AsZ; + protected final boolean replaceZeroOffsetAsZ; /** * Flag for JsonFormat.Feature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE @@ -108,14 +116,14 @@ protected InstantDeserializer(Class supportedType, Function fromMilliseconds, Function fromNanoseconds, BiFunction adjust, - boolean replace0000AsZ) + boolean replaceZeroOffsetAsZ) { super(supportedType, formatter); this.parsedToValue = parsedToValue; this.fromMilliseconds = fromMilliseconds; this.fromNanoseconds = fromNanoseconds; this.adjust = adjust == null ? ((d, z) -> d) : adjust; - this.replace0000AsZ = replace0000AsZ; + this.replaceZeroOffsetAsZ = replaceZeroOffsetAsZ; _adjustToContextTZOverride = null; } @@ -127,7 +135,7 @@ protected InstantDeserializer(InstantDeserializer base, DateTimeFormatter f) fromMilliseconds = base.fromMilliseconds; fromNanoseconds = base.fromNanoseconds; adjust = base.adjust; - replace0000AsZ = (_formatter == DateTimeFormatter.ISO_INSTANT); + replaceZeroOffsetAsZ = (_formatter == DateTimeFormatter.ISO_INSTANT); _adjustToContextTZOverride = base._adjustToContextTZOverride; } @@ -139,7 +147,7 @@ protected InstantDeserializer(InstantDeserializer base, Boolean adjustToConte fromMilliseconds = base.fromMilliseconds; fromNanoseconds = base.fromNanoseconds; adjust = base.adjust; - replace0000AsZ = base.replace0000AsZ; + replaceZeroOffsetAsZ = base.replaceZeroOffsetAsZ; _adjustToContextTZOverride = adjustToContextTimezoneOverride; } @@ -189,13 +197,8 @@ public T deserialize(JsonParser parser, DeserializationContext context) throws I // fall through to default handling, to get error there } } - // 24-May-2016, tatu: as per [datatype-jsr310#79] seems like we need - // some massaging in some cases... - if (replace0000AsZ) { - if (string.endsWith("+0000")) { - string = string.substring(0, string.length() - 5) + "Z"; - } - } + + string = replaceZeroOffsetAsZIfNecessary(string); } T value; @@ -285,6 +288,20 @@ private ZoneId getZone(DeserializationContext context) return (_valueClass == Instant.class) ? null : context.getTimeZone().toZoneId(); } + private String replaceZeroOffsetAsZIfNecessary(String text) + { + if (replaceZeroOffsetAsZ && endsWithZeroOffset(text)) { + return text.replaceFirst(ISO8601_UTC_ZERO_OFFSET_SUFFIX_REGEX, "Z"); + } + + return text; + } + + private boolean endsWithZeroOffset(String text) + { + return text.matches(ISO8601_UTC_ZERO_OFFSET_REGEX); + } + public static class FromIntegerArguments // since 2.8.3 { public final long value; From 9ce1a365ccfaca49e83994f8ac76ca78c12b48a5 Mon Sep 17 00:00:00 2001 From: kevinjom Date: Tue, 14 Mar 2017 10:10:23 +0800 Subject: [PATCH 2/3] Added test for supporting to parse zero zone offset time text. --- .../jsr310/deser/InstantDeserializer.java | 10 ++--- .../jsr310/TestInstantSerialization.java | 44 ++++++++++++++++--- .../TestOffsetDateTimeDeserialization.java | 18 ++++++++ 3 files changed, 60 insertions(+), 12 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 b8ae636c..a2c4890c 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 @@ -52,7 +52,7 @@ public class InstantDeserializer private static final long serialVersionUID = 1L; /** - * Constants used to check if the time offset is zero + * Constants used to check if the time offset is zero. See [jackson-modules-java8#18] * * @since 2.9.0 */ @@ -65,7 +65,7 @@ public class InstantDeserializer a -> Instant.ofEpochMilli(a.value), a -> Instant.ofEpochSecond(a.integer, a.fraction), null, - true // yes, replace +0000 with Z + true // yes, replace zero offset with Z ); public static final InstantDeserializer OFFSET_DATE_TIME = new InstantDeserializer<>( @@ -74,7 +74,7 @@ public class InstantDeserializer a -> OffsetDateTime.ofInstant(Instant.ofEpochMilli(a.value), a.zoneId), a -> OffsetDateTime.ofInstant(Instant.ofEpochSecond(a.integer, a.fraction), a.zoneId), (d, z) -> d.withOffsetSameInstant(z.getRules().getOffset(d.toLocalDateTime())), - true // yes, replace +0000 with Z + true // yes, replace zero offset with Z ); public static final InstantDeserializer ZONED_DATE_TIME = new InstantDeserializer<>( @@ -83,7 +83,7 @@ public class InstantDeserializer a -> ZonedDateTime.ofInstant(Instant.ofEpochMilli(a.value), a.zoneId), a -> ZonedDateTime.ofInstant(Instant.ofEpochSecond(a.integer, a.fraction), a.zoneId), ZonedDateTime::withZoneSameInstant, - false // keep +0000 and Z separate since zones explicitly supported + false // keep zero offset and Z separate since zones explicitly supported ); protected final Function fromMilliseconds; @@ -97,7 +97,7 @@ public class InstantDeserializer /** * In case of vanilla `Instant` we seem to need to translate "+0000 | +00:00 | +00" * timezone designator into plain "Z" for some reason; see - * [datatype-jsr310#79] for more info + * [jackson-modules-java8#18] for more info * * @since 2.9.0 */ diff --git a/datetime/src/test/java/com/fasterxml/jackson/datatype/jsr310/TestInstantSerialization.java b/datetime/src/test/java/com/fasterxml/jackson/datatype/jsr310/TestInstantSerialization.java index 27c21f27..989934ff 100644 --- a/datetime/src/test/java/com/fasterxml/jackson/datatype/jsr310/TestInstantSerialization.java +++ b/datetime/src/test/java/com/fasterxml/jackson/datatype/jsr310/TestInstantSerialization.java @@ -16,9 +16,11 @@ package com.fasterxml.jackson.datatype.jsr310; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertTrue; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import org.junit.Test; import java.time.Instant; import java.time.LocalDate; @@ -29,10 +31,9 @@ import java.time.temporal.ChronoUnit; import java.time.temporal.Temporal; -import com.fasterxml.jackson.annotation.JsonFormat; -import com.fasterxml.jackson.databind.*; - -import org.junit.Test; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; public class TestInstantSerialization extends ModuleTestBase { @@ -443,4 +444,33 @@ public void testRoundTripOfInstantAndJavaUtilDate() throws Exception assertEquals(givenInstant, actual); } + + // [jackson-modules-java8#18] + @Test + public void testDeserializationFromStringWithZeroZoneOffset01() throws Exception { + Instant date = Instant.now(); + String json = formatWithZeroZoneOffset(date, "+00:00"); + Instant result = MAPPER.readValue(json, Instant.class); + assertEquals("The value is not correct.", date, result); + } + + @Test + public void testDeserializationFromStringWithZeroZoneOffset02() throws Exception { + Instant date = Instant.now(); + String json = formatWithZeroZoneOffset(date, "+0000"); + Instant result = MAPPER.readValue(json, Instant.class); + assertEquals("The value is not correct.", date, result); + } + + @Test + public void testDeserializationFromStringWithZeroZoneOffset03() throws Exception { + Instant date = Instant.now(); + String json = formatWithZeroZoneOffset(date, "+00"); + Instant result = MAPPER.readValue(json, Instant.class); + assertEquals("The value is not correct.", date, result); + } + + private String formatWithZeroZoneOffset(Instant date, String offset){ + return '"' + FORMATTER.format(date).replaceFirst("Z$", offset) + '"'; + } } diff --git a/datetime/src/test/java/com/fasterxml/jackson/datatype/jsr310/TestOffsetDateTimeDeserialization.java b/datetime/src/test/java/com/fasterxml/jackson/datatype/jsr310/TestOffsetDateTimeDeserialization.java index 98cb0c4e..2cba6195 100644 --- a/datetime/src/test/java/com/fasterxml/jackson/datatype/jsr310/TestOffsetDateTimeDeserialization.java +++ b/datetime/src/test/java/com/fasterxml/jackson/datatype/jsr310/TestOffsetDateTimeDeserialization.java @@ -84,6 +84,24 @@ public void testBadDeserializationAsString01() throws Throwable expectFailure("'notanoffsetdatetime'"); } + @Test + public void testDeserializationAsWithZeroZoneOffset01() throws Exception + { + expectSuccess(OffsetDateTime.of(2000, 1, 1, 12, 0, 0, 0, ZoneOffset.UTC), "'2000-01-01T12:00+00:00'"); + } + + @Test + public void testDeserializationAsWithZeroZoneOffset02() throws Exception + { + expectSuccess(OffsetDateTime.of(2000, 1, 1, 12, 0, 0, 0, ZoneOffset.UTC), "'2000-01-01T12:00+0000'"); + } + + @Test + public void testDeserializationAsWithZeroZoneOffset03() throws Exception + { + expectSuccess(OffsetDateTime.of(2000, 1, 1, 12, 0, 0, 0, ZoneOffset.UTC), "'2000-01-01T12:00+00'"); + } + private void expectFailure(String json) throws Exception { try { read(json); From 898bd4fc0b10a45e3a383ec4f639d12b4adde9aa Mon Sep 17 00:00:00 2001 From: kevinjom Date: Tue, 14 Mar 2017 13:48:27 +0800 Subject: [PATCH 3/3] Used pre-compiled regex pattern and replace with `Z` directly without check if the input matches the pattern. --- .../datatype/jsr310/deser/InstantDeserializer.java | 13 ++++--------- 1 file changed, 4 insertions(+), 9 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 a2c4890c..dccfd26b 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 @@ -39,6 +39,7 @@ import java.time.temporal.TemporalAccessor; import java.util.function.BiFunction; import java.util.function.Function; +import java.util.regex.Pattern; /** * Deserializer for Java 8 temporal {@link Instant}s, {@link OffsetDateTime}, and {@link ZonedDateTime}s. @@ -56,8 +57,7 @@ public class InstantDeserializer * * @since 2.9.0 */ - private static final String ISO8601_UTC_ZERO_OFFSET_SUFFIX_REGEX = "\\+00:?(00)?$"; - private static final String ISO8601_UTC_ZERO_OFFSET_REGEX = "[^\\+]+" + ISO8601_UTC_ZERO_OFFSET_SUFFIX_REGEX; + private static final Pattern ISO8601_UTC_ZERO_OFFSET_SUFFIX_REGEX = Pattern.compile("\\+00:?(00)?$"); public static final InstantDeserializer INSTANT = new InstantDeserializer<>( Instant.class, DateTimeFormatter.ISO_INSTANT, @@ -290,18 +290,13 @@ private ZoneId getZone(DeserializationContext context) private String replaceZeroOffsetAsZIfNecessary(String text) { - if (replaceZeroOffsetAsZ && endsWithZeroOffset(text)) { - return text.replaceFirst(ISO8601_UTC_ZERO_OFFSET_SUFFIX_REGEX, "Z"); + if (replaceZeroOffsetAsZ) { + return ISO8601_UTC_ZERO_OFFSET_SUFFIX_REGEX.matcher(text).replaceFirst("Z"); } return text; } - private boolean endsWithZeroOffset(String text) - { - return text.matches(ISO8601_UTC_ZERO_OFFSET_REGEX); - } - public static class FromIntegerArguments // since 2.8.3 { public final long value;