Skip to content
Merged
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
1 change: 1 addition & 0 deletions changelog/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,14 +34,19 @@
* Since Cassandra's <code>timestamp</code> type preserves only
* milliseconds since epoch, any timezone information
* would normally be lost. By using a
* <code>tuple&lt;timestamp,varchar&gt;</code> a timezone ID can be
* <code>tuple&lt;timestamp,varchar&gt;</code> a timezone can be
* persisted in the <code>varchar</code> field such that when the
* value is deserialized the timezone is
* preserved.
* <p/>
* <strong>IMPORTANT</strong>: this codec's {@link #format(Object) format} method formats
* timestamps using an ISO-8601 format that includes milliseconds.
* <strong>This format is incompatible with Cassandra versions < 2.0.9.</strong>
* value is deserialized the timezone is preserved.
* <p>
* <strong>IMPORTANT</strong>
* <p>
* 1) The default timestamp formatter used by this codec produces CQL literals
* that may include milliseconds.
* <strong>This literal format is incompatible with Cassandra < 2.0.9.</strong>
* <p>
* 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 <a href="https://cassandra.apache.org/doc/cql3/CQL-2.2.html#usingtimestamps">'Working with timestamps' section of CQL specification</a>
*/
Expand All @@ -50,36 +55,74 @@
public class ZonedDateTimeCodec extends TypeCodec.AbstractTupleCodec<java.time.ZonedDateTime> {

/**
* 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.
* <p>
* The default formatters produce and parse CQL timestamp literals of the following form:
* <ol>
* <li>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}.</li>
* <li>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.</li>
* </ol>
*
* @param tupleType The tuple type this codec should handle.
* It must be a {@code tuple<timestamp,varchar>}.
* @throws IllegalArgumentException if the provided tuple type is not a {@code tuple<timestamp,varchar>}.
*/
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.
* <p>
* 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<timestamp,varchar>}.
* @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<timestamp,varchar>}.
*/
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<DataType> types = tupleType.getComponentTypes();
checkArgument(
types.size() == 2 && types.get(0).equals(DataType.timestamp()) && types.get(1).equals(DataType.varchar()),
Expand All @@ -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);
}
Expand All @@ -112,18 +155,18 @@ 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);
}

@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);
}
Expand All @@ -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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
Expand All @@ -47,7 +44,7 @@
* preserved.
* <p/>
* <strong>IMPORTANT</strong>: 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.
* <strong>This format is incompatible with Cassandra versions < 2.0.9.</strong>
*
* @see <a href="https://cassandra.apache.org/doc/cql3/CQL-2.2.html#usingtimestamps">'Working with timestamps' section of CQL specification</a>
Expand All @@ -56,21 +53,78 @@ public class DateTimeCodec extends TypeCodec.AbstractTupleCodec<DateTime> {

/**
* 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.
* <p>
* The default formatter and printer produce and parse CQL timestamp literals of the following form:
* <ol>
* <li>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}.</li>
* <li>The parser accepts most ISO-8601 date and time patterns, the time part (minutes, seconds, milliseconds) being optional.</li>
* </ol>
* <p>
* 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<timestamp,varchar>}.
* @throws IllegalArgumentException if the provided tuple type is not a {@code tuple<timestamp,varchar>}.
*/
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.
* <p>
* 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.
* <p>
* 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<timestamp,varchar>}.
* @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<timestamp,varchar>}.
*/
public DateTimeCodec(TupleType tupleType, DateTimeFormatter parser, DateTimeFormatter printer) {
super(tupleType, DateTime.class);
this.parser = parser;
this.printer = printer;
List<DataType> types = tupleType.getComponentTypes();
checkArgument(
types.size() == 2 && types.get(0).equals(DataType.timestamp()) && types.get(1).equals(DataType.varchar()),
Expand All @@ -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);
}
Expand All @@ -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);
}
Expand All @@ -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);
Expand Down
Loading