This is an automated email from the ASF dual-hosted git repository.

borinquenkid pushed a commit to branch chore/datamapping-core-validation-cleanup
in repository https://gitbox.apache.org/repos/asf/grails-core.git

commit 9f78cbebf68eaa47a890005d3565c8b494eb9bdf
Author: Walter Duque de Estrada <[email protected]>
AuthorDate: Fri Aug 21 12:31:59 2026 -0500

    Add test coverage and clean up grails-datamapping-core validation package
    
    Adds Spock specs for classes in org.grails.datastore.gorm.validation that
    had no unit tests in their owning module (ConfigurableParameterNameProvider,
    ConstraintViolationUtils, GormValidatorFactoryAdapter, MethodKey,
    MappingContextAwareConstraintFactory, ValidatorRegistries,
    JakartaValidatorRegistry, ValidationEventListener).
    
    Also fixes several static-analysis findings surfaced while reviewing the
    package: an invalid generic cast and dead store in ValidationEventListener,
    a self-bounded generic misuse in JakartaValidatorRegistry, a raw Class[]
    usage and stale/incorrect comment in MethodKey, an unused constructor
    parameter in MethodValidationImplementer's weaveParameterNameData, and
    redundant def modifiers on explicitly-typed generic methods in
    GormValidatorAdapter/GormValidatorFactoryAdapter.
    
    Co-Authored-By: Claude Sonnet 5 <[email protected]>
---
 .../validation/jakarta/GormValidatorAdapter.groovy |   2 +-
 .../jakarta/GormValidatorFactoryAdapter.groovy     |   2 +-
 .../jakarta/JakartaValidatorRegistry.groovy        |   6 +-
 .../gorm/validation/jakarta/MethodKey.java         |   8 +-
 .../MethodValidationImplementer.groovy             |  11 +-
 .../listener/ValidationEventListener.groovy        |   4 +-
 ...MappingContextAwareConstraintFactorySpec.groovy |  55 ++++++++
 .../ConfigurableParameterNameProviderSpec.groovy   |  79 +++++++++++
 .../jakarta/ConstraintViolationUtilsSpec.groovy    |  68 ++++++++++
 .../jakarta/GormValidatorFactoryAdapterSpec.groovy | 149 +++++++++++++++++++++
 .../jakarta/JakartaValidatorRegistrySpec.groovy    |  98 ++++++++++++++
 .../gorm/validation/jakarta/MethodKeySpec.groovy   |  58 ++++++++
 .../listener/ValidationEventListenerSpec.groovy    | 147 ++++++++++++++++++++
 .../support/ValidatorRegistriesSpec.groovy         |  67 +++++++++
 14 files changed, 739 insertions(+), 15 deletions(-)

diff --git 
a/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/validation/jakarta/GormValidatorAdapter.groovy
 
b/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/validation/jakarta/GormValidatorAdapter.groovy
index fce376c31a..e2a996d492 100644
--- 
a/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/validation/jakarta/GormValidatorAdapter.groovy
+++ 
b/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/validation/jakarta/GormValidatorAdapter.groovy
@@ -47,7 +47,7 @@ class GormValidatorAdapter extends SpringValidatorAdapter {
     }
 
     @Override
