Skip to content

Commit c20f24b

Browse files
adutraolim7t
authored andcommitted
JAVA-1161: Preserve full time zone info in ZonedDateTimeCodec and DateTimeCodec (apache#676)
This commit contains contributions by Benoît Tellier (@chibenwa).
1 parent 49fb168 commit c20f24b

5 files changed

Lines changed: 230 additions & 94 deletions

File tree

changelog/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
- [improvement] JAVA-743: Add JSON support to QueryBuilder.
77
- [improvement] JAVA-1233: Update HdrHistogram to 2.1.9.
88
- [improvement] JAVA-1233: Update Snappy to 1.1.2.6.
9+
- [bug] JAVA-1161: Preserve full time zone info in ZonedDateTimeCodec and DateTimeCodec.
910

1011
Merged from 3.0.x branch:
1112

driver-extras/src/main/java/com/datastax/driver/extras/codecs/jdk8/ZonedDateTimeCodec.java

Lines changed: 79 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -34,14 +34,19 @@
3434
* Since Cassandra's <code>timestamp</code> type preserves only
3535
* milliseconds since epoch, any timezone information
3636
* would normally be lost. By using a
37-
* <code>tuple&lt;timestamp,varchar&gt;</code> a timezone ID can be
37+
* <code>tuple&lt;timestamp,varchar&gt;</code> a timezone can be
3838
* persisted in the <code>varchar</code> field such that when the
39-
* value is deserialized the timezone is
40-
* preserved.
41-
* <p/>
42-
* <strong>IMPORTANT</strong>: this codec's {@link #format(Object) format} method formats
43-
* timestamps using an ISO-8601 format that includes milliseconds.
44-
* <strong>This format is incompatible with Cassandra versions < 2.0.9.</strong>
39+
* value is deserialized the timezone is preserved.
40+
* <p>
41+
* <strong>IMPORTANT</strong>
42+
* <p>
43+
* 1) The default timestamp formatter used by this codec produces CQL literals
44+
* that may include milliseconds.
45+
* <strong>This literal format is incompatible with Cassandra < 2.0.9.</strong>
46+
* <p>
47+
* 2) Even if the ISO-8601 standard accepts timestamps with nanosecond precision,
48+
* Cassandra timestamps have millisecond precision; therefore, any sub-millisecond
49+
* value set on a {@link java.time.ZonedDateTime} will be lost when persisted to Cassandra.
4550
*
4651
* @see <a href="https://cassandra.apache.org/doc/cql3/CQL-2.2.html#usingtimestamps">'Working with timestamps' section of CQL specification</a>
4752
*/
@@ -50,36 +55,74 @@
5055
public class ZonedDateTimeCodec extends TypeCodec.AbstractTupleCodec<java.time.ZonedDateTime> {
5156

5257
/**
53-
* A {@link java.time.format.DateTimeFormatter} that parses (most) of
58+
* The default {@link java.time.format.DateTimeFormatter} that parses (most) of
5459
* the ISO formats accepted in CQL.
5560
*/
56-
private static final java.time.format.DateTimeFormatter FORMATTER = new java.time.format.DateTimeFormatterBuilder()
57-
.parseCaseSensitive()
58-
.parseStrict()
59-
.append(java.time.format.DateTimeFormatter.ISO_LOCAL_DATE)
60-
.optionalStart()
61-
.appendLiteral('T')
62-
.appendValue(java.time.temporal.ChronoField.HOUR_OF_DAY, 2)
63-
.appendLiteral(':')
64-
.appendValue(java.time.temporal.ChronoField.MINUTE_OF_HOUR, 2)
65-
.optionalEnd()
66-
.optionalStart()
67-
.appendLiteral(':')
68-
.appendValue(java.time.temporal.ChronoField.SECOND_OF_MINUTE, 2)
69-
.optionalEnd()
70-
.optionalStart()
71-
.appendFraction(java.time.temporal.ChronoField.NANO_OF_SECOND, 0, 9, true)
72-
.optionalEnd()
73-
.optionalStart()
61+
private static final java.time.format.DateTimeFormatter DEFAULT_DATE_TIME_FORMATTER = java.time.format.DateTimeFormatter.ISO_DATE_TIME.withZone(java.time.ZoneOffset.UTC);
62+
63+
/**
64+
* The default {@link java.time.format.DateTimeFormatter} to parse and format zones.
65+
* It will use a time-zone ID, such as {@code Europe/Paris}, or an offset, such as {@code +02:00},
66+
* depending on the best available information.
67+
*/
68+
private static final java.time.format.DateTimeFormatter DEFAULT_ZONE_FORMATTER = new java.time.format.DateTimeFormatterBuilder()
7469
.appendZoneOrOffsetId()
75-
.optionalEnd()
76-
.toFormatter()
77-
.withZone(java.time.ZoneOffset.UTC);
70+
.toFormatter();
71+
72+
private final java.time.format.DateTimeFormatter dateTimeFormatter;
7873

79-
private static final java.time.format.DateTimeFormatter ZONE_FORMATTER = java.time.format.DateTimeFormatter.ofPattern("xxx");
74+
private final java.time.format.DateTimeFormatter zoneFormatter;
8075

76+
/**
77+
* Creates a new {@link ZonedDateTimeCodec} for the given tuple
78+
* and with default {@link java.time.format.DateTimeFormatter formatters} for
79+
* both the timestamp and the zone components.
80+
* <p>
81+
* The default formatters produce and parse CQL timestamp literals of the following form:
82+
* <ol>
83+
* <li>Timestamp component: an ISO-8601 full date and time pattern, including at least: year,
84+
* month, day, hour and minutes, and optionally, seconds and milliseconds,
85+
* followed by the zone ID {@code Z} (UTC),
86+
* e.g. {@code 2010-06-30T02:01Z} or {@code 2010-06-30T01:20:47.999Z};
87+
* note that timestamp components are always expressed in UTC time, hence the zone ID {@code Z}.</li>
88+
* <li>Zone component: a zone offset such as {@code -07:00}, or a zone ID such as {@code UTC} or {@code Europe/Paris},
89+
* depending on what information is available.</li>
90+
* </ol>
91+
*
92+
* @param tupleType The tuple type this codec should handle.
93+
* It must be a {@code tuple<timestamp,varchar>}.
94+
* @throws IllegalArgumentException if the provided tuple type is not a {@code tuple<timestamp,varchar>}.
95+
*/
8196
public ZonedDateTimeCodec(TupleType tupleType) {
97+
this(tupleType, DEFAULT_DATE_TIME_FORMATTER, DEFAULT_ZONE_FORMATTER);
98+
}
99+
100+
/**
101+
* Creates a new {@link ZonedDateTimeCodec} for the given tuple
102+
* and with the provided {@link java.time.format.DateTimeFormatter formatters} for
103+
* the timestamp and the zone components of the tuple.
104+
* <p>
105+
* Use this constructor if you intend to customize the way the codec
106+
* parses and formats timestamps and zones. Beware that Cassandra only accepts
107+
* timestamp literals in some of the most common ISO-8601 formats;
108+
* attempting to use non-standard formats could result in invalid CQL literals.
109+
*
110+
* @param tupleType The tuple type this codec should handle.
111+
* It must be a {@code tuple<timestamp,varchar>}.
112+
* @param dateTimeFormatter The {@link java.time.format.DateTimeFormatter DateTimeFormatter} to use
113+
* to parse and format the timestamp component of the tuple.
114+
* As a parser, it should be lenient enough to accept most of the ISO-8601 formats
115+
* accepted by Cassandra as valid CQL literals.
116+
* As a formatter, it should be configured to always format timestamps in UTC
117+
* (see {@link java.time.format.DateTimeFormatter#withZone(java.time.ZoneId)}.
118+
* @param zoneFormatter The {@link java.time.format.DateTimeFormatter DateTimeFormatter} to use
119+
* to parse and format the zone component of the tuple.
120+
* @throws IllegalArgumentException if the provided tuple type is not a {@code tuple<timestamp,varchar>}.
121+
*/
122+
public ZonedDateTimeCodec(TupleType tupleType, java.time.format.DateTimeFormatter dateTimeFormatter, java.time.format.DateTimeFormatter zoneFormatter) {
82123
super(tupleType, java.time.ZonedDateTime.class);
124+
this.dateTimeFormatter = dateTimeFormatter;
125+
this.zoneFormatter = zoneFormatter;
83126
List<DataType> types = tupleType.getComponentTypes();
84127
checkArgument(
85128
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
99142
return bigint().serializeNoBoxing(millis, protocolVersion);
100143
}
101144
if (index == 1) {
102-
return varchar().serialize(ZONE_FORMATTER.format(source.getOffset()), protocolVersion);
145+
return varchar().serialize(zoneFormatter.format(source), protocolVersion);
103146
}
104147
throw new IndexOutOfBoundsException("Tuple index out of bounds. " + index);
105148
}
@@ -112,18 +155,18 @@ protected java.time.ZonedDateTime deserializeAndSetField(ByteBuffer input, java.
112155
}
113156
if (index == 1) {
114157
String zoneId = varchar().deserialize(input, protocolVersion);
115-
return target.withZoneSameInstant(java.time.ZoneId.of(zoneId));
158+
return target.withZoneSameInstant(zoneFormatter.parse(zoneId, java.time.temporal.TemporalQueries.zone()));
116159
}
117160
throw new IndexOutOfBoundsException("Tuple index out of bounds. " + index);
118161
}
119162

