Skip to content
Merged
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 @@ -5,8 +5,11 @@
package org.hibernate.boot.beanvalidation;

import java.lang.annotation.Annotation;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.invoke.MethodHandles;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Locale;
import java.util.Map;
Expand Down Expand Up @@ -187,12 +190,23 @@ public static void applyRelationalConstraints(
final Class<?>[] groupsArray =
buildGroupsForOperation( GroupsPerOperation.Operation.DDL, settings, classLoaderAccess );
final Set<Class<?>> groups = new HashSet<>( asList( groupsArray ) );
final Map<Class<? extends Annotation>, Boolean> constraintCompositionTypeCache = new HashMap<>();

for ( PersistentClass persistentClass : persistentClasses ) {
final String className = persistentClass.getClassName();
if ( isNotEmpty( className ) ) {
final Class<?> clazz = entityClass( classLoaderAccess, className );
try {
applyDDL( "", persistentClass, clazz, factory, groups, true, dialect );
applyDDL(
"",
persistentClass,
clazz,
factory,
groups,
true,
dialect,
constraintCompositionTypeCache
);
}
catch (Exception e) {
LOG.unableToApplyConstraints( className, e );
Expand All @@ -217,7 +231,9 @@ private static void applyDDL(
ValidatorFactory factory,
Set<Class<?>> groups,
boolean activateNotNull,
Dialect dialect) {
Dialect dialect,
Map<Class<? extends Annotation>, Boolean> constraintCompositionTypeCache
) {
final BeanDescriptor descriptor = factory.getValidator().getConstraintsForClass( clazz );
//cno bean level constraints can be applied, go to the properties
for ( PropertyDescriptor propertyDesc : descriptor.getConstrainedProperties() ) {
Expand All @@ -230,7 +246,9 @@ private static void applyDDL(
propertyDesc,
groups,
activateNotNull,
dialect
false,
dialect,
constraintCompositionTypeCache
);
if ( property.isComposite() && propertyDesc.isCascaded() ) {
final Component component = (Component) property.getValue();
Expand All @@ -244,7 +262,8 @@ private static void applyDDL(
// activate not null and if the property is not null.
// Otherwise, all sub columns should be left nullable
activateNotNull && hasNotNull,
dialect
dialect,
constraintCompositionTypeCache
);
}
}
Expand All @@ -257,12 +276,18 @@ private static boolean applyConstraints(
PropertyDescriptor propertyDesc,
Set<Class<?>> groups,
boolean canApplyNotNull,
Dialect dialect) {
boolean hasNotNull = false;
boolean useOrLogicForComposedConstraint,
Dialect dialect,
Map<Class<? extends Annotation>, Boolean> constraintCompositionTypeCache) {

boolean firstItem = true;
boolean composedResultHasNotNull = false;
for ( ConstraintDescriptor<?> descriptor : constraintDescriptors ) {
boolean hasNotNull = false;

if ( groups == null || !disjoint( descriptor.getGroups(), groups ) ) {
if ( canApplyNotNull ) {
hasNotNull = hasNotNull || applyNotNull( property, descriptor );
hasNotNull = isNotNullDescriptor( descriptor );
}

// apply bean validation specific constraints
Expand All @@ -276,19 +301,70 @@ private static boolean applyConstraints(
// 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
);
// Composing constraints
if ( !descriptor.getComposingConstraints().isEmpty() ) {
// 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,
isConstraintCompositionOfTypeOr( descriptor, constraintCompositionTypeCache ),
dialect,
constraintCompositionTypeCache
);
hasNotNull |= hasNotNullFromComposingConstraints;
}
}

hasNotNull = hasNotNull || hasNotNullFromComposingConstraints;
if ( firstItem ) {
composedResultHasNotNull = hasNotNull;
firstItem = false;
}
else if ( !useOrLogicForComposedConstraint ) {
// If the constraint composition is of type AND (default) then only ONE constraint needs to
// be non-nullable for the property to be marked as 'not-null'.
composedResultHasNotNull |= hasNotNull;
}
else {
// If the constraint composition is of type OR then ALL constraints need to
// be non-nullable for the property to be marked as 'not-null'.
composedResultHasNotNull &= hasNotNull;
}
}

if ( composedResultHasNotNull ) {
markNotNull( property );
}
return hasNotNull;

return composedResultHasNotNull;
}

private static boolean isConstraintCompositionOfTypeOr(
ConstraintDescriptor<?> descriptor,
Map<Class<? extends Annotation>, Boolean> constraintCompositionTypeCache
) {
if ( descriptor.getComposingConstraints().size() < 2 ) {
return false;
}

final Class<? extends Annotation> composedAnnotation = descriptor.getAnnotation().annotationType();
return constraintCompositionTypeCache.computeIfAbsent( composedAnnotation, value -> {
for ( Annotation annotation : value.getAnnotations() ) {
if ( "org.hibernate.validator.constraints.ConstraintComposition"
.equals( annotation.annotationType().getName() ) ) {
try {
Method valueMethod = annotation.annotationType().getMethod( "value" );
Object result = valueMethod.invoke( annotation );
return result != null && "OR".equals( result.toString() );
}
catch ( NoSuchMethodException | IllegalAccessException | InvocationTargetException ex ) {
LOG.debug( "ConstraintComposition type could not be determined. Assuming AND", ex );
return false;
}
}
}
return false;
});
}

private static void applyMin(Property property, ConstraintDescriptor<?> descriptor, Dialect dialect) {
Expand Down Expand Up @@ -328,35 +404,33 @@ private static void applySQLCheck(Column column, String checkConstraint) {
column.addCheckConstraint( new CheckConstraint( checkConstraint ) );
}

private static boolean applyNotNull(Property property, ConstraintDescriptor<?> descriptor) {
boolean hasNotNull = false;
// NotNull, NotEmpty, and NotBlank annotation add not-null on column
private static boolean isNotNullDescriptor(ConstraintDescriptor<?> descriptor) {
final Class<? extends Annotation> annotationType = descriptor.getAnnotation().annotationType();
if ( NotNull.class.equals(annotationType)
return NotNull.class.equals(annotationType)
|| NotEmpty.class.equals(annotationType)
|| 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
if ( !property.isComposite() ) {
for ( Selectable selectable : property.getSelectables() ) {
if ( selectable instanceof Column column ) {
column.setNullable( false );
}
else {
LOG.debugf(
"@NotNull was applied to attribute [%s] which is defined (at least partially) " +
"by formula(s); formula portions will be skipped",
property.getName()
);
}
|| NotBlank.class.equals(annotationType);
}

private static void markNotNull(Property property) {
// 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
if ( !property.isComposite() ) {
for ( Selectable selectable : property.getSelectables() ) {
if ( selectable instanceof Column column ) {
column.setNullable( false );
}
else {
LOG.debugf(
"@NotNull was applied to attribute [%s] which is defined (at least partially) " +
"by formula(s); formula portions will be skipped",
property.getName()
);
}
}
}
hasNotNull = true;
}
property.setOptional( !hasNotNull );
return hasNotNull;
property.setOptional( false );
}

private static void applyDigits(Property property, ConstraintDescriptor<?> descriptor) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,13 @@ public class Address {

private String line1;
private String line2;
private String line3;
private String line4;
private String line5;
private String line6;
private String line7;
private String line8;
private String line9;
private String zip;
private String state;
@Size(max = 20)
Expand Down Expand Up @@ -52,6 +59,69 @@ public void setLine2(String line2) {
this.line2 = line2;
}

@CustomNullOrNotBlank
public String getLine3() {
return line3;
}

public void setLine3(String line3) {
this.line3 = line3;
}

@CustomNotNullOrNotBlank
public String getLine4() {
return line4;
}

public void setLine4(String line4) {
this.line4 = line4;
}

@CustomNullAndNotBlank
public String getLine5() {
return line5;
}

public void setLine5(String line5) {
this.line5 = line5;
}

@CustomNotNullAndNotBlank
public String getLine6() {
return line6;
}

public void setLine6(String line6) {
this.line6 = line6;
}

@CustomNullOrPattern
public String getLine7() {
return line7;
}

public void setLine7(String line7) {
this.line7 = line7;
}

@CustomNotNull
public String getLine8() {
return line8;
}

public void setLine8(String line8) {
this.line8 = line8;
}

@CustomNull
public String getLine9() {
return line9;
}

public void setLine9(String line9) {
this.line9 = line9;
}

@Size(max = 3)
@NotNull
public String getState() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*
* SPDX-License-Identifier: LGPL-2.1-or-later
* Copyright Red Hat Inc. and Hibernate Authors
*/
package org.hibernate.orm.test.annotations.beanvalidation;

import jakarta.validation.Constraint;
import jakarta.validation.Payload;
import jakarta.validation.constraints.NotNull;

import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
import static java.lang.annotation.ElementType.CONSTRUCTOR;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.ElementType.PARAMETER;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

/**
* Used to test constraint composition.
*/
@NotNull
@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER})
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = {})
public @interface CustomNotNull {

String message() default "{org.hibernate.validator.constraints.CustomNotNull.message}";

Class<?>[] groups() default {};

Class<? extends Payload>[] payload() default {};

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/*
* SPDX-License-Identifier: LGPL-2.1-or-later
* Copyright Red Hat Inc. and Hibernate Authors
*/
package org.hibernate.orm.test.annotations.beanvalidation;

import jakarta.validation.Constraint;
import jakarta.validation.Payload;
import jakarta.validation.ReportAsSingleViolation;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import org.hibernate.validator.constraints.ConstraintComposition;

import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
import static java.lang.annotation.ElementType.CONSTRUCTOR;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.ElementType.PARAMETER;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import static org.hibernate.validator.constraints.CompositionType.AND;

/**
* Used to test constraint composition with AND for nullability of columns.
*/
@ConstraintComposition(AND)
@NotNull
@NotBlank
@ReportAsSingleViolation
@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER})
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = {})
public @interface CustomNotNullAndNotBlank {

String message() default "{org.hibernate.validator.constraints.CustomNotNullAndNotBlank.message}";

Class<?>[] groups() default {};

Class<? extends Payload>[] payload() default {};

}
Loading