From 0480206df8a9fbe06bf8baa8e02b6fbaa79c84b5 Mon Sep 17 00:00:00 2001 From: Cowtowncoder Date: Wed, 8 Jul 2015 16:56:49 -0700 Subject: [PATCH] Fix #66 --- pom.xml | 2 +- release-notes/VERSION | 1 + .../joda/deser/DateTimeDeserializer.java | 36 +++++ .../datatype/joda/ser/DateTimeSerializer.java | 25 +++- .../jackson/datatype/joda/TimeZoneTest.java | 129 ++++++++++++++++++ 5 files changed, 189 insertions(+), 4 deletions(-) create mode 100644 src/test/java/com/fasterxml/jackson/datatype/joda/TimeZoneTest.java diff --git a/pom.xml b/pom.xml index 0c0a9f4f..c3798638 100644 --- a/pom.xml +++ b/pom.xml @@ -23,7 +23,7 @@ Joda (http://joda-time.sourceforge.net/) data types. 2.6.0-rc3 - 2.6.0-rc3 + 2.6.0-rc4-SNAPSHOT com/fasterxml/jackson/datatype/joda ${project.groupId}.joda diff --git a/release-notes/VERSION b/release-notes/VERSION index 5ae2a924..2d673c3c 100644 --- a/release-notes/VERSION +++ b/release-notes/VERSION @@ -11,6 +11,7 @@ Project: jackson-datatype-joda #62: Allow use of numbers-as-Strings for LocalDate (in array) (contributed by Michal Z) #64: Support `@JsonFormat(pattern=...)` for deserialization +#66: Support `SerializationFeature.WRITE_DATES_WITH_ZONE_ID` 2.5.4 (not yet released) diff --git a/src/main/java/com/fasterxml/jackson/datatype/joda/deser/DateTimeDeserializer.java b/src/main/java/com/fasterxml/jackson/datatype/joda/deser/DateTimeDeserializer.java index c0ac02d6..8e0e287b 100644 --- a/src/main/java/com/fasterxml/jackson/datatype/joda/deser/DateTimeDeserializer.java +++ b/src/main/java/com/fasterxml/jackson/datatype/joda/deser/DateTimeDeserializer.java @@ -52,9 +52,45 @@ public ReadableDateTime deserialize(JsonParser p, DeserializationContext ctxt) if (str.length() == 0) { // [JACKSON-360] return null; } + // 08-Jul-2015, tatu: as per [datatype-joda#44], optional TimeZone inclusion + // NOTE: on/off feature only for serialization; on deser should accept both + int ix = str.indexOf('['); + if (ix > 0) { + int ix2 = str.lastIndexOf(']'); + String tzId = (ix2 < ix) + ? str.substring(ix+1) + : str.substring(ix+1, ix2); + DateTimeZone tz; + try { + tz = DateTimeZone.forID(tzId); + } catch (IllegalArgumentException e) { + throw ctxt.mappingException(String.format("Unknown DateTimeZone id '%s'", tzId)); + } + str = str.substring(0, ix); + + // One more thing; do we have plain timestamp? + if (_allDigits(str)) { + return new DateTime(Long.parseLong(str), tz); + } + return _format.createParser(ctxt) + .parseDateTime(str) + .withZone(tz); + } + // Not sure if it should use timezone or not... return _format.createParser(ctxt).parseDateTime(str); } throw ctxt.mappingException(handledType()); } + + private static boolean _allDigits(String str) + { + for (int i = 0, len = str.length(); i < len; ++i) { + int c = str.charAt(i); + if (c > '9' | c < '0') { + return false; + } + } + return true; + } } diff --git a/src/main/java/com/fasterxml/jackson/datatype/joda/ser/DateTimeSerializer.java b/src/main/java/com/fasterxml/jackson/datatype/joda/ser/DateTimeSerializer.java index 933120b5..4580c919 100644 --- a/src/main/java/com/fasterxml/jackson/datatype/joda/ser/DateTimeSerializer.java +++ b/src/main/java/com/fasterxml/jackson/datatype/joda/ser/DateTimeSerializer.java @@ -35,10 +35,29 @@ public boolean isEmpty(SerializerProvider prov, DateTime value) { @Override public void serialize(DateTime value, JsonGenerator gen, SerializerProvider provider) throws IOException { - if (_useTimestamp(provider)) { - gen.writeNumber(value.getMillis()); + // First: simple, non-timezone-included output + if (!provider.isEnabled(SerializationFeature.WRITE_DATES_WITH_ZONE_ID)) { + if (_useTimestamp(provider)) { + gen.writeNumber(value.getMillis()); + } else { + gen.writeString(_format.createFormatter(provider).print(value)); + } } else { - gen.writeString(_format.createFormatter(provider).print(value)); + // and then as per [datatype-joda#44], optional TimeZone inclusion + + StringBuilder sb; + + if (_useTimestamp(provider)) { + sb = new StringBuilder(20) + .append(value.getMillis()); + } else { + sb = new StringBuilder(40) + .append(_format.createFormatter(provider).print(value)); + } + sb = sb.append('[') + .append(value.getZone()) + .append(']'); + gen.writeString(sb.toString()); } } } diff --git a/src/test/java/com/fasterxml/jackson/datatype/joda/TimeZoneTest.java b/src/test/java/com/fasterxml/jackson/datatype/joda/TimeZoneTest.java new file mode 100644 index 00000000..f30802fe --- /dev/null +++ b/src/test/java/com/fasterxml/jackson/datatype/joda/TimeZoneTest.java @@ -0,0 +1,129 @@ +package com.fasterxml.jackson.datatype.joda; + +import org.joda.time.DateTime; +import org.joda.time.DateTimeZone; + +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.databind.*; + +// for [datatype-joda#44] +public class TimeZoneTest extends JodaTestBase +{ + // 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"); + + private final DateTime DATE_JAN_1_1970_UTC = new DateTime(0L, DateTimeZone.UTC); + + @JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.WRAPPER_ARRAY, property = "@class") + private static interface TypeInfoMixIn { + } + + /* + /********************************************************** + /* Test methods + /********************************************************** + */ + + private final ObjectMapper MAPPER = jodaMapper(); + + public void testSimple() throws Exception + { + // First, no zone id included + ObjectWriter w = MAPPER.writer() + .without(SerializationFeature.WRITE_DATES_WITH_ZONE_ID); + + assertEquals("0", + w.with(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + .writeValueAsString(DATE_JAN_1_1970_UTC)); + assertEquals(quote("1970-01-01T00:00:00.000Z"), + w.without(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + .writeValueAsString(DATE_JAN_1_1970_UTC)); + + // then with zone id + + w = w.with(SerializationFeature.WRITE_DATES_WITH_ZONE_ID); + assertEquals(quote("0[UTC]"), + w.with(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + .writeValueAsString(DATE_JAN_1_1970_UTC)); + assertEquals(quote("1970-01-01T00:00:00.000Z[UTC]"), + w.without(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + .writeValueAsString(DATE_JAN_1_1970_UTC)); + } + + public void testRoundTrip() throws Exception + { + ObjectWriter w = MAPPER.writer() + .with(SerializationFeature.WRITE_DATES_WITH_ZONE_ID); + DateTime input = new DateTime(2014, 8, 24, 5, 17, 45, DateTimeZone.forID("America/Chicago")); // arbitrary + + // First as timestamp + + String json = w.with(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + .writeValueAsString(input); + DateTime result = MAPPER.readValue(json, DateTime.class); + assertEquals("Actual timepoints differ", input.getMillis(), result.getMillis()); + assertEquals("TimeZones differ", input, result); + + // then as regular tet + json = w.without(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + .writeValueAsString(input); + result = MAPPER.readValue(json, DateTime.class); + assertEquals("Actual timepoints differ", input.getMillis(), result.getMillis()); + assertEquals("TimeZones differ", input, result); + } + + /** + * 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. + */ + public void testFallBackTransition() throws Exception + { + 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); + + ObjectWriter w = MAPPER.writer() + .without(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + .with(SerializationFeature.WRITE_DATES_WITH_ZONE_ID); + + String firstOneAmStr = w.writeValueAsString(firstOneAm); + String secondOneAmStr = w.writeValueAsString(secondOneAm); + + DateTime firstRoundTrip = MAPPER.readValue(firstOneAmStr, DateTime.class); + DateTime secondRoundTrip = MAPPER.readValue(secondOneAmStr, DateTime.class); + + assertEquals("Actual timepoints differ", firstOneAm.getMillis(), firstRoundTrip.getMillis()); + assertEquals("TimeZones differ", firstOneAm, firstRoundTrip); + + assertEquals("Actual timepoints differ", secondOneAm.getMillis(), secondRoundTrip.getMillis()); + assertEquals("TimeZones differ", secondOneAm, secondRoundTrip); + } + + public void testSerializationWithTypeInfo() throws Exception + { + // but if re-configured to include the time zone + ObjectMapper m = jodaMapper(); + m.enable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); + m.enable(SerializationFeature.WRITE_DATES_WITH_ZONE_ID); + + m.addMixIn(DateTime.class, TypeInfoMixIn.class); + assertEquals("[\"org.joda.time.DateTime\",\"0[UTC]\"]", + m.writeValueAsString(DATE_JAN_1_1970_UTC)); + } +}