jdaugherty commented on code in PR #16624:
URL: https://github.com/apache/grails-core/pull/16624#discussion_r4413023626
##########
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:
Fixed in 237bf12e59: the visited set now holds the instance together with
the hints it is reached with, so the closing is validated in `OnArchive` after
the opening in `OnPublish`. The hints are computed before the check, and a
plain entity still ends a cycle on the instance alone, as its hints are always
empty. Covered by `JakartaValidatorRegistrySpec` (`SharedChapterBook`, checked
against Hibernate Validator's own result), `PersistentEntityValidatorSpec`
(`HintedDesk`, a lamp reached through two associations and twice in a list) and
the TCK `BeanValidationSpec` (`BeanValidatedDossier`).
##########
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:
Fixed in 237bf12e59: `getAssociationValidationHints` matches the conversions
against the groups the provider validates for each group of the owner,
`JakartaConstraintsTranslator.validatedGroupsOf`: the members of a sequence in
their order, and the groups a group extends. A group none of whose members is
converted is passed on unexpanded, so a sequence keeps stopping at its first
group with a violation; a converted one yields its converted members in order.
Covered by `JakartaValidatorRegistrySpec` (`SequencedBook`: the conversion
through the sequence, and the short-circuit, both checked against Hibernate
Validator's own result) and the TCK `BeanValidationSpec` (`BeanValidatedIssue`).
--
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]