We're using just such a relationship for trees of pages that inherit certain characteristics from their ancestors. In order to validate that our users aren't setting up circular references I implemented the following constraint:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package com.github.robfletcher.grails.validation | |
import org.codehaus.groovy.grails.validation.AbstractConstraint | |
import org.springframework.validation.Errors | |
class AcyclicConstraint extends AbstractConstraint { | |
static final String DEFAULT_MESSAGE_CODE = "default.acyclic.violation.message" | |
static final String NAME = "acyclic" | |
private boolean validateConstraint | |
protected void processValidate(Object target, Object propertyValue, Errors errors) { | |
if (validateConstraint && propertyValue) { | |
if (isCyclic(target, propertyValue)) { | |
def args = [constraintPropertyName, constraintOwningClass, propertyValue] as Object[] | |
rejectValue(target, errors, DEFAULT_MESSAGE_CODE, "${NAME}.violation", args) | |
} | |
} | |
} | |
void setParameter(Object constraintParameter) { | |
if (!(constraintParameter instanceof Boolean)) { | |
throw new IllegalArgumentException("Parameter for constraint [$NAME] of property [$constraintPropertyName] of class [$constraintOwningClass] must be a boolean value") | |
} | |
validateConstraint = constraintParameter | |
super.setParameter(constraintParameter) | |
} | |
boolean supports(Class type) { true } | |
String getName() { NAME } | |
private boolean isCyclic(original, node) { | |
boolean cyclic = false | |
while (node != null) { | |
if (node.id == original.id) { | |
cyclic = true | |
break | |
} | |
node = node."$propertyName" | |
} | |
return cyclic | |
} | |
} |
ConstrainedProperty.registerNewConstraint(AcyclicConstraint.NAME, AcyclicConstraint)
to grails-app/conf/spring/resources.groovy. Using the constraint is as simple as this:class Person { String name Person parent static constraints = { parent acyclic: true } }The constraint can be mixed with others such as
nullable: true
. The really nice thing is that the constraint implementation doesn't reference any of my domain classes directly, meaning it can be re-used in any other domain class.When you're using similar validation logic in multiple classes defining a constraint like this is a much better option than using a validator closure and like many things in Grails it turns out to be pretty easy to implement.