minor cleanups in TypeSafeActivator

This commit is contained in:
Gavin King 2024-10-12 14:21:43 +02:00
parent 968a66186d
commit 7afb5a0c50
1 changed files with 198 additions and 188 deletions

View File

@ -6,16 +6,20 @@ package org.hibernate.boot.beanvalidation;
import java.lang.annotation.Annotation;
import java.lang.invoke.MethodHandles;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.StringTokenizer;
import jakarta.validation.constraints.*;
import jakarta.validation.constraints.Digits;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import org.hibernate.AssertionFailure;
import org.hibernate.MappingException;
import org.hibernate.boot.internal.ClassLoaderAccessImpl;
@ -47,12 +51,19 @@ import jakarta.validation.metadata.BeanDescriptor;
import jakarta.validation.metadata.ConstraintDescriptor;
import jakarta.validation.metadata.PropertyDescriptor;
import static java.util.Arrays.asList;
import static java.util.Collections.disjoint;
import static org.hibernate.boot.beanvalidation.BeanValidationIntegrator.APPLY_CONSTRAINTS;
import static org.hibernate.boot.beanvalidation.GroupsPerOperation.buildGroupsForOperation;
import static org.hibernate.cfg.ValidationSettings.CHECK_NULLABILITY;
import static org.hibernate.cfg.ValidationSettings.JAKARTA_VALIDATION_FACTORY;
import static org.hibernate.cfg.ValidationSettings.JPA_VALIDATION_FACTORY;
import static org.hibernate.internal.util.StringHelper.isEmpty;
import static org.hibernate.internal.util.StringHelper.isNotEmpty;
/**
* Sets up Bean Validation {@linkplain BeanValidationEventListener event listener} and DDL-based constraints.
*
* @author Emmanuel Bernard
* @author Hardy Ferentschik
* @author Steve Ebersole
@ -69,87 +80,102 @@ class TypeSafeActivator {
@SuppressWarnings("unused")
public static void validateSuppliedFactory(Object object) {
if ( !(object instanceof ValidatorFactory) ) {
throw new IntegrationException(
"Given object was not an instance of " + ValidatorFactory.class.getName()
+ "[" + object.getClass().getName() + "]"
);
throw new IntegrationException( "Given object was not an instance of " + ValidatorFactory.class.getName()
+ " [" + object.getClass().getName() + "]" );
}
}
@SuppressWarnings("unused")
public static void activate(ActivationContext activationContext) {
public static void activate(ActivationContext context) {
final ValidatorFactory factory;
try {
factory = getValidatorFactory( activationContext );
factory = getValidatorFactory( context );
}
catch (IntegrationException e) {
if ( activationContext.getValidationModes().contains( ValidationMode.CALLBACK ) ) {
final Set<ValidationMode> validationModes = context.getValidationModes();
if ( validationModes.contains( ValidationMode.CALLBACK ) ) {
throw new IntegrationException( "Bean Validation provider was not available, but 'callback' validation was requested", e );
}
if ( activationContext.getValidationModes().contains( ValidationMode.DDL ) ) {
else if ( validationModes.contains( ValidationMode.DDL ) ) {
throw new IntegrationException( "Bean Validation provider was not available, but 'ddl' validation was requested", e );
}
LOG.debug( "Unable to acquire Bean Validation ValidatorFactory, skipping activation" );
return;
else {
LOG.debug( "Unable to acquire Bean Validation ValidatorFactory, skipping activation" );
return;
}
}
applyRelationalConstraints( factory, activationContext );
applyCallbackListeners( factory, activationContext );
applyRelationalConstraints( factory, context );
applyCallbackListeners( factory, context );
}
public static void applyCallbackListeners(ValidatorFactory validatorFactory, ActivationContext activationContext) {
final Set<ValidationMode> modes = activationContext.getValidationModes();
if ( !modes.contains( ValidationMode.CALLBACK ) && !modes.contains( ValidationMode.AUTO ) ) {
return;
public static void applyCallbackListeners(ValidatorFactory validatorFactory, ActivationContext context) {
if ( isValidationEnabled( context ) ) {
disableNullabilityChecking( context );
setupListener( validatorFactory, context.getServiceRegistry() );
}
}
final SessionFactoryServiceRegistry serviceRegistry = activationContext.getServiceRegistry();
final ConfigurationService cfgService = serviceRegistry.requireService( ConfigurationService.class );
private static boolean isValidationEnabled(ActivationContext context) {
final Set<ValidationMode> modes = context.getValidationModes();
return modes.contains( ValidationMode.CALLBACK )
|| modes.contains( ValidationMode.AUTO );
}
/**
* Deactivate not-null tracking at the core level when Bean Validation
* is present, unless the user explicitly asks for it.
*/
private static void disableNullabilityChecking(ActivationContext context) {
if ( isCheckNullabilityExplicit( context ) ) {
// no explicit setting, so disable null checking
context.getSessionFactory().getSessionFactoryOptions().setCheckNullability( false );
}
}
private static boolean isCheckNullabilityExplicit(ActivationContext context) {
return context.getServiceRegistry().requireService( ConfigurationService.class )
.getSettings().get( CHECK_NULLABILITY ) == null;
}
private static void setupListener(ValidatorFactory validatorFactory, SessionFactoryServiceRegistry serviceRegistry) {
final ClassLoaderService classLoaderService = serviceRegistry.requireService( ClassLoaderService.class );
final EventListenerRegistry listenerRegistry = serviceRegistry.requireService( EventListenerRegistry.class );
// de-activate not-null tracking at the core level when Bean Validation is present unless the user explicitly
// asks for it
if ( cfgService.getSettings().get( CHECK_NULLABILITY ) == null ) {
activationContext.getSessionFactory().getSessionFactoryOptions().setCheckNullability( false );
}
final ConfigurationService cfgService = serviceRegistry.requireService( ConfigurationService.class );
final BeanValidationEventListener listener =
new BeanValidationEventListener( validatorFactory, cfgService.getSettings(), classLoaderService );
final EventListenerRegistry listenerRegistry = serviceRegistry.requireService( EventListenerRegistry.class );
listenerRegistry.addDuplicationStrategy( DuplicationStrategyImpl.INSTANCE );
listenerRegistry.appendListeners( EventType.PRE_INSERT, listener );
listenerRegistry.appendListeners( EventType.PRE_UPDATE, listener );
listenerRegistry.appendListeners( EventType.PRE_DELETE, listener );
listenerRegistry.appendListeners( EventType.PRE_UPSERT, listener );
listener.initialize( cfgService.getSettings(), classLoaderService );
}
private static void applyRelationalConstraints(ValidatorFactory factory, ActivationContext activationContext) {
final SessionFactoryServiceRegistry serviceRegistry = activationContext.getServiceRegistry();
final ConfigurationService cfgService = serviceRegistry.requireService( ConfigurationService.class );
if ( !cfgService.getSetting( BeanValidationIntegrator.APPLY_CONSTRAINTS, StandardConverters.BOOLEAN, true ) ) {
private static boolean isConstraintBasedValidationEnabled(ActivationContext context) {
if ( context.getServiceRegistry().requireService( ConfigurationService.class )
.getSetting( APPLY_CONSTRAINTS, StandardConverters.BOOLEAN, true ) ) {
final Set<ValidationMode> modes = context.getValidationModes();
return modes.contains( ValidationMode.DDL )
|| modes.contains( ValidationMode.AUTO );
}
else {
LOG.debug( "Skipping application of relational constraints from legacy Hibernate Validator" );
return;
return false;
}
}
final Set<ValidationMode> modes = activationContext.getValidationModes();
if ( !modes.contains( ValidationMode.DDL ) && !modes.contains( ValidationMode.AUTO ) ) {
return;
private static void applyRelationalConstraints(ValidatorFactory factory, ActivationContext context) {
if ( isConstraintBasedValidationEnabled( context ) ) {
final SessionFactoryServiceRegistry serviceRegistry = context.getServiceRegistry();
applyRelationalConstraints(
factory,
context.getMetadata().getEntityBindings(),
serviceRegistry.requireService( ConfigurationService.class ).getSettings(),
serviceRegistry.requireService( JdbcServices.class ).getDialect(),
new ClassLoaderAccessImpl( null, serviceRegistry.getService( ClassLoaderService.class ) )
);
}
applyRelationalConstraints(
factory,
activationContext.getMetadata().getEntityBindings(),
cfgService.getSettings(),
serviceRegistry.requireService( JdbcServices.class ).getDialect(),
new ClassLoaderAccessImpl( null,
serviceRegistry.getService( ClassLoaderService.class ) )
);
}
public static void applyRelationalConstraints(
@ -160,27 +186,27 @@ class TypeSafeActivator {
ClassLoaderAccess classLoaderAccess) {
final Class<?>[] groupsArray =
buildGroupsForOperation( GroupsPerOperation.Operation.DDL, settings, classLoaderAccess );
final Set<Class<?>> groups = new HashSet<>( Arrays.asList( groupsArray ) );
final Set<Class<?>> groups = new HashSet<>( asList( groupsArray ) );
for ( PersistentClass persistentClass : persistentClasses ) {
final String className = persistentClass.getClassName();
if ( className == null || className.isEmpty() ) {
continue;
}
Class<?> clazz;
try {
clazz = classLoaderAccess.classForName( className );
}
catch ( ClassLoadingException e ) {
throw new AssertionFailure( "Entity class not found", e );
if ( isNotEmpty( className ) ) {
final Class<?> clazz = entityClass( classLoaderAccess, className );
try {
applyDDL( "", persistentClass, clazz, factory, groups, true, dialect );
}
catch (Exception e) {
LOG.unableToApplyConstraints( className, e );
}
}
}
}
try {
applyDDL( "", persistentClass, clazz, factory, groups, true, dialect );
}
catch (Exception e) {
LOG.unableToApplyConstraints( className, e );
}
private static Class<?> entityClass(ClassLoaderAccess classLoaderAccess, String className) {
try {
return classLoaderAccess.classForName( className );
}
catch (ClassLoadingException e) {
throw new AssertionFailure( "Entity class not found", e );
}
}
@ -193,8 +219,7 @@ class TypeSafeActivator {
boolean activateNotNull,
Dialect dialect) {
final BeanDescriptor descriptor = factory.getValidator().getConstraintsForClass( clazz );
//no bean level constraints can be applied, go to the properties
//cno bean level constraints can be applied, go to the properties
for ( PropertyDescriptor propertyDesc : descriptor.getConstrainedProperties() ) {
final Property property =
findPropertyByName( persistentClass, prefix + propertyDesc.getPropertyName() );
@ -209,19 +234,16 @@ class TypeSafeActivator {
);
if ( property.isComposite() && propertyDesc.isCascaded() ) {
final Component component = (Component) property.getValue();
/*
* we can apply not null if the upper component lets us activate not null
* and if the property is not null.
* Otherwise, all sub columns should be left nullable
*/
final boolean canSetNotNullOnColumns = activateNotNull && hasNotNull;
applyDDL(
prefix + propertyDesc.getPropertyName() + ".",
persistentClass,
component.getComponentClass(),
factory,
groups,
canSetNotNullOnColumns,
// we can apply not null if the upper component lets us
// activate not null and if the property is not null.
// Otherwise, all sub columns should be left nullable
activateNotNull && hasNotNull,
dialect
);
}
@ -238,34 +260,33 @@ class TypeSafeActivator {
Dialect dialect) {
boolean hasNotNull = false;
for ( ConstraintDescriptor<?> descriptor : constraintDescriptors ) {
if ( groups != null && Collections.disjoint( descriptor.getGroups(), groups ) ) {
continue;
if ( groups == null || !disjoint( descriptor.getGroups(), groups ) ) {
if ( canApplyNotNull ) {
hasNotNull = hasNotNull || applyNotNull( property, descriptor );
}
// apply bean validation specific constraints
applyDigits( property, descriptor );
applySize( property, descriptor, propertyDesc );
applyMin( property, descriptor, dialect );
applyMax( property, descriptor, dialect );
// Apply Hibernate Validator specific constraints - we cannot import any HV specific classes though!
// No need to check explicitly for @Range. @Range is a composed constraint using @Min and @Max which
// will be taken care later.
applyLength( property, descriptor, propertyDesc );
// pass an empty set as composing constraints inherit the main constraint and thus are matching already
final boolean hasNotNullFromComposingConstraints = applyConstraints(
descriptor.getComposingConstraints(),
property, propertyDesc, null,
canApplyNotNull,
dialect
);
hasNotNull = hasNotNull || hasNotNullFromComposingConstraints;
}
if ( canApplyNotNull ) {
hasNotNull = hasNotNull || applyNotNull( property, descriptor );
}
// apply bean validation specific constraints
applyDigits( property, descriptor );
applySize( property, descriptor, propertyDesc );
applyMin( property, descriptor, dialect );
applyMax( property, descriptor, dialect );
// apply hibernate validator specific constraints - we cannot import any HV specific classes though!
// no need to check explicitly for @Range. @Range is a composed constraint using @Min and @Max which
// will be taken care later
applyLength( property, descriptor, propertyDesc );
// pass an empty set as composing constraints inherit the main constraint and thus are matching already
boolean hasNotNullFromComposingConstraints = applyConstraints(
descriptor.getComposingConstraints(),
property, propertyDesc, null,
canApplyNotNull,
dialect
);
hasNotNull = hasNotNull || hasNotNullFromComposingConstraints;
}
return hasNotNull;
}
@ -275,11 +296,9 @@ class TypeSafeActivator {
@SuppressWarnings("unchecked")
final ConstraintDescriptor<Min> minConstraint = (ConstraintDescriptor<Min>) descriptor;
final long min = minConstraint.getAnnotation().value();
for ( Selectable selectable : property.getSelectables() ) {
if ( selectable instanceof Column column ) {
final String checkConstraint = column.getQuotedName( dialect ) + ">=" + min;
applySQLCheck( column, checkConstraint );
applySQLCheck( column, column.getQuotedName( dialect ) + ">=" + min );
}
}
}
@ -290,25 +309,23 @@ class TypeSafeActivator {
@SuppressWarnings("unchecked")
final ConstraintDescriptor<Max> maxConstraint = (ConstraintDescriptor<Max>) descriptor;
final long max = maxConstraint.getAnnotation().value();
for ( Selectable selectable : property.getSelectables() ) {
if ( selectable instanceof Column column ) {
final String checkConstraint = column.getQuotedName( dialect ) + "<=" + max;
applySQLCheck( column, checkConstraint );
applySQLCheck( column, column.getQuotedName( dialect ) + "<=" + max );
}
}
}
}
private static void applySQLCheck(Column col, String checkConstraint) {
private static void applySQLCheck(Column column, String checkConstraint) {
// need to check whether the new check is already part of the existing check,
// because applyDDL can be called multiple times
for ( CheckConstraint constraint : col.getCheckConstraints() ) {
for ( CheckConstraint constraint : column.getCheckConstraints() ) {
if ( constraint.getConstraint().equalsIgnoreCase( checkConstraint ) ) {
return; //EARLY EXIT
}
}
col.addCheckConstraint( new CheckConstraint( checkConstraint ) );
column.addCheckConstraint( new CheckConstraint( checkConstraint ) );
}
private static boolean applyNotNull(Property property, ConstraintDescriptor<?> descriptor) {
@ -320,11 +337,11 @@ class TypeSafeActivator {
|| NotBlank.class.equals(annotationType)) {
// single table inheritance should not be forced to null due to shared state
if ( !( property.getPersistentClass() instanceof SingleTableSubclass ) ) {
//composite should not add not-null on all columns
// composite should not add not-null on all columns
if ( !property.isComposite() ) {
for ( Selectable selectable : property.getSelectables() ) {
if ( selectable instanceof Column ) {
((Column) selectable).setNullable( false );
if ( selectable instanceof Column column ) {
column.setNullable( false );
}
else {
LOG.debugf(
@ -345,10 +362,9 @@ class TypeSafeActivator {
private static void applyDigits(Property property, ConstraintDescriptor<?> descriptor) {
if ( Digits.class.equals( descriptor.getAnnotation().annotationType() ) ) {
@SuppressWarnings("unchecked")
ConstraintDescriptor<Digits> digitsConstraint = (ConstraintDescriptor<Digits>) descriptor;
int integerDigits = digitsConstraint.getAnnotation().integer();
int fractionalDigits = digitsConstraint.getAnnotation().fraction();
final ConstraintDescriptor<Digits> digitsConstraint = (ConstraintDescriptor<Digits>) descriptor;
final int integerDigits = digitsConstraint.getAnnotation().integer();
final int fractionalDigits = digitsConstraint.getAnnotation().fraction();
for ( Selectable selectable : property.getSelectables() ) {
if ( selectable instanceof Column column ) {
column.setPrecision( integerDigits + fractionalDigits );
@ -363,9 +379,8 @@ class TypeSafeActivator {
if ( Size.class.equals( descriptor.getAnnotation().annotationType() )
&& String.class.equals( propertyDescriptor.getElementClass() ) ) {
@SuppressWarnings("unchecked")
ConstraintDescriptor<Size> sizeConstraint = (ConstraintDescriptor<Size>) descriptor;
int max = sizeConstraint.getAnnotation().max();
final ConstraintDescriptor<Size> sizeConstraint = (ConstraintDescriptor<Size>) descriptor;
final int max = sizeConstraint.getAnnotation().max();
for ( Column col : property.getColumns() ) {
if ( max < Integer.MAX_VALUE ) {
col.setLength( max );
@ -375,12 +390,9 @@ class TypeSafeActivator {
}
private static void applyLength(Property property, ConstraintDescriptor<?> descriptor, PropertyDescriptor propertyDescriptor) {
if ( "org.hibernate.validator.constraints.Length".equals(
descriptor.getAnnotation().annotationType().getName()
)
if ( isValidatorLengthAnnotation( descriptor )
&& String.class.equals( propertyDescriptor.getElementClass() ) ) {
int max = (Integer) descriptor.getAttributes().get( "max" );
final int max = (Integer) descriptor.getAttributes().get( "max" );
for ( Selectable selectable : property.getSelectables() ) {
if ( selectable instanceof Column column ) {
if ( max < Integer.MAX_VALUE ) {
@ -391,18 +403,21 @@ class TypeSafeActivator {
}
}
private static boolean isValidatorLengthAnnotation(ConstraintDescriptor<?> descriptor) {
return "org.hibernate.validator.constraints.Length"
.equals( descriptor.getAnnotation().annotationType().getName() );
}
/**
* Locate the property by path in a recursive way, including IdentifierProperty in the loop if propertyName is
* {@code null}. If propertyName is {@code null} or empty, the IdentifierProperty is returned
*/
private static Property findPropertyByName(PersistentClass associatedClass, String propertyName) {
Property property = null;
Property idProperty = associatedClass.getIdentifierProperty();
String idName = idProperty != null ? idProperty.getName() : null;
final Property idProperty = associatedClass.getIdentifierProperty();
final String idName = idProperty != null ? idProperty.getName() : null;
try {
if ( propertyName == null
|| propertyName.isEmpty()
|| propertyName.equals( idName ) ) {
if ( isEmpty( propertyName ) || propertyName.equals( idName ) ) {
//default to id
property = idProperty;
}
@ -411,9 +426,9 @@ class TypeSafeActivator {
property = idProperty;
propertyName = propertyName.substring( idName.length() + 1 );
}
StringTokenizer st = new StringTokenizer( propertyName, ".", false );
while ( st.hasMoreElements() ) {
String element = (String) st.nextElement();
final StringTokenizer tokens = new StringTokenizer( propertyName, ".", false );
while ( tokens.hasMoreTokens() ) {
final String element = tokens.nextToken();
if ( property == null ) {
property = associatedClass.getProperty( element );
}
@ -421,7 +436,9 @@ class TypeSafeActivator {
if ( !property.isComposite() ) {
return null;
}
property = ( (Component) property.getValue() ).getProperty( element );
else {
property = ( (Component) property.getValue() ).getProperty( element );
}
}
}
}
@ -432,17 +449,21 @@ class TypeSafeActivator {
if ( associatedClass.getIdentifierMapper() == null ) {
return null;
}
StringTokenizer st = new StringTokenizer( propertyName, ".", false );
while ( st.hasMoreElements() ) {
String element = (String) st.nextElement();
if ( property == null ) {
property = associatedClass.getIdentifierMapper().getProperty( element );
}
else {
if ( !property.isComposite() ) {
return null;
else {
final StringTokenizer tokens = new StringTokenizer( propertyName, ".", false );
while ( tokens.hasMoreTokens() ) {
final String element = tokens.nextToken();
if ( property == null ) {
property = associatedClass.getIdentifierMapper().getProperty( element );
}
else {
if ( !property.isComposite() ) {
return null;
}
else {
property = ( (Component) property.getValue() ).getProperty( element );
}
}
property = ( (Component) property.getValue() ).getProperty( element );
}
}
}
@ -453,7 +474,7 @@ class TypeSafeActivator {
return property;
}
private static ValidatorFactory getValidatorFactory(ActivationContext activationContext) {
private static ValidatorFactory getValidatorFactory(ActivationContext context) {
// IMPL NOTE : We can either be provided a ValidatorFactory or make one. We can be provided
// a ValidatorFactory in 2 different ways. So here we "get" a ValidatorFactory in the following order:
// 1) Look into SessionFactoryOptions.getValidatorFactoryReference()
@ -461,15 +482,17 @@ class TypeSafeActivator {
// 3) build a new ValidatorFactory
// 1 - look in SessionFactoryOptions.getValidatorFactoryReference()
ValidatorFactory factory = resolveProvidedFactory( activationContext.getSessionFactory().getSessionFactoryOptions() );
if ( factory != null ) {
return factory;
final ValidatorFactory providedFactory =
resolveProvidedFactory( context.getSessionFactory().getSessionFactoryOptions() );
if ( providedFactory != null ) {
return providedFactory;
}
// 2 - look in ConfigurationService
factory = resolveProvidedFactory( activationContext.getServiceRegistry().requireService( ConfigurationService.class ) );
if ( factory != null ) {
return factory;
final ValidatorFactory configuredFactory =
resolveProvidedFactory( context.getServiceRegistry().requireService( ConfigurationService.class ) );
if ( configuredFactory != null ) {
return configuredFactory;
}
// 3 - build our own
@ -486,10 +509,10 @@ class TypeSafeActivator {
if ( validatorFactoryReference == null ) {
return null;
}
try {
return (ValidatorFactory) validatorFactoryReference;
else if ( validatorFactoryReference instanceof ValidatorFactory result ) {
return result;
}
catch ( ClassCastException e ) {
else {
throw new IntegrationException(
String.format(
Locale.ENGLISH,
@ -505,42 +528,29 @@ class TypeSafeActivator {
private static ValidatorFactory resolveProvidedFactory(ConfigurationService cfgService) {
return cfgService.getSetting(
JPA_VALIDATION_FACTORY,
value -> {
try {
return (ValidatorFactory) value;
}
catch ( ClassCastException e ) {
throw new IntegrationException(
String.format(
Locale.ENGLISH,
"ValidatorFactory reference (provided via `%s` setting) was not castable to %s : %s",
JPA_VALIDATION_FACTORY,
ValidatorFactory.class.getName(),
value.getClass().getName()
)
);
}
},
value -> validatorFactory( value, JPA_VALIDATION_FACTORY ),
cfgService.getSetting(
JAKARTA_VALIDATION_FACTORY,
value -> {
try {
return (ValidatorFactory) value;
}
catch ( ClassCastException e ) {
throw new IntegrationException(
String.format(
Locale.ENGLISH,
"ValidatorFactory reference (provided via `%s` setting) was not castable to %s : %s",
JAKARTA_VALIDATION_FACTORY,
ValidatorFactory.class.getName(),
value.getClass().getName()
)
);
}
},
value -> validatorFactory( value, JAKARTA_VALIDATION_FACTORY ),
null
)
);
}
private static ValidatorFactory validatorFactory(Object value, String setting) {
if ( value instanceof ValidatorFactory validatorFactory ) {
return validatorFactory;
}
else {
throw new IntegrationException(
String.format(
Locale.ENGLISH,
"ValidatorFactory reference (provided via '%s' setting) was not an instance of '%s': %s",
setting,
ValidatorFactory.class.getName(),
value.getClass().getName()
)
);
}
}
}