jamesfredley commented on code in PR #16624:
URL: https://github.com/apache/grails-core/pull/16624#discussion_r4341193919


##########
grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/validation/jakarta/GormValidatorAdapter.groovy:
##########
@@ -29,32 +29,69 @@ import jakarta.validation.executable.ExecutableValidator
 import org.springframework.validation.Errors
 import org.springframework.validation.beanvalidation.SpringValidatorAdapter
 
+import grails.gorm.validation.ConstrainedEntity
+import grails.gorm.validation.ConstrainedProperty
+import grails.gorm.validation.PersistentEntityValidator
 import org.grails.datastore.gorm.GormValidateable
 import org.grails.datastore.gorm.validation.CascadingValidator
 
 /**
- * A validator adapter that applies translates the constraint errors into the 
Errors object of a GORM entity
+ * A validator adapter that applies translates the constraint errors into the 
Errors object of a GORM entity.
+ *
+ * When created for a persistent entity it also validates the GORM constraints 
of the entity (its {@code constraints}
+ * block) and exposes the constraints of the entity, including the ones 
describing its Bean Validation annotations,
+ * as a {@link ConstrainedEntity}.
  *
  * @author Graeme Rocher
  * @since 6.1
  */
 @CompileStatic
-class GormValidatorAdapter extends SpringValidatorAdapter implements 
CascadingValidator {
+class GormValidatorAdapter extends SpringValidatorAdapter implements 
CascadingValidator, ConstrainedEntity {
 
     public static final ThreadLocal<Boolean> CASCADE_VALIDATION = new 
ThreadLocal<>()
 
     final Validator thisValidator
 
+    /**
+     * The validator for the GORM constraints of the entity, null when only 
Bean Validation applies
+     */
+    final PersistentEntityValidator entityValidator
+
+    /**
+     * The constraints of the entity, including the ones describing its Bean 
Validation annotations
+     */
+    final Map<String, ConstrainedProperty> constrainedProperties
+
     GormValidatorAdapter(Validator targetValidator) {
+        this(targetValidator, null, Collections.<String, ConstrainedProperty> 
emptyMap())
+    }
+
+    /**
+     * @param targetValidator The Bean Validation validator
+     * @param entityValidator The validator for the GORM constraints of the 
entity, may be null
+     * @param constrainedProperties The constraints of the entity to expose
+     * @since 8.0.1
+     */
+    GormValidatorAdapter(Validator targetValidator, PersistentEntityValidator 
entityValidator, Map<String, ConstrainedProperty> constrainedProperties) {
         super(targetValidator)
-        thisValidator = targetValidator
+        this.thisValidator = targetValidator
+        this.entityValidator = entityValidator
+        this.constrainedProperties = 
Collections.unmodifiableMap(constrainedProperties)
+    }
+
+    @Override
+    void validate(Object target, Errors errors) {
+        validate(target, errors, true)
     }
 
     @Override
     void validate(Object obj, Errors errors, boolean cascade) {
-        CASCADE_VALIDATION.set(cascade)
+        // the GORM validator cascades to the associations with their own 
validators, so Bean Validation must not
+        // cascade as well or the associated instances would be validated twice
+        CASCADE_VALIDATION.set(entityValidator == null ? cascade : 
Boolean.FALSE)

Review Comment:
   Turning Bean Validation cascading off here assumes the GORM validator will 
validate the association. It will not, when the association's validator is 
another `GormValidatorAdapter`. `cascadeValidationToOne()` only extracts 
constraints from a `PersistentEntityValidator`.
   
   An annotated parent with an owning `@Valid` association to an annotated 
child can pass `validate()` with an invalid child. That used to fail.
   
   Cascade both systems for each associated annotated entity, with one cycle 
guard, and do not validate the translated metadata a second time. Please add a 
public `validate()` test for an annotated parent and child, a child constraints 
block, a single execution, and `deepValidate: false`.



##########
grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/validation/jakarta/JakartaConstraintsTranslator.groovy:
##########
@@ -0,0 +1,530 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    https://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+
+package org.grails.datastore.gorm.validation.jakarta
+
+import java.beans.Introspector
+import java.lang.annotation.Annotation
+import java.lang.reflect.Field
+import java.lang.reflect.Method
+import java.lang.reflect.Modifier
+import java.math.RoundingMode
+
+import groovy.transform.CompileStatic
+import groovy.util.logging.Slf4j
+
+import jakarta.validation.Constraint
+import jakarta.validation.constraints.AssertFalse
+import jakarta.validation.constraints.AssertTrue
+import jakarta.validation.constraints.DecimalMax
+import jakarta.validation.constraints.DecimalMin
+import jakarta.validation.constraints.Email
+import jakarta.validation.constraints.Max
+import jakarta.validation.constraints.Min
+import jakarta.validation.constraints.Negative
+import jakarta.validation.constraints.NegativeOrZero
+import jakarta.validation.constraints.NotBlank
+import jakarta.validation.constraints.NotEmpty
+import jakarta.validation.constraints.NotNull
+import jakarta.validation.constraints.Null
+import jakarta.validation.constraints.Pattern
+import jakarta.validation.constraints.Positive
+import jakarta.validation.constraints.PositiveOrZero
+import jakarta.validation.constraints.Size
+
+import org.springframework.context.support.StaticMessageSource
+import org.springframework.util.ClassUtils
+
+import grails.gorm.validation.ConstrainedProperty
+import grails.gorm.validation.DefaultConstrainedProperty
+import grails.gorm.validation.exceptions.ValidationConfigurationException
+import 
org.grails.datastore.gorm.validation.constraints.registry.ConstraintRegistry
+import 
org.grails.datastore.gorm.validation.constraints.registry.DefaultConstraintRegistry
+import org.grails.datastore.mapping.reflect.ClassPropertyFetcher
+
+/**
+ * Describes the Jakarta Bean Validation annotations of a class in terms of 
GORM constraints, so that the constraint
+ * metadata of an entity annotated with them ({@link 
grails.gorm.validation.ConstrainedEntity}) reflects the
+ * annotations as well as the {@code constraints} block. The annotations 
themselves are still validated by Bean
+ * Validation, the translated constraints only describe them: a property 
annotated with {@code @NotNull} reports
+ * {@code nullable == false}, one annotated with {@code @Size(max = 10)} 
reports {@code maxSize == 10}, and so on.
+ *
+ * Annotations without a GORM equivalent (such as {@code @Past}, {@code 
@Digits} or custom constraints) are described
+ * through the constraints composing them when they are composed constraints, 
and left out otherwise.
+ *
+ * @since 8.0.1
+ */
+@Slf4j
+@CompileStatic
+class JakartaConstraintsTranslator {
+
+    /**
+     * The constraint annotations on the fields and getters of the class and 
its superclasses, keyed by property name.
+     * An annotation is a constraint when its type is annotated with {@link 
Constraint}, which covers custom
+     * constraints. Repeatable containers such as {@code Pattern.List} are 
expanded.
+     *
+     * @param clazz The class
+     * @return The constraint annotations by property name
+     */
+    static Map<String, List<Annotation>> findConstraintAnnotations(Class<?> 
clazz) {
+        Map<String, List<Annotation>> result = new LinkedHashMap<>()
+        for (Class<?> current = clazz; current != null && current != Object; 
current = current.superclass) {
+            for (Field field in current.declaredFields) {
+                if (!field.synthetic && !Modifier.isStatic(field.modifiers)) {
+                    collectConstraintAnnotations(result, field.name, 
field.annotations)
+                }
+            }
+            for (Method method in current.declaredMethods) {
+                if (!method.synthetic && !Modifier.isStatic(method.modifiers) 
&& method.parameterCount == 0) {
+                    String propertyName = getterPropertyName(method)
+                    if (propertyName != null) {
+                        collectConstraintAnnotations(result, propertyName, 
method.annotations)
+                    }
+                }
+            }
+        }
+        return result
+    }
+
+    /**
+     * @param annotation The annotation
+     * @return Whether the annotation is a Bean Validation constraint
+     */
+    static boolean isConstraintAnnotation(Annotation annotation) {
+        return annotation.annotationType().isAnnotationPresent(Constraint)
+    }
+
+    /**
+     * The properties whose nullability is declared through an annotation 
({@code @NotNull}, {@code @NotBlank},
+     * {@code @NotEmpty} or {@code @Null}, directly or through a composed 
constraint).
+     *
+     * @param clazz The class
+     * @return The property names
+     */
+    static Set<String> findNullabilityConstrainedProperties(Class<?> clazz) {
+        Set<String> result = new LinkedHashSet<>()
+        findConstraintAnnotations(clazz).each { String propertyName, 
List<Annotation> annotations ->
+            if (annotations.any { Annotation annotation -> 
constrainsNullability(annotation) }) {
+                result.add(propertyName)
+            }
+        }
+        return result
+    }
+
+    private static boolean constrainsNullability(Annotation annotation) {
+        if (annotation instanceof NotNull || annotation instanceof NotBlank || 
annotation instanceof NotEmpty || annotation instanceof Null) {
+            return true
+        }
+        return composingConstraints(annotation).any { Annotation composing -> 
constrainsNullability(composing) }
+    }
+
+    /**
+     * Leaves the nullability of the given properties to Bean Validation by 
relaxing the GORM {@code nullable}
+     * constraint, so that a {@code null} value is reported once rather than 
by both validators. The constraints are
+     * modified in place.
+     *
+     * @param clazz The class the constraints were evaluated for
+     * @param constraints The evaluated GORM constraints
+     */
+    static void delegateNullabilityToBeanValidation(Class<?> clazz, 
Map<String, ConstrainedProperty> constraints) {
+        for (String propertyName in 
findNullabilityConstrainedProperties(clazz)) {
+            ConstrainedProperty constrained = constraints.get(propertyName)
+            if (constrained != null && 
constrained.hasAppliedConstraint(ConstrainedProperty.NULLABLE_CONSTRAINT)) {
+                
constrained.applyConstraint(ConstrainedProperty.NULLABLE_CONSTRAINT, true)

Review Comment:
   This sets `nullable: true` for every property that has any nullability 
annotation, including `@NotNull(groups = SpecialChecks)`. Ordinary `validate()` 
runs the default group, which does not enforce that annotation. The Grails 
block said `nullable: false`. Both validators then accept null, while the 
translated metadata still says the property is required.
   
   Keep the Grails nullability constraint unless an annotation that actually 
runs in this validation replaces it. Please assert that ordinary `validate()` 
and `save()` still reject null when the only `@NotNull` is on another group.



##########
grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/validation/jakarta/JakartaConstraintsTranslator.groovy:
##########
@@ -0,0 +1,530 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    https://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+
+package org.grails.datastore.gorm.validation.jakarta
+
+import java.beans.Introspector
+import java.lang.annotation.Annotation
+import java.lang.reflect.Field
+import java.lang.reflect.Method
+import java.lang.reflect.Modifier
+import java.math.RoundingMode
+
+import groovy.transform.CompileStatic
+import groovy.util.logging.Slf4j
+
+import jakarta.validation.Constraint
+import jakarta.validation.constraints.AssertFalse
+import jakarta.validation.constraints.AssertTrue
+import jakarta.validation.constraints.DecimalMax
+import jakarta.validation.constraints.DecimalMin
+import jakarta.validation.constraints.Email
+import jakarta.validation.constraints.Max
+import jakarta.validation.constraints.Min
+import jakarta.validation.constraints.Negative
+import jakarta.validation.constraints.NegativeOrZero
+import jakarta.validation.constraints.NotBlank
+import jakarta.validation.constraints.NotEmpty
+import jakarta.validation.constraints.NotNull
+import jakarta.validation.constraints.Null
+import jakarta.validation.constraints.Pattern
+import jakarta.validation.constraints.Positive
+import jakarta.validation.constraints.PositiveOrZero
+import jakarta.validation.constraints.Size
+
+import org.springframework.context.support.StaticMessageSource
+import org.springframework.util.ClassUtils
+
+import grails.gorm.validation.ConstrainedProperty
+import grails.gorm.validation.DefaultConstrainedProperty
+import grails.gorm.validation.exceptions.ValidationConfigurationException
+import 
org.grails.datastore.gorm.validation.constraints.registry.ConstraintRegistry
+import 
org.grails.datastore.gorm.validation.constraints.registry.DefaultConstraintRegistry
+import org.grails.datastore.mapping.reflect.ClassPropertyFetcher
+
+/**
+ * Describes the Jakarta Bean Validation annotations of a class in terms of 
GORM constraints, so that the constraint
+ * metadata of an entity annotated with them ({@link 
grails.gorm.validation.ConstrainedEntity}) reflects the
+ * annotations as well as the {@code constraints} block. The annotations 
themselves are still validated by Bean
+ * Validation, the translated constraints only describe them: a property 
annotated with {@code @NotNull} reports
+ * {@code nullable == false}, one annotated with {@code @Size(max = 10)} 
reports {@code maxSize == 10}, and so on.
+ *
+ * Annotations without a GORM equivalent (such as {@code @Past}, {@code 
@Digits} or custom constraints) are described
+ * through the constraints composing them when they are composed constraints, 
and left out otherwise.
+ *
+ * @since 8.0.1
+ */
+@Slf4j
+@CompileStatic
+class JakartaConstraintsTranslator {
+
+    /**
+     * The constraint annotations on the fields and getters of the class and 
its superclasses, keyed by property name.
+     * An annotation is a constraint when its type is annotated with {@link 
Constraint}, which covers custom
+     * constraints. Repeatable containers such as {@code Pattern.List} are 
expanded.
+     *
+     * @param clazz The class
+     * @return The constraint annotations by property name
+     */
+    static Map<String, List<Annotation>> findConstraintAnnotations(Class<?> 
clazz) {
+        Map<String, List<Annotation>> result = new LinkedHashMap<>()
+        for (Class<?> current = clazz; current != null && current != Object; 
current = current.superclass) {
+            for (Field field in current.declaredFields) {
+                if (!field.synthetic && !Modifier.isStatic(field.modifiers)) {
+                    collectConstraintAnnotations(result, field.name, 
field.annotations)
+                }
+            }
+            for (Method method in current.declaredMethods) {
+                if (!method.synthetic && !Modifier.isStatic(method.modifiers) 
&& method.parameterCount == 0) {
+                    String propertyName = getterPropertyName(method)
+                    if (propertyName != null) {
+                        collectConstraintAnnotations(result, propertyName, 
method.annotations)
+                    }
+                }
+            }
+        }
+        return result
+    }
+
+    /**
+     * @param annotation The annotation
+     * @return Whether the annotation is a Bean Validation constraint
+     */
+    static boolean isConstraintAnnotation(Annotation annotation) {
+        return annotation.annotationType().isAnnotationPresent(Constraint)
+    }
+
+    /**
+     * The properties whose nullability is declared through an annotation 
({@code @NotNull}, {@code @NotBlank},
+     * {@code @NotEmpty} or {@code @Null}, directly or through a composed 
constraint).
+     *
+     * @param clazz The class
+     * @return The property names
+     */
+    static Set<String> findNullabilityConstrainedProperties(Class<?> clazz) {
+        Set<String> result = new LinkedHashSet<>()
+        findConstraintAnnotations(clazz).each { String propertyName, 
List<Annotation> annotations ->
+            if (annotations.any { Annotation annotation -> 
constrainsNullability(annotation) }) {
+                result.add(propertyName)
+            }
+        }
+        return result
+    }
+
+    private static boolean constrainsNullability(Annotation annotation) {
+        if (annotation instanceof NotNull || annotation instanceof NotBlank || 
annotation instanceof NotEmpty || annotation instanceof Null) {
+            return true
+        }
+        return composingConstraints(annotation).any { Annotation composing -> 
constrainsNullability(composing) }
+    }
+
+    /**
+     * Leaves the nullability of the given properties to Bean Validation by 
relaxing the GORM {@code nullable}
+     * constraint, so that a {@code null} value is reported once rather than 
by both validators. The constraints are
+     * modified in place.
+     *
+     * @param clazz The class the constraints were evaluated for
+     * @param constraints The evaluated GORM constraints
+     */
+    static void delegateNullabilityToBeanValidation(Class<?> clazz, 
Map<String, ConstrainedProperty> constraints) {
+        for (String propertyName in 
findNullabilityConstrainedProperties(clazz)) {
+            ConstrainedProperty constrained = constraints.get(propertyName)
+            if (constrained != null && 
constrained.hasAppliedConstraint(ConstrainedProperty.NULLABLE_CONSTRAINT)) {
+                
constrained.applyConstraint(ConstrainedProperty.NULLABLE_CONSTRAINT, true)
+            }
+        }
+    }
+
+    /**
+     * Describes the Bean Validation annotations of the class as GORM 
constraints, on top of a copy of the given
+     * constraints. The given constraints are not modified, as they are the 
ones the GORM validator validates with.
+     *
+     * @param clazz The annotated class
+     * @param constraints The GORM constraints of the class, typically 
evaluated from its {@code constraints} block
+     * @param registry The registry to build the constraints with
+     * @return A new map with the merged constraints, or the given map when 
the class has no constraint annotations
+     */
+    Map<String, ConstrainedProperty> translate(Class<?> clazz, Map<String, 
ConstrainedProperty> constraints, ConstraintRegistry registry) {
+        Map<String, List<Annotation>> annotationsByProperty = 
findConstraintAnnotations(clazz)
+        if (annotationsByProperty.isEmpty()) {
+            return constraints
+        }
+        ConstraintRegistry targetRegistry = registry ?: new 
DefaultConstraintRegistry(new StaticMessageSource())
+        Map<String, ConstrainedProperty> merged = new 
LinkedHashMap<>(constraints ?: Collections.<String, ConstrainedProperty> 
emptyMap())
+        annotationsByProperty.each { String propertyName, List<Annotation> 
annotations ->
+            ConstrainedProperty existing = constraints?.get(propertyName)
+            DefaultConstrainedProperty constrained
+            if (existing != null) {
+                constrained = copy(existing, targetRegistry)
+            }
+            else {
+                Class<?> propertyType = 
ClassPropertyFetcher.getPropertyType(clazz, propertyName)
+                if (propertyType == null) {
+                    return
+                }
+                constrained = new DefaultConstrainedProperty(clazz, 
propertyName, propertyType, targetRegistry)
+                constrained.order = merged.size() + 1
+            }
+            for (Annotation annotation in annotations) {
+                applyAnnotation(constrained, annotation)
+            }
+            merged.put(propertyName, constrained)
+        }
+        return merged
+    }
+
+    /**
+     * A new constrained property with the same constraints and metadata as 
the given one.
+     *
+     * @param source The constrained property to copy
+     * @param registry The registry to build the constraints with
+     * @return The copy
+     */
+    static DefaultConstrainedProperty copy(ConstrainedProperty source, 
ConstraintRegistry registry) {
+        DefaultConstrainedProperty target = new 
DefaultConstrainedProperty(source.owner, source.propertyName, 
source.propertyType, registry)
+        for (grails.gorm.validation.Constraint constraint in 
source.appliedConstraints) {
+            try {
+                target.applyConstraint(constraint.name, constraint.parameter)
+            }
+            catch (Exception e) {
+                log.warn('Unable to copy the [{}] constraint of [{}.{}]: {}', 
constraint.name, source.owner.name, source.propertyName, e.message)
+            }
+        }
+        if (source instanceof DefaultConstrainedProperty) {
+            DefaultConstrainedProperty defaults = (DefaultConstrainedProperty) 
source
+            defaults.metaConstraints.each { String name, Object value -> 
target.addMetaConstraint(name, value) }
+            target.order = defaults.order
+            target.displayType = defaults.displayType
+            target.editable = defaults.editable
+            target.format = defaults.format
+            target.widget = defaults.widget
+            target.password = defaults.password
+            target.attributes = defaults.attributes
+        }
+        return target
+    }
+
+    /**
+     * Applies the GORM constraints describing the annotation. Bounds tighten 
the ones already applied rather than
+     * replace them.
+     *
+     * @param constrained The constrained property to describe the annotation 
on
+     * @param annotation The annotation
+     */
+    void applyAnnotation(DefaultConstrainedProperty constrained, Annotation 
annotation) {
+        switch (annotation) {
+            case NotNull:
+                applyIfSupported(constrained, 
ConstrainedProperty.NULLABLE_CONSTRAINT, false)
+                break
+            case NotBlank:
+                applyIfSupported(constrained, 
ConstrainedProperty.NULLABLE_CONSTRAINT, false)
+                applyIfSupported(constrained, 
ConstrainedProperty.BLANK_CONSTRAINT, false)
+                break
+            case NotEmpty:
+                applyIfSupported(constrained, 
ConstrainedProperty.NULLABLE_CONSTRAINT, false)
+                tightenMinSize(constrained, 1)
+                break
+            case Null:
+                applyIfSupported(constrained, 
ConstrainedProperty.NULLABLE_CONSTRAINT, true)
+                break
+            case Size:
+                Size size = (Size) annotation
+                if (size.min() > 0) {
+                    tightenMinSize(constrained, size.min())
+                }
+                if (size.max() < Integer.MAX_VALUE) {
+                    tightenMaxSize(constrained, size.max())
+                }
+                break
+            case Min:
+                tightenMin(constrained, ((Min) annotation).value())
+                break
+            case Max:
+                tightenMax(constrained, ((Max) annotation).value())
+                break
+            case DecimalMin:
+                DecimalMin decimalMin = (DecimalMin) annotation
+                BigDecimal min = new BigDecimal(decimalMin.value())
+                if (decimalMin.inclusive()) {
+                    tightenMin(constrained, min)
+                }
+                else if (isIntegral(constrained.propertyType)) {
+                    tightenMin(constrained, min.setScale(0, 
RoundingMode.FLOOR).add(BigDecimal.ONE))
+                }
+                break
+            case DecimalMax:
+                DecimalMax decimalMax = (DecimalMax) annotation
+                BigDecimal max = new BigDecimal(decimalMax.value())
+                if (decimalMax.inclusive()) {
+                    tightenMax(constrained, max)
+                }
+                else if (isIntegral(constrained.propertyType)) {
+                    tightenMax(constrained, max.setScale(0, 
RoundingMode.CEILING).subtract(BigDecimal.ONE))
+                }
+                break
+            case Positive:
+                if (isIntegral(constrained.propertyType)) {
+                    tightenMin(constrained, 1)
+                }
+                break
+            case PositiveOrZero:
+                tightenMin(constrained, 0)
+                break
+            case Negative:
+                if (isIntegral(constrained.propertyType)) {
+                    tightenMax(constrained, -1)
+                }
+                break
+            case NegativeOrZero:
+                tightenMax(constrained, 0)
+                break
+            case Email:
+                applyIfSupported(constrained, 
ConstrainedProperty.EMAIL_CONSTRAINT, true)
+                Email email = (Email) annotation
+                if (email.regexp() != '.*') {
+                    applyMatches(constrained, withInlineFlags(email.regexp(), 
email.flags()))
+                }
+                break
+            case Pattern:
+                Pattern pattern = (Pattern) annotation
+                applyMatches(constrained, withInlineFlags(pattern.regexp(), 
pattern.flags()))
+                break
+            case AssertTrue:
+                applyIfSupported(constrained, 
ConstrainedProperty.IN_LIST_CONSTRAINT, [Boolean.TRUE])
+                break
+            case AssertFalse:
+                applyIfSupported(constrained, 
ConstrainedProperty.IN_LIST_CONSTRAINT, [Boolean.FALSE])
+                break
+            default:
+                if (!applyHibernateValidatorAnnotation(constrained, 
annotation)) {
+                    for (Annotation composing in 
composingConstraints(annotation)) {
+                        applyAnnotation(constrained, composing)
+                    }
+                }
+        }
+    }
+
+    /**
+     * The Hibernate Validator specific annotations with a GORM equivalent, 
matched by name so that no provider is
+     * required.
+     */
+    private boolean 
applyHibernateValidatorAnnotation(DefaultConstrainedProperty constrained, 
Annotation annotation) {
+        switch (annotation.annotationType().name) {
+            case 'org.hibernate.validator.constraints.Length':
+                int min = (int) annotationMember(annotation, 'min')
+                int max = (int) annotationMember(annotation, 'max')
+                if (min > 0) {
+                    tightenMinSize(constrained, min)
+                }
+                if (max < Integer.MAX_VALUE) {
+                    tightenMaxSize(constrained, max)
+                }
+                return true
+            case 'org.hibernate.validator.constraints.Range':
+                tightenMin(constrained, (long) annotationMember(annotation, 
'min'))
+                tightenMax(constrained, (long) annotationMember(annotation, 
'max'))
+                return true
+            case 'org.hibernate.validator.constraints.URL':
+                applyIfSupported(constrained, 
ConstrainedProperty.URL_CONSTRAINT, true)
+                return true
+            case 'org.hibernate.validator.constraints.CreditCardNumber':
+                applyIfSupported(constrained, 
ConstrainedProperty.CREDIT_CARD_CONSTRAINT, true)
+                return true
+            default:
+                return false
+        }
+    }
+
+    /** The constraints a composed constraint is made of, i.e. the constraint 
annotations on its type */
+    private static List<Annotation> composingConstraints(Annotation 
annotation) {

Review Comment:
   Reading the raw meta-annotations drops `@OverridesAttribute`. A composed 
constraint that overrides `Size.max` to 10 is enforced at 10 by Hibernate 
Validator, but the metadata exposed here uses the original `@Size` declaration. 
Resolve the constraint descriptors, or apply the attribute overrides, so the 
exposed `maxSize` matches what validation enforces.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to