120163
@Override
121164
protected String formatField(java.time.ZonedDateTime value, int index) {
122165
if (index == 0) {
123-
return quote(FORMATTER.format(value));
166+
return quote(dateTimeFormatter.format(value));
124167
}
125168
if (index == 1) {
126-
return quote(ZONE_FORMATTER.format(value.getOffset()));
169+
return quote(zoneFormatter.format(value));
127170
}
128171
throw new IndexOutOfBoundsException("Tuple index out of bounds. " + index);
129172
}
@@ -143,14 +186,14 @@ protected java.time.ZonedDateTime parseAndSetField(String input, java.time.Zoned
143186
}
144187
}
145188
try {
146-
return java.time.ZonedDateTime.from(FORMATTER.parse(input));
189+
return java.time.ZonedDateTime.from(dateTimeFormatter.parse(input));
147190
} catch (java.time.format.DateTimeParseException e) {
148191
throw new InvalidTypeException(String.format("Cannot parse timestamp value from \"%s\"", target));
149192
}
150193
}
151194
if (index == 1) {
152195
String zoneId = varchar().parse(input);
153-
return target.withZoneSameInstant(java.time.ZoneId.of(zoneId));
196+
return target.withZoneSameInstant(zoneFormatter.parse(zoneId, java.time.temporal.TemporalQueries.zone()));
154197
}
155198
throw new IndexOutOfBoundsException("Tuple index out of bounds. " + index);
156199
}

