diff --git a/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/YearJavaTypeDescriptor.java b/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/YearJavaTypeDescriptor.java new file mode 100644 index 0000000000..54bd2d7bf0 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/YearJavaTypeDescriptor.java @@ -0,0 +1,85 @@ +/* + * 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.type.descriptor.java; + +import java.sql.Types; +import java.time.Year; +import java.time.format.DateTimeFormatter; + +import org.hibernate.type.descriptor.WrapperOptions; +import org.hibernate.type.descriptor.jdbc.JdbcType; +import org.hibernate.type.descriptor.jdbc.JdbcTypeDescriptorIndicators; + +/** + * Describes the {@link java.time.Year} Java type + */ +public class YearJavaTypeDescriptor extends AbstractClassJavaTypeDescriptor { + private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern( "yyyy" ); + public static final YearJavaTypeDescriptor INSTANCE = new YearJavaTypeDescriptor(); + + public YearJavaTypeDescriptor() { + super( Year.class ); + } + + @Override + public JdbcType getRecommendedJdbcType(JdbcTypeDescriptorIndicators context) { + return context.getTypeConfiguration().getJdbcTypeDescriptorRegistry().getDescriptor( Types.INTEGER ); + } + + @Override + public String toString(Year value) { + return value == null ? null : value.format( FORMATTER ); + } + + @Override + public Year fromString(CharSequence string) { + return string == null ? null : Year.parse( string, FORMATTER ); + } + + @SuppressWarnings("unchecked") + @Override + public X unwrap(Year value, Class type, WrapperOptions options) { + if ( value == null ) { + return null; + } + + if ( type.isInstance( value ) ) { + return (X) value; + } + + if ( Integer.class.isAssignableFrom( type ) ) { + return (X) Integer.valueOf( value.getValue() ); + } + + if ( Long.class.isAssignableFrom( type ) ) { + return (X) Long.valueOf( value.getValue() ); + } + + if ( String.class.isAssignableFrom( type ) ) { + return (X) toString( value ); + } + + throw unknownUnwrap( type ); + } + + @Override + public Year wrap(X value, WrapperOptions options) { + if ( value == null ) { + return null; + } + + if ( value instanceof Number ) { + return Year.of( ( (Number) value ).intValue() ); + } + + if ( value instanceof String ) { + return fromString( (String) value ); + } + + throw unknownWrap( value.getClass() ); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/ZoneIdJavaTypeDescriptor.java b/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/ZoneIdJavaTypeDescriptor.java new file mode 100644 index 0000000000..f39239d111 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/ZoneIdJavaTypeDescriptor.java @@ -0,0 +1,70 @@ +/* + * 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.type.descriptor.java; + +import java.sql.Types; +import java.time.ZoneId; + +import org.hibernate.type.descriptor.WrapperOptions; +import org.hibernate.type.descriptor.jdbc.JdbcType; +import org.hibernate.type.descriptor.jdbc.JdbcTypeDescriptorIndicators; + +/** + * Describes the {@link ZoneId} Java type + */ +public class ZoneIdJavaTypeDescriptor extends AbstractClassJavaTypeDescriptor { + /** + * Singleton access + */ + public static final ZoneIdJavaTypeDescriptor INSTANCE = new ZoneIdJavaTypeDescriptor(); + + public ZoneIdJavaTypeDescriptor() { + super( ZoneId.class ); + } + + @Override + public JdbcType getRecommendedJdbcType(JdbcTypeDescriptorIndicators indicators) { + return indicators.getTypeConfiguration().getJdbcTypeDescriptorRegistry().getDescriptor( Types.VARCHAR ); + } + + @Override + public String toString(ZoneId value) { + return value == null ? null : value.getId(); + } + + @Override + public ZoneId fromString(CharSequence string) { + return string == null ? null : ZoneId.of( string.toString() ); + } + + @SuppressWarnings("unchecked") + @Override + public X unwrap(ZoneId value, Class type, WrapperOptions options) { + if ( value == null ) { + return null; + } + + if ( String.class.isAssignableFrom( type ) ) { + return (X) toString( value ); + } + + throw unknownUnwrap( type ); + } + + @Override + public ZoneId wrap(X value, WrapperOptions options) { + if ( value == null ) { + return null; + } + + if ( value instanceof String ) { + return fromString( (String) value ); + } + + throw unknownWrap( value.getClass() ); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/ZoneOffsetJavaTypeDescriptor.java b/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/ZoneOffsetJavaTypeDescriptor.java index dd32f8d444..0e51739ad7 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/ZoneOffsetJavaTypeDescriptor.java +++ b/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/ZoneOffsetJavaTypeDescriptor.java @@ -56,6 +56,9 @@ public class ZoneOffsetJavaTypeDescriptor extends AbstractClassJavaTypeDescripto if ( String.class.isAssignableFrom( type ) ) { return (X) toString( value ); } + if ( Integer.class.isAssignableFrom( type ) ) { + return (X) Integer.valueOf( value.getTotalSeconds() ); + } throw unknownUnwrap( type ); } @@ -64,9 +67,12 @@ public class ZoneOffsetJavaTypeDescriptor extends AbstractClassJavaTypeDescripto if ( value == null ) { return null; } - if ( CharSequence.class.isInstance( value ) ) { + if ( value instanceof CharSequence ) { return fromString( (CharSequence) value ); } + if ( value instanceof Integer ) { + return ZoneOffset.ofTotalSeconds( (Integer) value ); + } throw unknownWrap( value.getClass() ); } diff --git a/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/spi/JavaTypeDescriptorBaseline.java b/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/spi/JavaTypeDescriptorBaseline.java index 2f245910bc..9ea10eeea3 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/spi/JavaTypeDescriptorBaseline.java +++ b/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/spi/JavaTypeDescriptorBaseline.java @@ -69,6 +69,8 @@ import org.hibernate.type.descriptor.java.StringJavaTypeDescriptor; import org.hibernate.type.descriptor.java.TimeZoneJavaTypeDescriptor; import org.hibernate.type.descriptor.java.UUIDJavaTypeDescriptor; import org.hibernate.type.descriptor.java.UrlJavaTypeDescriptor; +import org.hibernate.type.descriptor.java.YearJavaTypeDescriptor; +import org.hibernate.type.descriptor.java.ZoneIdJavaTypeDescriptor; import org.hibernate.type.descriptor.java.ZoneOffsetJavaTypeDescriptor; import org.hibernate.type.descriptor.java.ZonedDateTimeJavaTypeDescriptor; @@ -129,6 +131,9 @@ public class JavaTypeDescriptorBaseline { target.addBaselineDescriptor( OffsetDateTimeJavaTypeDescriptor.INSTANCE ); target.addBaselineDescriptor( OffsetTimeJavaTypeDescriptor.INSTANCE ); target.addBaselineDescriptor( ZonedDateTimeJavaTypeDescriptor.INSTANCE ); + target.addBaselineDescriptor( YearJavaTypeDescriptor.INSTANCE ); + target.addBaselineDescriptor( ZoneIdJavaTypeDescriptor.INSTANCE ); + target.addBaselineDescriptor( ZoneOffsetJavaTypeDescriptor.INSTANCE ); target.addBaselineDescriptor( CalendarJavaTypeDescriptor.INSTANCE ); target.addBaselineDescriptor( DateJavaTypeDescriptor.INSTANCE ); @@ -136,7 +141,6 @@ public class JavaTypeDescriptorBaseline { target.addBaselineDescriptor( java.sql.Time.class, JdbcTimeJavaTypeDescriptor.INSTANCE ); target.addBaselineDescriptor( java.sql.Timestamp.class, JdbcTimestampJavaTypeDescriptor.INSTANCE ); target.addBaselineDescriptor( TimeZoneJavaTypeDescriptor.INSTANCE ); - target.addBaselineDescriptor( ZoneOffsetJavaTypeDescriptor.INSTANCE ); target.addBaselineDescriptor( ClassJavaTypeDescriptor.INSTANCE ); diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/type/java/YearMappingTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/type/java/YearMappingTests.java new file mode 100644 index 0000000000..f3c6fac312 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/type/java/YearMappingTests.java @@ -0,0 +1,158 @@ +/* + * 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.orm.test.mapping.type.java; + +import java.sql.Types; +import java.time.Year; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.metamodel.mapping.PluralAttributeMapping; +import org.hibernate.metamodel.mapping.internal.BasicAttributeMapping; +import org.hibernate.metamodel.mapping.internal.BasicValuedCollectionPart; +import org.hibernate.persister.entity.EntityPersister; + +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.JiraKey; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; + +import org.junit.jupiter.api.Test; + +import jakarta.persistence.CollectionTable; +import jakarta.persistence.Column; +import jakarta.persistence.ElementCollection; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.MapKeyColumn; +import jakarta.persistence.Table; + +import static org.assertj.core.api.Assertions.assertThat; + +@DomainModel( annotatedClasses = YearMappingTests.YearMappingTestEntity.class ) +@SessionFactory +@JiraKey( "HHH-10558" ) +public class YearMappingTests { + @Test + public void basicAssertions(SessionFactoryScope scope) { + final SessionFactoryImplementor sessionFactory = scope.getSessionFactory(); + final EntityPersister entityDescriptor = sessionFactory.getMetamodel().entityPersister( YearMappingTestEntity.class ); + + { + final BasicAttributeMapping yearAttribute = (BasicAttributeMapping) entityDescriptor.findAttributeMapping( "year" ); + assertThat( yearAttribute.getJdbcMapping().getJdbcTypeDescriptor().getJdbcTypeCode() ).isEqualTo( Types.INTEGER ); + assertThat( yearAttribute.getJdbcMapping().getJavaTypeDescriptor().getJavaTypeClass() ).isEqualTo( Year.class ); + } + + { + final PluralAttributeMapping yearsAttribute = (PluralAttributeMapping) entityDescriptor.findAttributeMapping( "years" ); + final BasicValuedCollectionPart elementDescriptor = (BasicValuedCollectionPart) yearsAttribute.getElementDescriptor(); + assertThat( elementDescriptor.getJdbcMapping().getJdbcTypeDescriptor().getJdbcTypeCode() ).isEqualTo( Types.INTEGER ); + assertThat( elementDescriptor.getJdbcMapping().getJavaTypeDescriptor().getJavaTypeClass() ).isEqualTo( Year.class ); + } + + { + final PluralAttributeMapping countByYearAttribute = (PluralAttributeMapping) entityDescriptor.findAttributeMapping( "countByYear" ); + final BasicValuedCollectionPart keyDescriptor = (BasicValuedCollectionPart) countByYearAttribute.getIndexDescriptor(); + assertThat( keyDescriptor.getJdbcMapping().getJdbcTypeDescriptor().getJdbcTypeCode() ).isEqualTo( Types.INTEGER ); + assertThat( keyDescriptor.getJdbcMapping().getJavaTypeDescriptor().getJavaTypeClass() ).isEqualTo( Year.class ); + } + } + + @Test + public void testUsage(SessionFactoryScope scope) { + final YearMappingTestEntity entity = new YearMappingTestEntity( 1, "one", Year.now() ); + final YearMappingTestEntity entity2 = new YearMappingTestEntity( 2, "two", Year.parse( "+10000" ) ); + + scope.inTransaction( (session) -> { + session.save( entity ); + session.save( entity2 ); + } ); + + try { + scope.inTransaction( (session) -> session.createQuery( "from YearMappingTestEntity" ).list() ); + } + finally { + scope.inTransaction( session -> session.delete( entity ) ); + scope.inTransaction( session -> session.delete( entity2 ) ); + } + } + + @Entity( name = "YearMappingTestEntity" ) + @Table( name = "year_map_test_entity" ) + public static class YearMappingTestEntity { + private Integer id; + private String name; + private Year year; + private Set years = new HashSet<>(); + private Map countByYear = new HashMap<>(); + + public YearMappingTestEntity() { + } + + public YearMappingTestEntity(Integer id, String name, Year year) { + this.id = id; + this.name = name; + this.year = year; + } + + @Id + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Year getYear() { + return year; + } + + public void setYear(Year year) { + this.year = year; + } + + @ElementCollection + @CollectionTable( + name = "entity_year", + joinColumns = @JoinColumn( name = "entity_id" ) + ) + @Column( name = "year" ) + public Set getYears() { + return years; + } + + public void setYears(Set years) { + this.years = years; + } + + @ElementCollection + @CollectionTable( name = "count_by_year", joinColumns = @JoinColumn( name = "entity_id" ) ) + @MapKeyColumn( name = "year" ) + @Column( name = "cnt" ) + public Map getCountByYear() { + return countByYear; + } + + public void setCountByYear(Map countByYear) { + this.countByYear = countByYear; + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/type/java/ZoneMappingTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/type/java/ZoneMappingTests.java new file mode 100644 index 0000000000..efb0af9baa --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/type/java/ZoneMappingTests.java @@ -0,0 +1,163 @@ +/* + * 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.orm.test.mapping.type.java; + +import java.sql.Types; +import java.time.Year; +import java.time.ZoneId; +import java.time.ZoneOffset; +import java.util.HashSet; +import java.util.Set; + +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.metamodel.mapping.internal.BasicAttributeMapping; +import org.hibernate.persister.entity.EntityPersister; + +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.JiraKey; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; + +import org.junit.jupiter.api.Test; + +import jakarta.persistence.CollectionTable; +import jakarta.persistence.Column; +import jakarta.persistence.ElementCollection; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.Table; + +import static org.assertj.core.api.Assertions.assertThat; + +@JiraKey( "HHH-13393" ) +@DomainModel( annotatedClasses = ZoneMappingTests.ZoneMappingTestEntity.class ) +@SessionFactory +public class ZoneMappingTests { + + @Test + public void basicAssertions(SessionFactoryScope scope) { + final SessionFactoryImplementor sessionFactory = scope.getSessionFactory(); + final EntityPersister entityDescriptor = sessionFactory.getMetamodel().entityPersister( ZoneMappingTestEntity.class ); + + { + final BasicAttributeMapping zoneIdAttribute = (BasicAttributeMapping) entityDescriptor.findAttributeMapping( "zoneId" ); + assertThat( zoneIdAttribute.getJdbcMapping().getJdbcTypeDescriptor().getJdbcTypeCode() ).isEqualTo( Types.VARCHAR ); + assertThat( zoneIdAttribute.getJdbcMapping().getJavaTypeDescriptor().getJavaTypeClass() ).isEqualTo( ZoneId.class ); + } + + { + final BasicAttributeMapping zoneOffsetAttribute = (BasicAttributeMapping) entityDescriptor.findAttributeMapping( "zoneOffset" ); + assertThat( zoneOffsetAttribute.getJdbcMapping().getJdbcTypeDescriptor().getJdbcTypeCode() ).isEqualTo( Types.VARCHAR ); + assertThat( zoneOffsetAttribute.getJdbcMapping().getJavaTypeDescriptor().getJavaTypeClass() ).isEqualTo( ZoneOffset.class ); + } + } + + @Test + public void testUsage(SessionFactoryScope scope) { + final ZoneMappingTestEntity entity = new ZoneMappingTestEntity( 1, "one", ZoneId.systemDefault(), ZoneOffset.UTC ); + final ZoneMappingTestEntity entity2 = new ZoneMappingTestEntity( 2, "two", ZoneId.systemDefault(), ZoneOffset.ofHours( 0 ) ); + final ZoneMappingTestEntity entity3 = new ZoneMappingTestEntity( 3, "three", ZoneId.systemDefault(), ZoneOffset.ofHours( -10 ) ); + + scope.inTransaction( (session) -> { + session.persist( entity ); + session.persist( entity2 ); + session.persist( entity3 ); + } ); + + try { + scope.inTransaction( (session) -> { + session.createQuery( "from ZoneMappingTestEntity" ).list(); + }); + } + finally { + scope.inTransaction( (session) -> { + session.createQuery( "delete ZoneMappingTestEntity" ).executeUpdate(); + }); + } + } + + + @Entity( name = "ZoneMappingTestEntity" ) + @Table( name = "zone_map_test_entity" ) + public static class ZoneMappingTestEntity { + private Integer id; + + private String name; + + private ZoneId zoneId; + private Set zoneIds = new HashSet<>(); + + private ZoneOffset zoneOffset; + private Set zoneOffsets = new HashSet<>(); + + public ZoneMappingTestEntity() { + } + + public ZoneMappingTestEntity(Integer id, String name, ZoneId zoneId, ZoneOffset zoneOffset) { + this.id = id; + this.name = name; + this.zoneId = zoneId; + this.zoneOffset = zoneOffset; + } + + @Id + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public ZoneId getZoneId() { + return zoneId; + } + + public void setZoneId(ZoneId zoneId) { + this.zoneId = zoneId; + } + + @ElementCollection + @CollectionTable( name = "zone_ids", joinColumns = @JoinColumn( name = "entity_id" ) ) + @Column( name = "zone_id" ) + public Set getZoneIds() { + return zoneIds; + } + + public void setZoneIds(Set zoneIds) { + this.zoneIds = zoneIds; + } + + public ZoneOffset getZoneOffset() { + return zoneOffset; + } + + public void setZoneOffset(ZoneOffset zoneOffset) { + this.zoneOffset = zoneOffset; + } + + @ElementCollection + @CollectionTable( name = "zone_offsets", joinColumns = @JoinColumn( name = "entity_id" ) ) + @Column( name = "zone_offset" ) + public Set getZoneOffsets() { + return zoneOffsets; + } + + public void setZoneOffsets(Set zoneOffsets) { + this.zoneOffsets = zoneOffsets; + } + } +}