Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -1193,6 +1193,9 @@ else if ( aggregateComponent != null ) {
}

public void fillSimpleValue() {

basicValue.setMemberDetails( memberDetails );

basicValue.setExplicitTypeParams( explicitLocalCustomTypeParams );

// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Expand Down
81 changes: 69 additions & 12 deletions hibernate-core/src/main/java/org/hibernate/mapping/BasicValue.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -65,13 +68,15 @@
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;

import com.fasterxml.classmate.ResolvedType;
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;
Expand All @@ -85,6 +90,7 @@

/**
* @author Steve Ebersole
* @author Yanming Zhou
*/
public class BasicValue extends SimpleValue
implements JdbcTypeIndicators, Resolvable, JpaAttributeConverterCreationContext {
Expand Down Expand Up @@ -1080,9 +1086,10 @@ public void setExplicitCustomType(Class<? extends UserType<?>> 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,
Expand All @@ -1104,8 +1111,38 @@ private Properties getCustomTypeProperties() {
}

private UserType<?> getConfiguredUserTypeBean(
Class<? extends UserType<?>> explicitCustomType, Properties properties, Annotation typeAnnotation) {
final var typeInstance = instantiateUserType( explicitCustomType, properties, typeAnnotation );
Class<? extends UserType<?>> 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() ) );
Comment on lines +1119 to +1121
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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() ) );
throw new AnnotationException( String.format( "Custom type '%s' implements 'AnnotationBasedUserType' but no custom type annotation is present",
typeInstance.getClass().getName() ) );

}
AnnotationBasedUserType<Annotation, ?> annotationBased = (AnnotationBasedUserType<Annotation, ?>) typeInstance;
Copy link
Member

@gavinking gavinking Dec 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this an unchecked cast?

Copy link
Member

@gavinking gavinking Dec 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is. OK, here's my suggestion of how we should write this code, to get much better error messages instead of cryptic CCEs from weird undebuggable byte code. This approach uses wildcard capture and reflection. Since the reflection happens at start time, it should not cause a problem.

	private <T extends Annotation> void initializeType(Properties properties,
			Annotation typeAnnotation,
			MemberDetails memberDetails,
			AnnotationBasedUserType<T, ?> annotationBased) {
		annotationBased.initialize( castAnnotationType( typeAnnotation, annotationBased ),
				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;
					}
				} );
	}

	private <T extends Annotation> T castAnnotationType(
			Annotation typeAnnotation,
			AnnotationBasedUserType<T, ?> annotationBased) {
		final var annotationType = annotationBased.getClass();
		for ( var iface: annotationType.getGenericInterfaces() ) {
			if ( iface instanceof ParameterizedType parameterizedType
					&& parameterizedType.getRawType() == AnnotationBasedUserType.class ) {
				final var typeArguments = parameterizedType.getActualTypeArguments();
				if ( typeArguments.length > 0
						&& typeArguments[0] instanceof Class<?> annotationClass ) {
					if ( !annotationClass.isInstance( typeAnnotation ) ) {
						throw new AnnotationException( String.format( "Annotation '%s' is not assignable to '%s'",
								annotationType.getName(), iface.getTypeName() ) );
					}
					@SuppressWarnings("unchecked") // safe, we just checked it
					final var castAnnotation = (T) typeAnnotation;
					return castAnnotation;
				}
			}
		}
		throw new AssertionFailure( "Could not find implementing interface" );
	}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See the analogous change in this PR:

#11477

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() );
Expand All @@ -1127,21 +1164,41 @@ private UserType<?> getConfiguredUserTypeBean(
}

private <T extends UserType<?>> T instantiateUserType(
Class<T> customType, Properties properties, Annotation typeAnnotation) {
if ( typeAnnotation != null ) {
// attempt to instantiate it with the annotation as a constructor argument
Class<T> 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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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<?,?> ) ) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
else if ( parameters != null && !parameters.isEmpty() && !( type instanceof AnnotationBasedUserType<?,?> ) ) {
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" );
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}
Expand All @@ -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() );
Expand All @@ -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 );
Expand Down
Original file line number Diff line number Diff line change
@@ -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 <A> The user type annotation type supported by an implementation
* @param <J> The java type
*
* @author Yanming Zhou
*
* @since 7.3
*/
@Incubating
public interface AnnotationBasedUserType<A extends Annotation, J> extends UserType<J> {
/**
* 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);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -214,8 +215,9 @@
* }
* </pre>
* <p>
* 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}.
* <p>
* A custom type implemented as a {@code UserType} is treated as a
* non-composite value, and does not have persistent attributes which
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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();

}
Loading