1111import java .time .LocalTime ;
1212import java .time .OffsetDateTime ;
1313import java .time .OffsetTime ;
14- import java .time .ZoneOffset ;
14+ import java .time .ZoneId ;
1515import java .time .format .DateTimeFormatter ;
1616import java .time .temporal .ChronoField ;
1717import java .util .Calendar ;
@@ -92,59 +92,71 @@ public <X> X unwrap(OffsetTime offsetTime, Class<X> type, WrapperOptions options
9292 }
9393
9494 if ( LocalTime .class .isAssignableFrom ( type ) ) {
95- return (X ) offsetTime .withOffsetSameInstant ( getCurrentSystemOffset () ).toLocalTime ();
95+ return (X ) offsetTime .withOffsetSameInstant ( options . getSystemZoneOffset () ).toLocalTime ();
9696 }
9797
9898 if ( OffsetDateTime .class .isAssignableFrom ( type ) ) {
99- return (X ) offsetTime . atDate ( LocalDate . EPOCH );
99+ return (X ) offsetDateTimeAtEpoch ( offsetTime );
100100 }
101101
102102 // for legacy types, we assume that the JDBC timezone is passed to JDBC
103103 // (since PS.setTime() and friends do accept a timezone passed as a Calendar)
104104
105- final OffsetTime jdbcOffsetTime = offsetTime .withOffsetSameInstant ( getCurrentJdbcOffset (options ) );
106105
107106 if ( Time .class .isAssignableFrom ( type ) ) {
107+ // Use ZoneOffset rather than ZoneId in the conversion,
108+ // since the offset for a zone varies over time, but
109+ // a Time does not have an attached Date.
110+ final OffsetTime jdbcOffsetTime =
111+ offsetTime .withOffsetSameInstant ( options .getJdbcZoneOffset () ); // convert to the JDBC timezone
108112 final Time time = Time .valueOf ( jdbcOffsetTime .toLocalTime () );
109- if ( jdbcOffsetTime .getNano () == 0 ) {
110- return (X ) time ;
111- }
112- // Preserve milliseconds, which java.sql.Time supports
113- return (X ) new Time ( time .getTime () + DateTimeUtils .roundToPrecision ( jdbcOffsetTime .getNano (), 3 ) / 1000000 );
113+ // Time.valueOf() throws away milliseconds
114+ return (X ) withMillis ( jdbcOffsetTime , time );
114115 }
115116
116- final OffsetDateTime jdbcOffsetDateTime = jdbcOffsetTime .atDate ( LocalDate .EPOCH );
117-
118117 if ( Timestamp .class .isAssignableFrom ( type ) ) {
119- /*
120- * Workaround for HHH-13266 (JDK-8061577).
121- * Ideally we'd want to use Timestamp.from( jdbcOffsetDateTime.toInstant() ),
122- * but this won't always work since Timestamp.from() assumes the number of
123- * milliseconds since the epoch means the same thing in Timestamp and Instant,
124- * but it doesn't, in particular before 1900.
125- */
126- return (X ) Timestamp .valueOf ( jdbcOffsetDateTime .toLocalDateTime () );
118+ final OffsetTime jdbcOffsetTime =
119+ offsetTime .withOffsetSameInstant ( options .getJdbcZoneOffset () ); // convert to the JDBC timezone
120+ return (X ) Timestamp .valueOf ( offsetDateTimeAtEpoch ( jdbcOffsetTime ).toLocalDateTime () );
127121 }
128122
129123 if ( Calendar .class .isAssignableFrom ( type ) ) {
130- return (X ) GregorianCalendar .from ( jdbcOffsetDateTime .toZonedDateTime () );
124+ return (X ) GregorianCalendar .from ( offsetDateTimeAtEpoch ( offsetTime ) .toZonedDateTime () );
131125 }
132126
133127 // for instants, we assume that the JDBC timezone, if any, is ignored
134128
135- final Instant instant = offsetTime .atDate ( LocalDate .EPOCH ).toInstant ();
136-
137129 if ( Long .class .isAssignableFrom ( type ) ) {
138- return (X ) Long .valueOf ( instant .toEpochMilli () );
130+ return (X ) Long .valueOf ( instantAtEpoch ( offsetTime ) .toEpochMilli () );
139131 }
140132
141133 if ( Date .class .isAssignableFrom ( type ) ) {
142- return (X ) Date .from ( instant );
134+ return (X ) Date .from ( instantAtEpoch ( offsetTime ) );
143135 }
144136
145137 throw unknownUnwrap ( type );
146138 }
147139
140+ private static Time withMillis (OffsetTime jdbcOffsetTime , Time time ) {
141+ final int nanos = jdbcOffsetTime .getNano ();
142+ if ( nanos == 0 ) {
143+ return time ;
144+ }
145+ else {
146+ // Preserve milliseconds, which java.sql.Time supports
147+ final long millis = DateTimeUtils .roundToPrecision ( nanos , 3 ) / 1_000_000 ;
148+ return new Time ( time .getTime () + millis );
149+ }
150+ }
151+
152+ private static OffsetDateTime offsetDateTimeAtEpoch (OffsetTime jdbcOffsetTime ) {
153+ return jdbcOffsetTime .atDate ( LocalDate .EPOCH );
154+ }
155+
156+ private static Instant instantAtEpoch (OffsetTime offsetTime ) {
157+ return offsetDateTimeAtEpoch ( offsetTime ).toInstant ();
158+ }
159+
148160 @ Override
149161 public <X > OffsetTime wrap (X value , WrapperOptions options ) {
150162 if ( value == null ) {
@@ -159,67 +171,39 @@ public <X> OffsetTime wrap(X value, WrapperOptions options) {
159171 }
160172
161173 if (value instanceof LocalTime localTime ) {
162- return localTime .atOffset ( getCurrentSystemOffset () );
174+ return localTime .atOffset ( options . getSystemZoneOffset () );
163175 }
164176
165177 if ( value instanceof OffsetDateTime offsetDateTime ) {
166178 return offsetDateTime .toOffsetTime ();
167179 }
168180
169- /*
170- * Also, in order to fix HHH-13357, and to be consistent with the conversion to Time (see above),
171- * we set the offset to the current offset of the JVM (OffsetDateTime.now().getOffset()).
172- * This is different from setting the *zone* to the current *zone* of the JVM (ZoneId.systemDefault()),
173- * since a zone has a varying offset over time,
174- * thus the zone might have a different offset for the given timezone than it has for the current date/time.
175- * For example, if the timestamp represents 1970-01-01TXX:YY,
176- * and the JVM is set to use Europe/Paris as a timezone, and the current time is 2019-04-16-08:53,
177- * then applying the JVM timezone to the timestamp would result in the offset +01:00,
178- * but applying the JVM offset would result in the offset +02:00, since DST is in effect at 2019-04-16-08:53.
179- *
180- * Of course none of this would be a problem if we just stored the offset in the database,
181- * but I guess there are historical reasons that explain why we don't.
182- */
183-
184- // for legacy types, we assume that the JDBC timezone is passed to JDBC
185- // (since PS.setTime() and friends do accept a timezone passed as a Calendar)
186-
187181 if (value instanceof Time time ) {
188- final OffsetTime offsetTime = time .toLocalTime ()
189- .atOffset ( getCurrentJdbcOffset ( options ) )
190- .withOffsetSameInstant ( getCurrentSystemOffset () );
191- long millis = time .getTime () % 1000 ;
192- if ( millis == 0 ) {
193- return offsetTime ;
194- }
195- if ( millis < 0 ) {
196- // The milliseconds for a Time could be negative,
197- // which usually means the time is in a different time zone
198- millis += 1_000L ;
199- }
200- return offsetTime .with ( ChronoField .NANO_OF_SECOND , millis * 1_000_000L );
182+ // Use ZoneOffset rather than ZoneId in the conversion,
183+ // since the offset for a zone varies over time, but
184+ // a Time does not have an attached Date.
185+ final OffsetTime offsetTime =
186+ time .toLocalTime ().atOffset ( options .getJdbcZoneOffset () ) // the Timestamp is in the current JDBC timezone offset
187+ .withOffsetSameInstant ( options .getSystemZoneOffset () ); // convert back to the VM timezone
188+ // Time.toLocalTime() strips off nanos
189+ return withNanos ( time , offsetTime );
201190 }
202191
203192 if (value instanceof Timestamp timestamp ) {
204- /*
205- * Workaround for HHH-13266 (JDK-8061577).
206- * Ideally we'd want to use OffsetDateTime.ofInstant( ts.toInstant(), ... ),
207- * but this won't always work since ts.toInstant() assumes the number of
208- * milliseconds since the epoch means the same thing in Timestamp and Instant,
209- * but it doesn't, in particular before 1900.
210- */
211- return timestamp .toLocalDateTime ().toLocalTime ().atOffset ( getCurrentJdbcOffset (options ) )
212- .withOffsetSameInstant ( getCurrentSystemOffset () );
193+ return timestamp .toLocalDateTime ()
194+ .atZone ( options .getJdbcZoneId () ) // the Timestamp is in the JDBC timezone
195+ .withZoneSameInstant ( ZoneId .systemDefault () ) // convert back to the VM timezone
196+ .toOffsetDateTime ().toOffsetTime (); // return the time part
213197 }
214198
215199 if (value instanceof Date date ) {
216- return OffsetTime .ofInstant ( date .toInstant (), getCurrentSystemOffset () );
200+ return OffsetTime .ofInstant ( date .toInstant (), options . getSystemZoneOffset () );
217201 }
218202
219203 // for instants, we assume that the JDBC timezone, if any, is ignored
220204
221205 if (value instanceof Long millis ) {
222- return OffsetTime .ofInstant ( Instant .ofEpochMilli (millis ), getCurrentSystemOffset () );
206+ return OffsetTime .ofInstant ( Instant .ofEpochMilli (millis ), options . getSystemZoneOffset () );
223207 }
224208
225209 if (value instanceof Calendar calendar ) {
@@ -229,17 +213,21 @@ public <X> OffsetTime wrap(X value, WrapperOptions options) {
229213 throw unknownWrap ( value .getClass () );
230214 }
231215
232- private static ZoneOffset getCurrentJdbcOffset (WrapperOptions options ) {
233- if ( options .getJdbcTimeZone () != null ) {
234- return OffsetDateTime .now ().atZoneSameInstant ( options .getJdbcTimeZone ().toZoneId () ).getOffset ();
216+ private static OffsetTime withNanos (Time time , OffsetTime offsetTime ) {
217+ final long millis = time .getTime () % 1000 ;
218+ final long nanos ;
219+ if ( millis == 0 ) {
220+ return offsetTime ;
221+ }
222+ else if ( millis < 0 ) {
223+ // The milliseconds for a Time could be negative,
224+ // which usually means the time is in a different time zone
225+ nanos = (millis + 1_000L ) * 1_000_000L ;
235226 }
236227 else {
237- return getCurrentSystemOffset () ;
228+ nanos = millis * 1_000_000L ;
238229 }
239- }
240-
241- private static ZoneOffset getCurrentSystemOffset () {
242- return OffsetDateTime .now ().getOffset ();
230+ return offsetTime .with ( ChronoField .NANO_OF_SECOND , nanos );
243231 }
244232
245233 @ Override
0 commit comments