5
5
package org .hibernate .boot .beanvalidation ;
6
6
7
7
import java .lang .annotation .Annotation ;
8
+ import java .lang .reflect .InvocationTargetException ;
9
+ import java .lang .reflect .Method ;
8
10
import java .lang .invoke .MethodHandles ;
9
11
import java .util .Collection ;
12
+ import java .util .HashMap ;
10
13
import java .util .HashSet ;
11
14
import java .util .Locale ;
12
15
import java .util .Map ;
@@ -187,12 +190,23 @@ public static void applyRelationalConstraints(
187
190
final Class <?>[] groupsArray =
188
191
buildGroupsForOperation ( GroupsPerOperation .Operation .DDL , settings , classLoaderAccess );
189
192
final Set <Class <?>> groups = new HashSet <>( asList ( groupsArray ) );
193
+ final Map <Class <? extends Annotation >, Boolean > constraintCompositionTypeCache = new HashMap <>();
194
+
190
195
for ( PersistentClass persistentClass : persistentClasses ) {
191
196
final String className = persistentClass .getClassName ();
192
197
if ( isNotEmpty ( className ) ) {
193
198
final Class <?> clazz = entityClass ( classLoaderAccess , className );
194
199
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
+ );
196
210
}
197
211
catch (Exception e ) {
198
212
LOG .unableToApplyConstraints ( className , e );
@@ -217,7 +231,9 @@ private static void applyDDL(
217
231
ValidatorFactory factory ,
218
232
Set <Class <?>> groups ,
219
233
boolean activateNotNull ,
220
- Dialect dialect ) {
234
+ Dialect dialect ,
235
+ Map <Class <? extends Annotation >, Boolean > constraintCompositionTypeCache
236
+ ) {
221
237
final BeanDescriptor descriptor = factory .getValidator ().getConstraintsForClass ( clazz );
222
238
//cno bean level constraints can be applied, go to the properties
223
239
for ( PropertyDescriptor propertyDesc : descriptor .getConstrainedProperties () ) {
@@ -230,7 +246,9 @@ private static void applyDDL(
230
246
propertyDesc ,
231
247
groups ,
232
248
activateNotNull ,
233
- dialect
249
+ false ,
250
+ dialect ,
251
+ constraintCompositionTypeCache
234
252
);
235
253
if ( property .isComposite () && propertyDesc .isCascaded () ) {
236
254
final Component component = (Component ) property .getValue ();
@@ -244,7 +262,8 @@ private static void applyDDL(
244
262
// activate not null and if the property is not null.
245
263
// Otherwise, all sub columns should be left nullable
246
264
activateNotNull && hasNotNull ,
247
- dialect
265
+ dialect ,
266
+ constraintCompositionTypeCache
248
267
);
249
268
}
250
269
}
@@ -257,12 +276,18 @@ private static boolean applyConstraints(
257
276
PropertyDescriptor propertyDesc ,
258
277
Set <Class <?>> groups ,
259
278
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 ;
262
285
for ( ConstraintDescriptor <?> descriptor : constraintDescriptors ) {
286
+ boolean hasNotNull = false ;
287
+
263
288
if ( groups == null || !disjoint ( descriptor .getGroups (), groups ) ) {
264
289
if ( canApplyNotNull ) {
265
- hasNotNull = hasNotNull || applyNotNull ( property , descriptor );
290
+ hasNotNull = isNotNullDescriptor ( descriptor );
266
291
}
267
292
268
293
// apply bean validation specific constraints
@@ -276,19 +301,70 @@ private static boolean applyConstraints(
276
301
// will be taken care later.
277
302
applyLength ( property , descriptor , propertyDesc );
278
303
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
+ }
286
318
287
- hasNotNull = hasNotNull || hasNotNullFromComposingConstraints ;
319
+ if ( firstItem ) {
320
+ composedResultHasNotNull = hasNotNull ;
321
+ firstItem = false ;
288
322
}
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
+ }
289
334
335
+ if ( composedResultHasNotNull ) {
336
+ markNotNull ( property );
290
337
}
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
+ });
292
368
}
293
369
294
370
private static void applyMin (Property property , ConstraintDescriptor <?> descriptor , Dialect dialect ) {
@@ -328,35 +404,33 @@ private static void applySQLCheck(Column column, String checkConstraint) {
328
404
column .addCheckConstraint ( new CheckConstraint ( checkConstraint ) );
329
405
}
330
406
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 ) {
334
408
final Class <? extends Annotation > annotationType = descriptor .getAnnotation ().annotationType ();
335
- if ( NotNull .class .equals (annotationType )
409
+ return NotNull .class .equals (annotationType )
336
410
|| 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
+ );
353
429
}
354
430
}
355
431
}
356
- hasNotNull = true ;
357
432
}
358
- property .setOptional ( !hasNotNull );
359
- return hasNotNull ;
433
+ property .setOptional ( false );
360
434
}
361
435
362
436
private static void applyDigits (Property property , ConstraintDescriptor <?> descriptor ) {
0 commit comments