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 c3133357..dfd320e9 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 @@ -29,11 +29,7 @@ import java.io.IOException; import java.math.BigDecimal; -import java.time.DateTimeException; -import java.time.Instant; -import java.time.OffsetDateTime; -import java.time.ZoneId; -import java.time.ZonedDateTime; +import java.time.*; import java.time.format.DateTimeFormatter; import java.time.temporal.Temporal; import java.time.temporal.TemporalAccessor; @@ -69,7 +65,15 @@ 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) -> { + ZoneOffset o = z.getRules().getOffset(d.toLocalDateTime()); + // Only adjust the OffsetDateTime when it is normalizable + if (isAdjustable(d, o)) { + return d.withOffsetSameInstant(o); + } else { + return d; + } + }, true // yes, replace zero offset with Z ); @@ -320,6 +324,24 @@ private String replaceZeroOffsetAsZIfNecessary(String text) return text; } + /** + * Checks whether an {@link OffsetDateTime} is normalizable to a certain + * {@link ZoneOffset}. See [jackson-modules-java8#166]. + * + * @param d The {@link OffsetDateTime} to normalize + * @param o The desired {@link ZoneOffset} of the normalization + * @return true, when the {@link OffsetDateTime} can be normalized to the + * given {@link ZoneOffset}, false else + */ + private static boolean isAdjustable(OffsetDateTime d, ZoneOffset o) + { + OffsetDateTime lowerBoundEx = OffsetDateTime.MIN + .plusHours(18).minusSeconds(o.getTotalSeconds()).minusNanos(1); + OffsetDateTime upperBoundEx = OffsetDateTime.MAX + .minusHours(18).minusSeconds(o.getTotalSeconds()).plusNanos(1); + return d.isAfter(lowerBoundEx) && d.isBefore(upperBoundEx); + } + 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 57933bcf..8acf1596 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 @@ -522,6 +522,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(18).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(18); + 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(18).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(18); + 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