driver-extras/src/main/java/com/datastax/driver/extras/codecs/joda/DateTimeCodec.java

Lines changed: 75 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,7 @@
1919
import com.datastax.driver.core.exceptions.InvalidTypeException;
2020
import org.joda.time.DateTime;
2121
import org.joda.time.DateTimeZone;
22-
import org.joda.time.format.DateTimeFormat;
2322
import org.joda.time.format.DateTimeFormatter;
24-
import org.joda.time.format.DateTimeFormatterBuilder;
2523
import org.joda.time.format.ISODateTimeFormat;
2624

2725
import java.nio.ByteBuffer;
@@ -30,7 +28,6 @@
3028
import static com.datastax.driver.core.ParseUtils.isLongLiteral;
3129
import static com.datastax.driver.core.ParseUtils.quote;
3230
import static com.google.common.base.Preconditions.checkArgument;
33-
import static org.joda.time.DateTimeZone.UTC;
3431

3532
/**
3633
* {@link TypeCodec} that maps
@@ -47,7 +44,7 @@
4744
* preserved.
4845
* <p/>
4946
* <strong>IMPORTANT</strong>: this codec's {@link #format(Object) format} method formats
50-
* timestamps using an ISO-8601 format that includes nanoseconds.
47+
* timestamps as CQL literal strings using an ISO-8601 format that includes milliseconds.
5148
* <strong>This format is incompatible with Cassandra versions < 2.0.9.</strong>
5249
*
5350
* @see <a href="https://cassandra.apache.org/doc/cql3/CQL-2.2.html#usingtimestamps">'Working with timestamps' section of CQL specification</a>
@@ -56,21 +53,78 @@ public class DateTimeCodec extends TypeCodec.AbstractTupleCodec<DateTime> {
5653

5754
/**
5855
* A {@link DateTimeFormatter} that parses (most) of
59-
* the ISO formats accepted in CQL.
56+
* the ISO-8601 formats accepted in CQL.
6057
*/
61-
private static final DateTimeFormatter FORMATTER = new DateTimeFormatterBuilder()
62-
.append(ISODateTimeFormat.dateOptionalTimeParser().getParser())
63-
.appendOptional(
64-
new DateTimeFormatterBuilder()
65-
.appendTimeZoneOffset("Z", true, 2, 4)
66-
.toParser())
67-
.toFormatter()
68-
.withZoneUTC();
58+
private static final DateTimeFormatter DEFAULT_PARSER = ISODateTimeFormat.dateOptionalTimeParser();
6959

