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:
+ *
+ * - 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}.
+ * - 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.
+ *
+ *
+ * @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:
+ *
+ * - 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}.
+ * - The parser accepts most ISO-8601 date and time patterns, the time part (minutes, seconds, milliseconds) being optional.
+ *
+ *
+ * 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);