JSR 303 ("Bean Validation") is the upcoming standard for the declaration and evaluation of constraints at Java object models, either by using annotations or XML descriptors.
Having been in the works for quite a while, the Bean Validation API comprises a lot of the features of previously existing validation frameworks such as Hibernate Validator or the OVal framework.
One feature I'm particularly loving about OVal is not part of the new standard API: the possibility to define constraints using scripting or expression languages. This is quite practical, if the validation of one bean property depends on the value of another property of the same bean. Imagine for instance a calendar application, where the start date of a calendar event shall always be earlier than the end date.
Using the Bean Validation API, this problem could be solved by implementing a custom class-level constraint. Unfortunately this requires you to implement a dedicated constraint, whenever such inter-property validation is required.
With the help of a generic script annotation that takes an arbitrary script expression to be evaluated, this effort can be saved (at the cost of losing some compile-time safety, though). By using a simple Groovy expression for instance the constraint mentioned above could be expressed like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
@ScriptAssert(lang = "groovy", script = "_this.startDate.before(_this.endDate)") public class CalendarEvent { private String title; private Date startDate; private Date endDate; public CalendarEvent(String title, Date startDate, Date endDate) { this.title = title; this.startDate = startDate; this.endDate = endDate; } public String getTitle() { return title; } public Date getStartDate() { return startDate; } public Date getEndDate() { return endDate; } } |
But how to implement the ScriptAssert constraint shown in the example? Luckily the JSR 303 spec was designed to make it really easy to create custom constraints, so this is not a big dial. Basically it takes three steps to create a custom constraint:
- define a constraint annotation
- implement a validator class able to check the constraint
- define an error message for the case that the constraint is violated
For the evaluation of script statements, we will leverage the scripting API defined by JSR 223 ("Scripting for the JavaTM Platform"), which is part of the JDK since Java 6. By doing so arbitrary scripting and expression languages, for which a JSR 223 compatible engine exists, can be used in the ScriptAssert annotation (a list of such engines can be found here).
Defining the annotation
At first we have to define the annotation type itself:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
@Target( { TYPE }) @Retention(RUNTIME) @Constraint(validatedBy = ScriptAssertValidator.class) @Documented public @interface ScriptAssert { String message() default "{de.gmorling.beanvalidation.scriptassert}"; Class<?>[] groups() default {}; Class<? extends ConstraintPayload>[] payload() default {}; String lang(); String script(); @Target( { TYPE }) @Retention(RUNTIME) @Documented public @interface List { ScriptAssert[] value(); } } |
The Bean Validation API requires each constraint annotation to define the three attributes
- message (for specifying the key used to resolve the error text in case of constraint violations)
- groups (allowing the constraint to be assigned to validation groups, if required)
- payload (not used by the Bean Validation API itself, but might be used by validation clients to assign custom payloads to a constraint)
Besides these obligatory attributes we define the two attributes
- lang (used to specify the name of this constraint's script language as understood by the JSR 223 ScriptEngineManager)
- script (used to specify the script to be evaluated).
Furthermore we specify, that the constraint shall only be allowed at type declarations (by using the @Target meta-annotation) and that the class ScriptAssertValidator shall be used to evaluate the constraint.
By defining an additional inner annotation @List, which takes an array of ScriptAsserts as value, we follow the JSR's recommendation to provide a way for placing multiple constraints of the same type at one object.
Implementing the constraint validator
Having defined the constraint annotation it's time to implement the accompanying validator class:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
public class ScriptAssertValidator implements ConstraintValidator<ScriptAssert, Object> { private String script; private String languageName; private ScriptEngineManager manager = new ScriptEngineManager(); public void initialize(ScriptAssert constraintAnnotation) { this.script = constraintAnnotation.script(); this.languageName = constraintAnnotation.lang(); } public boolean isValid(Object value, ConstraintValidatorContext constraintValidatorContext) { ScriptEngine engine = manager.getEngineByName(languageName); if (engine == null) { throw new IllegalArgumentException( "No JSR 223 script engine found for language " + languageName); } engine.put("_this", value); try { return Boolean.TRUE.equals(engine.eval(script)); } catch (ScriptException e) { throw new RuntimeException(e); } } } |
In the initialize() method we store the given ScriptAssert annotation's values for the language name and the script contents.
The isValid() method is called by the Bean Validation runtime, whenever the constraint shall be evaluated. First we fetch a JSR 223 ScriptEngine for the given language. In order to make the object to be validated accessible inside the script, we put it into the engine's context under the name "_this". At last we call ScriptEngine's eval() method to evaluate the given script and check, whether the script's output equals to Boolean.TRUE.
Defining the error message
In case the constraint is violated, an error message is required to be put into the ConstraintViolation object created by the Bean Validation runtime. Error messages are defined in a resource file called ValidationMessages.properties (resp. localized derivations of that). So let's create that file with the following content:
1 |
de.gmorling.beanvalidation.scriptassert=Script statement "{script}" didn't evaluate to TRUE. |
Testing the ScriptAssert annotation
Finally it's time to give our ScriptAssert constraint a little test run. Taking the CalendarEvent class from the introductory example, a test could look as follows:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
public class CalendarEventTest { private static Validator validator; private static Date startDate; private static Date endDate; @BeforeClass public static void setUpValidatorAndDates() { validator = Validation.buildDefaultValidatorFactory().getValidator(); startDate = new GregorianCalendar(2009, 8, 20).getTime(); endDate = new GregorianCalendar(2009, 8, 21).getTime(); } @Test public void validCalendarEvent() { CalendarEvent testEvent = new CalendarEvent("Team meeting", startDate, endDate); assertTrue(validator.validate(testEvent).isEmpty()); } @Test public void invalidCalendarEvent() { CalendarEvent testEvent = new CalendarEvent("Team meeting", endDate, startDate); Set<ConstraintViolation<CalendarEvent>> constraintViolations = validator.validate(testEvent); assertEquals(1, constraintViolations.size()); assertEquals( "Script statement \"_this.startDate.before(_this.endDate)\" didn't evaluate to TRUE.", constraintViolations.iterator().next().getMessage()); } } |
In validCalendarEvent() the event's start date is earlier than the end date, resulting in an empty set of constraint violations when validating the object. The opposite case is shown in invalidCalendarEvent(): We retrieve a ConstraintViolation as the event's start and end date are mixed up.
Note that as we are using Groovy as scripting language in the example, we need to have the Groovy library in the classpath (which already contains a JSR 223 compatible script engine implementation). Using Apache Maven, only the following dependency has to be added to your project's pom.xml:
1 2 3 4 5 6 7 8 |
... <dependency> <groupId>org.codehaus.groovy</groupId> <artifactId>groovy-all</artifactId> <version>1.6.4</version> <scope>test</scope> </dependency> ... |
Trying it out yourself
I put all the sources of this post into a little project over at GitHub. A binary can be found in my Maven repository. So if want to give it a test run, first add the repo to your pom.xml/settings.xml:
1 2 3 4 5 6 7 8 |
... <repositories> <repository> <id>http://gunnarmorling-maven-repo.googlecode.com/svn/repo/</id> <url>http://gunnarmorling-maven-repo.googlecode.com/svn/repo/</url> </repository> </repositories> ... |
Then simply add the following dependency to your POM:
1 2 3 4 5 6 7 |
... <dependency> <groupId>de.gmorling</groupId> <artifactId>script-assert</artifactId> <version>0.2</version> </dependency> ... |
Furthermore you need the (runtime) dependency to a Bean Validation implementation (the script-assert project only has a dependency to the API). I recommend to take the reference implementation, so add a dependency to the RI itself as well as a binding for sl4j (used for logging purposes):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
... <dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-validator</artifactId> <version>4.0.0.Beta3</version> <scope>runtime</scope> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-jdk14</artifactId> <version>1.5.6</version> <scope>runtime</scope> </dependency> ... |
As always any feedback on the usefulness of this post and any ideas for further improvement would be highly appreciated.