jamesfredley commented on code in PR #16624:
URL: https://github.com/apache/grails-core/pull/16624#discussion_r4420902803
##########
grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/validation/jakarta/GormValidatorAdapter.groovy:
##########
@@ -60,6 +74,147 @@ class GormValidatorAdapter extends SpringValidatorAdapter
implements CascadingVa
}
}
+ /**
+ * Validates the object in the given groups, or in the default group when
there are none
+ *
+ * @param obj The object to validate
+ * @param errors The errors to report the violations to
+ * @param cascade Whether validation cascades to the persistent
associations of the entity through the
+ * properties marked {@code @Valid}; it cascades through the other
properties marked {@code @Valid} either way
+ * @param groups The groups to validate
+ * @since 8.0.1
+ */
+ void validate(Object obj, Errors errors, boolean cascade, Class<?>[]
groups) {
+ CASCADE_VALIDATION.set(cascade)
+ try {
+ validate(obj, errors, (Object[]) groups)
+ } finally {
+ CASCADE_VALIDATION.remove()
+ }
+ }
+
+ /**
+ * Validates the object in the given groups as {@link #validate(Object,
Errors, boolean, Class[])} does,
+ * reporting only the violations of the constraints declared in one of the
reported groups. The provider
+ * validates a group together with the groups it extends; when validation
cascades from an object validated in
+ * a group whose extended groups a {@code @ConvertGroup} converts, the
associated object is validated in the
+ * group alone, so the violations of the converted groups are left out.
The reported groups are the ones of the
+ * object; for an object the provider cascades to through the properties
marked {@code @Valid}, they are converted
+ * along the path of the violation as the {@code @ConvertGroup}
annotations of those properties declare, the way
+ * the provider converts the groups it validates in, see {@link
#isReported}.
+ *
+ * @param obj The object to validate
+ * @param errors The errors to report the violations to
+ * @param cascade Whether validation cascades to the persistent
associations of the entity
+ * @param groups The groups to validate
+ * @param reportedGroups The groups whose violations to report, null to
report every violation
+ * @since 8.0.1
+ */
+ void validate(Object obj, Errors errors, boolean cascade, Class<?>[]
groups, Set<Class<?>> reportedGroups) {
+ if (reportedGroups == null) {
+ validate(obj, errors, cascade, groups)
+ return
+ }
+ CASCADE_VALIDATION.set(cascade)
+ try {
+ Set<ConstraintViolation<Object>> reported = new LinkedHashSet<>()
+ for (ConstraintViolation<Object> violation :
thisValidator.validate(obj, groups)) {
+ if (isReported(violation, reportedGroups)) {
+ reported.add(violation)
+ }
+ }
+ processConstraintViolations(reported, errors)
+ } finally {
+ CASCADE_VALIDATION.remove()
+ }
+ }
+
+ /**
+ * Whether the violation is of a constraint declared in one of the
reported groups, as they apply to the bean
+ * holding the constraint. The provider cascades to a bean through a
property marked {@code @Valid} once for each
+ * group it validates the owner in, converting the group as the {@code
@ConvertGroup} annotations of the property
+ * declare: a converted group is validated as the group it converts to,
with the groups that one extends or
+ * sequences, and an unconverted group alone. The reported groups follow
the same conversions along the path of
+ * the violation, from the validated object to the bean holding the
constraint, so a violation the provider found
+ * through a conversion of a reported group is reported, and one it found
through a group that is not reported is
+ * not. The default group of a bean stands for the groups of the sequence
redefining it, if any.
+ */
+ private boolean isReported(ConstraintViolation<?> violation, Set<Class<?>>
reportedGroups) {
+ List<Path.Node> nodes = violation.propertyPath.toList()
+ Class<?> beanClass = violation.rootBeanClass
+ Set<Class<?>> groups = reportedGroups
+ // the last node is the constrained element, the nodes before it the
properties the provider cascaded through
+ for (int i = 0; i < nodes.size() - 1; i++) {
+ Path.Node node = nodes.get(i)
+ if (node.kind != ElementKind.PROPERTY) {
+ continue
+ }
+ PropertyDescriptor property =
thisValidator.getConstraintsForClass(beanClass).getConstraintsForProperty(node.name)
+ CascadableDescriptor cascaded =
JakartaConstraintsTranslator.cascadedDescriptorOf(property)
+ if (cascaded != null) {
+ groups = convertedGroupsOf(withDefaultGroupsOf(groups,
beanClass), cascaded.groupConversions)
+ }
+ beanClass = i == nodes.size() - 2
Review Comment:
An owner in `Extended` converts `Base` to `Converted`. Its transient `@Valid
Object details` holds a runtime object whose nested `@Valid` property converts
`Extended` to `DetailsChecks`, and that nested value fails `@NotBlank(groups =
DetailsChecks)`. The filter walks the declared `Object`, misses the conversion,
and drops the violation. Follow the runtime bean.
##########
grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/validation/jakarta/GormValidatorAdapter.groovy:
##########
@@ -60,6 +74,147 @@ class GormValidatorAdapter extends SpringValidatorAdapter
implements CascadingVa
}
}
+ /**
+ * Validates the object in the given groups, or in the default group when
there are none
+ *
+ * @param obj The object to validate
+ * @param errors The errors to report the violations to
+ * @param cascade Whether validation cascades to the persistent
associations of the entity through the
+ * properties marked {@code @Valid}; it cascades through the other
properties marked {@code @Valid} either way
+ * @param groups The groups to validate
+ * @since 8.0.1
+ */
+ void validate(Object obj, Errors errors, boolean cascade, Class<?>[]
groups) {
+ CASCADE_VALIDATION.set(cascade)
+ try {
+ validate(obj, errors, (Object[]) groups)
+ } finally {
Review Comment:
A custom constraint that validates another domain instance removes the
enclosing `CASCADE_VALIDATION=false` flag. The outer validation then skips a
deeper transient cascade, so `outer.details.leaf.name` is reported on its own
and disappears when the outer constraint runs. Restore the previous flag in
both new overloads.
##########
grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/validation/jakarta/JakartaPersistentEntityValidator.groovy:
##########
@@ -0,0 +1,339 @@
+/*
+ * 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.GroupConversionDescriptor
+
+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 constrained with Jakarta Bean Validation
annotations: 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. The GORM validator cascades to the persistent
associations as it does for any
+ * entity, and validates an annotated associated entity both ways through
{@link #validateBeyondConstraints};
+ * Bean Validation cascades only through the other properties marked {@code
@Valid}, such as a transient one. An
+ * object is validated in the default group, or in the groups given as
validation hints to
+ * {@link #validate(Object, Errors, Object...)}, 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, see {@link
#getAssociationValidationHints}. Any other associated object is
+ * validated in the default group, as it is on its own. An associated object
reached through two associations is
+ * validated once in each distinct list of groups. A group sequence is
validated group by group, each group over
+ * the object and the objects it cascades to, and stops at the first group
with a violation among them, see
+ * {@link #validateAssociationsAndBeyond}.
+ * <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 {
+
+ private static final List<Class<?>> DEFAULT_GROUP_LIST =
Collections.<Class<?>> singletonList(Default)
+
+ /**
+ * 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<Object>, 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)
Review Comment:
A `@GroupSequenceProvider` changes the default groups per instance.
Nullability is decided here from the static annotations only, so `nullable:
false` plus `@Null(groups = Retired)` is rejected even when the provider's
sequence includes `Retired` and Bean Validation accepts the value. Use the
groups for this instance before deciding which nullability constraints stay
active.
--
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]