diff --git a/hibernate-core/src/test/java/org/hibernate/test/type/OffsetTimeTest.java b/hibernate-core/src/test/java/org/hibernate/test/type/OffsetTimeTest.java new file mode 100644 index 0000000000..6f14192208 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/test/type/OffsetTimeTest.java @@ -0,0 +1,203 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.test.type; + +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Time; +import java.sql.Timestamp; +import java.sql.Types; +import java.time.OffsetDateTime; +import java.time.OffsetTime; +import java.time.ZoneId; +import java.time.ZoneOffset; +import java.util.List; +import javax.persistence.Basic; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Id; + +import org.hibernate.type.descriptor.sql.BigIntTypeDescriptor; +import org.hibernate.type.descriptor.sql.TimestampTypeDescriptor; + +import org.junit.runners.Parameterized; + +/** + * Tests for storage of LocalTime properties. + */ +public class OffsetTimeTest extends AbstractJavaTimeTypeTest { + + private static class ParametersBuilder extends AbstractParametersBuilder { + public ParametersBuilder add(int hour, int minute, int second, int nanosecond, String offset, ZoneId defaultTimeZone) { + if ( !isNanosecondPrecisionSupported() ) { + nanosecond = 0; + } + return add( defaultTimeZone, hour, minute, second, nanosecond, offset, 1970, 1, 1 ); + } + + public ParametersBuilder addPersistedWithoutHibernate(int yearWhenPersistedWithoutHibernate, + int monthWhenPersistedWithoutHibernate, int dayWhenPersistedWithoutHibernate, + int hour, int minute, int second, int nanosecond, String offset, ZoneId defaultTimeZone) { + if ( !isNanosecondPrecisionSupported() ) { + nanosecond = 0; + } + return add( + defaultTimeZone, hour, minute, second, nanosecond, offset, + yearWhenPersistedWithoutHibernate, + monthWhenPersistedWithoutHibernate, dayWhenPersistedWithoutHibernate + ); + } + } + + @Parameterized.Parameters(name = "{1}:{2}:{3}.{4}[{5}] (JDBC write date: {6}-{7}-{8}) {0}") + public static List data() { + return new ParametersBuilder() + .alsoTestRemappingsWithH2( TimeAsTimestampRemappingH2Dialect.class ) + // None of these values was affected by HHH-13266 (JDK-8061577) + .add( 19, 19, 1, 0, "+10:00", ZONE_UTC_MINUS_8 ) + .add( 19, 19, 1, 0, "+01:30", ZONE_UTC_MINUS_8 ) + .add( 19, 19, 1, 0, "-06:00", ZONE_UTC_MINUS_8 ) + .add( 19, 19, 1, 0, "+10:00", ZONE_PARIS ) + .add( 19, 19, 1, 0, "+01:30", ZONE_PARIS ) + .add( 19, 19, 1, 500, "+01:00", ZONE_PARIS ) + .add( 19, 19, 1, 0, "-08:00", ZONE_PARIS ) + .add( 0, 0, 0, 0, "+01:00", ZONE_GMT ) + .add( 0, 0, 0, 0, "+00:00", ZONE_GMT ) + .add( 0, 0, 0, 0, "-01:00", ZONE_GMT ) + .add( 0, 0, 0, 0, "+00:00", ZONE_OSLO ) + .add( 0, 9, 20, 0, "+00:09:21", ZONE_PARIS ) + .add( 0, 19, 31, 0, "+00:19:32", ZONE_PARIS ) + .add( 0, 19, 31, 0, "+00:19:32", ZONE_AMSTERDAM ) + .add( 0, 0, 0, 0, "+00:19:32", ZONE_AMSTERDAM ) + .addPersistedWithoutHibernate( 1892, 1, 1, 0, 0, 0, 0, "+00:00", ZONE_OSLO ) + .addPersistedWithoutHibernate( 1900, 1, 1, 0, 9, 20, 0, "+00:09:21", ZONE_PARIS ) + .addPersistedWithoutHibernate( 1900, 1, 1, 0, 19, 31, 0, "+00:19:32", ZONE_PARIS ) + .addPersistedWithoutHibernate( 1900, 1, 1, 0, 19, 31, 0, "+00:19:32", ZONE_AMSTERDAM ) + .addPersistedWithoutHibernate( 1600, 1, 1, 0, 0, 0, 0, "+00:19:32", ZONE_AMSTERDAM ) + .build(); + } + + private final int hour; + private final int minute; + private final int second; + private final int nanosecond; + private final String offset; + + private final int yearWhenPersistedWithoutHibernate; + private final int monthWhenPersistedWithoutHibernate; + private final int dayWhenPersistedWithoutHibernate; + + public OffsetTimeTest(EnvironmentParameters env, int hour, int minute, int second, int nanosecond, String offset, + int yearWhenPersistedWithoutHibernate, int monthWhenPersistedWithoutHibernate, int dayWhenPersistedWithoutHibernate) { + super( env ); + this.hour = hour; + this.minute = minute; + this.second = second; + this.nanosecond = nanosecond; + this.offset = offset; + this.yearWhenPersistedWithoutHibernate = yearWhenPersistedWithoutHibernate; + this.monthWhenPersistedWithoutHibernate = monthWhenPersistedWithoutHibernate; + this.dayWhenPersistedWithoutHibernate = dayWhenPersistedWithoutHibernate; + } + + @Override + protected Class getEntityType() { + return EntityWithOffsetTime.class; + } + + @Override + protected EntityWithOffsetTime createEntityForHibernateWrite(int id) { + return new EntityWithOffsetTime( id, getOriginalPropertyValue() ); + } + + protected OffsetTime getOriginalPropertyValue() { + return OffsetTime.of( hour, minute, second, nanosecond, ZoneOffset.of( offset ) ); + } + + @Override + protected OffsetTime getExpectedPropertyValueAfterHibernateRead() { + // For some reason, the offset is not stored, so the restored values use the offset from the default JVM timezone. + if ( TimeAsTimestampRemappingH2Dialect.class.equals( getRemappingDialectClass() ) ) { + return getOriginalPropertyValue().withOffsetSameLocal( OffsetDateTime.now().getOffset() ); + } + else { + // When storing time as java.sql.Time, we only get second precision (not nanosecond) + return getOriginalPropertyValue().withNano( 0 ).withOffsetSameLocal( OffsetDateTime.now().getOffset() ); + } + } + + @Override + protected OffsetTime getActualPropertyValue(EntityWithOffsetTime entity) { + return entity.value; + } + + @Override + protected void setJdbcValueForNonHibernateWrite(PreparedStatement statement, int parameterIndex) + throws SQLException { + if ( TimeAsTimestampRemappingH2Dialect.class.equals( getRemappingDialectClass() ) ) { + statement.setTimestamp( + parameterIndex, + new Timestamp( + yearWhenPersistedWithoutHibernate - 1900, + monthWhenPersistedWithoutHibernate - 1, + dayWhenPersistedWithoutHibernate, + hour, minute, second, nanosecond + ) + ); + } + else { + statement.setTime( parameterIndex, new Time( hour, minute, second ) ); + } + } + + @Override + protected Object getExpectedJdbcValueAfterHibernateWrite() { + if ( TimeAsTimestampRemappingH2Dialect.class.equals( getRemappingDialectClass() ) ) { + return new Timestamp( 1970 - 1900, 0, 1, hour, minute, second, nanosecond ); + } + else { + return new Time( hour, minute, second ); + } + } + + @Override + protected Object getActualJdbcValue(ResultSet resultSet, int columnIndex) throws SQLException { + return resultSet.getTimestamp( columnIndex ); + } + + @Entity(name = ENTITY_NAME) + static final class EntityWithOffsetTime { + @Id + @Column(name = ID_COLUMN_NAME) + private Integer id; + + @Basic + @Column(name = PROPERTY_COLUMN_NAME) + private OffsetTime value; + + protected EntityWithOffsetTime() { + } + + private EntityWithOffsetTime(int id, OffsetTime value) { + this.id = id; + this.value = value; + } + } + + public static class TimeAsTimestampRemappingH2Dialect extends AbstractRemappingH2Dialect { + public TimeAsTimestampRemappingH2Dialect() { + super( Types.TIME, TimestampTypeDescriptor.INSTANCE ); + } + } + + public static class TimeAsBigIntRemappingH2Dialect extends AbstractRemappingH2Dialect { + public TimeAsBigIntRemappingH2Dialect() { + super( Types.TIME, BigIntTypeDescriptor.INSTANCE ); + } + } +}