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


##########
grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/validation/jakarta/JakartaConstraintsTranslator.groovy:
##########
@@ -0,0 +1,532 @@
+/*
+ *  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.lang.annotation.Annotation
+import java.math.RoundingMode
+
+import groovy.transform.CompileStatic
+import groovy.util.logging.Slf4j
+
+import jakarta.validation.GroupSequence
+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 jakarta.validation.groups.Default
+import jakarta.validation.metadata.BeanDescriptor
+import jakarta.validation.metadata.ConstraintDescriptor
+import jakarta.validation.metadata.PropertyDescriptor
+
+import org.springframework.context.support.StaticMessageSource
+import org.springframework.util.ClassUtils
+import org.springframework.util.ReflectionUtils
+
+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 constraints 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.
+ * <p>
+ * The constraints are read from the {@link BeanDescriptor} of the class, as 
the Bean Validation provider sees them:
+ * a composed constraint is described through the constraints composing it, 
with its attribute overrides applied,
+ * and only the constraints of the default group of the class are described, 
as {@code validate()} validates no
+ * other group. A constraint on a getter without a backing field, such as a 
cross field {@code @AssertTrue}, is
+ * validated but not described, unless the {@code constraints} block 
constrains the property. Annotations without
+ * a GORM equivalent (such as {@code @Past}, {@code @Digits} or custom 
constraints) are left out.
+ *
+ * @since 8.0.1
+ */
+@Slf4j
+@CompileStatic
+class JakartaConstraintsTranslator {
+
+    /**
+     * The descriptor of the class
+     */
+    final BeanDescriptor beanDescriptor
+
+    /**
+     * The constraints of the default group by property name, the properties 
without any left out
+     */
+    final Map<String, List<ConstraintDescriptor<?>>> constraintDescriptors
+
+    /**
+     * @param beanDescriptor The descriptor of the class, from {@link 
jakarta.validation.Validator#getConstraintsForClass}
+     */
+    JakartaConstraintsTranslator(BeanDescriptor beanDescriptor) {
+        this.beanDescriptor = beanDescriptor
+        this.constraintDescriptors = 
Collections.unmodifiableMap(findConstraintDescriptors(beanDescriptor))
+    }
+
+    /**
+     * @return The described class
+     */
+    Class<?> getBeanClass() {
+        return beanDescriptor.elementClass
+    }
+
+    private static Map<String, List<ConstraintDescriptor<?>>> 
findConstraintDescriptors(BeanDescriptor beanDescriptor) {
+        Set<Class<?>> defaultGroups = 
defaultGroupsOf(beanDescriptor.elementClass)
+        Map<String, List<ConstraintDescriptor<?>>> result = new 
LinkedHashMap<>()
+        List<PropertyDescriptor> properties = 
beanDescriptor.constrainedProperties.toSorted { PropertyDescriptor a, 
PropertyDescriptor b ->
+            a.propertyName <=> b.propertyName
+        }
+        for (PropertyDescriptor property in properties) {
+            Collection<ConstraintDescriptor<?>> descriptors = 
property.constraintDescriptors.findAll { ConstraintDescriptor<?> descriptor ->
+                !Collections.disjoint(descriptor.groups, defaultGroups)
+            }
+            if (descriptors) {
+                // the provider reports the constraints of a property as a 
set, so they are ordered for the
+                // description to be the same on every run when two of them 
describe the same GORM constraint
+                result.put(property.propertyName, descriptors.toSorted { 
ConstraintDescriptor<?> a, ConstraintDescriptor<?> b ->
+                    a.annotation.toString() <=> b.annotation.toString()
+                })
+            }
+        }
+        return result
+    }
+
+    /**
+     * The groups {@code validate()} validates: the {@link Default} group, and 
the groups of the sequence the class
+     * redefines it with, if any, in which the class itself stands for the 
{@link Default} group. As the provider
+     * validates them, a group of the sequence brings the groups it extends, 
and a nested sequence its own groups.
+     */
+    private static Set<Class<?>> defaultGroupsOf(Class<?> beanClass) {
+        Set<Class<?>> groups = new HashSet<>()
+        groups.add(Default)
+        GroupSequence sequence = beanClass.getAnnotation(GroupSequence)

Review Comment:
   A superclass @GroupSequence that puts @Null(groups = PublishChecks) String 
retired into the default group is not seen on a subclass, because GroupSequence 
is not inherited and this lookup reads only the concrete class. Bean Validation 
still applies that sequence, so a null retired is valid, but GORM reports 
nullable. Read the default sequence of the class that declares the constraint.



##########
grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/validation/jakarta/JakartaPersistentEntityValidator.groovy:
##########
@@ -0,0 +1,98 @@
+/*
+ *  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 groovy.transform.CompileStatic
+
+import jakarta.validation.Validator
+
+import org.springframework.context.MessageSource
+import org.springframework.validation.Errors
+
+import grails.gorm.validation.ConstrainedProperty
+import grails.gorm.validation.PersistentEntityValidator
+import 
org.grails.datastore.gorm.validation.constraints.eval.ConstraintsEvaluator
+import 
org.grails.datastore.gorm.validation.constraints.registry.ConstraintRegistry
+import org.grails.datastore.mapping.model.PersistentEntity
+
+/**
+ * Validates a persistent entity annotated with Jakarta Bean Validation 
constraints: its GORM constraints (the
+ * {@code constraints} block and the defaults) as a {@link 
PersistentEntityValidator}, then its annotations with the
+ * Bean Validation provider, whose violations are reported as field errors 
with Spring's Bean Validation message
+ * codes (such as {@code NotNull.member.name}, {@code NotNull.name} and {@code 
NotNull}).
+ * <p>
+ * The GORM {@code nullable} constraint is left to Bean Validation for a 
property whose nullability an annotation
+ * of the default group declares ({@code @NotNull}, {@code @NotBlank}, {@code 
@NotEmpty} or {@code @Null}), so a
+ * null value is reported once. Bean Validation does not cascade: the GORM 
validator cascades to the associations
+ * as it does for any entity, and validates an annotated associated entity 
both ways through
+ * {@link #validateBeyondConstraints}.
+ * <p>
+ * {@link #getConstrainedProperties()} describes the annotations as GORM 
constraints next to the ones of the block,
+ * see {@link JakartaConstraintsTranslator}.
+ *
+ * @since 8.0.1
+ */
+@CompileStatic
+class JakartaPersistentEntityValidator extends PersistentEntityValidator {
+
+    /**
+     * The validator for the Bean Validation annotations of the entity
+     */
+    final GormValidatorAdapter beanValidator
+
+    private final Map<String, ConstrainedProperty> 
describedConstrainedProperties
+
+    /**
+     * @param entity The entity
+     * @param messageSource The message source
+     * @param constraintsEvaluator The evaluator of the GORM constraints
+     * @param constraintRegistry The registry the constraints describing the 
annotations are built with
+     * @param beanValidator The Bean Validation validator
+     */
+    JakartaPersistentEntityValidator(PersistentEntity entity, MessageSource 
messageSource, ConstraintsEvaluator constraintsEvaluator,
+            ConstraintRegistry constraintRegistry, Validator beanValidator) {
+        super(entity, messageSource, constraintsEvaluator)
+        this.beanValidator = beanValidator instanceof GormValidatorAdapter
+                ? (GormValidatorAdapter) beanValidator
+                : new GormValidatorAdapter(beanValidator)
+        JakartaConstraintsTranslator translator = new 
JakartaConstraintsTranslator(this.beanValidator.getConstraintsForClass(targetClass))
+        Map<String, ConstrainedProperty> validated = 
getValidatedConstrainedProperties()
+        translator.delegateNullabilityToBeanValidation(validated)
+        this.describedConstrainedProperties = 
Collections.unmodifiableMap(translator.translate(validated, constraintRegistry))
+    }
+
+    /**
+     * @return The constraints of the entity, the Bean Validation annotations 
described as GORM constraints next to
+     * the ones of the {@code constraints} block. The annotations themselves 
are validated by Bean Validation, see
+     * {@link #getValidatedConstrainedProperties()} for the constraints this 
validator validates.
+     */
+    @Override
+    Map<String, ConstrainedProperty> getConstrainedProperties() {
+        return describedConstrainedProperties
+    }
+
+    /**
+     * Validates the Bean Validation annotations of the object, without 
cascading, as the GORM validator cascades.
+     */
+    @Override
+    protected void validateBeyondConstraints(Object obj, Errors errors) {
+        beanValidator.validate(obj, errors, false)

Review Comment:
   A parent with @Valid @ConvertGroup(from = Default, to = PublishChecks) Child 
child, and a child @NotBlank(groups = PublishChecks) String label left blank, 
is accepted. This call turns provider cascading off, and the GORM cascade 
validates the child in the default group, so PublishChecks never runs. The 
provider used to follow @Valid and convert the group. Keep one cascade, and 
keep the group conversion.



##########
grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/validation/jakarta/JakartaConstraintsTranslator.groovy:
##########
@@ -0,0 +1,532 @@
+/*
+ *  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.lang.annotation.Annotation
+import java.math.RoundingMode
+
+import groovy.transform.CompileStatic
+import groovy.util.logging.Slf4j
+
+import jakarta.validation.GroupSequence
+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 jakarta.validation.groups.Default
+import jakarta.validation.metadata.BeanDescriptor
+import jakarta.validation.metadata.ConstraintDescriptor
+import jakarta.validation.metadata.PropertyDescriptor
+
+import org.springframework.context.support.StaticMessageSource
+import org.springframework.util.ClassUtils
+import org.springframework.util.ReflectionUtils
+
+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 constraints 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.
+ * <p>
+ * The constraints are read from the {@link BeanDescriptor} of the class, as 
the Bean Validation provider sees them:
+ * a composed constraint is described through the constraints composing it, 
with its attribute overrides applied,
+ * and only the constraints of the default group of the class are described, 
as {@code validate()} validates no
+ * other group. A constraint on a getter without a backing field, such as a 
cross field {@code @AssertTrue}, is
+ * validated but not described, unless the {@code constraints} block 
constrains the property. Annotations without
+ * a GORM equivalent (such as {@code @Past}, {@code @Digits} or custom 
constraints) are left out.
+ *
+ * @since 8.0.1
+ */
+@Slf4j
+@CompileStatic
+class JakartaConstraintsTranslator {
+
+    /**
+     * The descriptor of the class
+     */
+    final BeanDescriptor beanDescriptor
+
+    /**
+     * The constraints of the default group by property name, the properties 
without any left out
+     */
+    final Map<String, List<ConstraintDescriptor<?>>> constraintDescriptors
+
+    /**
+     * @param beanDescriptor The descriptor of the class, from {@link 
jakarta.validation.Validator#getConstraintsForClass}
+     */
+    JakartaConstraintsTranslator(BeanDescriptor beanDescriptor) {
+        this.beanDescriptor = beanDescriptor
+        this.constraintDescriptors = 
Collections.unmodifiableMap(findConstraintDescriptors(beanDescriptor))
+    }
+
+    /**
+     * @return The described class
+     */
+    Class<?> getBeanClass() {
+        return beanDescriptor.elementClass
+    }
+
+    private static Map<String, List<ConstraintDescriptor<?>>> 
findConstraintDescriptors(BeanDescriptor beanDescriptor) {
+        Set<Class<?>> defaultGroups = 
defaultGroupsOf(beanDescriptor.elementClass)
+        Map<String, List<ConstraintDescriptor<?>>> result = new 
LinkedHashMap<>()
+        List<PropertyDescriptor> properties = 
beanDescriptor.constrainedProperties.toSorted { PropertyDescriptor a, 
PropertyDescriptor b ->
+            a.propertyName <=> b.propertyName
+        }
+        for (PropertyDescriptor property in properties) {
+            Collection<ConstraintDescriptor<?>> descriptors = 
property.constraintDescriptors.findAll { ConstraintDescriptor<?> descriptor ->
+                !Collections.disjoint(descriptor.groups, defaultGroups)
+            }
+            if (descriptors) {
+                // the provider reports the constraints of a property as a 
set, so they are ordered for the
+                // description to be the same on every run when two of them 
describe the same GORM constraint
+                result.put(property.propertyName, descriptors.toSorted { 
ConstraintDescriptor<?> a, ConstraintDescriptor<?> b ->
+                    a.annotation.toString() <=> b.annotation.toString()
+                })
+            }
+        }
+        return result
+    }
+
+    /**
+     * The groups {@code validate()} validates: the {@link Default} group, and 
the groups of the sequence the class
+     * redefines it with, if any, in which the class itself stands for the 
{@link Default} group. As the provider
+     * validates them, a group of the sequence brings the groups it extends, 
and a nested sequence its own groups.
+     */
+    private static Set<Class<?>> defaultGroupsOf(Class<?> beanClass) {
+        Set<Class<?>> groups = new HashSet<>()
+        groups.add(Default)
+        GroupSequence sequence = beanClass.getAnnotation(GroupSequence)
+        if (sequence != null) {
+            Set<Class<?>> visitedSequences = new HashSet<>()
+            // the class in its own sequence stands for the Default group, 
which is already added
+            visitedSequences.add(beanClass)
+            addSequencedGroups(sequence.value(), groups, visitedSequences)
+        }
+        return groups
+    }
+
+    private static void addSequencedGroups(Class<?>[] sequence, Set<Class<?>> 
groups, Set<Class<?>> visitedSequences) {
+        for (Class<?> group in sequence) {
+            GroupSequence nested = group.getAnnotation(GroupSequence)
+            if (nested == null) {
+                addWithInheritedGroups(group, groups)
+            }
+            else if (visitedSequences.add(group)) {
+                addSequencedGroups(nested.value(), groups, visitedSequences)
+            }
+        }
+    }
+
+    private static void addWithInheritedGroups(Class<?> group, Set<Class<?>> 
groups) {
+        if (groups.add(group)) {
+            for (Class<?> inherited in group.interfaces) {
+                addWithInheritedGroups(inherited, groups)
+            }
+        }
+    }
+
+    /**
+     * The properties whose nullability a constraint of the default group 
declares ({@code @NotNull},
+     * {@code @NotBlank}, {@code @NotEmpty} or {@code @Null}, directly or 
through a composed constraint).
+     *
+     * @return The property names
+     */
+    Set<String> getNullabilityConstrainedProperties() {
+        Set<String> result = new LinkedHashSet<>()
+        constraintDescriptors.each { String propertyName, 
List<ConstraintDescriptor<?>> descriptors ->
+            if (descriptors.any { ConstraintDescriptor<?> descriptor -> 
constrainsNullability(descriptor) }) {
+                result.add(propertyName)
+            }
+        }
+        return result
+    }
+
+    private static boolean constrainsNullability(ConstraintDescriptor<?> 
descriptor) {
+        Annotation annotation = descriptor.annotation
+        if (annotation instanceof NotNull || annotation instanceof NotBlank || 
annotation instanceof NotEmpty || annotation instanceof Null) {
+            return true
+        }
+        return descriptor.composingConstraints.any { ConstraintDescriptor<?> 
composing -> constrainsNullability(composing) }
+    }
+
+    /**
+     * Leaves the nullability of the {@link 
#getNullabilityConstrainedProperties() properties Bean Validation checks
+     * for null} to it by relaxing their 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 constraints The evaluated GORM constraints of the class
+     */
+    void delegateNullabilityToBeanValidation(Map<String, ConstrainedProperty> 
constraints) {
+        for (String propertyName in nullabilityConstrainedProperties) {
+            ConstrainedProperty constrained = constraints.get(propertyName)
+            if (constrained != null && 
constrained.hasAppliedConstraint(ConstrainedProperty.NULLABLE_CONSTRAINT)) {
+                
constrained.applyConstraint(ConstrainedProperty.NULLABLE_CONSTRAINT, true)
+            }
+        }
+    }
+
+    /**
+     * Describes the Bean Validation constraints 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 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 constraints to describe
+     */
+    Map<String, ConstrainedProperty> translate(Map<String, 
ConstrainedProperty> constraints, ConstraintRegistry registry) {
+        if (constraintDescriptors.isEmpty()) {
+            return constraints
+        }
+        ConstraintRegistry targetRegistry = registry ?: new 
DefaultConstraintRegistry(new StaticMessageSource())
+        Map<String, ConstrainedProperty> merged = new 
LinkedHashMap<>(constraints ?: Collections.<String, ConstrainedProperty> 
emptyMap())
+        Class<?> beanClass = this.beanClass
+        constraintDescriptors.each { String propertyName, 
List<ConstraintDescriptor<?>> descriptors ->
+            ConstrainedProperty existing = constraints?.get(propertyName)
+            DefaultConstrainedProperty constrained
+            if (existing != null) {
+                constrained = copy(existing, targetRegistry)
+            }
+            else if (ReflectionUtils.findField(beanClass, propertyName) == 
null) {
+                // a getter without a backing field, such as a cross field 
@AssertTrue, is not a property to describe
+                return
+            }
+            else {
+                Class<?> propertyType = 
ClassPropertyFetcher.getPropertyType(beanClass, propertyName)
+                if (propertyType == null) {
+                    return
+                }
+                constrained = new DefaultConstrainedProperty(beanClass, 
propertyName, propertyType, targetRegistry)
+                constrained.order = merged.size() + 1
+            }
+            for (ConstraintDescriptor<?> descriptor in descriptors) {
+                applyConstraint(constrained, descriptor)
+            }
+            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 constraint. Bounds tighten 
the ones already applied rather than
+     * replace them. A composed constraint without an equivalent of its own is 
described through the constraints
+     * composing it, with the attributes it overrides applied.
+     *
+     * @param constrained The constrained property to describe the constraint 
on
+     * @param descriptor The constraint
+     */
+    void applyConstraint(DefaultConstrainedProperty constrained, 
ConstraintDescriptor<?> descriptor) {
+        Annotation annotation = descriptor.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)

Review Comment:
   @DecimalMin('1.5') Integer quantity passes 1.5 into a conversion that 
rejects a fractional exact integer and drops the minimum. @DecimalMax('4.5') 
drops the maximum the same way. Bean Validation still enforces the bound. Round 
an inclusive minimum up and an inclusive maximum down before converting an 
integral property.



-- 
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