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


##########
grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/validation/jakarta/JakartaPersistentEntityValidator.groovy:
##########
@@ -0,0 +1,173 @@
+/*
+ *  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.util.concurrent.ConcurrentHashMap
+import java.util.function.Function
+
+import groovy.transform.CompileStatic
+
+import jakarta.validation.Validator
+import jakarta.validation.groups.Default
+import jakarta.validation.metadata.CascadableDescriptor
+import jakarta.validation.metadata.ContainerElementTypeDescriptor
+import jakarta.validation.metadata.GroupConversionDescriptor
+import jakarta.validation.metadata.PropertyDescriptor
+
+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
+import org.grails.datastore.mapping.model.types.Association
+
+/**
+ * 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 validated groups 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}. An object is validated in the default 
group, and an object associated
+ * through a property marked {@code @Valid} in the groups of the object owning 
it, converted as the
+ * {@code @ConvertGroup} annotations of the property declare, as Bean 
Validation cascades. Any other associated
+ * object is validated in the default group, as it is on its own.
+ * <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 JakartaConstraintsTranslator translator
+    private final ConstraintsEvaluator constraintsEvaluator
+    private final Map<String, ConstrainedProperty> 
describedConstrainedProperties
+    private final Map<List<Class<?>>, Map<String, ConstrainedProperty>> 
validatedConstrainedPropertiesByGroups = new ConcurrentHashMap<>()
+
+    /**
+     * @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)
+        this.constraintsEvaluator = constraintsEvaluator
+        this.translator = new JakartaConstraintsTranslator(this.beanValidator, 
targetClass)
+        Map<String, ConstrainedProperty> validated = 
super.getValidatedConstrainedProperties(NO_VALIDATION_HINTS)
+        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
+    }
+
+    /**
+     * The constraints of the block, with the nullability of the properties an 
annotation of the groups the object
+     * is validated in checks for null left to Bean Validation.
+     */
+    @Override
+    protected Map<String, ConstrainedProperty> 
getValidatedConstrainedProperties(Object[] hints) {
+        List<Class<?>> groups = groupsOf(hints)
+        if (groups.isEmpty()) {
+            return super.getValidatedConstrainedProperties(hints)
+        }
+        return validatedConstrainedPropertiesByGroups.computeIfAbsent(groups, 
{ List<Class<?>> key ->
+            Map<String, ConstrainedProperty> evaluated = 
constraintsEvaluator.evaluate(targetClass)
+            translator.delegateNullabilityToBeanValidation(evaluated, 
key.toArray(new Class<?>[0]))
+            return Collections.unmodifiableMap(evaluated)
+        } as Function<List<Class<?>>, Map<String, ConstrainedProperty>>)
+    }
+
+    /**
+     * Validates the Bean Validation annotations of the object in the groups 
it is validated in, without cascading,
+     * as the GORM validator cascades.
+     */
+    @Override
+    protected void validateBeyondConstraints(Object obj, Errors errors, 
Object[] hints) {
+        beanValidator.validate(obj, errors, false, groupsOf(hints).toArray(new 
Class<?>[0]))
+    }
+
+    /**
+     * The groups an object associated through a property marked {@code 
@Valid} is validated in: the groups of the
+     * object owning it, converted as the {@code @ConvertGroup} annotations of 
the property declare. An object
+     * associated through any other property is validated in the default 
group, as it is on its own.
+     */
+    @Override
+    protected Object[] getAssociationValidationHints(Association association, 
Object[] hints) {
+        CascadableDescriptor cascaded = 
findCascadedDescriptor(association.name)
+        if (cascaded == null) {
+            return NO_VALIDATION_HINTS
+        }
+        Map<Class<?>, Class<?>> conversions = new HashMap<>()
+        for (GroupConversionDescriptor conversion in 
cascaded.groupConversions) {
+            conversions.put(conversion.from, conversion.to)
+        }
+        List<Class<?>> groups = groupsOf(hints) ?: Collections.<Class<?>> 
singletonList(Default)
+        List<Class<?>> converted = groups.collect { Class<?> group -> 
conversions.get(group) ?: group }

Review Comment:
   groupsOf does not expand a GroupSequence. A conversion of Default to 
OrderedChecks, whose sequence is A, is passed through as OrderedChecks. A later 
ConvertGroup from A to B never matches, so the grandchild is checked in A and a 
violation in B is accepted. Expand sequence members before matching 
conversions, and keep the sequence order.



##########
grails-datamapping-validation/src/main/groovy/grails/gorm/validation/PersistentEntityValidator.groovy:
##########
@@ -237,14 +314,20 @@ class PersistentEntityValidator implements 
CascadingValidator, ConstrainedEntity
             otherSide = association.getInverseSide()
         }
 
-        Map associatedConstrainedProperties
+        Object[] hints = CASCADE_HINTS.get()

Review Comment:
   validatedObjects already contains this child when a second association 
reaches it, so cascadeValidationToOne returns at the contains check before 
these hints are computed. Two associations of the same child, converting 
Default to different groups, validate only the first group. A child that fails 
only the second group is saved. Track the groups the child was validated in, 
not only the instance.



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