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 fce376c31ab..e2a996d4922 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 Set> validate(T object, Class[] groups) { + Set> 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 47b6f148e59..00f02035fc9 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 unwrap(Class type) { + T unwrap(Class 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 3c3ed0aaa69..85927de75f0 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 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 unwrap(Class aClass) { + T unwrap(Class 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 d659fa68cba..c5a4fd54bea 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 @@ */ 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 @@ public boolean equals(Object o) { 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 4c8eb154334..c11bc9f0921 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 b9df7ee32ef..c7a7c752b8b 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) entityObject.getClass(), connectionSourceName) + GormValidationApi validationApi = GormEnhancer.findValidationApi((Class) (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 00000000000..7d3e5f0c925 --- /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 00000000000..6cd458238d9 --- /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('', [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 00000000000..51b4058bbda --- /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> 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 violations = validator.validate(target) as Set + + 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 00000000000..c2a3e0630b6 --- /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 00000000000..269fc6abf19 --- /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 00000000000..e91eae3af77 --- /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 00000000000..3a2cdf5469c --- /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 00000000000..98c982f2ff4 --- /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 +}