-    def <T> Set<ConstraintViolation<T>> validate(T object, Class<?>[] groups) {
+    <T> Set<ConstraintViolation<T>> validate(T object, Class<?>[] groups) {
         def constraintViolations = super.validate(object, groups)
         if (object instanceof GormValidateable) {
             def errors = ((GormValidateable) object).getErrors()
diff --git 
a/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/validation/jakarta/GormValidatorFactoryAdapter.groovy
 
b/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/validation/jakarta/GormValidatorFactoryAdapter.groovy
index 47b6f148e5..00f02035fc 100644
--- 
a/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/validation/jakarta/GormValidatorFactoryAdapter.groovy
+++ 
b/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/validation/jakarta/GormValidatorFactoryAdapter.groovy
@@ -87,7 +87,7 @@ class GormValidatorFactoryAdapter implements ValidatorFactory 
 {
     }
 
     @Override
-    def <T> T unwrap(Class<T> type) {
+    <T> T unwrap(Class<T> type) {
         return factory.unwrap(type)
     }
 
diff --git 
a/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/validation/jakarta/JakartaValidatorRegistry.groovy
 
b/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/validation/jakarta/JakartaValidatorRegistry.groovy
index 3c3ed0aaa6..85927de75f 100644
--- 
a/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/validation/jakarta/JakartaValidatorRegistry.groovy
+++ 
b/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/validation/jakarta/JakartaValidatorRegistry.groovy
@@ -73,7 +73,7 @@ class JakartaValidatorRegistry extends 
DefaultValidatorRegistry implements Valid
      *
      * @return The configuration
      */
-    protected Configuration<?> buildConfiguration() {
+    protected Configuration buildConfiguration() {
         MappingContext context = this.mappingContext
         MessageSource ms = messageSource
         return buildConfigurationFor(context, ms)
@@ -86,7 +86,7 @@ class JakartaValidatorRegistry extends 
DefaultValidatorRegistry implements Valid
      * @return The configuration
      */
     static Configuration buildConfigurationFor(MappingContext context, 
MessageSource messageSource) {
-        Configuration<? extends Configuration> validatorConfiguration = 
Validation.byDefaultProvider()
+        Configuration validatorConfiguration = Validation.byDefaultProvider()
                 .configure()
         validatorConfiguration = 
validatorConfiguration.ignoreXmlConfiguration()
         validatorConfiguration = 
validatorConfiguration.traversableResolver(new 
MappingContextTraversableResolver(context))
@@ -164,7 +164,7 @@ class JakartaValidatorRegistry extends 
DefaultValidatorRegistry implements Valid
     }
 
     @Override
-    def <T> T unwrap(Class<T> aClass) {
+    <T> T unwrap(Class<T> aClass) {
         return validatorFactory.unwrap(aClass)
     }
 
diff --git 
a/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/validation/jakarta/MethodKey.java
 
b/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/validation/jakarta/MethodKey.java
index d659fa68cb..c5a4fd54be 100644
--- 
a/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/validation/jakarta/MethodKey.java
+++ 
b/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/validation/jakarta/MethodKey.java
@@ -20,6 +20,7 @@
 package org.grails.datastore.gorm.validation.jakarta;
 
 import java.util.Arrays;
+import java.util.Objects;
 
 /**
  * A method key used to store information about a method
@@ -29,9 +30,9 @@ import java.util.Arrays;
  */
 class MethodKey {
     private final String name;
-    private final Class[] parameterTypes;
+    private final Class<?>[] parameterTypes;
 
-    public MethodKey(String name, Class[] parameterTypes) {
+    public MethodKey(String name, Class<?>[] parameterTypes) {
         this.name = name;
         this.parameterTypes = parameterTypes;
     }
@@ -43,8 +44,7 @@ class MethodKey {
 
         MethodKey methodKey = (MethodKey) o;
 
-        if (name != null ? !name.equals(methodKey.name) : methodKey.name != 
null) return false;
-        // Probably incorrect - comparing Object[] arrays with Arrays.equals
+        if (!Objects.equals(name, methodKey.name)) return false;
         return Arrays.equals(parameterTypes, methodKey.parameterTypes);
     }
 
diff --git 
a/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/validation/jakarta/services/implementers/MethodValidationImplementer.groovy
 
b/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/validation/jakarta/services/implementers/MethodValidationImplementer.groovy
index 4c8eb15433..c11bc9f092 100644
--- 
a/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/validation/jakarta/services/implementers/MethodValidationImplementer.groovy
+++ 
b/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/validation/jakarta/services/implementers/MethodValidationImplementer.groovy
@@ -83,8 +83,9 @@ class MethodValidationImplementer implements ServiceEnhancer {
     }
 
     @Override
+    @SuppressWarnings('unused')
     void implement(ClassNode domainClassNode, MethodNode abstractMethodNode, 
MethodNode newMethodNode, ClassNode targetClassNode) {
-        // no-op
+        // no-op: doesImplement() always returns false, so the framework never 
invokes this
     }
 
     @Override
@@ -108,7 +109,7 @@ class MethodValidationImplementer implements 
ServiceEnhancer {
         Statement body = (Statement) newMethodNode.code
 
         // add parameter name data for the service
-        weaveParameterNameData(domainClassNode, newMethodNode, 
abstractMethodNode)
+        weaveParameterNameData(newMethodNode, abstractMethodNode)
 
         // weave the ValidatedService trait
         AbstractTraitApplyingGormASTTransformation.weaveTraitWithGenerics(
@@ -142,7 +143,9 @@ class MethodValidationImplementer implements 
ServiceEnhancer {
 
         // add a first line to the method body that validates the method
         ArrayExpression argArray = new ArrayExpression(OBJECT_TYPE, 
validateArgsList)
-        String validateMethodName = 
abstractMethodNode.exceptions?.contains(make(ConstraintViolationException)) ? 
'jakartaValidate' : 'validate'
+        boolean throwsConstraintViolationException = 
abstractMethodNode.exceptions != null &&
+                
Arrays.asList(abstractMethodNode.exceptions).contains(make(ConstraintViolationException))
+        String validateMethodName = throwsConstraintViolationException ? 
'jakartaValidate' : 'validate'
         MethodCallExpression validateCall = callThisD(ValidatedService, 
validateMethodName, args(varThis(), varX(methodField), argArray))
         if (body instanceof BlockStatement) {
             ((BlockStatement) body).statements.add(0, stmt(validateCall))
@@ -157,7 +160,7 @@ class MethodValidationImplementer implements 
ServiceEnhancer {
 
     }
 
-    protected void weaveParameterNameData(ClassNode domainClassNode, 
MethodNode newMethodNode, MethodNode abstractMethodNode) {
+    protected void weaveParameterNameData(MethodNode newMethodNode, MethodNode 
abstractMethodNode) {
         ClassNode newClass = newMethodNode.declaringClass
         ModuleNode module = abstractMethodNode.declaringClass.module
         String innerClassName = 
"${newClass.name}\$${ParameterNameProvider.simpleName}"
diff --git 
a/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/validation/listener/ValidationEventListener.groovy
 
b/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/validation/listener/ValidationEventListener.groovy
index b9df7ee32e..c7a7c752b8 100644
--- 
a/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/validation/listener/ValidationEventListener.groovy
+++ 
b/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/validation/listener/ValidationEventListener.groovy
@@ -67,10 +67,10 @@ class ValidationEventListener extends 
AbstractPersistenceEventListener {
                 FlushModeType previousFlushMode = currentSession.flushMode
                 try {
                     currentSession.setFlushMode(FlushModeType.COMMIT)
-                    boolean hasErrors = false
+                    boolean hasErrors
                     if (source instanceof ConnectionSourcesProvider) {
                         def connectionSourceName = 
((ConnectionSourcesProvider) 
source).connectionSources.defaultConnectionSource.name
-                        GormValidationApi validationApi = 
GormEnhancer.findValidationApi((Class<Object>) entityObject.getClass(), 
connectionSourceName)
+                        GormValidationApi validationApi = 
GormEnhancer.findValidationApi((Class<Object>) (Class) entityObject.getClass(), 
connectionSourceName)
                         hasErrors = !validationApi.validate((Object) 
entityObject)
                     }
                     else {
diff --git 
a/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/validation/constraints/MappingContextAwareConstraintFactorySpec.groovy
 
b/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/validation/constraints/MappingContextAwareConstraintFactorySpec.groovy
new file mode 100644
index 0000000000..7d3e5f0c92
--- /dev/null
+++ 
b/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/validation/constraints/MappingContextAwareConstraintFactorySpec.groovy
@@ -0,0 +1,55 @@
+/*
+ *  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.constraints
+
+import grails.gorm.annotation.Entity
+import 
org.grails.datastore.gorm.validation.constraints.builtin.UniqueConstraint
+import 
org.grails.datastore.mapping.keyvalue.mapping.config.KeyValueMappingContext
+import org.grails.datastore.mapping.model.MappingContext
+import org.springframework.context.support.StaticMessageSource
+import spock.lang.Specification
+
+class MappingContextAwareConstraintFactorySpec extends Specification {
+
+    MappingContext mappingContext = new KeyValueMappingContext("test")
+    MappingContextAwareConstraintFactory factory =
+            new MappingContextAwareConstraintFactory(UniqueConstraint, new 
StaticMessageSource(), mappingContext)
+
+    void "builds a constraint when the owning class is a registered persistent 
entity"() {
+        given:
+        mappingContext.addPersistentEntities(FactoryBook)
+        mappingContext.initialize()
+
+        when:
+        def constraint = factory.build(FactoryBook, 'title', true)
+
+        then:
+        constraint instanceof UniqueConstraint
+    }
+
+    void "returns null when the owning class is not a registered persistent 
entity"() {
+        expect:
+        factory.build(String, 'title', true) == null
+    }
+}
+
+@Entity
+class FactoryBook {
+    String title
+}
diff --git 
a/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/validation/jakarta/ConfigurableParameterNameProviderSpec.groovy
 
b/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/validation/jakarta/ConfigurableParameterNameProviderSpec.groovy
new file mode 100644
index 0000000000..6cd458238d
--- /dev/null
+++ 
b/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/validation/jakarta/ConfigurableParameterNameProviderSpec.groovy
@@ -0,0 +1,79 @@
+/*
+ *  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 spock.lang.Specification
+
+class ConfigurableParameterNameProviderSpec extends Specification {
+
+    ConfigurableParameterNameProvider provider = new 
ConfigurableParameterNameProvider()
+
+    void "returns registered parameter names for a method"() {
+        given:
+        def method = Sample.getMethod('greet', String, Integer)
+        provider.addParameterNames('greet', [String, Integer] as Class[], 
['name', 'times'])
+
+        expect:
+        provider.getParameterNames(method) == ['name', 'times']
+    }
+
+    void "returns default arg-prefixed names for an unregistered method"() {
+        given:
+        def method = Sample.getMethod('greet', String, Integer)
+
+        expect:
+        provider.getParameterNames(method) == ['arg0', 'arg1']
+    }
+
+    void "returns registered parameter names for a constructor"() {
+        given:
+        def constructor = Sample.getConstructor(String)
+        provider.addParameterNames('<init>', [String] as Class[], ['name'])
+
+        expect:
+        provider.getParameterNames(constructor) == ['name']
+    }
+
+    void "returns default arg-prefixed names for an unregistered 
constructor"() {
+        given:
+        def constructor = Sample.getConstructor(String)
+
+        expect:
+        provider.getParameterNames(constructor) == ['arg0']
+    }
+
+    void "does not register names when any argument is null"() {
+        when:
+        provider.addParameterNames(null, [String] as Class[], ['name'])
+        provider.addParameterNames('greet', null, ['name'])
+        provider.addParameterNames('greet', [String, Integer] as Class[], null)
+
+        then:
+        provider.getParameterNames(Sample.getMethod('greet', String, Integer)) 
== ['arg0', 'arg1']
+    }
+}
+
+class Sample {
+
+    Sample(String name) {
+    }
+
+    void greet(String name, Integer times) {
+    }
+}
diff --git 
a/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/validation/jakarta/ConstraintViolationUtilsSpec.groovy
 
b/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/validation/jakarta/ConstraintViolationUtilsSpec.groovy
new file mode 100644
index 0000000000..51b4058bbd
--- /dev/null
+++ 
b/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/validation/jakarta/ConstraintViolationUtilsSpec.groovy
@@ -0,0 +1,68 @@
+/*
+ *  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 jakarta.validation.ConstraintViolation
+import jakarta.validation.ConstraintViolationException
+import jakarta.validation.Validation
+import jakarta.validation.Validator
+import jakarta.validation.constraints.NotBlank
+
+import org.springframework.validation.Errors
+
+import spock.lang.Specification
+
+class ConstraintViolationUtilsSpec extends Specification {
+
+    Validator validator = 
Validation.byDefaultProvider().configure().buildValidatorFactory().getValidator()
+
+    void "converts a ConstraintViolationException to Errors using the target's 
simple class name"() {
+        given:
+        def target = new Product(name: '')
+        Set<ConstraintViolation<Product>> violations = 
validator.validate(target)
+        def exception = new ConstraintViolationException(violations)
+
+        when:
+        Errors errors = ConstraintViolationUtils.asErrors(target, exception)
+
+        then:
+        errors.objectName == 'Product'
+        errors.hasFieldErrors('name')
+        errors.getFieldError('name').rejectedValue == ''
+    }
+
+    void "converts a set of ConstraintViolation instances to Errors"() {
+        given:
+        def target = new Product(name: '')
+        Set<ConstraintViolation> violations = validator.validate(target) as 
Set<ConstraintViolation>
+
+        when:
+        Errors errors = ConstraintViolationUtils.asErrors(target, violations)
+
+        then:
+        errors.objectName == 'Product'
+        errors.hasFieldErrors('name')
+        errors.getFieldError('name').rejectedValue == ''
+    }
+}
+
+class Product {
+    @NotBlank
+    String name
+}
diff --git 
a/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/validation/jakarta/GormValidatorFactoryAdapterSpec.groovy
 
b/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/validation/jakarta/GormValidatorFactoryAdapterSpec.groovy
new file mode 100644
index 0000000000..c2a3e0630b
--- /dev/null
+++ 
b/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/validation/jakarta/GormValidatorFactoryAdapterSpec.groovy
@@ -0,0 +1,149 @@
+/*
+ *  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 jakarta.validation.ClockProvider
+import jakarta.validation.ConstraintValidatorFactory
+import jakarta.validation.MessageInterpolator
+import jakarta.validation.ParameterNameProvider
+import jakarta.validation.TraversableResolver
+import jakarta.validation.Validator
+import jakarta.validation.ValidatorContext
+import jakarta.validation.ValidatorFactory
+import jakarta.validation.valueextraction.ValueExtractor
+
+import spock.lang.Specification
+
+class GormValidatorFactoryAdapterSpec extends Specification {
+
+    ValidatorFactory delegate = Mock(ValidatorFactory)
+    GormValidatorFactoryAdapter adapter = new 
GormValidatorFactoryAdapter(delegate)
+
+    void "getValidator wraps the delegate validator in a 
GormValidatorAdapter"() {
+        given:
+        Validator delegateValidator = Mock(Validator)
+
+        when:
+        Validator result = adapter.getValidator()
+
+        then:
+        1 * delegate.getValidator() >> delegateValidator
+        result instanceof GormValidatorAdapter
+        ((GormValidatorAdapter) result).thisValidator.is(delegateValidator)
+    }
+
+    void "delegates simple accessor methods to the wrapped factory"() {
+        given:
+        ClockProvider clockProvider = Mock(ClockProvider)
+        MessageInterpolator messageInterpolator = Mock(MessageInterpolator)
+        TraversableResolver traversableResolver = Mock(TraversableResolver)
+        ConstraintValidatorFactory constraintValidatorFactory = 
Mock(ConstraintValidatorFactory)
+        ParameterNameProvider parameterNameProvider = 
Mock(ParameterNameProvider)
+        delegate.getClockProvider() >> clockProvider
+        delegate.getMessageInterpolator() >> messageInterpolator
+        delegate.getTraversableResolver() >> traversableResolver
+        delegate.getConstraintValidatorFactory() >> constraintValidatorFactory
+        delegate.getParameterNameProvider() >> parameterNameProvider
+
+        expect:
+        adapter.clockProvider.is(clockProvider)
+        adapter.messageInterpolator.is(messageInterpolator)
+        adapter.traversableResolver.is(traversableResolver)
+        adapter.constraintValidatorFactory.is(constraintValidatorFactory)
+        adapter.parameterNameProvider.is(parameterNameProvider)
+    }
+
+    void "unwrap delegates to the wrapped factory"() {
+        given:
+        def unwrapped = new Object()
+        delegate.unwrap(Object) >> unwrapped
+
+        expect:
+        adapter.unwrap(Object).is(unwrapped)
+    }
+
+    void "close delegates to the wrapped factory"() {
+        when:
+        adapter.close()
+
+        then:
+        1 * delegate.close()
+    }
+
+    void "usingContext wraps the delegate context in a GormValidatorContext"() 
{
+        given:
+        ValidatorContext delegateContext = Mock(ValidatorContext)
+        delegate.usingContext() >> delegateContext
+
+        when:
+        ValidatorContext context = adapter.usingContext()
+
+        then:
+        context instanceof GormValidatorFactoryAdapter.GormValidatorContext
+    }
+
+    void "GormValidatorContext#getValidator wraps the delegate context's 
validator"() {
+        given:
+        ValidatorContext delegateContext = Mock(ValidatorContext)
+        Validator delegateValidator = Mock(Validator)
+        delegateContext.getValidator() >> delegateValidator
+        def context = new 
GormValidatorFactoryAdapter.GormValidatorContext(delegateContext)
+
+        when:
+        Validator wrapped = context.getValidator()
+
+        then:
+        wrapped instanceof GormValidatorAdapter
+        ((GormValidatorAdapter) wrapped).thisValidator.is(delegateValidator)
+    }
+
+    void "GormValidatorContext builder methods delegate to the wrapped context 
and return themselves"() {
+        given:
+        ValidatorContext delegateContext = Mock(ValidatorContext)
+        def context = new 
GormValidatorFactoryAdapter.GormValidatorContext(delegateContext)
+        MessageInterpolator messageInterpolator = Mock(MessageInterpolator)
+        TraversableResolver traversableResolver = Mock(TraversableResolver)
+        ConstraintValidatorFactory constraintValidatorFactory = 
Mock(ConstraintValidatorFactory)
+        ParameterNameProvider parameterNameProvider = 
Mock(ParameterNameProvider)
+        ClockProvider clockProvider = Mock(ClockProvider)
+        ValueExtractor valueExtractor = Mock(ValueExtractor)
+
+        when:
+        def r1 = context.messageInterpolator(messageInterpolator)
+        def r2 = context.traversableResolver(traversableResolver)
+        def r3 = context.constraintValidatorFactory(constraintValidatorFactory)
+        def r4 = context.parameterNameProvider(parameterNameProvider)
+        def r5 = context.clockProvider(clockProvider)
+        def r6 = context.addValueExtractor(valueExtractor)
+
+        then:
+        1 * delegateContext.messageInterpolator(messageInterpolator)
+        1 * delegateContext.traversableResolver(traversableResolver)
+        1 * 
delegateContext.constraintValidatorFactory(constraintValidatorFactory)
+        1 * delegateContext.parameterNameProvider(parameterNameProvider)
+        1 * delegateContext.clockProvider(clockProvider)
+        1 * delegateContext.addValueExtractor(valueExtractor)
+        r1.is(context)
+        r2.is(context)
+        r3.is(context)
+        r4.is(context)
+        r5.is(context)
+        r6.is(context)
+    }
+}
diff --git 
a/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/validation/jakarta/JakartaValidatorRegistrySpec.groovy
 
b/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/validation/jakarta/JakartaValidatorRegistrySpec.groovy
new file mode 100644
index 0000000000..269fc6abf1
--- /dev/null
+++ 
b/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/validation/jakarta/JakartaValidatorRegistrySpec.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 jakarta.validation.constraints.NotBlank
+
+import org.springframework.validation.Validator as SpringValidator
+import org.springframework.validation.annotation.Validated
+
+import grails.gorm.annotation.Entity
+import grails.gorm.validation.PersistentEntityValidator
+import org.grails.datastore.mapping.core.connections.ConnectionSourceSettings
+import 
org.grails.datastore.mapping.keyvalue.mapping.config.KeyValueMappingContext
+import org.grails.datastore.mapping.model.MappingContext
+import spock.lang.Specification
+
+class JakartaValidatorRegistrySpec extends Specification {
+
+    MappingContext mappingContext = new KeyValueMappingContext("test")
+    JakartaValidatorRegistry registry
+
+    void setup() {
+        mappingContext.addPersistentEntities(PlainBook, ValidatedBook)
+        mappingContext.initialize()
+        registry = new JakartaValidatorRegistry(mappingContext, new 
ConnectionSourceSettings())
+    }
+
+    void cleanup() {
+        registry.close()
+    }
+
+    void "isAvailable reports that jakarta.validation is on the classpath"() {
+        expect:
+        JakartaValidatorRegistry.isAvailable()
+    }
+
+    void "returns a jakarta backed validator for entities annotated with 
@Validated"() {
+        given:
+        def entity = mappingContext.getPersistentEntity(ValidatedBook.name)
+
+        when:
+        SpringValidator validator = registry.getValidator(entity)
+
+        then:
+        validator instanceof GormValidatorAdapter
+    }
+
+    void "falls back to the default constraint based validator for entities 
without @Validated"() {
+        given:
+        def entity = mappingContext.getPersistentEntity(PlainBook.name)
+
+        when:
+        SpringValidator validator = registry.getValidator(entity)
+
+        then:
+        validator instanceof PersistentEntityValidator
+    }
+
+    void "exposes the underlying jakarta ValidatorFactory operations"() {
+        expect:
+        registry.validator != null
+        registry.usingContext() != null
+        registry.messageInterpolator != null
+        registry.traversableResolver != null
+        registry.constraintValidatorFactory != null
+        registry.parameterNameProvider != null
+        registry.clockProvider != null
+    }
+
+}
+
+@Entity
+class PlainBook {
+    String title
+}
+
+@Entity
+@Validated
+class ValidatedBook {
+    @NotBlank
+    String title
+}
diff --git 
a/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/validation/jakarta/MethodKeySpec.groovy
 
b/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/validation/jakarta/MethodKeySpec.groovy
new file mode 100644
index 0000000000..e91eae3af7
--- /dev/null
+++ 
b/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/validation/jakarta/MethodKeySpec.groovy
@@ -0,0 +1,58 @@
+/*
+ *  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 spock.lang.Specification
+
+class MethodKeySpec extends Specification {
+
+    void "keys with the same name and parameter types are equal and share a 
hash code"() {
+        given:
+        def a = new MethodKey('save', [String, Integer] as Class[])
+        def b = new MethodKey('save', [String, Integer] as Class[])
+
+        expect:
+        a == b
+        a.hashCode() == b.hashCode()
+    }
+
+    void "keys with different names are not equal"() {
+        expect:
+        new MethodKey('save', [String] as Class[]) != new MethodKey('delete', 
[String] as Class[])
+    }
+
+    void "keys with different parameter types are not equal"() {
+        expect:
+        new MethodKey('save', [String] as Class[]) != new MethodKey('save', 
[Integer] as Class[])
+    }
+
+    void "a key is not equal to null or an unrelated type"() {
+        expect:
+        new MethodKey('save', [String] as Class[]) != null
+        new MethodKey('save', [String] as Class[]) != 'save'
+    }
+
+    void "a key is equal to itself"() {
+        given:
+        def key = new MethodKey('save', [String] as Class[])
+
+        expect:
+        key == key
+    }
+}
diff --git 
a/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/validation/listener/ValidationEventListenerSpec.groovy
 
b/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/validation/listener/ValidationEventListenerSpec.groovy
new file mode 100644
index 0000000000..3a2cdf5469
--- /dev/null
+++ 
b/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/validation/listener/ValidationEventListenerSpec.groovy
@@ -0,0 +1,147 @@
+/*
+ *  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.listener
+
+import jakarta.persistence.FlushModeType
+
+import org.springframework.validation.Errors
+
+import org.grails.datastore.gorm.GormValidateable
+import org.grails.datastore.mapping.core.Datastore
+import org.grails.datastore.mapping.core.Session
+import org.grails.datastore.mapping.engine.EntityAccess
+import org.grails.datastore.mapping.engine.event.PostInsertEvent
+import org.grails.datastore.mapping.engine.event.PreInsertEvent
+import org.grails.datastore.mapping.engine.event.PreUpdateEvent
+import org.grails.datastore.mapping.model.PersistentEntity
+import spock.lang.Specification
+import spock.lang.Unroll
+
+class ValidationEventListenerSpec extends Specification {
+
+    Session session = Mock(Session) {
+        getFlushMode() >> FlushModeType.AUTO
+    }
+    Datastore datastore = Mock(Datastore) {
+        getCurrentSession() >> session
+    }
+    ValidationEventListener listener = new ValidationEventListener(datastore)
+
+    PreInsertEvent buildEvent(Object entityObject) {
+        PersistentEntity entity = Stub(PersistentEntity)
+        EntityAccess entityAccess = Stub(EntityAccess) {
+            getEntity() >> entityObject
+        }
+        new PreInsertEvent(datastore, entity, entityAccess)
+    }
+
+    void "supports pre-insert and pre-update event types but not others"() {
+        expect:
+        listener.supportsEventType(PreInsertEvent)
+        listener.supportsEventType(PreUpdateEvent)
+
+        and:
+        !listener.supportsEventType(PostInsertEvent)
+    }
+
+    void "does not cancel the event when the entity is not GormValidateable"() 
{
+        given:
+        PreInsertEvent event = buildEvent(new Object())
+
+        when:
+        listener.onApplicationEvent(event)
+
+        then:
+        !event.isCancelled()
+    }
+
+    @Unroll
+    void "cancels the event when validation is skipped and errors are already 
present: #expectedCancelled"() {
+        given:
+        Errors errors = Stub(Errors) {
+            hasErrors() >> expectedCancelled
+        }
+        GormValidateable entityObject = Mock(GormValidateable) {
+            shouldSkipValidation() >> true
+            getErrors() >> errors
+        }
+        PreInsertEvent event = buildEvent(entityObject)
+
+        when:
+        listener.onApplicationEvent(event)
+
+        then:
+        event.isCancelled() == expectedCancelled
+
+        where:
+        expectedCancelled << [true, false]
+    }
+
+    void "does not cancel the event when validation is skipped and there are 
no errors to report"() {
+        given:
+        GormValidateable entityObject = Mock(GormValidateable) {
+            shouldSkipValidation() >> true
+            getErrors() >> null
+        }
+        PreInsertEvent event = buildEvent(entityObject)
+
+        when:
+        listener.onApplicationEvent(event)
+
+        then:
+        !event.isCancelled()
+    }
+
+    @Unroll
+    void "cancels the event based on the validate() outcome when validation is 
not skipped: #validationResult"() {
+        given:
+        GormValidateable entityObject = Mock(GormValidateable) {
+            shouldSkipValidation() >> false
+            validate() >> validationResult
+        }
+        PreUpdateEvent event = new PreUpdateEvent(datastore, 
Stub(PersistentEntity), Stub(EntityAccess) {
+            getEntity() >> entityObject
+        })
+
+        when:
+        listener.onApplicationEvent(event)
+
+        then:
+        event.isCancelled() == !validationResult
+
+        where:
+        validationResult << [true, false]
+    }
+
+    void "sets the flush mode to COMMIT while validating and restores the 
previous mode afterwards"() {
+        given:
+        GormValidateable entityObject = Mock(GormValidateable) {
+            shouldSkipValidation() >> false
+            validate() >> true
+        }
+        PreInsertEvent event = buildEvent(entityObject)
+
+        when:
+        listener.onApplicationEvent(event)
+
+        then:
+        1 * session.setFlushMode(FlushModeType.COMMIT)
+        1 * session.setFlushMode(FlushModeType.AUTO)
+    }
+}
diff --git 
a/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/validation/registry/support/ValidatorRegistriesSpec.groovy
 
b/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/validation/registry/support/ValidatorRegistriesSpec.groovy
new file mode 100644
index 0000000000..98c982f2ff
--- /dev/null
+++ 
b/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/validation/registry/support/ValidatorRegistriesSpec.groovy
@@ -0,0 +1,67 @@
+/*
+ *  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.registry.support
+
+import grails.gorm.annotation.Entity
+import org.grails.datastore.gorm.validation.jakarta.JakartaValidatorRegistry
+import org.grails.datastore.mapping.core.connections.ConnectionSourceSettings
+import 
org.grails.datastore.mapping.keyvalue.mapping.config.KeyValueMappingContext
+import org.grails.datastore.mapping.model.MappingContext
+import org.grails.datastore.mapping.validation.ValidatorRegistry
+import org.springframework.context.support.StaticMessageSource
+import spock.lang.Specification
+
+class ValidatorRegistriesSpec extends Specification {
+
+    MappingContext mappingContext = new KeyValueMappingContext("test")
+
+    void setup() {
+        mappingContext.addPersistentEntities(RegistryBook)
+        mappingContext.initialize()
+    }
+
+    void "reports that jakarta.validation is available on the classpath"() {
+        expect:
+        ValidatorRegistries.isJakartaValidationAvailable()
+    }
+
+    void "creates a Jakarta backed registry with a default static message 
source"() {
+        when:
+        ValidatorRegistry registry = 
ValidatorRegistries.createValidatorRegistry(mappingContext, new 
ConnectionSourceSettings())
+
+        then:
+        registry instanceof JakartaValidatorRegistry
+    }
+
+    void "creates a registry using the supplied message source"() {
+        given:
+        def messageSource = new StaticMessageSource()
+
+        when:
+        def registry = (JakartaValidatorRegistry) 
ValidatorRegistries.createValidatorRegistry(mappingContext, new 
ConnectionSourceSettings(), messageSource)
+
+        then:
+        registry.messageSource.is(messageSource)
+    }
+}
+
+@Entity
+class RegistryBook {
+    String title
+}

Reply via email to