Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Only normalize OffsetDateTimes when they are normalizable #173

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,18 @@ public class InstantDeserializer<T extends Temporal>
*/
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> INSTANT = new InstantDeserializer<>(
Instant.class, DateTimeFormatter.ISO_INSTANT,
Instant::from,
Expand All @@ -73,7 +85,7 @@ public class InstantDeserializer<T extends Temporal>
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
);

Expand Down Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down