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

include the time zone name when serializing DateTimes, not just the offs... #44

Closed
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 @@ -53,10 +53,20 @@ public ReadableDateTime deserialize(JsonParser jp, DeserializationContext ctxt)
if (str.length() == 0) { // [JACKSON-360]
return null;
}
if (ctxt.isEnabled(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE))
return new DateTime(str, dtz);
else
return DateTime.parse(str);
// Split the string at the first slash. If there's no first
// slash, assume we're dealing with an ISO8601 serialization.
int firstSlash = str.indexOf('/');
if (firstSlash == -1) {
if (ctxt.isEnabled(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE))
return new DateTime(str, dtz);
else
return DateTime.parse(str);
}

String millisStr = str.substring(0, firstSlash);
String zoneStr = str.substring(firstSlash + 1);

return new DateTime(Long.valueOf(millisStr), DateTimeZone.forID(zoneStr));
}
// TODO: in 2.4, use 'handledType()'
throw ctxt.mappingException(getValueClass());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,39 @@ public void serialize(DateTime value, JsonGenerator jgen, SerializerProvider pro
if (_useTimestamp(provider)) {
jgen.writeNumber(value.getMillis());
} else {
jgen.writeString(_format.createFormatter(provider).print(value));
// Unfortunately, I don't see a way to include milliseconds since
// the epoch in a DateTimeFormatter or this code (and the
// deserializer) could likely be simpler...Perhaps like this:
//
// // The provider has a time zone, but let's not use it. Use the time
// // zone from value instead since the goal is to preserve that across de/serialization.
// DateTimeFormatter providerFormatter = _format.createFormatter(provider);
// DateTimeFormatter valueFormatter = providerFormatter.withZone(value.getZone());
// jgen.writeString(valueFormatter.print(value));
//
// Instead, if someone has set an explicit format, use it.
if (_format != DEFAULT_FORMAT) {
jgen.writeString(_format.createFormatter(provider).print(value));
} else {
// Store value's time zone explicitly in addition to some
// representation of the time so it survives a de/serialization
// round trip. Assume that this is the desired behavior for all
// DateTime instances. If someone only cares about the instant
// in time, suggest s/he use Instant.
StringBuilder timeWithZone = new StringBuilder();

// Store the time in UTC to avoid the ambiguity that occurs on a
// "fall back" DST transition. For example, on November 3, 2013
// in America/Los_Angeles, 1:00a happens twice...once at 8:00:00
// UTC (before falling back) and once at 9:00:00 UTC (after
// falling back). From there we have a choice between an
// ISO8601 string or milliseconds since the epoch. I'm going
// with milliseconds since the epoch since it's more compact.
timeWithZone.append(value.getMillis());
timeWithZone.append('/');
timeWithZone.append(value.getZone().toString());
jgen.writeString(timeWithZone.toString());
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,21 @@ private static interface ObjectConfiguration {

private final DateTime DATE_JAN_1_1970_UTC = new DateTime(0L, DateTimeZone.UTC);

// November 3, 2013 at 1:00a is the fall back DST transition that year in much of the US.
private static final int FALL_BACK_YEAR = 2013;

private static final int FALL_BACK_MONTH = 11;

private static final int FALL_BACK_DAY = 3;

// The first one for America/Los_Angeles happens at 8:00 UTC.
private static final int FIRST_FALL_BACK_HOUR = 8;

// And the second one happens at 9:00 UTC
private static final int SECOND_FALL_BACK_HOUR = 9;

private static final DateTimeZone AMERICA_LOS_ANGELES = DateTimeZone.forID("America/Los_Angeles");

/**
* First: let's ensure that serialization does not fail
* with an error (see [JACKSON-157]).
Expand All @@ -64,7 +79,53 @@ public void testSerializationFeatureNoTimestamp() throws IOException
{
ObjectMapper m = jodaMapper();
m.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
assertEquals(quote("1970-01-01T00:00:00.000Z"), m.writeValueAsString(DATE_JAN_1_1970_UTC));
assertEquals(quote("0/UTC"), m.writeValueAsString(DATE_JAN_1_1970_UTC));
}

/**
* Test that both the instant and the time zone are preserved across de/serialization.
*
* @throws IOException on a parsing error
*/
public void testRoundTrip() throws IOException
{
DateTime test = new DateTime(2014, 8, 24, 5, 17, 45, DateTimeZone.forID("America/Chicago")); // arbitrary

ObjectMapper m = jodaMapper();
m.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);

String serialized = m.writeValueAsString(test);
DateTime deserialized = m.readValue(serialized, DateTime.class);
assertEquals(test,deserialized);
}

/**
* Test that de/serializing an ambiguous time (e.g. a 'fall back' DST transition) works and preserves the proper
* instants in time and time zones.
*
* @throws IOException on a parsing error
*/
public void testFallBackTransition() throws IOException
{
DateTime firstOneAmUtc = new DateTime(FALL_BACK_YEAR, FALL_BACK_MONTH, FALL_BACK_DAY, FIRST_FALL_BACK_HOUR, 0, 0,
DateTimeZone.UTC);
DateTime secondOneAmUtc = new DateTime(FALL_BACK_YEAR, FALL_BACK_MONTH, FALL_BACK_DAY, SECOND_FALL_BACK_HOUR, 0, 0,
DateTimeZone.UTC);

DateTime firstOneAm = new DateTime(firstOneAmUtc, AMERICA_LOS_ANGELES);
DateTime secondOneAm = new DateTime(secondOneAmUtc, AMERICA_LOS_ANGELES);

ObjectMapper m = jodaMapper();
m.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);

String firstOneAmStr = m.writeValueAsString(firstOneAm);
String secondOneAmStr = m.writeValueAsString(secondOneAm);

DateTime firstRoundTrip = m.readValue(firstOneAmStr, DateTime.class);
DateTime secondRoundTrip = m.readValue(secondOneAmStr, DateTime.class);

assertEquals(firstOneAm, firstRoundTrip);
assertEquals(secondOneAm, secondRoundTrip);
}

public void testAnnotationAsText() throws IOException
Expand All @@ -90,11 +151,11 @@ public void testSerializationWithTypeInfo() throws IOException
// by default, dates use timestamp, so:
assertEquals("0", MAPPER.writeValueAsString(dt));

// but if re-configured, as regular ISO-8601 string
// but if re-configured to include the time zone
ObjectMapper m = jodaMapper();
m.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
m.addMixInAnnotations(DateTime.class, ObjectConfiguration.class);
assertEquals("[\"org.joda.time.DateTime\",\"1970-01-01T00:00:00.000Z\"]",
assertEquals("[\"org.joda.time.DateTime\",\"0/UTC\"]",
m.writeValueAsString(dt));
}
}