From b72d1137f32c9b2fb62689110a7ff927d62aa814 Mon Sep 17 00:00:00 2001 From: Alexandre Dutra Date: Thu, 28 Apr 2016 11:13:00 +0200 Subject: [PATCH] JAVA-1161: Preserve full time zone info in ZonedDateTimeCodec and DateTimeCodec MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit contains contributions by BenoƮt Tellier (@chibenwa). --- changelog/README.md | 1 + .../codecs/jdk8/ZonedDateTimeCodec.java | 115 ++++++++++++------ .../extras/codecs/joda/DateTimeCodec.java | 93 +++++++++++--- .../codecs/jdk8/ZonedDateTimeCodecTest.java | 63 ++++++---- .../extras/codecs/joda/DateTimeCodecTest.java | 52 +++++--- 5 files changed, 230 insertions(+), 94 deletions(-) diff --git a/changelog/README.md b/changelog/README.md index cf0e41f2bed..bb644b34b22 100644 --- a/changelog/README.md +++ b/changelog/README.md @@ -6,6 +6,7 @@ - [improvement] JAVA-743: Add JSON support to QueryBuilder. - [improvement] JAVA-1233: Update HdrHistogram to 2.1.9. - [improvement] JAVA-1233: Update Snappy to 1.1.2.6. +- [bug] JAVA-1161: Preserve full time zone info in ZonedDateTimeCodec and DateTimeCodec. Merged from 3.0.x branch: diff --git a/driver-extras/src/main/java/com/datastax/driver/extras/codecs/jdk8/ZonedDateTimeCodec.java b/driver-extras/src/main/java/com/datastax/driver/extras/codecs/jdk8/ZonedDateTimeCodec.java index 0851c110ca5..7149cb0ac45 100644 --- a/driver-extras/src/main/java/com/datastax/driver/extras/codecs/jdk8/ZonedDateTimeCodec.java +++ b/driver-extras/src/main/java/com/datastax/driver/extras/codecs/jdk8/ZonedDateTimeCodec.java @@ -34,14 +34,19 @@ * Since Cassandra's timestamp type preserves only * milliseconds since epoch, any timezone information * would normally be lost. By using a - * tuple<timestamp,varchar> a timezone ID can be + * tuple<timestamp,varchar> a timezone can be * persisted in the varchar field such that when the - * value is deserialized the timezone is - * preserved. - *

- * IMPORTANT: this codec's {@link #format(Object) format} method formats - * timestamps using an ISO-8601 format that includes milliseconds. - * This format is incompatible with Cassandra versions < 2.0.9. + * value is deserialized the timezone is preserved. + *

+ * IMPORTANT + *

+ * 1) The default timestamp formatter used by this codec produces CQL literals + * that may include milliseconds. + * This literal format is incompatible with Cassandra < 2.0.9. + *

