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..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. @@ -51,13 +52,20 @@ public class InstantDeserializer { private static final long serialVersionUID = 1L; + /** + * Constants used to check if the time offset is zero. See [jackson-modules-java8#18] + * + * @since 2.9.0 + */ + 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, Instant::from, 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<>( @@ -66,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<>( @@ -75,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; @@ -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 + * [jackson-modules-java8#18] 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,15 @@ private ZoneId getZone(DeserializationContext context) return (_valueClass == Instant.class) ? null : context.getTimeZone().toZoneId(); } + private String replaceZeroOffsetAsZIfNecessary(String text) + { + if (replaceZeroOffsetAsZ) { + return ISO8601_UTC_ZERO_OFFSET_SUFFIX_REGEX.matcher(text).replaceFirst("Z"); + } + + 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/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);