Skip to content

Commit 0e343dc

Browse files
HHH-18724 Take constraint composition type into account when determining nullable state of a Column
1 parent 6913234 commit 0e343dc

File tree

5 files changed

+180
-28
lines changed

5 files changed

+180
-28
lines changed

hibernate-core/src/main/java/org/hibernate/boot/beanvalidation/TypeSafeActivator.java

Lines changed: 68 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,18 @@
55
package org.hibernate.boot.beanvalidation;
66

77
import java.lang.annotation.Annotation;
8+
import java.lang.reflect.InvocationTargetException;
9+
import java.lang.reflect.Method;
810
import java.lang.invoke.MethodHandles;
911
import java.util.Collection;
12+
import java.util.HashMap;
1013
import java.util.HashSet;
14+
import java.util.List;
1115
import java.util.Locale;
1216
import java.util.Map;
1317
import java.util.Set;
1418
import java.util.StringTokenizer;
19+
import java.util.function.BiFunction;
1520

1621
import jakarta.validation.constraints.Digits;
1722
import jakarta.validation.constraints.Max;
@@ -72,6 +77,9 @@ class TypeSafeActivator {
7277

7378
private static final CoreMessageLogger LOG = Logger.getMessageLogger( MethodHandles.lookup(), CoreMessageLogger.class, TypeSafeActivator.class.getName() );
7479

80+
private static final Map<Class<? extends Annotation>, Boolean>
81+
CONSTRAINT_COMPOSITION_TYPE_CACHE = new HashMap<>();
82+
7583
/**
7684
* Used to validate a supplied ValidatorFactory instance as being castable to ValidatorFactory.
7785
*
@@ -230,6 +238,7 @@ private static void applyDDL(
230238
propertyDesc,
231239
groups,
232240
activateNotNull,
241+
false,
233242
dialect
234243
);
235244
if ( property.isComposite() && propertyDesc.isCascaded() ) {
@@ -257,12 +266,20 @@ private static boolean applyConstraints(
257266
PropertyDescriptor propertyDesc,
258267
Set<Class<?>> groups,
259268
boolean canApplyNotNull,
269+
boolean useOrLogicForComposedConstraint,
260270
Dialect dialect) {
261271
boolean hasNotNull = false;
262272
for ( ConstraintDescriptor<?> descriptor : constraintDescriptors ) {
263273
if ( groups == null || !disjoint( descriptor.getGroups(), groups ) ) {
274+
// If the composition logic is of type OR, then all the nested constraints need to be not-null in order
275+
// for the property to be marked not-null.
276+
// If the composition logic is of type AND (default), then only one needs to be not-null.
277+
BiFunction<Boolean, Boolean, Boolean> compositionLogic = useOrLogicForComposedConstraint ?
278+
Boolean::logicalAnd :
279+
Boolean::logicalOr;
280+
264281
if ( canApplyNotNull ) {
265-
hasNotNull = hasNotNull || applyNotNull( property, descriptor );
282+
hasNotNull = compositionLogic.apply( hasNotNull, isNotNullDescriptor( descriptor ) );
266283
}
267284

268285
// apply bean validation specific constraints
@@ -281,16 +298,43 @@ private static boolean applyConstraints(
281298
descriptor.getComposingConstraints(),
282299
property, propertyDesc, null,
283300
canApplyNotNull,
301+
isConstraintCompositionOfTypeOr( descriptor ),
284302
dialect
285303
);
286304

287-
hasNotNull = hasNotNull || hasNotNullFromComposingConstraints;
305+
hasNotNull = compositionLogic.apply( hasNotNull, hasNotNullFromComposingConstraints );
288306
}
289-
307+
}
308+
if ( hasNotNull ) {
309+
markNotNull( property );
290310
}
291311
return hasNotNull;
292312
}
293313

314+
private static boolean isConstraintCompositionOfTypeOr(final ConstraintDescriptor<?> descriptor) {
315+
if ( descriptor.getComposingConstraints() == null ||
316+
descriptor.getComposingConstraints().size() < 2 ) {
317+
return false;
318+
}
319+
320+
return CONSTRAINT_COMPOSITION_TYPE_CACHE.computeIfAbsent( descriptor.getAnnotation().getClass(), unused -> {
321+
// This check assumes that Hibernate Validator is being used providing an
322+
// implementation that gives us the composition type that is being used.
323+
try {
324+
Method compositionTypeMethod = descriptor.getClass()
325+
.getMethod( "getCompositionType" );
326+
Object result = compositionTypeMethod.invoke( descriptor );
327+
if ( result != null && "OR".equals( result.toString() ) ) {
328+
return true;
329+
}
330+
}
331+
catch ( NoSuchMethodException | IllegalAccessException | InvocationTargetException ex ) {
332+
LOG.debug( "ConstraintComposition type could not be determined. Assuming AND", ex );
333+
}
334+
return false;
335+
});
336+
}
337+
294338
private static void applyMin(Property property, ConstraintDescriptor<?> descriptor, Dialect dialect) {
295339
if ( Min.class.equals( descriptor.getAnnotation().annotationType() ) ) {
296340
@SuppressWarnings("unchecked")
@@ -328,35 +372,31 @@ private static void applySQLCheck(Column column, String checkConstraint) {
328372
column.addCheckConstraint( new CheckConstraint( checkConstraint ) );
329373
}
330374

331-
private static boolean applyNotNull(Property property, ConstraintDescriptor<?> descriptor) {
332-
boolean hasNotNull = false;
333-
// NotNull, NotEmpty, and NotBlank annotation add not-null on column
334-
final Class<? extends Annotation> annotationType = descriptor.getAnnotation().annotationType();
335-
if ( NotNull.class.equals(annotationType)
336-
|| NotEmpty.class.equals(annotationType)
337-
|| NotBlank.class.equals(annotationType)) {
338-
// single table inheritance should not be forced to null due to shared state
339-
if ( !( property.getPersistentClass() instanceof SingleTableSubclass ) ) {
340-
// composite should not add not-null on all columns
341-
if ( !property.isComposite() ) {
342-
for ( Selectable selectable : property.getSelectables() ) {
343-
if ( selectable instanceof Column column ) {
344-
column.setNullable( false );
345-
}
346-
else {
347-
LOG.debugf(
348-
"@NotNull was applied to attribute [%s] which is defined (at least partially) " +
349-
"by formula(s); formula portions will be skipped",
350-
property.getName()
351-
);
352-
}
375+
private static boolean isNotNullDescriptor(ConstraintDescriptor<?> descriptor) {
376+
return List.of( NotNull.class, NotEmpty.class, NotBlank.class )
377+
.contains( descriptor.getAnnotation().annotationType() );
378+
}
379+
380+
private static void markNotNull(Property property) {
381+
// single table inheritance should not be forced to null due to shared state
382+
if ( !( property.getPersistentClass() instanceof SingleTableSubclass ) ) {
383+
//composite should not add not-null on all columns
384+
if ( !property.isComposite() ) {
385+
for ( Selectable selectable : property.getSelectables() ) {
386+
if ( selectable instanceof Column ) {
387+
((Column) selectable).setNullable( false );
388+
}
389+
else {
390+
LOG.debugf(
391+
"@NotNull was applied to attribute [%s] which is defined (at least partially) " +
392+
"by formula(s); formula portions will be skipped",
393+
property.getName()
394+
);
353395
}
354396
}
355397
}
356-
hasNotNull = true;
357398
}
358-
property.setOptional( !hasNotNull );
359-
return hasNotNull;
399+
property.setOptional( false );
360400
}
361401

362402
private static void applyDigits(Property property, ConstraintDescriptor<?> descriptor) {

hibernate-core/src/test/java/org/hibernate/orm/test/annotations/beanvalidation/Address.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ public class Address {
1515

1616
private String line1;
1717
private String line2;
18+
private String line3;
19+
private String line4;
1820
private String zip;
1921
private String state;
2022
@Size(max = 20)
@@ -52,6 +54,24 @@ public void setLine2(String line2) {
5254
this.line2 = line2;
5355
}
5456

57+
@NullOrNotBlank
58+
public String getLine3() {
59+
return line3;
60+
}
61+
62+
public void setLine3(String line3) {
63+
this.line3 = line3;
64+
}
65+
66+
@NullAndNotBlank
67+
public String getLine4() {
68+
return line4;
69+
}
70+
71+
public void setLine4(String line4) {
72+
this.line4 = line4;
73+
}
74+
5575
@Size(max = 3)
5676
@NotNull
5777
public String getState() {

hibernate-core/src/test/java/org/hibernate/orm/test/annotations/beanvalidation/DDLTest.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,16 @@ public void testNotNullDDL() {
4646

4747
Column line2Column = classMapping.getProperty( "line2" ).getColumns().get(0);
4848
assertFalse("Validator annotations are applied on line2 as it is @NotBlank", line2Column.isNullable());
49+
50+
Column line3Column = classMapping.getProperty("line3").getColumns().get(0);
51+
assertTrue(
52+
"Validator composition of type OR should result in line3 being nullable",
53+
line3Column.isNullable());
54+
55+
Column line4Column = classMapping.getProperty("line4" ).getColumns().get(0);
56+
assertFalse(
57+
"Validator composition of type AND should result in line4 being not-null",
58+
line4Column.isNullable());
4959
}
5060

5161
@Test
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/*
2+
* SPDX-License-Identifier: LGPL-2.1-or-later
3+
* Copyright Red Hat Inc. and Hibernate Authors
4+
*/
5+
package org.hibernate.orm.test.annotations.beanvalidation;
6+
7+
import jakarta.validation.Constraint;
8+
import jakarta.validation.Payload;
9+
import jakarta.validation.ReportAsSingleViolation;
10+
import jakarta.validation.constraints.NotBlank;
11+
import jakarta.validation.constraints.Null;
12+
import org.hibernate.validator.constraints.ConstraintComposition;
13+
14+
import java.lang.annotation.Documented;
15+
import java.lang.annotation.Retention;
16+
import java.lang.annotation.Target;
17+
18+
import static java.lang.annotation.ElementType.*;
19+
import static java.lang.annotation.RetentionPolicy.RUNTIME;
20+
import static org.hibernate.validator.constraints.CompositionType.AND;
21+
22+
/**
23+
* Used to test constraint composition with AND for nullability of columns.
24+
*/
25+
@ConstraintComposition(AND)
26+
@Null
27+
@NotBlank
28+
@ReportAsSingleViolation
29+
@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER})
30+
@Retention(RUNTIME)
31+
@Documented
32+
@Constraint(validatedBy = {})
33+
public @interface NullAndNotBlank {
34+
35+
String message() default "{org.hibernate.validator.constraints.NullAndNotBlank.message}";
36+
37+
Class<?>[] groups() default {};
38+
39+
Class<? extends Payload>[] payload() default {};
40+
41+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/*
2+
* SPDX-License-Identifier: LGPL-2.1-or-later
3+
* Copyright Red Hat Inc. and Hibernate Authors
4+
*/
5+
package org.hibernate.orm.test.annotations.beanvalidation;
6+
7+
import jakarta.validation.Constraint;
8+
import jakarta.validation.Payload;
9+
import jakarta.validation.ReportAsSingleViolation;
10+
import jakarta.validation.constraints.NotBlank;
11+
import jakarta.validation.constraints.Null;
12+
import org.hibernate.validator.constraints.ConstraintComposition;
13+
14+
import java.lang.annotation.Documented;
15+
import java.lang.annotation.Retention;
16+
import java.lang.annotation.Target;
17+
18+
import static java.lang.annotation.ElementType.*;
19+
import static java.lang.annotation.RetentionPolicy.RUNTIME;
20+
import static org.hibernate.validator.constraints.CompositionType.OR;
21+
22+
/**
23+
* Used to test constraint composition with OR for nullability of columns.
24+
*/
25+
@ConstraintComposition(OR)
26+
@Null
27+
@NotBlank
28+
@ReportAsSingleViolation
29+
@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER})
30+
@Retention(RUNTIME)
31+
@Documented
32+
@Constraint(validatedBy = {})
33+
public @interface NullOrNotBlank {
34+
35+
String message() default "{org.hibernate.validator.constraints.NullOrNotBlank.message}";
36+
37+
Class<?>[] groups() default {};
38+
39+
Class<? extends Payload>[] payload() default {};
40+
41+
}

0 commit comments

Comments
 (0)