From 89d304a00e9d12e15d64c2d0ffa84c2640868a2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yoann=20Rodi=C3=A8re?= Date: Fri, 26 Jul 2019 13:30:13 +0200 Subject: [PATCH] HHH-13379 Do not change the instant when storing a java.time type representing an instant at DST end in the default JVM timezone Note problems can still occur in the JDBC driver if the JDBC timezone is not forced to GMT/UTC. --- .../java/InstantJavaDescriptor.java | 39 +++++++++++++----- .../java/OffsetDateTimeJavaDescriptor.java | 40 ++++++++++++++----- .../java/ZonedDateTimeJavaDescriptor.java | 40 ++++++++++++++----- 3 files changed, 89 insertions(+), 30 deletions(-) diff --git a/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/InstantJavaDescriptor.java b/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/InstantJavaDescriptor.java index db84b82ac3e8..9bc9a52e0a03 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/InstantJavaDescriptor.java +++ b/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/InstantJavaDescriptor.java @@ -62,12 +62,22 @@ public X unwrap(Instant instant, Class type, WrapperOptions options) { if ( java.sql.Timestamp.class.isAssignableFrom( type ) ) { /* - * Workaround for HHH-13266 (JDK-8061577). - * Ideally we'd want to use Timestamp.from(), but this won't always work. - * Timestamp.from() assumes the number of milliseconds since the epoch - * means the same thing in Timestamp and Instant, but it doesn't, in particular before 1900. + * This works around two bugs: + * - HHH-13266 (JDK-8061577): around and before 1900, + * the number of milliseconds since the epoch does not mean the same thing + * for java.util and java.time, so conversion must be done using the year, month, day, hour, etc. + * - HHH-13379 (JDK-4312621): after 1908 (approximately), + * Daylight Saving Time introduces ambiguity in the year/month/day/hour/etc representation once a year + * (on DST end), so conversion must be done using the number of milliseconds since the epoch. + * - around 1905, both methods are equally valid, so we don't really care which one is used. */ - return (X) Timestamp.valueOf( instant.atZone( ZoneId.systemDefault() ).toLocalDateTime() ); + ZonedDateTime zonedDateTime = instant.atZone( ZoneId.systemDefault() ); + if ( zonedDateTime.getYear() < 1905 ) { + return (X) Timestamp.valueOf( zonedDateTime.toLocalDateTime() ); + } + else { + return (X) Timestamp.from( instant ); + } } if ( java.sql.Date.class.isAssignableFrom( type ) ) { @@ -102,12 +112,21 @@ public Instant wrap(X value, WrapperOptions options) { if ( Timestamp.class.isInstance( value ) ) { final Timestamp ts = (Timestamp) value; /* - * Workaround for HHH-13266 (JDK-8061577). - * Ideally we'd want to use ts.toInstant(), but this won't always work. - * ts.toInstant() assumes the number of milliseconds since the epoch - * means the same thing in Timestamp and Instant, but it doesn't, in particular before 1900. + * This works around two bugs: + * - HHH-13266 (JDK-8061577): around and before 1900, + * the number of milliseconds since the epoch does not mean the same thing + * for java.util and java.time, so conversion must be done using the year, month, day, hour, etc. + * - HHH-13379 (JDK-4312621): after 1908 (approximately), + * Daylight Saving Time introduces ambiguity in the year/month/day/hour/etc representation once a year + * (on DST end), so conversion must be done using the number of milliseconds since the epoch. + * - around 1905, both methods are equally valid, so we don't really care which one is used. */ - return ts.toLocalDateTime().atZone( ZoneId.systemDefault() ).toInstant(); + if ( ts.getYear() < 5 ) { // Timestamp year 0 is 1900 + return ts.toLocalDateTime().atZone( ZoneId.systemDefault() ).toInstant(); + } + else { + return ts.toInstant(); + } } if ( Long.class.isInstance( value ) ) { diff --git a/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/OffsetDateTimeJavaDescriptor.java b/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/OffsetDateTimeJavaDescriptor.java index 61f0ccc4766e..c9f624f1050e 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/OffsetDateTimeJavaDescriptor.java +++ b/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/OffsetDateTimeJavaDescriptor.java @@ -60,12 +60,23 @@ public X unwrap(OffsetDateTime offsetDateTime, Class type, WrapperOptions if ( Timestamp.class.isAssignableFrom( type ) ) { /* - * Workaround for HHH-13266 (JDK-8061577). - * Ideally we'd want to use Timestamp.from( offsetDateTime.toInstant() ), but this won't always work. - * Timestamp.from() assumes the number of milliseconds since the epoch - * means the same thing in Timestamp and Instant, but it doesn't, in particular before 1900. + * This works around two bugs: + * - HHH-13266 (JDK-8061577): around and before 1900, + * the number of milliseconds since the epoch does not mean the same thing + * for java.util and java.time, so conversion must be done using the year, month, day, hour, etc. + * - HHH-13379 (JDK-4312621): after 1908 (approximately), + * Daylight Saving Time introduces ambiguity in the year/month/day/hour/etc representation once a year + * (on DST end), so conversion must be done using the number of milliseconds since the epoch. + * - around 1905, both methods are equally valid, so we don't really care which one is used. */ - return (X) Timestamp.valueOf( offsetDateTime.atZoneSameInstant( ZoneId.systemDefault() ).toLocalDateTime() ); + if ( offsetDateTime.getYear() < 1905 ) { + return (X) Timestamp.valueOf( + offsetDateTime.atZoneSameInstant( ZoneId.systemDefault() ).toLocalDateTime() + ); + } + else { + return (X) Timestamp.from( offsetDateTime.toInstant() ); + } } if ( java.sql.Date.class.isAssignableFrom( type ) ) { @@ -100,12 +111,21 @@ public OffsetDateTime wrap(X value, WrapperOptions options) { if ( Timestamp.class.isInstance( value ) ) { final Timestamp ts = (Timestamp) value; /* - * Workaround for HHH-13266 (JDK-8061577). - * Ideally we'd want to use OffsetDateTime.ofInstant( ts.toInstant(), ... ), but this won't always work. - * ts.toInstant() assumes the number of milliseconds since the epoch - * means the same thing in Timestamp and Instant, but it doesn't, in particular before 1900. + * This works around two bugs: + * - HHH-13266 (JDK-8061577): around and before 1900, + * the number of milliseconds since the epoch does not mean the same thing + * for java.util and java.time, so conversion must be done using the year, month, day, hour, etc. + * - HHH-13379 (JDK-4312621): after 1908 (approximately), + * Daylight Saving Time introduces ambiguity in the year/month/day/hour/etc representation once a year + * (on DST end), so conversion must be done using the number of milliseconds since the epoch. + * - around 1905, both methods are equally valid, so we don't really care which one is used. */ - return ts.toLocalDateTime().atZone( ZoneId.systemDefault() ).toOffsetDateTime(); + if ( ts.getYear() < 5 ) { // Timestamp year 0 is 1900 + return ts.toLocalDateTime().atZone( ZoneId.systemDefault() ).toOffsetDateTime(); + } + else { + return OffsetDateTime.ofInstant( ts.toInstant(), ZoneId.systemDefault() ); + } } if ( Date.class.isInstance( value ) ) { diff --git a/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/ZonedDateTimeJavaDescriptor.java b/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/ZonedDateTimeJavaDescriptor.java index fafb2e25262a..a10bb47a8ecb 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/ZonedDateTimeJavaDescriptor.java +++ b/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/ZonedDateTimeJavaDescriptor.java @@ -60,12 +60,23 @@ public X unwrap(ZonedDateTime zonedDateTime, Class type, WrapperOptions o if ( Timestamp.class.isAssignableFrom( type ) ) { /* - * Workaround for HHH-13266 (JDK-8061577). - * Ideally we'd want to use Timestamp.from( zonedDateTime.toInstant() ), but this won't always work. - * Timestamp.from() assumes the number of milliseconds since the epoch - * means the same thing in Timestamp and Instant, but it doesn't, in particular before 1900. + * This works around two bugs: + * - HHH-13266 (JDK-8061577): around and before 1900, + * the number of milliseconds since the epoch does not mean the same thing + * for java.util and java.time, so conversion must be done using the year, month, day, hour, etc. + * - HHH-13379 (JDK-4312621): after 1908 (approximately), + * Daylight Saving Time introduces ambiguity in the year/month/day/hour/etc representation once a year + * (on DST end), so conversion must be done using the number of milliseconds since the epoch. + * - around 1905, both methods are equally valid, so we don't really care which one is used. */ - return (X) Timestamp.valueOf( zonedDateTime.withZoneSameInstant( ZoneId.systemDefault() ).toLocalDateTime() ); + if ( zonedDateTime.getYear() < 1905 ) { + return (X) Timestamp.valueOf( + zonedDateTime.withZoneSameInstant( ZoneId.systemDefault() ).toLocalDateTime() + ); + } + else { + return (X) Timestamp.from( zonedDateTime.toInstant() ); + } } if ( java.sql.Date.class.isAssignableFrom( type ) ) { @@ -100,12 +111,21 @@ public ZonedDateTime wrap(X value, WrapperOptions options) { if ( java.sql.Timestamp.class.isInstance( value ) ) { final Timestamp ts = (Timestamp) value; /* - * Workaround for HHH-13266 (JDK-8061577). - * Ideally we'd want to use ZonedDateTime.ofInstant( ts.toInstant(), ... ), but this won't always work. - * ts.toInstant() assumes the number of milliseconds since the epoch - * means the same thing in Timestamp and Instant, but it doesn't, in particular before 1900. + * This works around two bugs: + * - HHH-13266 (JDK-8061577): around and before 1900, + * the number of milliseconds since the epoch does not mean the same thing + * for java.util and java.time, so conversion must be done using the year, month, day, hour, etc. + * - HHH-13379 (JDK-4312621): after 1908 (approximately), + * Daylight Saving Time introduces ambiguity in the year/month/day/hour/etc representation once a year + * (on DST end), so conversion must be done using the number of milliseconds since the epoch. + * - around 1905, both methods are equally valid, so we don't really care which one is used. */ - return ts.toLocalDateTime().atZone( ZoneId.systemDefault() ); + if ( ts.getYear() < 5 ) { // Timestamp year 0 is 1900 + return ts.toLocalDateTime().atZone( ZoneId.systemDefault() ); + } + else { + return ts.toInstant().atZone( ZoneId.systemDefault() ); + } } if ( java.util.Date.class.isInstance( value ) ) {