+ * 2) Even if the ISO-8601 standard accepts timestamps with nanosecond precision, + * Cassandra timestamps have millisecond precision; therefore, any sub-millisecond + * value set on a {@link java.time.ZonedDateTime} will be lost when persisted to Cassandra. * * @see 'Working with timestamps' section of CQL specification */ @@ -50,36 +55,74 @@ public class ZonedDateTimeCodec extends TypeCodec.AbstractTupleCodec { /** - * A {@link java.time.format.DateTimeFormatter} that parses (most) of + * The default {@link java.time.format.DateTimeFormatter} that parses (most) of * the ISO formats accepted in CQL. */ - private static final java.time.format.DateTimeFormatter FORMATTER = new java.time.format.DateTimeFormatterBuilder() - .parseCaseSensitive() - .parseStrict() - .append(java.time.format.DateTimeFormatter.ISO_LOCAL_DATE) - .optionalStart() - .appendLiteral('T') - .appendValue(java.time.temporal.ChronoField.HOUR_OF_DAY, 2) - .appendLiteral(':') - .appendValue(java.time.temporal.ChronoField.MINUTE_OF_HOUR, 2) - .optionalEnd() - .optionalStart() - .appendLiteral(':') - .appendValue(java.time.temporal.ChronoField.SECOND_OF_MINUTE, 2) - .optionalEnd() - .optionalStart() - .appendFraction(java.time.temporal.ChronoField.NANO_OF_SECOND, 0, 9, true) - .optionalEnd() - .optionalStart() + private static final java.time.format.DateTimeFormatter DEFAULT_DATE_TIME_FORMATTER = java.time.format.DateTimeFormatter.ISO_DATE_TIME.withZone(java.time.ZoneOffset.UTC); + + /** + * The default {@link java.time.format.DateTimeFormatter} to parse and format zones. + * It will use a time-zone ID, such as {@code Europe/Paris}, or an offset, such as {@code +02:00}, + * depending on the best available information. + */ + private static final java.time.format.DateTimeFormatter DEFAULT_ZONE_FORMATTER = new java.time.format.DateTimeFormatterBuilder() .appendZoneOrOffsetId() - .optionalEnd() - .toFormatter() - .withZone(java.time.ZoneOffset.UTC); + .toFormatter(); + + private final java.time.format.DateTimeFormatter dateTimeFormatter; - private static final java.time.format.DateTimeFormatter ZONE_FORMATTER = java.time.format.DateTimeFormatter.ofPattern("xxx"); + private final java.time.format.DateTimeFormatter zoneFormatter; + /** + * Creates a new {@link ZonedDateTimeCodec} for the given tuple + * and with default {@link java.time.format.DateTimeFormatter formatters} for + * both the timestamp and the zone components. + *

+ * The default formatters produce and parse CQL timestamp literals of the following form: + *

    + *
  1. Timestamp component: an ISO-8601 full date and time pattern, including at least: year, + * month, day, hour and minutes, and optionally, seconds and milliseconds, + * followed by the zone ID {@code Z} (UTC), + * e.g. {@code 2010-06-30T02:01Z} or {@code 2010-06-30T01:20:47.999Z}; + * note that timestamp components are always expressed in UTC time, hence the zone ID {@code Z}.
  2. + *
  3. Zone component: a zone offset such as {@code -07:00}, or a zone ID such as {@code UTC} or {@code Europe/Paris}, + * depending on what information is available.
  4. + *
+ * + * @param tupleType The tuple type this codec should handle. + * It must be a {@code tuple}. + * @throws IllegalArgumentException if the provided tuple type is not a {@code tuple}. + */ public ZonedDateTimeCodec(TupleType tupleType) { + this(tupleType, DEFAULT_DATE_TIME_FORMATTER, DEFAULT_ZONE_FORMATTER); + } + + /** + * Creates a new {@link ZonedDateTimeCodec} for the given tuple + * and with the provided {@link java.time.format.DateTimeFormatter formatters} for + * the timestamp and the zone components of the tuple. + *

+ * Use this constructor if you intend to customize the way the codec + * parses and formats timestamps and zones. Beware that Cassandra only accepts + * timestamp literals in some of the most common ISO-8601 formats; + * attempting to use non-standard formats could result in invalid CQL literals. + * + * @param tupleType The tuple type this codec should handle. + * It must be a {@code tuple}. + * @param dateTimeFormatter The {@link java.time.format.DateTimeFormatter DateTimeFormatter} to use + * to parse and format the timestamp component of the tuple. + * As a parser, it should be lenient enough to accept most of the ISO-8601 formats + * accepted by Cassandra as valid CQL literals. + * As a formatter, it should be configured to always format timestamps in UTC + * (see {@link java.time.format.DateTimeFormatter#withZone(java.time.ZoneId)}. + * @param zoneFormatter The {@link java.time.format.DateTimeFormatter DateTimeFormatter} to use + * to parse and format the zone component of the tuple. + * @throws IllegalArgumentException if the provided tuple type is not a {@code tuple}. + */ + public ZonedDateTimeCodec(TupleType tupleType, java.time.format.DateTimeFormatter dateTimeFormatter, java.time.format.DateTimeFormatter zoneFormatter) { super(tupleType, java.time.ZonedDateTime.class); + this.dateTimeFormatter = dateTimeFormatter; + this.zoneFormatter = zoneFormatter; List types = tupleType.getComponentTypes(); checkArgument( types.size() == 2 && types.get(0).equals(DataType.timestamp()) && types.get(1).equals(DataType.varchar()), @@ -99,7 +142,7 @@ protected ByteBuffer serializeField(java.time.ZonedDateTime source, int index, P return bigint().serializeNoBoxing(millis, protocolVersion); } if (index == 1) { - return varchar().serialize(ZONE_FORMATTER.format(source.getOffset()), protocolVersion); + return varchar().serialize(zoneFormatter.format(source), protocolVersion); } throw new IndexOutOfBoundsException("Tuple index out of bounds. " + index); } @@ -112,7 +155,7 @@ protected java.time.ZonedDateTime deserializeAndSetField(ByteBuffer input, java. } if (index == 1) { String zoneId = varchar().deserialize(input, protocolVersion); - return target.withZoneSameInstant(java.time.ZoneId.of(zoneId)); + return target.withZoneSameInstant(zoneFormatter.parse(zoneId, java.time.temporal.TemporalQueries.zone())); } throw new IndexOutOfBoundsException("Tuple index out of bounds. " + index); } @@ -120,10 +163,10 @@ protected java.time.ZonedDateTime deserializeAndSetField(ByteBuffer input, java. @Override protected String formatField(java.time.ZonedDateTime value, int index) { if (index == 0) { - return quote(FORMATTER.format(value)); + return quote(dateTimeFormatter.format(value)); } if (index == 1) { - return quote(ZONE_FORMATTER.format(value.getOffset())); + return quote(zoneFormatter.format(value)); } throw new IndexOutOfBoundsException("Tuple index out of bounds. " + index); } @@ -143,14 +186,14 @@ protected java.time.ZonedDateTime parseAndSetField(String input, java.time.Zoned } } try { - return java.time.ZonedDateTime.from(FORMATTER.parse(input)); + return java.time.ZonedDateTime.from(dateTimeFormatter.parse(input)); } catch (java.time.format.DateTimeParseException e) { throw new InvalidTypeException(String.format("Cannot parse timestamp value from \"%s\"", target)); } } if (index == 1) { String zoneId = varchar().parse(input); - return target.withZoneSameInstant(java.time.ZoneId.of(zoneId)); + return target.withZoneSameInstant(zoneFormatter.parse(zoneId, java.time.temporal.TemporalQueries.zone())); } throw new IndexOutOfBoundsException("Tuple index out of bounds. " + index); } diff --git a/driver-extras/src/main/java/com/datastax/driver/extras/codecs/joda/DateTimeCodec.java b/driver-extras/src/main/java/com/datastax/driver/extras/codecs/joda/DateTimeCodec.java index a6e72e45413..1fcf68d1c63 100644 --- a/driver-extras/src/main/java/com/datastax/driver/extras/codecs/joda/DateTimeCodec.java +++ b/driver-extras/src/main/java/com/datastax/driver/extras/codecs/joda/DateTimeCodec.java @@ -19,9 +19,7 @@ import com.datastax.driver.core.exceptions.InvalidTypeException; import org.joda.time.DateTime; import org.joda.time.DateTimeZone; -import org.joda.time.format.DateTimeFormat; import org.joda.time.format.DateTimeFormatter; -import org.joda.time.format.DateTimeFormatterBuilder; import org.joda.time.format.ISODateTimeFormat; import java.nio.ByteBuffer; @@ -30,7 +28,6 @@ import static com.datastax.driver.core.ParseUtils.isLongLiteral; import static com.datastax.driver.core.ParseUtils.quote; import static com.google.common.base.Preconditions.checkArgument; -import static org.joda.time.DateTimeZone.UTC; /** * {@link TypeCodec} that maps @@ -47,7 +44,7 @@ * preserved. *

* IMPORTANT: this codec's {@link #format(Object) format} method formats - * timestamps using an ISO-8601 format that includes nanoseconds. + * timestamps as CQL literal strings using an ISO-8601 format that includes milliseconds. * This format is incompatible with Cassandra versions < 2.0.9. * * @see 'Working with timestamps' section of CQL specification @@ -56,21 +53,78 @@ public class DateTimeCodec extends TypeCodec.AbstractTupleCodec { /** * A {@link DateTimeFormatter} that parses (most) of - * the ISO formats accepted in CQL. + * the ISO-8601 formats accepted in CQL. */ - private static final DateTimeFormatter FORMATTER = new DateTimeFormatterBuilder() - .append(ISODateTimeFormat.dateOptionalTimeParser().getParser()) - .appendOptional( - new DateTimeFormatterBuilder() - .appendTimeZoneOffset("Z", true, 2, 4) - .toParser()) - .toFormatter() - .withZoneUTC(); + private static final DateTimeFormatter DEFAULT_PARSER = ISODateTimeFormat.dateOptionalTimeParser(); - private static final DateTimeFormatter ZONE_FORMATTER = DateTimeFormat.forPattern("ZZ"); + /** + * A {@link DateTimeFormatter} that prints timestamps + * with a full ISO-8601 date and time format, including the time zone (Z). + */ + private static final DateTimeFormatter DEFFAULT_PRINTER = ISODateTimeFormat.dateTime().withZoneUTC(); + + private final DateTimeFormatter parser; + private final DateTimeFormatter printer; + + /** + * Creates a new {@link DateTimeCodec} for the given tuple, + * using a default parser and a default printer to handle + * the timestamp component of the tuple. + *

+ * The default formatter and printer produce and parse CQL timestamp literals of the following form: + *

    + *
  1. The printer will always produce a full ISO-8601 date and time pattern, including year, + * month, day, hour, minutes, seconds and milliseconds, + * followed by the zone ID {@code Z} (UTC), e.g. {@code 2010-06-30T01:20:47.999Z}; + * note that timestamp components are always printed in UTC time, hence the zone ID {@code Z}.
  2. + *
  3. The parser accepts most ISO-8601 date and time patterns, the time part (minutes, seconds, milliseconds) being optional.
  4. + *
+ *

+ * Note that it is not possible to customize the parsing and printing of + * the zone component of the tuple. This codec prints either a zone offset such as {@code -07:00}, + * or a zone ID such as {@code UTC} or {@code Europe/Paris}, + * depending on what is the best information is available. + * + * @param tupleType The tuple type this codec should handle. + * It must be a {@code tuple}. + * @throws IllegalArgumentException if the provided tuple type is not a {@code tuple}. + */ public DateTimeCodec(TupleType tupleType) { + this(tupleType, DEFAULT_PARSER, DEFFAULT_PRINTER); + } + + /** + * Creates a new {@link DateTimeCodec} for the given tuple, + * using the provided {@link DateTimeFormatter parser} and {@link DateTimeFormatter printer} + * to format and print the timestamp component of the tuple. + *

+ * Use this constructor if you intend to customize the way the codec + * parses and formats timestamps. Beware that Cassandra only accepts + * timestamp literals in some of the most common ISO-8601 formats; + * attempting to use non-standard formats could result in invalid CQL literals. + *

+ * Note that it is not possible to customize the parsing and printing of + * the zone component of the tuple. This codec prints either a zone offset such as {@code -07:00}, + * or a zone ID such as {@code UTC} or {@code Europe/Paris}, + * depending on what information is available. + * + * @param tupleType The tuple type this codec should handle. + * It must be a {@code tuple}. + * @param parser The {@link DateTimeFormatter parser} to use + * to parse the timestamp component of the tuple. + * It should be lenient enough to accept most of the ISO-8601 formats + * accepted by Cassandra as valid CQL literals. + * @param printer The {@link DateTimeFormatter printer} to use + * to format the timestamp component of the tuple. + * This printer should be configured to always format timestamps in UTC + * (see {@link DateTimeFormatter#withZoneUTC()}. + * @throws IllegalArgumentException if the provided tuple type is not a {@code tuple}. + */ + public DateTimeCodec(TupleType tupleType, DateTimeFormatter parser, DateTimeFormatter printer) { super(tupleType, DateTime.class); + this.parser = parser; + this.printer = printer; List types = tupleType.getComponentTypes(); checkArgument( types.size() == 2 && types.get(0).equals(DataType.timestamp()) && types.get(1).equals(DataType.varchar()), @@ -90,7 +144,7 @@ protected ByteBuffer serializeField(DateTime source, int index, ProtocolVersion return bigint().serializeNoBoxing(millis, protocolVersion); } if (index == 1) { - return varchar().serialize(ZONE_FORMATTER.print(source), protocolVersion); + return varchar().serialize(source.getZone().getID(), protocolVersion); } throw new IndexOutOfBoundsException("Tuple index out of bounds. " + index); } @@ -111,10 +165,10 @@ protected DateTime deserializeAndSetField(ByteBuffer input, DateTime target, int @Override protected String formatField(DateTime value, int index) { if (index == 0) { - return quote(value.withZone(UTC).toString()); + return quote(printer.print(value)); } if (index == 1) { - return quote(ZONE_FORMATTER.print(value)); + return quote(value.getZone().getID()); } throw new IndexOutOfBoundsException("Tuple index out of bounds. " + index); } @@ -134,13 +188,16 @@ protected DateTime parseAndSetField(String input, DateTime target, int index) { } } try { - return FORMATTER.parseDateTime(input); + return parser.parseDateTime(input); } catch (RuntimeException e) { throw new InvalidTypeException(String.format("Cannot parse timestamp value from \"%s\"", target)); } } if (index == 1) { String zoneId = varchar().parse(input); + // Joda time does not recognize "Z" + if ("Z".equals(zoneId)) + return target.withZone(DateTimeZone.UTC); return target.withZone(DateTimeZone.forID(zoneId)); } throw new IndexOutOfBoundsException("Tuple index out of bounds. " + index); diff --git a/driver-extras/src/test/java/com/datastax/driver/extras/codecs/jdk8/ZonedDateTimeCodecTest.java b/driver-extras/src/test/java/com/datastax/driver/extras/codecs/jdk8/ZonedDateTimeCodecTest.java index 7cbd827cad6..7677fe493ed 100644 --- a/driver-extras/src/test/java/com/datastax/driver/extras/codecs/jdk8/ZonedDateTimeCodecTest.java +++ b/driver-extras/src/test/java/com/datastax/driver/extras/codecs/jdk8/ZonedDateTimeCodecTest.java @@ -16,57 +16,75 @@ package com.datastax.driver.extras.codecs.jdk8; import com.datastax.driver.core.Assertions; +import com.datastax.driver.core.CodecRegistry; +import com.datastax.driver.core.ProtocolVersion; import com.datastax.driver.core.TupleType; import org.testng.annotations.DataProvider; import org.testng.annotations.Test; -import java.time.ZoneOffset; import java.time.ZonedDateTime; import static com.datastax.driver.core.DataType.timestamp; import static com.datastax.driver.core.DataType.varchar; import static com.datastax.driver.core.ProtocolVersion.V4; -import static com.google.common.collect.Lists.newArrayList; -import static java.time.Instant.ofEpochMilli; -import static java.time.Instant.parse; -import static java.time.ZoneOffset.UTC; -import static java.time.ZonedDateTime.ofInstant; +import static java.time.ZonedDateTime.parse; import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; +@SuppressWarnings("Since15") public class ZonedDateTimeCodecTest { - private final TupleType tupleType = mock(TupleType.class); + private final TupleType tupleType = TupleType.of(ProtocolVersion.V4, CodecRegistry.DEFAULT_INSTANCE, timestamp(), varchar()); @DataProvider(name = "ZonedDateTimeCodecTest.parse") public Object[][] parseParameters() { return new Object[][]{ - {null, null}, - {"", null}, - {"NULL", null}, - {"(0 ,'+01:00')", ofInstant(ofEpochMilli(0), ZoneOffset.of("+01:00"))}, - {"(1277860847999 ,'+01:00')", ofInstant(parse("2010-06-30T01:20:47.999Z"), ZoneOffset.of("+01:00"))}, - {"('2010-06-30T01:20Z' ,'+01:00')", ofInstant(parse("2010-06-30T01:20:00.000Z"), ZoneOffset.of("+01:00"))}, - {"('2010-06-30T01:20:47Z' ,'+01:00')", ofInstant(parse("2010-06-30T01:20:47.000Z"), ZoneOffset.of("+01:00"))}, - {"('2010-06-30T01:20:47.999Z','+01:00')", ofInstant(parse("2010-06-30T01:20:47.999Z"), ZoneOffset.of("+01:00"))} + //@formatter:off + {null , null}, + {"" , null}, + {"NULL" , null}, + // timestamps as milliseconds since the Epoch, offsets without zone id + {"(0 ,'+00:00')" , parse("1970-01-01T00:00:00.000+00:00")}, + {"(0 ,'+01:00')" , parse("1970-01-01T01:00:00.000+01:00")}, + {"(1277860847999 ,'+01:00')" , parse("2010-06-30T02:20:47.999+01:00")}, + // timestamps as valid CQL literals with different precisions, offsets without zone id + {"('2010-06-30T01:20Z' ,'+01:00')" , parse("2010-06-30T02:20:00.000+01:00")}, + {"('2010-06-30T01:20:47Z' ,'+01:00')" , parse("2010-06-30T02:20:47.000+01:00")}, + {"('2010-06-30T01:20:47.999Z','+01:00')" , parse("2010-06-30T02:20:47.999+01:00")}, + // zone ids with different precisions + {"('2016-04-06T19:01Z' ,'Z')" , parse("2016-04-06T19:01:00.000+00:00[Z]")}, + {"('2016-04-06T19:01Z' ,'UTC')" , parse("2016-04-06T19:01:00.000+00:00[UTC]")}, + {"('2016-04-06T19:01Z' ,'GMT')" , parse("2016-04-06T19:01:00.000+00:00[GMT]")}, + {"('2016-04-06T19:01Z' ,'Etc/GMT')" , parse("2016-04-06T19:01:00.000+00:00[Etc/GMT]")}, + {"('2016-04-06T19:01Z' ,'Asia/Vientiane')" , parse("2016-04-07T02:01:00.000+07:00[Asia/Vientiane]")}, + {"('2016-04-06T19:01:32Z' ,'Asia/Vientiane')" , parse("2016-04-07T02:01:32.000+07:00[Asia/Vientiane]")}, + {"('2016-04-06T19:01:32.999Z','Asia/Vientiane')" , parse("2016-04-07T02:01:32.999+07:00[Asia/Vientiane]")} + //@formatter:on }; } @DataProvider(name = "ZonedDateTimeCodecTest.format") public Object[][] formatParameters() { return new Object[][]{ - {null, "NULL"}, - {ofInstant(ofEpochMilli(0), UTC), "('1970-01-01T00:00:00Z','+00:00')"}, - {ZonedDateTime.parse("2010-06-30T01:20:47.999+01:00"), "('2010-06-30T00:20:47.999Z','+01:00')"} + //@formatter:off + {null , "NULL"}, + {parse("1970-01-01T00:00Z") , "('1970-01-01T00:00:00Z','Z')"}, + {parse("1970-01-01T00:00:00Z") , "('1970-01-01T00:00:00Z','Z')"}, + {parse("1970-01-01T00:00:00.000Z") , "('1970-01-01T00:00:00Z','Z')"}, + {parse("2010-06-30T01:20+01:00") , "('2010-06-30T00:20:00Z','+01:00')"}, + {parse("2010-06-30T01:20:47+01:00") , "('2010-06-30T00:20:47Z','+01:00')"}, + {parse("2010-06-30T01:20:47+01:00") , "('2010-06-30T00:20:47Z','+01:00')"}, + {parse("2010-06-30T01:20:47.999+01:00") , "('2010-06-30T00:20:47.999Z','+01:00')"}, + {parse("2010-06-30T01:20:47.999+00:00[UTC]") , "('2010-06-30T01:20:47.999Z','UTC')"}, + {parse("2010-06-30T01:20:47.999+00:00[GMT]") , "('2010-06-30T01:20:47.999Z','GMT')"}, + {parse("2010-06-30T01:20:47.999+00:00[Etc/GMT]"), "('2010-06-30T01:20:47.999Z','Etc/GMT')"}, + {parse("2016-04-07T02:01+07:00[Asia/Vientiane]"), "('2016-04-06T19:01:00Z','Asia/Vientiane')"} + //@formatter:on }; } @Test(groups = "unit", dataProvider = "ZonedDateTimeCodecTest.parse") public void should_parse_valid_formats(String input, ZonedDateTime expected) { // given - TupleType tupleType = mock(TupleType.class); - when(tupleType.getComponentTypes()).thenReturn(newArrayList(timestamp(), varchar())); ZonedDateTimeCodec codec = new ZonedDateTimeCodec(tupleType); // when ZonedDateTime actual = codec.parse(input); @@ -77,7 +95,6 @@ public void should_parse_valid_formats(String input, ZonedDateTime expected) { @Test(groups = "unit", dataProvider = "ZonedDateTimeCodecTest.format") public void should_serialize_and_format_valid_object(ZonedDateTime input, String expected) { // given - when(tupleType.getComponentTypes()).thenReturn(newArrayList(timestamp(), varchar())); ZonedDateTimeCodec codec = new ZonedDateTimeCodec(tupleType); // when String actual = codec.format(input); diff --git a/driver-extras/src/test/java/com/datastax/driver/extras/codecs/joda/DateTimeCodecTest.java b/driver-extras/src/test/java/com/datastax/driver/extras/codecs/joda/DateTimeCodecTest.java index 80d1d078350..6e678303378 100644 --- a/driver-extras/src/test/java/com/datastax/driver/extras/codecs/joda/DateTimeCodecTest.java +++ b/driver-extras/src/test/java/com/datastax/driver/extras/codecs/joda/DateTimeCodecTest.java @@ -16,6 +16,8 @@ package com.datastax.driver.extras.codecs.joda; import com.datastax.driver.core.Assertions; +import com.datastax.driver.core.CodecRegistry; +import com.datastax.driver.core.ProtocolVersion; import com.datastax.driver.core.TupleType; import org.joda.time.DateTime; import org.testng.annotations.DataProvider; @@ -24,28 +26,37 @@ import static com.datastax.driver.core.DataType.timestamp; import static com.datastax.driver.core.DataType.varchar; import static com.datastax.driver.core.ProtocolVersion.V4; -import static com.google.common.collect.Lists.newArrayList; import static org.assertj.core.api.Assertions.assertThat; -import static org.joda.time.DateTimeZone.UTC; import static org.joda.time.DateTimeZone.forID; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; public class DateTimeCodecTest { - private final TupleType tupleType = mock(TupleType.class); + private final TupleType tupleType = TupleType.of(ProtocolVersion.V4, CodecRegistry.DEFAULT_INSTANCE, timestamp(), varchar()); @DataProvider(name = "DateTimeCodecTest.parse") public Object[][] parseParameters() { return new Object[][]{ - {null, null}, - {"", null}, - {"NULL", null}, - {"(0 ,'UTC')", new DateTime(0, forID("UTC"))}, - {"(1277860847999 ,'+01:00')", new DateTime("2010-06-30T02:20:47.999+01:00", forID("+01:00"))}, - {"('2010-06-30T01:20Z' ,'+01:00')", new DateTime("2010-06-30T02:20:00.000+01:00", forID("+01:00"))}, - {"('2010-06-30T01:20:47Z' ,'+01:00')", new DateTime("2010-06-30T02:20:47.000+01:00", forID("+01:00"))}, - {"('2010-06-30T01:20:47.999Z','+01:00')", new DateTime("2010-06-30T02:20:47.999+01:00", forID("+01:00"))} + //@formatter:off + {null , null}, + {"" , null}, + {"NULL" , null}, + // timestamps as milliseconds since the Epoch, offsets without zone id + {"(0 ,'+00:00')" , new DateTime("1970-01-01T00:00:00.000+00:00", forID("+00:00"))}, + {"(0 ,'+01:00')" , new DateTime("1970-01-01T01:00:00.000+01:00", forID("+01:00"))}, + {"(1277860847999 ,'+01:00')" , new DateTime("2010-06-30T02:20:47.999+01:00", forID("+01:00"))}, + // timestamps as valid CQL literals with different precisions, offsets without zone id + {"('2010-06-30T01:20Z' ,'+01:00')" , new DateTime("2010-06-30T02:20:00.000+01:00", forID("+01:00"))}, + {"('2010-06-30T01:20:47Z' ,'+01:00')" , new DateTime("2010-06-30T02:20:47.000+01:00", forID("+01:00"))}, + {"('2010-06-30T01:20:47.999Z','+01:00')" , new DateTime("2010-06-30T02:20:47.999+01:00", forID("+01:00"))}, + // zone ids with different precisions + {"('2016-04-06T19:01Z' ,'Z')" , new DateTime("2016-04-06T19:01:00.000+00:00", forID("UTC"))}, + {"('2016-04-06T19:01Z' ,'UTC')" , new DateTime("2016-04-06T19:01:00.000+00:00", forID("UTC"))}, + {"('2016-04-06T19:01Z' ,'GMT')" , new DateTime("2016-04-06T19:01:00.000+00:00", forID("GMT"))}, + {"('2016-04-06T19:01Z' ,'Etc/GMT')" , new DateTime("2016-04-06T19:01:00.000+00:00", forID("GMT"))}, + {"('2016-04-06T19:01Z' ,'Asia/Vientiane')" , new DateTime("2016-04-07T02:01:00.000+07:00", forID("Asia/Vientiane"))}, + {"('2016-04-06T19:01:32Z' ,'Asia/Vientiane')" , new DateTime("2016-04-07T02:01:32.000+07:00", forID("Asia/Vientiane"))}, + {"('2016-04-06T19:01:32.999Z','Asia/Vientiane')" , new DateTime("2016-04-07T02:01:32.999+07:00", forID("Asia/Vientiane"))}, + //@formatter:on }; } @@ -53,15 +64,23 @@ public Object[][] parseParameters() { public Object[][] formatParameters() { return new Object[][]{ {null, "NULL"}, - {new DateTime(0).withZone(UTC), "('1970-01-01T00:00:00.000Z','+00:00')"}, - {new DateTime("2010-06-30T01:20:47.999+01:00", forID("+01:00")), "('2010-06-30T00:20:47.999Z','+01:00')"} + //@formatter:off + {new DateTime("1970-01-01T00:00Z" , forID("+00:00")) , "('1970-01-01T00:00:00.000Z','UTC')"}, + {new DateTime("1970-01-01T00:00:00Z" , forID("+00:00")) , "('1970-01-01T00:00:00.000Z','UTC')"}, + {new DateTime("1970-01-01T00:00:00.000Z" , forID("+00:00")) , "('1970-01-01T00:00:00.000Z','UTC')"}, + {new DateTime("2010-06-30T01:20+01:00" , forID("+01:00")) , "('2010-06-30T00:20:00.000Z','+01:00')"}, + {new DateTime("2010-06-30T01:20:47+01:00" , forID("+01:00")) , "('2010-06-30T00:20:47.000Z','+01:00')"}, + {new DateTime("2010-06-30T01:20:47.999+01:00" , forID("+01:00")) , "('2010-06-30T00:20:47.999Z','+01:00')"}, + {new DateTime("2016-04-07T02:01Z" , forID("UTC")) , "('2016-04-07T02:01:00.000Z','UTC')"}, + {new DateTime("2016-04-07T02:01Z" , forID("GMT")) , "('2016-04-07T02:01:00.000Z','Etc/GMT')"}, + {new DateTime("2016-04-07T02:01+07:00" , forID("Asia/Vientiane")), "('2016-04-06T19:01:00.000Z','Asia/Vientiane')"} + //@formatter:on }; } @Test(groups = "unit", dataProvider = "DateTimeCodecTest.parse") public void should_parse_valid_formats(String input, DateTime expected) { // given - when(tupleType.getComponentTypes()).thenReturn(newArrayList(timestamp(), varchar())); DateTimeCodec codec = new DateTimeCodec(tupleType); // when DateTime actual = codec.parse(input); @@ -72,7 +91,6 @@ public void should_parse_valid_formats(String input, DateTime expected) { @Test(groups = "unit", dataProvider = "DateTimeCodecTest.format") public void should_serialize_and_format_valid_object(DateTime input, String expected) { // given - when(tupleType.getComponentTypes()).thenReturn(newArrayList(timestamp(), varchar())); DateTimeCodec codec = new DateTimeCodec(tupleType); // when String actual = codec.format(input);