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 ;
14+ import java .util .List ;
1115import java .util .Locale ;
1216import java .util .Map ;
1317import java .util .Set ;
1418import java .util .StringTokenizer ;
19+ import java .util .function .BiFunction ;
1520
1621import jakarta .validation .constraints .Digits ;
1722import 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 ) {
0 commit comments