diff --git a/hibernate-core/src/main/java/org/hibernate/annotations/Type.java b/hibernate-core/src/main/java/org/hibernate/annotations/Type.java index 9b5724aee625..91ccc3c2b889 100644 --- a/hibernate-core/src/main/java/org/hibernate/annotations/Type.java +++ b/hibernate-core/src/main/java/org/hibernate/annotations/Type.java @@ -31,7 +31,7 @@ * BigDecimal amount; * *
- * we may define an annotation type: + * we may define a custom annotation type: *
* @Retention(RUNTIME)
* @Target({METHOD,FIELD})
@@ -47,6 +47,27 @@
*
* which is much cleaner.
*
+ * An implementation of {@link UserType} applied via a custom annotation
+ * may declare a constructor which accepts the annotation instance,
+ * allowing the annotation to be used to configure the type.
+ *
+ * @Retention(RUNTIME)
+ * @Target({METHOD,FIELD})
+ * @Type(MonetaryAmountUserType.class)
+ * public @interface MonetaryAmount {
+ * public Currency currency();
+ * }
+ *
+ *
+ * public class MonetaryAmountUserType implements UserType<Amount> {
+ * private final Currency currency;
+ * public MonetaryAmountUserType(MonetaryAmount annotation) {
+ * currency = annotation.currency();
+ * }
+ * ...
+ * }
+ *
+ *
* The use of a {@code UserType} is usually mutually exclusive with the
* compositional approach of {@link JavaType} and {@link JdbcType}.
*
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 227c78dc08d4..00b0bc74b4d8 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
@@ -108,7 +108,7 @@ public BasicValue.Resolution> resolve(
MetadataBuildingContext context,
JdbcTypeIndicators indicators) {
if ( isEmpty( localConfigParameters ) ) {
- // we can use the re-usable resolution...
+ // we can use the reusable resolution...
if ( reusableResolution == null ) {
reusableResolution = createResolution( this.name, emptyMap(), indicators, context );
}
@@ -144,16 +144,15 @@ private static BasicValue.Resolution createResolution(
MetadataBuildingContext context) {
final var bootstrapContext = context.getBootstrapContext();
final var typeConfiguration = bootstrapContext.getTypeConfiguration();
- final var 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 6658fc70de8e..fdd9ec4389c1 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
@@ -102,7 +102,8 @@ public enum Kind {
// in-flight info
private Class extends UserType>> explicitCustomType;
- private Map explicitLocalTypeParams;
+ private Map explicitLocalCustomTypeParams;
+ private Annotation explicitCustomTypeAnnotation;
private Function explicitJdbcTypeAccess;
private Function> explicitJavaTypeAccess;
@@ -338,8 +339,9 @@ private boolean applyCustomType(MemberDetails memberDetails, TypeDetails typeDet
final var modelContext = getSourceModelContext();
final var userTypeImpl = kind.mappingAccess.customType( memberDetails, modelContext );
if ( userTypeImpl != null ) {
- applyExplicitType( userTypeImpl,
- kind.mappingAccess.customTypeParameters( memberDetails, modelContext ) );
+ this.explicitCustomType = userTypeImpl;
+ this.explicitLocalCustomTypeParams = kind.mappingAccess.customTypeParameters( memberDetails, modelContext );
+ this.explicitCustomTypeAnnotation = kind.mappingAccess.customTypeAnnotation( memberDetails, modelContext );
// An explicit custom UserType has top precedence when we get to BasicValue resolution.
return true;
}
@@ -349,7 +351,8 @@ private boolean applyCustomType(MemberDetails memberDetails, TypeDetails typeDet
final var registeredUserTypeImpl =
getMetadataCollector().findRegisteredUserType( basicClass );
if ( registeredUserTypeImpl != null ) {
- applyExplicitType( registeredUserTypeImpl, emptyMap() );
+ this.explicitCustomType = registeredUserTypeImpl;
+ this.explicitLocalCustomTypeParams = emptyMap();
return true;
}
}
@@ -384,11 +387,6 @@ private void prepareValue(MemberDetails value, TypeDetails typeDetails, @Nullabl
}
}
- private void applyExplicitType(Class extends UserType>> userTypeImpl, Map parameters) {
- explicitCustomType = userTypeImpl;
- explicitLocalTypeParams = parameters;
- }
-
private void prepareCollectionId(MemberDetails attribute) {
final var collectionId = attribute.getDirectAnnotationUsage( CollectionId.class );
if ( collectionId == null ) {
@@ -1195,7 +1193,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
@@ -1211,6 +1209,10 @@ public void fillSimpleValue() {
basicValue.setTypeParameters( createDynamicParameterizedTypeParameters() );
}
+ if ( explicitCustomType != null ) {
+ basicValue.setTypeAnnotation( explicitCustomTypeAnnotation );
+ }
+
if ( converterDescriptor != null ) {
basicValue.setJpaAttributeConverterDescriptor( converterDescriptor );
}
@@ -1282,8 +1284,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;
@@ -1295,6 +1297,7 @@ private Map createDynamicParameterizedTypeParameters() {
private interface BasicMappingAccess {
Class extends UserType>> customType(MemberDetails attribute, ModelsContext context);
Map customTypeParameters(MemberDetails attribute, ModelsContext context);
+ Annotation customTypeAnnotation(MemberDetails attribute, ModelsContext context);
}
private static class ValueMappingAccess implements BasicMappingAccess {
@@ -1311,6 +1314,12 @@ public Map customTypeParameters(MemberDetails attribute, ModelsCo
final var customType = attribute.locateAnnotationUsage( Type.class, context );
return customType == null ? null : extractParameterMap( customType.parameters() );
}
+
+ @Override
+ public Annotation customTypeAnnotation(MemberDetails attribute, ModelsContext context) {
+ final var annotations = attribute.getMetaAnnotated( Type.class, context );
+ return annotations == null || annotations.isEmpty() ? null : annotations.get( 0 );
+ }
}
private static class AnyDiscriminatorMappingAccess implements BasicMappingAccess {
@@ -1325,6 +1334,11 @@ public Class extends UserType>> customType(MemberDetails attribute, ModelsCo
public Map customTypeParameters(MemberDetails attribute, ModelsContext context) {
return emptyMap();
}
+
+ @Override
+ public Annotation customTypeAnnotation(MemberDetails attribute, ModelsContext context) {
+ return null;
+ }
}
private static class AnyKeyMappingAccess implements BasicMappingAccess {
@@ -1339,6 +1353,11 @@ public Class extends UserType>> customType(MemberDetails attribute, ModelsCo
public Map customTypeParameters(MemberDetails attribute, ModelsContext context) {
return emptyMap();
}
+
+ @Override
+ public Annotation customTypeAnnotation(MemberDetails attribute, ModelsContext context) {
+ return null;
+ }
}
private static class MapKeyMappingAccess implements BasicMappingAccess {
@@ -1357,6 +1376,12 @@ public Map customTypeParameters(MemberDetails attribute, ModelsCo
return customType == null ? null : extractParameterMap( customType.parameters() );
}
+
+ @Override
+ public Annotation customTypeAnnotation(MemberDetails attribute, ModelsContext context) {
+ final var annotations = attribute.getMetaAnnotated( MapKeyType.class, context );
+ return annotations == null || annotations.isEmpty() ? null : annotations.get( 0 );
+ }
}
private static class CollectionIdMappingAccess implements BasicMappingAccess {
@@ -1373,7 +1398,12 @@ public Class extends UserType>> customType(MemberDetails attribute, ModelsCo
public Map customTypeParameters(MemberDetails attribute, ModelsContext context) {
final var customType = attribute.locateAnnotationUsage( CollectionIdType.class, context );
return customType == null ? null : extractParameterMap( customType.parameters() );
+ }
+ @Override
+ public Annotation customTypeAnnotation(MemberDetails attribute, ModelsContext context) {
+ final var annotations = attribute.getMetaAnnotated( CollectionIdType.class, context );
+ return annotations == null || annotations.isEmpty() ? null : annotations.get( 0 );
}
}
@@ -1389,6 +1419,11 @@ public Class extends UserType>> customType(MemberDetails attribute, ModelsCo
public Map customTypeParameters(MemberDetails attribute, ModelsContext context) {
return emptyMap();
}
+
+ @Override
+ public Annotation customTypeAnnotation(MemberDetails attribute, ModelsContext 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 7194d44cf498..7cc38f9088ae 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
@@ -2029,9 +2029,9 @@ private BasicType> anyDiscriminatorType(Any anyBinding, TypeResolution discrim
);
}
else {
- return metadataBuildingContext.getBootstrapContext()
- .getTypeConfiguration().getBasicTypeRegistry()
- .resolve( StandardBasicTypes.STRING );
+ return
+ metadataBuildingContext.getBootstrapContext().getTypeConfiguration().getBasicTypeRegistry()
+ .resolve( StandardBasicTypes.STRING );
}
}
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 ff4db8c1906e..57bccc922226 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,8 @@
*/
package org.hibernate.mapping;
+import java.lang.annotation.Annotation;
+import java.lang.reflect.InvocationTargetException;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
@@ -1055,13 +1057,15 @@ public void setExplicitCustomType(Class extends UserType>> explicitCustomTyp
throw new UnsupportedOperationException( "Unsupported attempt to set an explicit-custom-type when value is already resolved" );
}
else {
+ final var typeProperties = getCustomTypeProperties();
+ final var typeAnnotation = getTypeAnnotation();
resolution = new UserTypeResolution<>(
new CustomType<>(
- getConfiguredUserTypeBean( explicitCustomType, getCustomTypeProperties() ),
+ getConfiguredUserTypeBean( explicitCustomType, typeProperties, typeAnnotation ),
getTypeConfiguration()
),
null,
- getCustomTypeProperties()
+ typeProperties
);
}
}
@@ -1078,11 +1082,9 @@ private Properties getCustomTypeProperties() {
return properties;
}
- private UserType> getConfiguredUserTypeBean(Class extends UserType>> explicitCustomType, Properties properties) {
- final var typeInstance =
- getBuildingContext().getBuildingOptions().isAllowExtensionsInCdi()
- ? getUserTypeBean( explicitCustomType, properties ).getBeanInstance()
- : FallbackBeanInstanceProducer.INSTANCE.produceBeanInstance( explicitCustomType );
+ private UserType> getConfiguredUserTypeBean(
+ Class extends UserType>> explicitCustomType, Properties properties, Annotation typeAnnotation) {
+ final var typeInstance = instantiateUserType( explicitCustomType, properties, typeAnnotation );
if ( typeInstance instanceof TypeConfigurationAware configurationAware ) {
configurationAware.setTypeConfiguration( getTypeConfiguration() );
@@ -1103,6 +1105,28 @@ private UserType> getConfiguredUserTypeBean(Class extends UserType>> 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 var 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 extends T> getUserTypeBean(Class explicitCustomType, Properties properties) {
final var producer = getBootstrapContext().getCustomTypeProducer();
final var managedBeanRegistry = getManagedBeanRegistry();
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 4cabe13f4194..6732f0ed9db9 100644
--- a/hibernate-core/src/main/java/org/hibernate/mapping/SimpleValue.java
+++ b/hibernate-core/src/main/java/org/hibernate/mapping/SimpleValue.java
@@ -88,6 +88,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;
@@ -127,6 +128,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;
@@ -792,11 +794,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() );
@@ -818,6 +828,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/main/java/org/hibernate/usertype/UserType.java b/hibernate-core/src/main/java/org/hibernate/usertype/UserType.java
index 96757961a1bf..f0383c0ab5a5 100644
--- a/hibernate-core/src/main/java/org/hibernate/usertype/UserType.java
+++ b/hibernate-core/src/main/java/org/hibernate/usertype/UserType.java
@@ -116,6 +116,13 @@
* And then use the {@code @TimePeriod} annotation to apply our {@code UserType}:
* @TimePeriod Period period;
*
+ * The {@code @TimePeriod} annotation might even have members:
+ *
@TimePeriod(precision=DAYS) Period period;
+ *
+ * In this case, the implementation of {@code UserType} may declare a constructor
+ * which accepts the annotation type, and use the values assigned to the members
+ * to customize its behavior.
+ *
* Finally, we could ask for our custom type to be used by default:
*
@TypeRegistration(basicClass = Period.class, userType = PeriodType.class)
*
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 c8604cab145b..601bfc781421 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..cadaa75f2330
--- /dev/null
+++ b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/basic/bitset/BitSetMetaUserTypeTest.java
@@ -0,0 +1,139 @@
+/*
+ * SPDX-License-Identifier: Apache-2.0
+ * 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/MetaUserTypeTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/basic/bitset/MetaUserTypeTest.java
new file mode 100644
index 000000000000..8081d74d8080
--- /dev/null
+++ b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/basic/bitset/MetaUserTypeTest.java
@@ -0,0 +1,142 @@
+/*
+ * SPDX-License-Identifier: Apache-2.0
+ * 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.Integer.parseInt;
+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;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+@Jpa(annotatedClasses = MetaUserTypeTest.Thing.class)
+public class MetaUserTypeTest {
+
+ @Test void test(EntityManagerFactoryScope scope) {
+ scope.inTransaction( em -> {
+ Thing thing = new Thing();
+ thing.period = Period.of( 1, 2, 3 );
+ thing.days = Period.ofDays( 42 );
+ em.persist( thing );
+ } );
+ scope.inTransaction( em -> {
+ Thing thing = em.find( Thing.class, 1 );
+ assertEquals( Period.of( 1, 2, 3 ), thing.period );
+ assertEquals( Period.ofDays( 42 ), thing.days );
+ } );
+ }
+
+ @Entity static class Thing {
+ @Id @GeneratedValue
+ long id;
+ @TimePeriod
+ Period period;
+ @TimePeriod(days = true)
+ Period days;
+ }
+
+ @Type(PeriodType. class)
+ @Target({METHOD, FIELD})
+ @Retention(RUNTIME)
+ public @interface TimePeriod {
+ boolean days() default false;
+ }
+
+ static class PeriodType implements UserType {
+ private final boolean days;
+
+ PeriodType(TimePeriod timePeriod) {
+ days = timePeriod.days();
+ }
+
+ @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);
+ if ( rs.wasNull() ) {
+ return null;
+ }
+ else if ( days ) {
+ return Period.ofDays( parseInt( string ) );
+ }
+ else {
+ return 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 if ( days ) {
+ st.setString(index, Integer.toString(value.getDays()));
+ }
+ 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
+ }
+ }
+}