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