55package org .hibernate .boot .beanvalidation ;
66
77import java .lang .annotation .Annotation ;
8+ import java .lang .reflect .InvocationTargetException ;
9+ import java .lang .reflect .Method ;
810import java .lang .invoke .MethodHandles ;
911import java .util .Collection ;
12+ import java .util .HashMap ;
1013import java .util .HashSet ;
1114import java .util .Locale ;
1215import java .util .Map ;
@@ -187,12 +190,23 @@ public static void applyRelationalConstraints(
187190 final Class <?>[] groupsArray =
188191 buildGroupsForOperation ( GroupsPerOperation .Operation .DDL , settings , classLoaderAccess );
189192 final Set <Class <?>> groups = new HashSet <>( asList ( groupsArray ) );
193+ final Map <Class <? extends Annotation >, Boolean > constraintCompositionTypeCache = new HashMap <>();
194+
190195 for ( PersistentClass persistentClass : persistentClasses ) {
191196 final String className = persistentClass .getClassName ();
192197 if ( isNotEmpty ( className ) ) {
193198 final Class <?> clazz = entityClass ( classLoaderAccess , className );
194199 try {
195- applyDDL ( "" , persistentClass , clazz , factory , groups , true , dialect );
200+ applyDDL (
201+ "" ,
202+ persistentClass ,
203+ clazz ,
204+ factory ,
205+ groups ,
206+ true ,
207+ dialect ,
208+ constraintCompositionTypeCache
209+ );
196210 }
197211 catch (Exception e ) {
198212 LOG .unableToApplyConstraints ( className , e );
@@ -217,7 +231,9 @@ private static void applyDDL(
217231 ValidatorFactory factory ,
218232 Set <Class <?>> groups ,
219233 boolean activateNotNull ,
220- Dialect dialect ) {
234+ Dialect dialect ,
235+ Map <Class <? extends Annotation >, Boolean > constraintCompositionTypeCache
236+ ) {
221237 final BeanDescriptor descriptor = factory .getValidator ().getConstraintsForClass ( clazz );
222238 //cno bean level constraints can be applied, go to the properties
223239 for ( PropertyDescriptor propertyDesc : descriptor .getConstrainedProperties () ) {
@@ -230,7 +246,9 @@ private static void applyDDL(
230246 propertyDesc ,
231247 groups ,
232248 activateNotNull ,
233- dialect
249+ false ,
250+ dialect ,
251+ constraintCompositionTypeCache
234252 );
235253 if ( property .isComposite () && propertyDesc .isCascaded () ) {
236254 final Component component = (Component ) property .getValue ();
@@ -244,7 +262,8 @@ private static void applyDDL(
244262 // activate not null and if the property is not null.
245263 // Otherwise, all sub columns should be left nullable
246264 activateNotNull && hasNotNull ,
247- dialect
265+ dialect ,
266+ constraintCompositionTypeCache
248267 );
249268 }
250269 }
@@ -257,12 +276,18 @@ private static boolean applyConstraints(
257276 PropertyDescriptor propertyDesc ,
258277 Set <Class <?>> groups ,
259278 boolean canApplyNotNull ,
260- Dialect dialect ) {
261- boolean hasNotNull = false ;
279+ boolean useOrLogicForComposedConstraint ,
280+ Dialect dialect ,
281+ Map <Class <? extends Annotation >, Boolean > constraintCompositionTypeCache ) {
282+
283+ boolean firstItem = true ;
284+ boolean composedResultHasNotNull = false ;
262285 for ( ConstraintDescriptor <?> descriptor : constraintDescriptors ) {
286+ boolean hasNotNull = false ;
287+
263288 if ( groups == null || !disjoint ( descriptor .getGroups (), groups ) ) {
264289 if ( canApplyNotNull ) {
265- hasNotNull = hasNotNull || applyNotNull ( property , descriptor );
290+ hasNotNull = isNotNullDescriptor ( descriptor );
266291 }
267292
268293 // apply bean validation specific constraints
@@ -276,19 +301,70 @@ private static boolean applyConstraints(
276301 // will be taken care later.
277302 applyLength ( property , descriptor , propertyDesc );
278303
279- // pass an empty set as composing constraints inherit the main constraint and thus are matching already
280- final boolean hasNotNullFromComposingConstraints = applyConstraints (
281- descriptor .getComposingConstraints (),
282- property , propertyDesc , null ,
283- canApplyNotNull ,
284- dialect
285- );
304+ // Composing constraints
305+ if ( !descriptor .getComposingConstraints ().isEmpty () ) {
306+ // pass an empty set as composing constraints inherit the main constraint and thus are matching already
307+ final boolean hasNotNullFromComposingConstraints = applyConstraints (
308+ descriptor .getComposingConstraints (),
309+ property , propertyDesc , null ,
310+ canApplyNotNull ,
311+ isConstraintCompositionOfTypeOr ( descriptor , constraintCompositionTypeCache ),
312+ dialect ,
313+ constraintCompositionTypeCache
314+ );
315+ hasNotNull |= hasNotNullFromComposingConstraints ;
316+ }
317+ }
286318
287- hasNotNull = hasNotNull || hasNotNullFromComposingConstraints ;
319+ if ( firstItem ) {
320+ composedResultHasNotNull = hasNotNull ;
321+ firstItem = false ;
288322 }
323+ else if ( !useOrLogicForComposedConstraint ) {
324+ // If the constraint composition is of type AND (default) then only ONE constraint needs to
325+ // be non-nullable for the property to be marked as 'not-null'.
326+ composedResultHasNotNull |= hasNotNull ;
327+ }
328+ else {
329+ // If the constraint composition is of type OR then ALL constraints need to
330+ // be non-nullable for the property to be marked as 'not-null'.
331+ composedResultHasNotNull &= hasNotNull ;
332+ }
333+ }
289334
335+ if ( composedResultHasNotNull ) {
336+ markNotNull ( property );
290337 }
291- return hasNotNull ;
338+
339+ return composedResultHasNotNull ;
340+ }
341+
342+ private static boolean isConstraintCompositionOfTypeOr (
343+ ConstraintDescriptor <?> descriptor ,
344+ Map <Class <? extends Annotation >, Boolean > constraintCompositionTypeCache
345+ ) {
346+ if ( descriptor .getComposingConstraints ().size () < 2 ) {
347+ return false ;
348+ }
349+
350+ final Class <? extends Annotation > composedAnnotation = descriptor .getAnnotation ().annotationType ();
351+ return constraintCompositionTypeCache .computeIfAbsent ( composedAnnotation , value -> {
352+ for ( Annotation annotation : value .getAnnotations () ) {
353+ if ( "org.hibernate.validator.constraints.ConstraintComposition"
354+ .equals ( annotation .annotationType ().getName () ) ) {
355+ try {
356+ Method valueMethod = annotation .annotationType ().getMethod ( "value" );
357+ Object result = valueMethod .invoke ( annotation );
358+ return result != null && "OR" .equals ( result .toString () );
359+ }
360+ catch ( NoSuchMethodException | IllegalAccessException | InvocationTargetException ex ) {
361+ LOG .debug ( "ConstraintComposition type could not be determined. Assuming AND" , ex );
362+ return false ;
363+ }
364+ }
365+ }
366+ return false ;
367+ });
292368 }
293369
294370 private static void applyMin (Property property , ConstraintDescriptor <?> descriptor , Dialect dialect ) {
@@ -328,35 +404,33 @@ private static void applySQLCheck(Column column, String checkConstraint) {
328404 column .addCheckConstraint ( new CheckConstraint ( checkConstraint ) );
329405 }
330406
331- private static boolean applyNotNull (Property property , ConstraintDescriptor <?> descriptor ) {
332- boolean hasNotNull = false ;
333- // NotNull, NotEmpty, and NotBlank annotation add not-null on column
407+ private static boolean isNotNullDescriptor (ConstraintDescriptor <?> descriptor ) {
334408 final Class <? extends Annotation > annotationType = descriptor .getAnnotation ().annotationType ();
335- if ( NotNull .class .equals (annotationType )
409+ return NotNull .class .equals (annotationType )
336410 || 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- }
411+ || NotBlank .class .equals (annotationType );
412+ }
413+
414+ private static void markNotNull (Property property ) {
415+ // single table inheritance should not be forced to null due to shared state
416+ if ( !( property .getPersistentClass () instanceof SingleTableSubclass ) ) {
417+ // composite should not add not-null on all columns
418+ if ( !property .isComposite () ) {
419+ for ( Selectable selectable : property .getSelectables () ) {
420+ if ( selectable instanceof Column column ) {
421+ column .setNullable ( false );
422+ }
423+ else {
424+ LOG .debugf (
425+ "@NotNull was applied to attribute [%s] which is defined (at least partially) " +
426+ "by formula(s); formula portions will be skipped" ,
427+ property .getName ()
428+ );
353429 }
354430 }
355431 }
356- hasNotNull = true ;
357432 }
358- property .setOptional ( !hasNotNull );
359- return hasNotNull ;
433+ property .setOptional ( false );
360434 }
361435
362436 private static void applyDigits (Property property , ConstraintDescriptor <?> descriptor ) {
0 commit comments