diff --git a/hibernate-core/src/main/java/org/hibernate/boot/model/TypeDefinition.java b/hibernate-core/src/main/java/org/hibernate/boot/model/TypeDefinition.java index 4cf7117bc119..356dc3db3234 100644 --- a/hibernate-core/src/main/java/org/hibernate/boot/model/TypeDefinition.java +++ b/hibernate-core/src/main/java/org/hibernate/boot/model/TypeDefinition.java @@ -7,7 +7,6 @@ import java.io.Serializable; import java.sql.Types; import java.util.Arrays; -import java.util.Collections; import java.util.Map; import java.util.Objects; import java.util.Properties; @@ -39,6 +38,7 @@ import org.hibernate.type.spi.TypeConfigurationAware; import org.hibernate.usertype.UserType; +import static java.util.Collections.emptyMap; import static org.hibernate.boot.model.process.internal.InferredBasicValueResolver.resolveSqlTypeIndicators; import static org.hibernate.mapping.MappingHelper.injectParameters; @@ -104,7 +104,7 @@ public BasicValue.Resolution resolve( if ( CollectionHelper.isEmpty( localConfigParameters ) ) { // we can use the re-usable resolution... if ( reusableResolution == null ) { - reusableResolution = createResolution( name, Collections.emptyMap(), indicators, context ); + reusableResolution = createResolution( name, emptyMap(), indicators, context ); } return reusableResolution; } @@ -138,16 +138,17 @@ private static BasicValue.Resolution createResolution( MetadataBuildingContext context) { final BootstrapContext bootstrapContext = context.getBootstrapContext(); final TypeConfiguration typeConfiguration = bootstrapContext.getTypeConfiguration(); - final BeanInstanceProducer instanceProducer = bootstrapContext.getCustomTypeProducer(); + final boolean isKnownType = Type.class.isAssignableFrom( typeImplementorClass ) || UserType.class.isAssignableFrom( typeImplementorClass ); // support for AttributeConverter would be nice too if ( isKnownType ) { + final T typeInstance = instantiateType( bootstrapContext.getServiceRegistry(), context.getBuildingOptions(), - name, typeImplementorClass, instanceProducer ); + name, typeImplementorClass, bootstrapContext.getCustomTypeProducer() ); if ( typeInstance instanceof TypeConfigurationAware configurationAware ) { configurationAware.setTypeConfiguration( typeConfiguration ); diff --git a/hibernate-core/src/main/java/org/hibernate/boot/model/internal/BasicValueBinder.java b/hibernate-core/src/main/java/org/hibernate/boot/model/internal/BasicValueBinder.java index 2ac51779c7a7..fd1792d23e58 100644 --- a/hibernate-core/src/main/java/org/hibernate/boot/model/internal/BasicValueBinder.java +++ b/hibernate-core/src/main/java/org/hibernate/boot/model/internal/BasicValueBinder.java @@ -109,7 +109,8 @@ public enum Kind { // in-flight info private Class> explicitCustomType; - private Map explicitLocalTypeParams; + private Map explicitLocalCustomTypeParams; + private Annotation explicitCustomTypeAnnotation; private Function explicitJdbcTypeAccess; private Function explicitJavaTypeAccess; @@ -324,9 +325,11 @@ public void setType( isLob = value.hasDirectAnnotationUsage( Lob.class ); } + final SourceModelBuildingContext context = getSourceModelContext(); + if ( getDialect().getNationalizationSupport() == NationalizationSupport.EXPLICIT ) { isNationalized = buildingContext.getBuildingOptions().useNationalizedCharacterData() - || value.locateAnnotationUsage( Nationalized.class, getSourceModelContext() ) != null; + || value.locateAnnotationUsage( Nationalized.class, context ) != null; } if ( converterDescriptor != null ) { @@ -334,20 +337,21 @@ public void setType( } final Class> userTypeImpl = - kind.mappingAccess.customType( value, getSourceModelContext() ); + kind.mappingAccess.customType( value, context ); if ( userTypeImpl != null ) { - applyExplicitType( userTypeImpl, - kind.mappingAccess.customTypeParameters( value, getSourceModelContext() ) ); + this.explicitCustomType = userTypeImpl; + this.explicitLocalCustomTypeParams = kind.mappingAccess.customTypeParameters( value, context ); + this.explicitCustomTypeAnnotation = kind.mappingAccess.customTypeAnnotation( value, context ); // An explicit custom UserType has top precedence when we get to BasicValue resolution. return; } else if ( modelClassDetails != null ) { - final ClassDetails rawClassDetails = modelClassDetails.determineRawClass(); - final Class basicClass = rawClassDetails.toJavaClass(); final Class> registeredUserTypeImpl = - getMetadataCollector().findRegisteredUserType( basicClass ); + getMetadataCollector() + .findRegisteredUserType( modelClassDetails.determineRawClass().toJavaClass() ); if ( registeredUserTypeImpl != null ) { - applyExplicitType( registeredUserTypeImpl, emptyMap() ); + this.explicitCustomType = registeredUserTypeImpl; + this.explicitLocalCustomTypeParams = emptyMap(); return; } } @@ -380,11 +384,6 @@ else if ( modelClassDetails != null ) { } - private void applyExplicitType(Class> impl, Map params) { - this.explicitCustomType = impl; - this.explicitLocalTypeParams = params; - } - private void prepareCollectionId(MemberDetails attribute) { final CollectionId collectionIdAnn = attribute.getDirectAnnotationUsage( CollectionId.class ); if ( collectionIdAnn == null ) { @@ -1277,7 +1276,7 @@ else if ( aggregateComponent != null ) { } public void fillSimpleValue() { - basicValue.setExplicitTypeParams( explicitLocalTypeParams ); + basicValue.setExplicitTypeParams( explicitLocalCustomTypeParams ); // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // todo (6.0) : we are dropping support for @Type and @TypeDef from annotations @@ -1293,6 +1292,10 @@ public void fillSimpleValue() { basicValue.setTypeParameters( createDynamicParameterizedTypeParameters() ); } + if ( explicitCustomType != null ) { + basicValue.setTypeAnnotation( explicitCustomTypeAnnotation ); + } + if ( converterDescriptor != null ) { basicValue.setJpaAttributeConverterDescriptor( converterDescriptor ); } @@ -1364,8 +1367,8 @@ private Map createDynamicParameterizedTypeParameters() { parameters.put( DynamicParameterizedType.ACCESS_TYPE, accessType.getType() ); } - if ( explicitLocalTypeParams != null ) { - parameters.putAll( explicitLocalTypeParams ); + if ( explicitLocalCustomTypeParams != null ) { + parameters.putAll( explicitLocalCustomTypeParams ); } return parameters; @@ -1377,6 +1380,7 @@ private Map createDynamicParameterizedTypeParameters() { private interface BasicMappingAccess { Class> customType(MemberDetails attribute, SourceModelBuildingContext context); Map customTypeParameters(MemberDetails attribute, SourceModelBuildingContext context); + Annotation customTypeAnnotation(MemberDetails attribute, SourceModelBuildingContext context); } private static class ValueMappingAccess implements BasicMappingAccess { @@ -1393,6 +1397,12 @@ public Map customTypeParameters(MemberDetails attribute, SourceMo final Type customType = attribute.locateAnnotationUsage( Type.class, context ); return customType == null ? null : extractParameterMap( customType.parameters() ); } + + @Override + public Annotation customTypeAnnotation(MemberDetails attribute, SourceModelBuildingContext context) { + final List metaAnnotated = attribute.getMetaAnnotated( Type.class, context ); + return metaAnnotated.size() == 1 ? metaAnnotated.get( 0 ) : null; + } } private static class AnyDiscriminatorMappingAccess implements BasicMappingAccess { @@ -1407,6 +1417,11 @@ public Class> customType(MemberDetails attribute, SourceMo public Map customTypeParameters(MemberDetails attribute, SourceModelBuildingContext context) { return emptyMap(); } + + @Override + public Annotation customTypeAnnotation(MemberDetails attribute, SourceModelBuildingContext context) { + return null; + } } private static class AnyKeyMappingAccess implements BasicMappingAccess { @@ -1421,6 +1436,11 @@ public Class> customType(MemberDetails attribute, SourceMo public Map customTypeParameters(MemberDetails attribute, SourceModelBuildingContext context) { return emptyMap(); } + + @Override + public Annotation customTypeAnnotation(MemberDetails attribute, SourceModelBuildingContext context) { + return null; + } } private static class MapKeyMappingAccess implements BasicMappingAccess { @@ -1439,6 +1459,12 @@ public Map customTypeParameters(MemberDetails attribute, SourceMo return customType == null ? null : extractParameterMap( customType.parameters() ); } + + @Override + public Annotation customTypeAnnotation(MemberDetails attribute, SourceModelBuildingContext context) { + final List metaAnnotated = attribute.getMetaAnnotated( MapKeyType.class, context ); + return metaAnnotated.size() == 1 ? metaAnnotated.get( 0 ) : null; + } } private static class CollectionIdMappingAccess implements BasicMappingAccess { @@ -1455,7 +1481,12 @@ public Class> customType(MemberDetails attribute, SourceMo public Map customTypeParameters(MemberDetails attribute, SourceModelBuildingContext context) { final CollectionIdType customType = attribute.locateAnnotationUsage( CollectionIdType.class, context ); return customType == null ? null : extractParameterMap( customType.parameters() ); + } + @Override + public Annotation customTypeAnnotation(MemberDetails attribute, SourceModelBuildingContext context) { + final List metaAnnotated = attribute.getMetaAnnotated( CollectionIdType.class, context ); + return metaAnnotated.size() == 1 ? metaAnnotated.get( 0 ) : null; } } @@ -1471,6 +1502,11 @@ public Class> customType(MemberDetails attribute, SourceMo public Map customTypeParameters(MemberDetails attribute, SourceModelBuildingContext context) { return emptyMap(); } + + @Override + public Annotation customTypeAnnotation(MemberDetails attribute, SourceModelBuildingContext context) { + return null; + } } private static AnnotatedJoinColumns convertToJoinColumns(AnnotatedColumns columns, MetadataBuildingContext context) { diff --git a/hibernate-core/src/main/java/org/hibernate/boot/model/source/internal/hbm/ModelBinder.java b/hibernate-core/src/main/java/org/hibernate/boot/model/source/internal/hbm/ModelBinder.java index 49a4aa98eae3..8680d730dd1b 100644 --- a/hibernate-core/src/main/java/org/hibernate/boot/model/source/internal/hbm/ModelBinder.java +++ b/hibernate-core/src/main/java/org/hibernate/boot/model/source/internal/hbm/ModelBinder.java @@ -2185,8 +2185,9 @@ private void bindAny( } else { discriminatorTypeName = StandardBasicTypes.STRING.getName(); - discriminatorType = metadataBuildingContext.getBootstrapContext().getTypeConfiguration().getBasicTypeRegistry() - .resolve( StandardBasicTypes.STRING ); + discriminatorType = + metadataBuildingContext.getBootstrapContext().getTypeConfiguration().getBasicTypeRegistry() + .resolve( StandardBasicTypes.STRING ); } anyBinding.setMetaType( discriminatorTypeName ); @@ -2196,7 +2197,9 @@ private void bindAny( anyMapping.getDiscriminatorSource().getValueMappings().forEach( (discriminatorValueString, entityName) -> { try { - final Object discriminatorValue = discriminatorType.getJavaTypeDescriptor().fromString( discriminatorValueString ); + final Object discriminatorValue = + discriminatorType.getJavaTypeDescriptor() + .fromString( discriminatorValueString ); discriminatorValueToEntityNameMap.put( discriminatorValue, entityName ); } catch (Exception e) { diff --git a/hibernate-core/src/main/java/org/hibernate/mapping/BasicValue.java b/hibernate-core/src/main/java/org/hibernate/mapping/BasicValue.java index 54455555a866..fc089e152d4e 100644 --- a/hibernate-core/src/main/java/org/hibernate/mapping/BasicValue.java +++ b/hibernate-core/src/main/java/org/hibernate/mapping/BasicValue.java @@ -4,6 +4,9 @@ */ package org.hibernate.mapping; +import java.lang.annotation.Annotation; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; import java.util.HashMap; import java.util.Map; import java.util.Properties; @@ -632,14 +635,14 @@ private Resolution resolution(BasicJavaType explicitJavaType, JavaType jav throw new MappingException( "Unable to determine JavaType to use : " + this ); } - if ( basicJavaType instanceof BasicJavaType - && ( !basicJavaType.getJavaTypeClass().isEnum() || enumerationStyle == null ) ) { + final MetadataBuildingContext context = getBuildingContext(); + if ( basicJavaType instanceof BasicJavaType castType + && ( !basicJavaType.getJavaTypeClass().isEnum() || enumerationStyle == null ) ) { final TypeDefinition autoAppliedTypeDef = - getBuildingContext().getTypeDefinitionRegistry() - .resolveAutoApplied( (BasicJavaType) basicJavaType ); + context.getTypeDefinitionRegistry().resolveAutoApplied( castType ); if ( autoAppliedTypeDef != null ) { log.debug("BasicValue resolution matched auto-applied type-definition"); - return autoAppliedTypeDef.resolve( getTypeParameters(), null, getBuildingContext(), this ); + return autoAppliedTypeDef.resolve( getTypeParameters(), null, context, this ); } } @@ -654,7 +657,7 @@ private Resolution resolution(BasicJavaType explicitJavaType, JavaType jav getColumn(), ownerName, propertyName, - getBuildingContext() + context ); } @@ -1021,13 +1024,15 @@ public void setExplicitCustomType(Class> explicitCustomTyp throw new UnsupportedOperationException( "Unsupported attempt to set an explicit-custom-type when value is already resolved" ); } else { + final Properties typeProperties = getCustomTypeProperties(); + final Annotation typeAnnotation = getTypeAnnotation(); resolution = new UserTypeResolution<>( new CustomType<>( - getConfiguredUserTypeBean( explicitCustomType, getCustomTypeProperties() ), + getConfiguredUserTypeBean( explicitCustomType, typeProperties, typeAnnotation ), getTypeConfiguration() ), null, - getCustomTypeProperties() + typeProperties ); } } @@ -1044,11 +1049,9 @@ private Properties getCustomTypeProperties() { return properties; } - private UserType getConfiguredUserTypeBean(Class> explicitCustomType, Properties properties) { - final UserType typeInstance = - getBuildingContext().getBuildingOptions().isAllowExtensionsInCdi() - ? getUserTypeBean( explicitCustomType, properties ).getBeanInstance() - : FallbackBeanInstanceProducer.INSTANCE.produceBeanInstance( explicitCustomType ); + private UserType getConfiguredUserTypeBean( + Class> explicitCustomType, Properties properties, Annotation typeAnnotation) { + final UserType typeInstance = instantiateUserType( explicitCustomType, properties, typeAnnotation ); if ( typeInstance instanceof TypeConfigurationAware configurationAware ) { configurationAware.setTypeConfiguration( getTypeConfiguration() ); @@ -1069,6 +1072,28 @@ private UserType getConfiguredUserTypeBean(Class> expli return typeInstance; } + private > T instantiateUserType( + Class customType, Properties properties, Annotation typeAnnotation) { + if ( typeAnnotation != null ) { + // attempt to instantiate it with the annotation as a constructor argument + try { + final Constructor constructor = customType.getDeclaredConstructor( typeAnnotation.annotationType() ); + constructor.setAccessible( true ); + return constructor.newInstance( typeAnnotation ); + } + catch ( NoSuchMethodException ignored ) { + // no such constructor, instantiate it the old way + } + catch (InvocationTargetException | InstantiationException | IllegalAccessException e) { + throw new org.hibernate.InstantiationException( "Could not instantiate custom type", customType, e ); + } + } + + return getBuildingContext().getBuildingOptions().isAllowExtensionsInCdi() + ? getUserTypeBean( customType, properties ).getBeanInstance() + : FallbackBeanInstanceProducer.INSTANCE.produceBeanInstance( customType ); + } + private ManagedBean getUserTypeBean(Class explicitCustomType, Properties properties) { final BeanInstanceProducer producer = getBuildingContext().getBootstrapContext().getCustomTypeProducer(); if ( isNotEmpty( properties ) ) { diff --git a/hibernate-core/src/main/java/org/hibernate/mapping/SimpleValue.java b/hibernate-core/src/main/java/org/hibernate/mapping/SimpleValue.java index 49c3b8558cc9..af87803e0aa4 100644 --- a/hibernate-core/src/main/java/org/hibernate/mapping/SimpleValue.java +++ b/hibernate-core/src/main/java/org/hibernate/mapping/SimpleValue.java @@ -86,6 +86,7 @@ public abstract class SimpleValue implements KeyValue { private String typeName; private Properties typeParameters; + private Annotation typeAnnotation; private boolean isVersion; private boolean isNationalized; private boolean isLob; @@ -125,6 +126,7 @@ protected SimpleValue(SimpleValue original) { this.partitionKey = original.partitionKey; this.typeName = original.typeName; this.typeParameters = original.typeParameters == null ? null : new Properties( original.typeParameters ); + this.typeAnnotation = original.typeAnnotation; this.isVersion = original.isVersion; this.isNationalized = original.isNationalized; this.isLob = original.isLob; @@ -800,11 +802,19 @@ public void setTypeParameters(Map parameters) { } } + public void setTypeAnnotation(Annotation typeAnnotation) { + this.typeAnnotation = typeAnnotation; + } + public Properties getTypeParameters() { return typeParameters; } - public void copyTypeFrom( SimpleValue sourceValue ) { + public Annotation getTypeAnnotation() { + return typeAnnotation; + } + + public void copyTypeFrom(SimpleValue sourceValue ) { setTypeName( sourceValue.getTypeName() ); setTypeParameters( sourceValue.getTypeParameters() ); @@ -826,6 +836,7 @@ public boolean isSame(SimpleValue other) { return Objects.equals( columns, other.columns ) && Objects.equals( typeName, other.typeName ) && Objects.equals( typeParameters, other.typeParameters ) + && Objects.equals( typeAnnotation, other.typeAnnotation ) && Objects.equals( table, other.table ) && Objects.equals( foreignKeyName, other.foreignKeyName ) && Objects.equals( foreignKeyDefinition, other.foreignKeyDefinition ); diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/basic/bitset/BitSetHelper.java b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/basic/bitset/BitSetHelper.java index e3fc44e776e8..f9f423b48898 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/basic/bitset/BitSetHelper.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/basic/bitset/BitSetHelper.java @@ -16,7 +16,7 @@ public class BitSetHelper { public static String bitSetToString(BitSet bitSet) { StringBuilder builder = new StringBuilder(); for (long token : bitSet.toLongArray()) { - if (builder.length() > 0) { + if ( !builder.isEmpty() ) { builder.append(DELIMITER); } builder.append(Long.toString(token, 2)); diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/basic/bitset/BitSetMetaUserTypeTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/basic/bitset/BitSetMetaUserTypeTest.java new file mode 100644 index 000000000000..88d25cfdb63d --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/basic/bitset/BitSetMetaUserTypeTest.java @@ -0,0 +1,139 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.mapping.basic.bitset; + +import jakarta.persistence.Column; +import jakarta.persistence.ColumnResult; +import jakarta.persistence.ConstructorResult; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.NamedNativeQuery; +import jakarta.persistence.SqlResultSetMapping; +import jakarta.persistence.Table; +import org.hibernate.annotations.Type; +import org.hibernate.testing.junit4.BaseCoreFunctionalTestCase; +import org.junit.Test; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.BitSet; + +import static org.hibernate.testing.transaction.TransactionUtil.doInHibernate; +import static org.junit.Assert.assertEquals; + + +public class BitSetMetaUserTypeTest extends BaseCoreFunctionalTestCase { + + @Override + protected Class[] getAnnotatedClasses() { + return new Class[] { + Product.class + }; + } + + @Test + public void test() { + + BitSet bitSet = BitSet.valueOf(new long[] {1, 2, 3}); + + doInHibernate(this::sessionFactory, session -> { + Product product = new Product(); + product.setId(1); + product.setBitSet(bitSet); + session.persist(product); + }); + + doInHibernate(this::sessionFactory, session -> { + Product product = session.get(Product.class, 1); + assertEquals(bitSet, product.getBitSet()); + }); + } + + @Test + public void testNativeQuery() { + BitSet bitSet = BitSet.valueOf(new long[] {1, 2, 3}); + + doInHibernate(this::sessionFactory, session -> { + Product product = new Product(); + product.setId(1); + product.setBitSet(bitSet); + session.persist(product); + }); + + doInHibernate(this::sessionFactory, session -> { + Product product = session.createNamedQuery( + "find_person_by_bitset", Product.class) + .setParameter("id", 1L) + .getSingleResult(); + + assertEquals(bitSet, product.getBitSet()); + }); + } + + @Override + protected boolean isCleanupTestDataRequired() { + return true; + } + + @Type(BitSetUserType.class) + @Retention(RetentionPolicy.RUNTIME) + @interface BitSetType {} + + @NamedNativeQuery( + name = "find_person_by_bitset", + query = + "SELECT " + + " pr.id AS \"pr.id\", " + + " pr.bitset_col AS \"pr.bitset\" " + + "FROM products pr " + + "WHERE pr.id = :id", + resultSetMapping = "Person" + ) + @SqlResultSetMapping( + name = "Person", + classes = @ConstructorResult( + targetClass = Product.class, + columns = { + @ColumnResult(name = "pr.id"), + @ColumnResult(name = "pr.bitset", type = BitSetUserType.class) + } + ) + ) + @Entity(name = "Product") + @Table(name = "products") + public static class Product { + + @Id + private Integer id; + + @BitSetType + @Column(name = "bitset_col") + private BitSet bitSet; + + public Product() { + } + + public Product(Number id, BitSet bitSet) { + this.id = id.intValue(); + this.bitSet = bitSet; + } + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public BitSet getBitSet() { + return bitSet; + } + + public void setBitSet(BitSet bitSet) { + this.bitSet = bitSet; + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/basic/bitset/BitSetUserTypeTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/basic/bitset/BitSetUserTypeTest.java index f8a514ba9a7c..c221783884d4 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/basic/bitset/BitSetUserTypeTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/basic/bitset/BitSetUserTypeTest.java @@ -77,7 +77,7 @@ public void testNativeQuery() { }); doInHibernate(this::sessionFactory, session -> { - Product product = (Product) session.createNamedQuery( + Product product = session.createNamedQuery( "find_person_by_bitset", Product.class) .setParameter("id", 1L) .getSingleResult(); diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/basic/bitset/MetaUserTypeTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/basic/bitset/MetaUserTypeTest.java new file mode 100644 index 000000000000..879423d589f1 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/basic/bitset/MetaUserTypeTest.java @@ -0,0 +1,119 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.mapping.basic.bitset; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import org.hibernate.annotations.Type; +import org.hibernate.testing.orm.junit.EntityManagerFactoryScope; +import org.hibernate.testing.orm.junit.Jpa; +import org.hibernate.type.descriptor.WrapperOptions; +import org.hibernate.usertype.UserType; +import org.junit.jupiter.api.Test; + +import java.io.Serializable; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.time.Period; +import java.util.Objects; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.RetentionPolicy.RUNTIME; +import static java.sql.Types.VARCHAR; + +@Jpa(annotatedClasses = MetaUserTypeTest.Thing.class) +public class MetaUserTypeTest { + + @Test void test(EntityManagerFactoryScope scope) { + scope.inTransaction( em -> { + Period p = Period.of( 1, 2, 3 ); + Thing thing = new Thing(); + thing.period = p; + em.persist( thing ); + } ); + } + + @Entity static class Thing { + @Id @GeneratedValue + long id; + @TimePeriod + Period period; + } + + @Type(PeriodType. class) + @Target({METHOD, FIELD}) + @Retention(RUNTIME) + public @interface TimePeriod {} + + static class PeriodType implements UserType { + + PeriodType(TimePeriod timePeriod) { + + } + + @Override + public int getSqlType() { + return VARCHAR; + } + + @Override + public Class returnedClass() { + return Period.class; + } + + @Override + public boolean equals(Period x, Period y) { + return Objects.equals(x, y); + } + + @Override + public int hashCode(Period x) { + return x.hashCode(); + } + + @Override + public Period nullSafeGet(ResultSet rs, int position, WrapperOptions options) + throws SQLException { + String string = rs.getString(position); + return rs. wasNull() ? null : Period.parse(string); + } + + @Override + public void nullSafeSet(PreparedStatement st, Period value, int index, WrapperOptions options) + throws SQLException { + if ( value == null ) { + st.setNull(index, VARCHAR); + } + else { + st.setString(index, value.toString()); + } + } + + @Override + public boolean isMutable() { + return false; + } + + @Override + public Period deepCopy(Period value) { + return value; // Period is immutable + } + + @Override + public Serializable disassemble(Period period) { + return period; // Period is immutable + } + + @Override + public Period assemble(Serializable cached, Object owner) { + return (Period) cached; // Period is immutable + } + } +}