70-
private static final DateTimeFormatter ZONE_FORMATTER = DateTimeFormat.forPattern("ZZ");
60+
/**
61+
* A {@link DateTimeFormatter} that prints timestamps
62+
* with a full ISO-8601 date and time format, including the time zone (Z).
63+
*/
64+
private static final DateTimeFormatter DEFFAULT_PRINTER = ISODateTimeFormat.dateTime().withZoneUTC();
65+
66+
private final DateTimeFormatter parser;
7167

68+
private final DateTimeFormatter printer;
69+
70+
/**
71+
* Creates a new {@link DateTimeCodec} for the given tuple,
72+
* using a default parser and a default printer to handle
73+
* the timestamp component of the tuple.
74+
* <p>
75+
* The default formatter and printer produce and parse CQL timestamp literals of the following form:
76+
* <ol>
77+
* <li>The printer will always produce a full ISO-8601 date and time pattern, including year,
78+
* month, day, hour, minutes, seconds and milliseconds,
79+
* followed by the zone ID {@code Z} (UTC), e.g. {@code 2010-06-30T01:20:47.999Z};
80+
* note that timestamp components are always printed in UTC time, hence the zone ID {@code Z}.</li>
81+
* <li>The parser accepts most ISO-8601 date and time patterns, the time part (minutes, seconds, milliseconds) being optional.</li>
82+
* </ol>
83+
* <p>
84+
* Note that it is not possible to customize the parsing and printing of
85+
* the zone component of the tuple. This codec prints either a zone offset such as {@code -07:00},
86+
* or a zone ID such as {@code UTC} or {@code Europe/Paris},
87+
* depending on what is the best information is available.
88+
*
89+
* @param tupleType The tuple type this codec should handle.
90+
* It must be a {@code tuple<timestamp,varchar>}.
91+
* @throws IllegalArgumentException if the provided tuple type is not a {@code tuple<timestamp,varchar>}.
92+
*/
7293
public DateTimeCodec(TupleType tupleType) {
94+
this(tupleType, DEFAULT_PARSER, DEFFAULT_PRINTER);
95+
}
96+
97+
/**
98+
* Creates a new {@link DateTimeCodec} for the given tuple,
99+
* using the provided {@link DateTimeFormatter parser} and {@link DateTimeFormatter printer}
100+
* to format and print the timestamp component of the tuple.
101+
* <p>
102+
* Use this constructor if you intend to customize the way the codec
103+
* parses and formats timestamps. Beware that Cassandra only accepts
104+
* timestamp literals in some of the most common ISO-8601 formats;
105+
* attempting to use non-standard formats could result in invalid CQL literals.
106+
* <p>
107+
* Note that it is not possible to customize the parsing and printing of
108+
* the zone component of the tuple. This codec prints either a zone offset such as {@code -07:00},
109+
* or a zone ID such as {@code UTC} or {@code Europe/Paris},
110+
* depending on what information is available.
111+
*
112+
* @param tupleType The tuple type this codec should handle.
113+
* It must be a {@code tuple<timestamp,varchar>}.
114+
* @param parser The {@link DateTimeFormatter parser} to use
115+
* to parse the timestamp component of the tuple.
116+
* It should be lenient enough to accept most of the ISO-8601 formats
117+
* accepted by Cassandra as valid CQL literals.
118+
* @param printer The {@link DateTimeFormatter printer} to use
119+
* to format the timestamp component of the tuple.
120+
* This printer should be configured to always format timestamps in UTC
121+
* (see {@link DateTimeFormatter#withZoneUTC()}.
122+
* @throws IllegalArgumentException if the provided tuple type is not a {@code tuple<timestamp,varchar>}.
123+
*/
124+
public DateTimeCodec(TupleType tupleType, DateTimeFormatter parser, DateTimeFormatter printer) {
73125
super(tupleType, DateTime.class);
126+
this.parser = parser;
127+
this.printer = printer;
74128
List<DataType> types = tupleType.getComponentTypes();
75129
checkArgument(
76130
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
90144
return bigint().serializeNoBoxing(millis, protocolVersion);
91145
}
92146
if (index == 1) {
93-
return varchar().serialize(ZONE_FORMATTER.print(source), protocolVersion);
147+
return varchar().serialize(source.getZone().getID(), protocolVersion);
94148
}
95149
throw new IndexOutOfBoundsException("Tuple index out of bounds. " + index);
96150
}
@@ -111,10 +165,10 @@ protected DateTime deserializeAndSetField(ByteBuffer input, DateTime target, int
111165
@Override
112166
protected String formatField(DateTime value, int index) {
113167
if (index == 0) {
114-
return quote(value.withZone(UTC).toString());
168+
return quote(printer.print(value));
115169
}
116170
if (index == 1) {
117-
return quote(ZONE_FORMATTER.print(value));
171+
return quote(value.getZone().getID());
118172
}
119173
throw new IndexOutOfBoundsException("Tuple index out of bounds. " + index);
120174
}
@@ -134,13 +188,16 @@ protected DateTime parseAndSetField(String input, DateTime target, int index) {
134188
}
135189
}
136190
try {
137-
return FORMATTER.parseDateTime(input);
191+
return parser.parseDateTime(input);
138192
} catch (RuntimeException e) {
139193
throw new InvalidTypeException(String.format("Cannot parse timestamp value from \"%s\"", target));
140194
}
141195
}
142196
if (index == 1) {
143197
String zoneId = varchar().parse(input);
198+
// Joda time does not recognize "Z"
199+
if ("Z".equals(zoneId))
200+
return target.withZone(DateTimeZone.UTC);
144201
return target.withZone(DateTimeZone.forID(zoneId));
145202
}
146203
throw new IndexOutOfBoundsException("Tuple index out of bounds. " + index);

0 commit comments

Comments
 (0)