From 1761f5452c2a5fc4aeb89a49a8aee0842c61f0bf Mon Sep 17 00:00:00 2001 From: Moritz Orth Date: Sat, 16 May 2020 13:00:18 +0200 Subject: [PATCH] Only normalize OffsetDateTimes when they are normalizable Fix for #166. This commit introduces static constants for the minimum and the maximum OffsetDateTimes that are adjustable to all possible time zones in InstantDeserializer and adds a check that an OffsetDateTime lies in these boundaries before it is adjusted. --- .../jsr310/deser/InstantDeserializer.java | 30 +++++++- .../jsr310/deser/OffsetDateTimeDeserTest.java | 73 +++++++++++++++++++ 2 files changed, 102 insertions(+), 1 deletion(-) 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 880f7c39..55e0c7be 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 @@ -59,6 +59,18 @@ public class InstantDeserializer */ private static final Pattern ISO8601_UTC_ZERO_OFFSET_SUFFIX_REGEX = Pattern.compile("\\+00:?(00)?$"); + /** + * Constant that defines the lower bound for {@link OffsetDateTime}s being adjustable to any + * other time zone. See [jackson-modules-java8#166]. + */ + private static final OffsetDateTime OFFSET_DATE_TIME_MIN_ADJ = OffsetDateTime.MIN.plusHours(36); + + /** + * Constant that defines the upper bound for {@link OffsetDateTime}s being adjustable to any + * other time zone. See [jackson-modules-java8#166]. + */ + private static final OffsetDateTime OFFSET_DATE_TIME_MAX_ADJ = OffsetDateTime.MAX.minusHours(36); + public static final InstantDeserializer INSTANT = new InstantDeserializer<>( Instant.class, DateTimeFormatter.ISO_INSTANT, Instant::from, @@ -73,7 +85,7 @@ public class InstantDeserializer OffsetDateTime::from, 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())), + (d, z) -> isAdjustable(d) ? d.withOffsetSameInstant(z.getRules().getOffset(d.toLocalDateTime())) : d, true // yes, replace zero offset with Z ); @@ -326,6 +338,22 @@ private String replaceZeroOffsetAsZIfNecessary(String text) return text; } + /** + * Checks whether an {@link OffsetDateTime} is adjustable to any other time offset. We can + * safely regard an {@link OffsetDateTime} as adjustable to all time zones when it's geq than + * OffsetDateTime.MIN + 36 hours and leq than OffsetDateTime.MAX - 36 hours. + * See [jackson-modules-java8#166]. + * + * @param d The {@link OffsetDateTime} that should be adjusted + * @return true, when the {@link OffsetDateTime} can be adjusted to any other time zone, false else + */ + private static boolean isAdjustable(OffsetDateTime d) + { + return (d.isAfter(OFFSET_DATE_TIME_MIN_ADJ) && d.isBefore(OFFSET_DATE_TIME_MAX_ADJ)) + || d.isEqual(OFFSET_DATE_TIME_MIN_ADJ) + || d.isEqual(OFFSET_DATE_TIME_MAX_ADJ); + } + 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/OffsetDateTimeDeserTest.java b/datetime/src/test/java/com/fasterxml/jackson/datatype/jsr310/deser/OffsetDateTimeDeserTest.java index 3752f2ed..e647edd2 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 @@ -614,6 +614,79 @@ public void testRoundTripOfOffsetDateTimeAndJavaUtilDate() throws Exception assertEquals(givenInstant.atOffset(ZoneOffset.UTC), actual); } + /* + * Tests for the deserialization of OffsetDateTimes that cannot be + * normalized with ADJUST_DATES_TO_CONTEXT_TIME_ZONE enabled. The expected + * behaviour is that normalization is skipped for those OffsetDateTimes + * that cannot be normalized. See [jackson-modules-java8#166]. + */ + + @Test + public void testDeserializationOfOffsetDateTimeMin() throws Exception + { + OffsetDateTime date = OffsetDateTime.MIN; + OffsetDateTime value = MAPPER.readerFor(OffsetDateTime.class) + .with(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE) + .readValue('"' + FORMATTER.format(date) + '"'); + assertIsEqual(date, value); + assertNotEquals("The time zone has been normalized.", ZoneOffset.UTC, value.getOffset()); + } + + @Test + public void testDeserializationOfUnadjustableOffsetDateTimeNearMin() throws Exception + { + OffsetDateTime date = OffsetDateTime.MIN.plusHours(36).minusNanos(1); + OffsetDateTime value = MAPPER.readerFor(OffsetDateTime.class) + .with(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE) + .readValue('"' + FORMATTER.format(date) + '"'); + assertIsEqual(date, value); + assertNotEquals("The time zone has been normalized.", ZoneOffset.UTC, value.getOffset()); + } + + @Test + public void testDeserializationOfAdjustableOffsetDateTimeNearMin() throws Exception + { + OffsetDateTime date = OffsetDateTime.MIN.plusHours(36); + OffsetDateTime value = MAPPER.readerFor(OffsetDateTime.class) + .with(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE) + .readValue('"' + FORMATTER.format(date) + '"'); + assertIsEqual(date, value); + assertEquals("The time zone is not correct.", ZoneOffset.UTC, value.getOffset()); + } + + @Test + public void testDeserializationOfOffsetDateTimeMax() throws Exception + { + OffsetDateTime date = OffsetDateTime.MAX; + OffsetDateTime value = MAPPER.readerFor(OffsetDateTime.class) + .with(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE) + .readValue('"' + FORMATTER.format(date) + '"'); + assertIsEqual(date, value); + assertNotEquals("The time zone has been normalized.", ZoneOffset.UTC, value.getOffset()); + } + + @Test + public void testDeserializationOfUnadjustableOffsetDateTimeNearMax() throws Exception + { + OffsetDateTime date = OffsetDateTime.MAX.minusHours(36).plusNanos(1); + OffsetDateTime value = MAPPER.readerFor(OffsetDateTime.class) + .with(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE) + .readValue('"' + FORMATTER.format(date) + '"'); + assertIsEqual(date, value); + assertNotEquals("The time zone has been normalized.", ZoneOffset.UTC, value.getOffset()); + } + + @Test + public void testDeserializationOfAdjustableOffsetDateTimeNearMax() throws Exception + { + OffsetDateTime date = OffsetDateTime.MAX.minusHours(36); + OffsetDateTime value = MAPPER.readerFor(OffsetDateTime.class) + .with(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE) + .readValue('"' + FORMATTER.format(date) + '"'); + assertIsEqual(date, value); + assertEquals("The time zone is not correct.", ZoneOffset.UTC, value.getOffset()); + } + /* /********************************************************** /* Tests for empty string handling