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<timestamp,varchar></code> a timezone ID can be
37+ * <code>tuple<timestamp,varchar></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 */
5055public 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 }
0 commit comments