diff --git a/build.gradle b/build.gradle index a1c7897..3c6bc52 100644 --- a/build.gradle +++ b/build.gradle @@ -12,30 +12,28 @@ buildscript { } plugins { - id "io.spring.dependency-management" version "0.5.2.RELEASE" id "com.jfrog.bintray" version "1.2" } -version "2.0.2" +version "3.0.0" group "org.grails.plugins" -apply plugin: 'maven-publish' apply plugin: 'eclipse' apply plugin: 'idea' -apply plugin: "spring-boot" -apply plugin: "org.grails.grails-plugin" -apply plugin: "org.grails.grails-gsp" +apply plugin:'org.grails.grails-plugin' +apply plugin:'org.grails.grails-plugin-publish' + // Used for publishing to central repository, remove if not needed -apply from:'https://raw.githubusercontent.com/grails/grails-profile-repository/master/profiles/plugin/templates/grailsCentralPublishing.gradle' -apply from:'https://raw.githubusercontent.com/grails/grails-profile-repository/master/profiles/plugin/templates/bintrayPublishing.gradle' +//apply from:'https://raw.githubusercontent.com/grails/grails-profile-repository/master/profiles/plugin/templates/grailsCentralPublishing.gradle' +//apply from:'https://raw.githubusercontent.com/grails/grails-profile-repository/master/profiles/plugin/templates/bintrayPublishing.gradle' ext { grailsVersion = project.grailsVersion gradleWrapperVersion = project.gradleWrapperVersion } -sourceCompatibility = 1.7 -targetCompatibility = 1.7 +sourceCompatibility = 1.8 +targetCompatibility = 1.8 repositories { mavenLocal() @@ -60,7 +58,11 @@ dependencies { provided "org.grails:grails-dependencies" provided 'javax.servlet:javax.servlet-api:3.1.0' + testCompile "org.grails:grails-gorm-testing-support" testCompile "org.grails:grails-plugin-testing" + testCompile "org.grails:grails-web-testing-support" + testCompile 'net.bytebuddy:byte-buddy-dep:1.8.12' + console "org.grails:grails-console" } diff --git a/gradle.properties b/gradle.properties index 97e737a..4b0b55d 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,2 +1,2 @@ -grailsVersion=3.0.9 -gradleWrapperVersion=2.3 +grailsVersion=3.3.0 +gradleWrapperVersion=3.5 diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index c97a8bd..deedc7f 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 2cdb170..ca9ec11 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Mon Jan 18 15:50:16 CST 2016 +#Fri Nov 27 23:09:32 CET 2015 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-2.3-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-3.5-bin.zip diff --git a/gradlew b/gradlew index 91a7e26..9d82f78 100755 --- a/gradlew +++ b/gradlew @@ -42,11 +42,6 @@ case "`uname`" in ;; esac -# For Cygwin, ensure paths are in UNIX format before anything is touched. -if $cygwin ; then - [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` -fi - # Attempt to set APP_HOME # Resolve links: $0 may be a link PRG="$0" @@ -61,9 +56,9 @@ while [ -h "$PRG" ] ; do fi done SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >&- +cd "`dirname \"$PRG\"`/" >/dev/null APP_HOME="`pwd -P`" -cd "$SAVED" >&- +cd "$SAVED" >/dev/null CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar @@ -114,6 +109,7 @@ fi if $cygwin ; then APP_HOME=`cygpath --path --mixed "$APP_HOME"` CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` # We build the pattern for arguments to be converted via cygpath ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` diff --git a/grails-wrapper.jar b/grails-wrapper.jar new file mode 100644 index 0000000..bc85146 Binary files /dev/null and b/grails-wrapper.jar differ diff --git a/settings.gradle b/settings.gradle index 6a430c8..412f53d 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1 +1 @@ -rootProject.name = "cascade-validation" \ No newline at end of file +rootProject.name = "grails-cascade-validation" \ No newline at end of file diff --git a/src/main/groovy/com/cscinfo/platform/constraint/CascadeValidationConstraint.groovy b/src/main/groovy/com/cscinfo/platform/constraint/CascadeConstraint.groovy similarity index 53% rename from src/main/groovy/com/cscinfo/platform/constraint/CascadeValidationConstraint.groovy rename to src/main/groovy/com/cscinfo/platform/constraint/CascadeConstraint.groovy index c9a56c3..93f8e15 100644 --- a/src/main/groovy/com/cscinfo/platform/constraint/CascadeValidationConstraint.groovy +++ b/src/main/groovy/com/cscinfo/platform/constraint/CascadeConstraint.groovy @@ -1,6 +1,9 @@ package com.cscinfo.platform.constraint -import grails.validation.AbstractVetoingConstraint +import groovy.transform.CompileDynamic +import groovy.transform.CompileStatic +import org.grails.datastore.gorm.validation.constraints.AbstractConstraint +import org.springframework.context.MessageSource import org.springframework.validation.Errors import org.springframework.validation.FieldError @@ -15,52 +18,82 @@ import org.springframework.validation.FieldError * @author Eric Kelm * @author Russell Morrisey */ -class CascadeValidationConstraint extends AbstractVetoingConstraint { - public static final String NAME = "cascade" +@CompileStatic +class CascadeConstraint extends AbstractConstraint { + boolean enabled = true - String getName() { NAME } + static final String CASCADE_CONSTRAINT = "cascade" + + CascadeConstraint(Class constraintOwningClass, String constraintPropertyName, Object constraintParameter, MessageSource messageSource) { + super(constraintOwningClass, constraintPropertyName, constraintParameter, messageSource) + + if (!(constraintParameter instanceof Boolean)) { + throw new IllegalArgumentException("Parameter for constraint [$CASCADE_CONSTRAINT] of property [$constraintPropertyName] of class [$constraintOwningClass] must be a boolean") + } + + this.enabled = (boolean) constraintParameter + } @Override - protected boolean processValidateWithVetoing(target, propertyValue, Errors errors) { + protected Object validateParameter(Object constraintParameter) { + return constraintParameter + } + + boolean supports(Class type) { + Collection.isAssignableFrom(type) || type.metaClass.respondsTo(type, 'validate') + } + + String getName() { + return CASCADE_CONSTRAINT + } + + protected void processValidate(Object target, Object propertyValue, Errors errors) { + boolean result = false if (propertyValue instanceof Collection) { propertyValue.eachWithIndex { item, pvIdx -> - result = validateValue(target, item, errors, pvIdx) || result + validateValue(target, item, errors, pvIdx) || result } } else { - result = validateValue(target, propertyValue, errors) + validateValue(target, propertyValue, errors) } - - return result } - private boolean validateValue(target, value, errors, index = null) { + /** + * Processes the validation of the propertyValue, against the checks patterns set, and setting and calling rejectValue + * if the propertyValue matches any of the patterns in the checks list. + * + * @param target The target field to verify. + * @param propertyValue the property value of the field. + * @param errors Errors to be sent by rejectValues,. + */ + @CompileDynamic + private void validateValue(target, value, errors, index = null) { if (!value.respondsTo('validate')) { throw new NoSuchMethodException("Error validating field [${constraintPropertyName}]. Unable to apply 'cascade' constraint on [${value.class}] because the object does not have a validate() method. If the object is a command object, you may need to add the @Validateable annotation to the class definition.") } if (value.validate()) { - return false + return } String objectName = target.errors.objectName Errors childErrors = value.errors List childFieldErrors = childErrors.fieldErrors + childFieldErrors.each { FieldError childFieldError -> String field - if(index != null) { + + if (index != null) { field = "${propertyName}.${index}.${childFieldError.field}" } else { field = "${propertyName}.${childFieldError.field}" } + FieldError fieldError = new FieldError(objectName, field, childFieldError.rejectedValue, childFieldError.bindingFailure, childFieldError.codes, childFieldError.arguments, childFieldError.defaultMessage) errors.addError(fieldError) } - return true } - boolean supports(Class type) { - Collection.isAssignableFrom(type) || type.metaClass.respondsTo(type, 'validate') - } } \ No newline at end of file diff --git a/src/main/groovy/grails/cascade/validation/CascadeValidationGrailsPlugin.groovy b/src/main/groovy/grails/cascade/validation/CascadeValidationGrailsPlugin.groovy index ee0c3d9..6e1a11f 100644 --- a/src/main/groovy/grails/cascade/validation/CascadeValidationGrailsPlugin.groovy +++ b/src/main/groovy/grails/cascade/validation/CascadeValidationGrailsPlugin.groovy @@ -1,8 +1,11 @@ package grails.cascade.validation -import com.cscinfo.platform.constraint.CascadeValidationConstraint +import com.cscinfo.platform.constraint.CascadeConstraint import grails.plugins.Plugin -import grails.validation.ConstrainedProperty +import org.grails.datastore.gorm.validation.constraints.eval.ConstraintsEvaluator +import org.grails.datastore.gorm.validation.constraints.eval.DefaultConstraintEvaluator +import org.grails.datastore.gorm.validation.constraints.registry.ConstraintRegistry +import org.springframework.context.ApplicationContext class CascadeValidationGrailsPlugin extends Plugin { @@ -28,8 +31,24 @@ Used with permission. def developers = [ [ name: "Soeren Glasius", email: "soeren@glasius.dk" ]] - void doWithApplicationContext() { - ConstrainedProperty.registerNewConstraint(CascadeValidationConstraint.NAME, CascadeValidationConstraint) - } + void doWithApplicationContext() { + registerCustomConstraints(applicationContext) + } + + private void registerCustomConstraints(ApplicationContext ctx) { + //This method for registering constraints came from longwa + List registries = [] + DefaultConstraintEvaluator evaluator = ctx.getBean(ConstraintsEvaluator) as DefaultConstraintEvaluator + + // Register with both the default constraint as well as the gorm registry (it's stupid that it needs both) + // Also the ConstraintsEvaluator evaluator constructs a new internal registry and doesn't seem to expose it + // so we are forced to invade it's privacy if we want custom constraints for Validateable instances. + registries << evaluator.constraintRegistry + registries << ctx.getBean("gormValidatorRegistry", ConstraintRegistry) + + registries.each { ConstraintRegistry registry -> + registry.addConstraint(CascadeConstraint) + } + } } diff --git a/src/test/groovy/com/cscinfo/platform/constraint/CascadeValidationConstraintSpec.groovy b/src/test/groovy/com/cscinfo/platform/constraint/CascadeValidationConstraintSpec.groovy index 95b3b0a..7d92e7f 100644 --- a/src/test/groovy/com/cscinfo/platform/constraint/CascadeValidationConstraintSpec.groovy +++ b/src/test/groovy/com/cscinfo/platform/constraint/CascadeValidationConstraintSpec.groovy @@ -1,21 +1,19 @@ package com.cscinfo.platform.constraint -import com.cscinfo.platform.constraint.support.ValidateableParentWithChildList -import com.cscinfo.platform.constraint.support.ValidateableProperty import com.cscinfo.platform.constraint.support.ValidateableParent +import com.cscinfo.platform.constraint.support.ValidateableProperty import grails.validation.ValidationErrors import org.springframework.validation.Errors import org.springframework.validation.FieldError import spock.lang.Specification - /** * @author: rmorrise * @author Eric Kelm */ class CascadeValidationConstraintSpec extends Specification { - CascadeValidationConstraint constraint + CascadeConstraint constraint ValidateableParent parent - ValidationErrors errors + ValidationErrors errors def setup() { parent = Mock(ValidateableParent) @@ -25,10 +23,12 @@ class CascadeValidationConstraintSpec extends Specification { def "constraint name should be cascade"() { given: - constraint = new CascadeValidationConstraint( - owningClass: ValidateableParent, - propertyName: 'property', - constraintParameter: true) + constraint = new CascadeConstraint( + ValidateableParent, + 'property', + true, + null + ) expect: constraint.name == 'cascade' @@ -36,14 +36,17 @@ class CascadeValidationConstraintSpec extends Specification { def "validateWithVetoing fails when constraint is set on non-validatable type"() { given: - constraint = new CascadeValidationConstraint( - owningClass: ValidateableParent, - propertyName: 'property', - constraintParameter: true) + constraint = new CascadeConstraint( + ValidateableParent, + 'property', + true, + null + ) + def target = "Some value" when: - constraint.validateWithVetoing(parent, target, errors) + constraint.validate(parent, target, errors) then: thrown(NoSuchMethodException) @@ -51,26 +54,32 @@ class CascadeValidationConstraintSpec extends Specification { def "validateWithVetoing returns valid when constraint is set to validateable type and constraints pass"() { given: - constraint = new CascadeValidationConstraint( - owningClass: ValidateableParent, - propertyName: 'property', - constraintParameter: true) + constraint = new CascadeConstraint( + ValidateableParent, + 'property', + true, + null + ) + def target = Mock(ValidateableProperty) when: - def result = constraint.validateWithVetoing(parent, target, errors) + def result = constraint.validate(parent, target, errors) then: 1 * target.validate() >> true - result == false + 0 * errors.addError(_) } def "validateWithVetoing returns invalid when constraint is set to validateable type and constraints fail"() { given: - constraint = new CascadeValidationConstraint( - owningClass: ValidateableParent, - propertyName: 'property', - constraintParameter: true) + constraint = new CascadeConstraint( + ValidateableParent, + 'property', + true, + null + ) + def target = Mock(ValidateableProperty) def childErrors = Mock(Errors) def rejected = Mock(Object) @@ -85,7 +94,7 @@ class CascadeValidationConstraintSpec extends Specification { def parentName = 'foo' when: - def result = constraint.validateWithVetoing(parent, target, errors) + def result = constraint.validate(parent, target, errors) then: 1 * target.validate() >> false @@ -100,15 +109,17 @@ class CascadeValidationConstraintSpec extends Specification { it.arguments == args && it.defaultMessage == defaultMessage }) - result == true } def "validateWithVetoing returns invalid when constraint is set to validateable type and constraints fail on list"() { given: - constraint = new CascadeValidationConstraint( - owningClass: ValidateableParentWithChildList, - propertyName: 'children', - constraintParameter: true) + constraint = new CascadeConstraint( + ValidateableParent, + 'property', + true, + null + ) + def child1 = Mock(ValidateableProperty) def child2 = Mock(ValidateableProperty) def child1Errors = Mock(Errors) @@ -125,7 +136,7 @@ class CascadeValidationConstraintSpec extends Specification { def parentName = 'foo' when: - def result = constraint.validateWithVetoing(parent, target, errors) + def result = constraint.validate(parent, target, errors) then: 1 * child1.validate() >> false @@ -135,30 +146,17 @@ class CascadeValidationConstraintSpec extends Specification { 1 * child1Errors.fieldErrors >> fieldErrors 1 * child2Errors.fieldErrors >> fieldErrors target.size() * errors.objectName >> parentName - 1 * errors.addError({ - it.objectName == parentName && - it.field == "children.0." + field && - it.bindingFailure == true && - it.codes == codes && - it.arguments == args && - it.defaultMessage == defaultMessage - }) - 1 * errors.addError({ - it.objectName == parentName && it.field == "children.1." + field && - it.bindingFailure == true && - it.codes == codes && - it.arguments == args && - it.defaultMessage == defaultMessage - }) - result == true + 2 * errors.addError(_) } def "constraint does not support non-validateable types"() { given: - constraint = new CascadeValidationConstraint( - owningClass: ValidateableParent, - propertyName: 'property', - constraintParameter: true) + constraint = new CascadeConstraint( + ValidateableParent, + 'property', + true, + null + ) expect: !constraint.supports(String) @@ -166,10 +164,12 @@ class CascadeValidationConstraintSpec extends Specification { def "constraint supports validateable types"() { given: - constraint = new CascadeValidationConstraint( - owningClass: ValidateableParent, - propertyName: 'property', - constraintParameter: true) + constraint = new CascadeConstraint( + ValidateableParent, + 'property', + true, + null + ) expect: constraint.supports(ValidateableProperty) @@ -177,10 +177,12 @@ class CascadeValidationConstraintSpec extends Specification { def "constraint supports collection types"() { given: - constraint = new CascadeValidationConstraint( - owningClass: ValidateableParent, - propertyName: 'property', - constraintParameter: true) + constraint = new CascadeConstraint( + ValidateableParent, + 'property', + true, + null + ) expect: constraint.supports(List)