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 3a13ed1fdc26..28f42fb9addb 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 @@ -1193,6 +1193,9 @@ else if ( aggregateComponent != null ) { } public void fillSimpleValue() { + + basicValue.setMemberDetails( memberDetails ); + basicValue.setExplicitTypeParams( explicitLocalCustomTypeParams ); // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 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 67ebc1e8590a..03e815ea8fbc 100644 --- a/hibernate-core/src/main/java/org/hibernate/mapping/BasicValue.java +++ b/hibernate-core/src/main/java/org/hibernate/mapping/BasicValue.java @@ -12,9 +12,12 @@ import java.util.function.Consumer; import java.util.function.Function; +import org.hibernate.AnnotationException; import org.hibernate.Incubating; import org.hibernate.Internal; import org.hibernate.MappingException; +import org.hibernate.models.spi.MemberDetails; +import org.hibernate.service.ServiceRegistry; import org.hibernate.type.TimeZoneStorageStrategy; import org.hibernate.annotations.SoftDelete; import org.hibernate.annotations.SoftDeleteType; @@ -65,6 +68,7 @@ import org.hibernate.type.internal.ConvertedBasicTypeImpl; import org.hibernate.type.spi.TypeConfiguration; import org.hibernate.type.spi.TypeConfigurationAware; +import org.hibernate.usertype.AnnotationBasedUserType; import org.hibernate.usertype.DynamicParameterizedType; import org.hibernate.usertype.UserType; @@ -72,6 +76,7 @@ import jakarta.persistence.AttributeConverter; import jakarta.persistence.EnumType; import jakarta.persistence.TemporalType; +import org.hibernate.usertype.UserTypeCreationContext; import static java.lang.Boolean.parseBoolean; import static org.hibernate.boot.model.convert.spi.ConverterDescriptor.TYPE_NAME_PREFIX; @@ -85,6 +90,7 @@ /** * @author Steve Ebersole + * @author Yanming Zhou */ public class BasicValue extends SimpleValue implements JdbcTypeIndicators, Resolvable, JpaAttributeConverterCreationContext { @@ -1080,9 +1086,10 @@ public void setExplicitCustomType(Class> explicitCustomTyp else { final var typeProperties = getCustomTypeProperties(); final var typeAnnotation = getTypeAnnotation(); + final var memberDetails = getMemberDetails(); resolution = new UserTypeResolution<>( new CustomType<>( - getConfiguredUserTypeBean( explicitCustomType, typeProperties, typeAnnotation ), + getConfiguredUserTypeBean( explicitCustomType, typeProperties, typeAnnotation, memberDetails ), getTypeConfiguration() ), null, @@ -1104,8 +1111,38 @@ private Properties getCustomTypeProperties() { } private UserType getConfiguredUserTypeBean( - Class> explicitCustomType, Properties properties, Annotation typeAnnotation) { - final var typeInstance = instantiateUserType( explicitCustomType, properties, typeAnnotation ); + Class> explicitCustomType, Properties properties, Annotation typeAnnotation, MemberDetails memberDetails) { + final var typeInstance = instantiateUserType( explicitCustomType, properties, typeAnnotation, memberDetails ); + + if ( typeInstance instanceof AnnotationBasedUserType ) { + if ( typeAnnotation == null ) { + throw new AnnotationException( String.format( "'UserType' implementation '%s' implements '%s' but no custom annotation present," + + " please refer to the Javadoc of '%s'.", + typeInstance.getClass().getName(), AnnotationBasedUserType.class.getName(), UserType.class.getName() ) ); + } + AnnotationBasedUserType annotationBased = (AnnotationBasedUserType) typeInstance; + annotationBased.initialize( typeAnnotation, new UserTypeCreationContext() { + @Override + public MetadataBuildingContext getBuildingContext() { + return BasicValue.this.getBuildingContext(); + } + + @Override + public ServiceRegistry getServiceRegistry() { + return BasicValue.this.getServiceRegistry(); + } + + @Override + public MemberDetails getMemberDetails() { + return memberDetails; + } + + @Override + public Properties getParameters() { + return properties; + } + } ); + } if ( typeInstance instanceof TypeConfigurationAware configurationAware ) { configurationAware.setTypeConfiguration( getTypeConfiguration() ); @@ -1127,21 +1164,41 @@ private UserType getConfiguredUserTypeBean( } private > T instantiateUserType( - Class customType, Properties properties, Annotation typeAnnotation) { - if ( typeAnnotation != null ) { - // attempt to instantiate it with the annotation as a constructor argument + Class customType, Properties properties, Annotation typeAnnotation, MemberDetails memberDetails ) { + try { + // attempt to instantiate it with the member as a constructor argument try { - final var constructor = customType.getDeclaredConstructor( typeAnnotation.annotationType() ); + final var constructor = customType.getDeclaredConstructor( MemberDetails.class ); constructor.setAccessible( true ); - return constructor.newInstance( typeAnnotation ); + return constructor.newInstance( memberDetails ); } - catch ( NoSuchMethodException ignored ) { - // no such constructor, instantiate it the old way + catch (NoSuchMethodException ignored) { } - catch (InvocationTargetException | InstantiationException | IllegalAccessException e) { - throw new org.hibernate.InstantiationException( "Could not instantiate custom type", customType, e ); + + 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) { + // attempt to instantiate it with the annotation and member as constructor arguments + try { + final var constructor = customType.getDeclaredConstructor( typeAnnotation.annotationType(), + MemberDetails.class ); + constructor.setAccessible( true ); + return constructor.newInstance( typeAnnotation, memberDetails ); + } + 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() diff --git a/hibernate-core/src/main/java/org/hibernate/mapping/MappingHelper.java b/hibernate-core/src/main/java/org/hibernate/mapping/MappingHelper.java index 26ddb606ffc6..00ceaf35545b 100644 --- a/hibernate-core/src/main/java/org/hibernate/mapping/MappingHelper.java +++ b/hibernate-core/src/main/java/org/hibernate/mapping/MappingHelper.java @@ -16,6 +16,7 @@ import org.hibernate.resource.beans.internal.FallbackBeanInstanceProducer; import org.hibernate.resource.beans.spi.ManagedBean; import org.hibernate.resource.beans.spi.ProvidedInstanceManagedBeanImpl; +import org.hibernate.usertype.AnnotationBasedUserType; import org.hibernate.usertype.ParameterizedType; import org.hibernate.usertype.UserCollectionType; @@ -75,9 +76,9 @@ public static void injectParameters(Object type, Properties parameters) { if ( type instanceof ParameterizedType parameterizedType ) { parameterizedType.setParameterValues( parameters == null ? EMPTY_PROPERTIES : parameters ); } - else if ( parameters != null && !parameters.isEmpty() ) { + else if ( parameters != null && !parameters.isEmpty() && !( type instanceof AnnotationBasedUserType ) ) { throw new MappingException( "'UserType' implementation '" + type.getClass().getName() - + "' does not implement 'ParameterizedType' but parameters were provided" ); + + "' does not implement 'ParameterizedType' or 'AnnotationBasedUserType' but parameters were provided" ); } } 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 7b47ad459e2e..8c53272782b3 100644 --- a/hibernate-core/src/main/java/org/hibernate/mapping/SimpleValue.java +++ b/hibernate-core/src/main/java/org/hibernate/mapping/SimpleValue.java @@ -89,6 +89,7 @@ public abstract class SimpleValue implements KeyValue { private String typeName; private Properties typeParameters; private Annotation typeAnnotation; + private MemberDetails memberDetails; private boolean isVersion; private boolean isNationalized; private boolean isLob; @@ -135,6 +136,7 @@ protected SimpleValue(SimpleValue original) { this.typeName = original.typeName; this.typeParameters = original.typeParameters == null ? null : new Properties( original.typeParameters ); this.typeAnnotation = original.typeAnnotation; + this.memberDetails = original.memberDetails; this.isVersion = original.isVersion; this.isNationalized = original.isNationalized; this.isLob = original.isLob; @@ -809,6 +811,10 @@ public void setTypeAnnotation(Annotation typeAnnotation) { this.typeAnnotation = typeAnnotation; } + public void setMemberDetails(MemberDetails memberDetails) { + this.memberDetails = memberDetails; + } + public Properties getTypeParameters() { return typeParameters; } @@ -817,6 +823,10 @@ public Annotation getTypeAnnotation() { return typeAnnotation; } + public MemberDetails getMemberDetails() { + return memberDetails; + } + public void copyTypeFrom(SimpleValue sourceValue ) { setTypeName( sourceValue.getTypeName() ); setTypeParameters( sourceValue.getTypeParameters() ); @@ -840,6 +850,7 @@ public boolean isSame(SimpleValue other) { && Objects.equals( typeName, other.typeName ) && Objects.equals( typeParameters, other.typeParameters ) && Objects.equals( typeAnnotation, other.typeAnnotation ) + && Objects.equals( memberDetails, other.memberDetails ) && 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/AnnotationBasedUserType.java b/hibernate-core/src/main/java/org/hibernate/usertype/AnnotationBasedUserType.java new file mode 100644 index 000000000000..a10ceace9f3f --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/usertype/AnnotationBasedUserType.java @@ -0,0 +1,35 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.usertype; + +import org.hibernate.Incubating; + +import java.lang.annotation.Annotation; + + +/** + * A {@link UserType} which receives parameters from a custom annotation. + * + * @param The user type annotation type supported by an implementation + * @param The java type + * + * @author Yanming Zhou + * + * @since 7.3 + */ +@Incubating +public interface AnnotationBasedUserType extends UserType { + /** + * Initializes this generation strategy for the given annotation instance. + * + * @param annotation an instance of the user type annotation type. Typically, + * implementations will retrieve the annotation's attribute + * values and store them in fields. + * @param context a {@link UserTypeCreationContext}. + * @throws org.hibernate.HibernateException in case an error occurred during initialization, e.g. if + * an implementation can't create a value for the given property type. + */ + void initialize(A annotation, UserTypeCreationContext context); +} 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 a296a06c9ed6..726ac2df2c2e 100644 --- a/hibernate-core/src/main/java/org/hibernate/usertype/UserType.java +++ b/hibernate-core/src/main/java/org/hibernate/usertype/UserType.java @@ -16,6 +16,7 @@ import org.hibernate.engine.jdbc.Size; import org.hibernate.engine.spi.SharedSessionContractImplementor; import org.hibernate.metamodel.mapping.JdbcMapping; +import org.hibernate.models.spi.MemberDetails; import org.hibernate.type.descriptor.WrapperOptions; import org.hibernate.type.descriptor.jdbc.JdbcType; @@ -214,8 +215,9 @@ * } * *

- * Every implementor of {@code UserType} must be immutable and must - * declare a public default constructor. + * Every implementor of {@code UserType} must be immutable and could + * declare a constructor accepts the {@link MemberDetails}, + * or the annotation type, or the annotation type and the {@link MemberDetails}. *

* A custom type implemented as a {@code UserType} is treated as a * non-composite value, and does not have persistent attributes which @@ -242,6 +244,7 @@ * * @see org.hibernate.type.Type * @see org.hibernate.type.CustomType + * @see org.hibernate.usertype.AnnotationBasedUserType * * @see org.hibernate.annotations.Type * @see org.hibernate.annotations.TypeRegistration diff --git a/hibernate-core/src/main/java/org/hibernate/usertype/UserTypeCreationContext.java b/hibernate-core/src/main/java/org/hibernate/usertype/UserTypeCreationContext.java new file mode 100644 index 000000000000..d30b90cd112d --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/usertype/UserTypeCreationContext.java @@ -0,0 +1,47 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.usertype; + +import org.hibernate.Incubating; +import org.hibernate.annotations.Type; +import org.hibernate.boot.spi.MetadataBuildingContext; +import org.hibernate.models.spi.MemberDetails; +import org.hibernate.service.ServiceRegistry; + +import java.util.Properties; + +/** + * Access to information useful during {@linkplain UserType} creation and initialization. + * + * @author Yanming Zhou + * @see AnnotationBasedUserType + * + * @since 7.3 + */ +@Incubating +public interface UserTypeCreationContext { + /** + * Access to the {@link MetadataBuildingContext}. + */ + MetadataBuildingContext getBuildingContext(); + + /** + * Access to available services. + */ + ServiceRegistry getServiceRegistry(); + + /** + * Access to the {@link MemberDetails}. + */ + MemberDetails getMemberDetails(); + + /** + * Access to the parameters. + * + * @see Type#parameters() + */ + Properties getParameters(); + +} 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 index 254752ae1c0a..b319f52bbd8c 100644 --- 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 @@ -8,16 +8,21 @@ import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.Id; +import org.hibernate.annotations.Parameter; import org.hibernate.annotations.Type; +import org.hibernate.models.spi.MemberDetails; import org.hibernate.testing.orm.junit.EntityManagerFactoryScope; import org.hibernate.testing.orm.junit.Jpa; import org.hibernate.type.descriptor.WrapperOptions; +import org.hibernate.usertype.AnnotationBasedUserType; import org.hibernate.usertype.UserType; +import org.hibernate.usertype.UserTypeCreationContext; import org.junit.jupiter.api.Test; import java.io.Serializable; import java.lang.annotation.Retention; import java.lang.annotation.Target; +import java.lang.reflect.Field; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; @@ -33,7 +38,8 @@ import static java.sql.Types.VARCHAR; import static org.junit.jupiter.api.Assertions.assertEquals; -@Jpa(annotatedClasses = {MetaUserTypeTest.Thing.class, MetaUserTypeTest.Things.class}) +@Jpa(annotatedClasses = {MetaUserTypeTest.Thing.class, MetaUserTypeTest.SecondThing.class, + MetaUserTypeTest.ThirdThing.class, MetaUserTypeTest.FourthThing.class, MetaUserTypeTest.Things.class}) public class MetaUserTypeTest { @Test void test(EntityManagerFactoryScope scope) { @@ -48,6 +54,42 @@ public class MetaUserTypeTest { assertEquals( Period.of( 1, 2, 3 ), thing.period ); assertEquals( Period.ofDays( 42 ), thing.days ); } ); + + scope.inTransaction( em -> { + SecondThing thing = new SecondThing(); + thing.period = Period.of( 1, 2, 3 ); + thing.days = Period.ofDays( 42 ); + em.persist( thing ); + } ); + scope.inTransaction( em -> { + SecondThing thing = em.find( SecondThing.class, 1 ); + assertEquals( Period.of( 1, 2, 3 ), thing.period ); + assertEquals( Period.ofDays( 42 ), thing.days ); + } ); + + scope.inTransaction( em -> { + ThirdThing thing = new ThirdThing(); + thing.period = Period.of( 1, 2, 3 ); + thing.days = Period.ofDays( 42 ); + em.persist( thing ); + } ); + scope.inTransaction( em -> { + ThirdThing thing = em.find( ThirdThing.class, 1 ); + assertEquals( Period.of( 1, 2, 3 ), thing.period ); + assertEquals( Period.ofDays( 42 ), thing.days ); + } ); + + scope.inTransaction( em -> { + FourthThing thing = new FourthThing(); + thing.period = Period.of( 1, 2, 3 ); + thing.days = Period.ofDays( 42 ); + em.persist( thing ); + } ); + scope.inTransaction( em -> { + FourthThing thing = em.find( FourthThing.class, 1 ); + assertEquals( Period.of( 1, 2, 3 ), thing.period ); + assertEquals( Period.ofDays( 42 ), thing.days ); + } ); } @Test void testCollection(EntityManagerFactoryScope scope) { @@ -73,6 +115,33 @@ public class MetaUserTypeTest { Period days; } + @Entity static class SecondThing { + @Id @GeneratedValue + long id; + @SecondTimePeriod + Period period; + @SecondTimePeriod(days = true) + Period days; + } + + @Entity static class ThirdThing { + @Id @GeneratedValue + long id; + @ThirdTimePeriod + Period period; + @ThirdTimePeriod(days = true) + Period days; + } + + @Entity static class FourthThing { + @Id @GeneratedValue + long id; + @FourthTimePeriod + Period period; + @FourthTimePeriod(days = true) + Period days; + } + @Entity static class Things { @Id @GeneratedValue long id; @@ -89,11 +158,76 @@ public class MetaUserTypeTest { boolean days() default false; } - static class PeriodType implements UserType { - private final boolean days; + @Type(SecondPeriodType.class) + @Target({METHOD, FIELD}) + @Retention(RUNTIME) + public @interface SecondTimePeriod { + boolean days() default false; + } + + @Type(ThirdPeriodType.class) + @Target({METHOD, FIELD}) + @Retention(RUNTIME) + public @interface ThirdTimePeriod { + boolean days() default false; + } + + @Type(value = FourthPeriodType.class, parameters = @Parameter(name="foo", value ="bar")) + @Target({METHOD, FIELD}) + @Retention(RUNTIME) + public @interface FourthTimePeriod { + boolean days() default false; + } + + static class PeriodType extends AbstractPeriodType { PeriodType(TimePeriod timePeriod) { + super(timePeriod.days()); + } + + } + + static class SecondPeriodType extends AbstractPeriodType { + + SecondPeriodType(MemberDetails memberDetails) { + super( ( (Field) memberDetails.toJavaMember() ).getAnnotation( SecondTimePeriod.class ).days() ); + } + + } + + static class ThirdPeriodType extends AbstractPeriodType { + + ThirdPeriodType(ThirdTimePeriod timePeriod, MemberDetails memberDetails) { + super(timePeriod.days()); + if ( !timePeriod.equals( ( (Field) memberDetails.toJavaMember() ).getAnnotation( ThirdTimePeriod.class ) )) { + throw new IllegalArgumentException(memberDetails.toJavaMember() + " should be annotated with " + timePeriod); + } + } + + } + + static class FourthPeriodType extends AbstractPeriodType implements AnnotationBasedUserType { + + FourthPeriodType() { + super(false); + } + + @Override + public void initialize(FourthTimePeriod timePeriod, UserTypeCreationContext context) { days = timePeriod.days(); + if ( !timePeriod.equals( ( (Field) context.getMemberDetails().toJavaMember() ).getAnnotation( FourthTimePeriod.class ) )) { + // only for validation + throw new IllegalArgumentException(context.getMemberDetails().toJavaMember() + " should be annotated with " + timePeriod); + } + assertEquals( "bar", context.getParameters().get("foo") ); + } + } + + static abstract class AbstractPeriodType implements UserType { + boolean days; + + AbstractPeriodType(boolean days) { + this.days = days; } @Override