Friday, April 9, 2010

Data Validation - using the Hibernate Validator

Hi, Recently I've started to fiddle with a Data Validation problem.
Given a data model represented as an object graph of POJO-s, we should validate it.
The data model should contain some declarative validation rules and the validation infrastructure should get the object graph as an input, traverse it and produce the series of validation errors (if any)

I've figured out that the data validation problem has been addressed by JSR 303
The project Hibernate Validator 4.x is a reference implementation of this spec. So I've decided to investigate ;)

This project contains a fairly good tutorial here
So I've started to use this framework in my project. One thing that I've noticed is that the default message resolution wasn't flexible enough for my needs. Obviously after reading the tutorial, I've come to conclusion that I should define a custom "message interpolator". In this article I want to provide an example of how to use it.

My requirements are:

- I would like to override the default message bundle

- I would like define different messages for the same validated entity. For instance, if I have 2 (or more) different usages for the same data model, then theoretically I would like to show the different messages when validating the same data.

I've created the new project by running maven as the tutorial suggests:

mvn archetype:generate -DarchetypeCatalog=http://repository.jboss.com/maven2/archetype-catalog.xml -DgroupId=mark.test -DartifactId=beanvalidation-test -Dversion=1.0-SNAPSHOT -Dpackage=mark.test

This command generated the project with the following list of dependencies:



Now I'm ready to start my tests.
We will need six classes:

Class DataObject will represent our domain data and this class will have the validation annotation defined on it's getters:



Just pay attention to the annotations. The field 'foo' can't be null and The field 'bar' can't be less than 10

Another class we will need is the ResourceBundle storage.
This class is a message bundle container it contains a matrix of strings. There are two columns in this matrix: the column 0 is a key, the column 1 is a message itself.
In this example I've implemented the resouce bundle that can be created from the given stream (in our case it could be a properties file). One can think about the better design, but its just a simple examlpe...



The loaded bundles should be stored in a library and supplied with the unique identifier that will be accessible from the application. This can be done by the class that represents the bundle library and the enum that represents a unique ID:





And now the most interesting parts - The message interpolator itself and the code that actually makes a validation.




The Main class looks like this:


In the main method I:
- build the resource bundles library
- create the validator (see the 'Bootstraping' chapter of the aforementioned tutorial for addition information)
- create a data object
- validate the data object
- reconfigure the message interpolator so that it will take the messages from another source
- validate the data object again
- once more reconfigure the message interpolator
- validate the data object again

Note that I supplied two property files during the library creation.
Each property file contains the various messages that can be used in the framework.
The keys are predefined and can be found in the hibernate-validator-4.0.2.GA.jar
in the file named ValidationMessages.properties

I've redefined the message I need (obviously for the NotNull and for the Size).
If the message is not found in the message bundle, automatically the default bundle will be used...
So, for example, my main_app_bundle.properties file contains the single line:


javax.validation.constraints.NotNull.message=In my application this value may not be null


(I intentionally don't touch the Size message to keep it default here, In real application I would probably redefine them all)
My my_custom_bundle.properties files contains two lines (again to handle NotNull error in my own way):


javax.validation.constraints.NotNull.message=Yet another 'not null' message example


Now the output:

may not be null
must be greater than or equal to 10
=====================
In my application this value may not be null
must be greater than or equal to 10
=====================
Yet another 'not null' message example
In my application this value must be greater than or equal to 10
=====================

The first section as expected shows message that can be found in the default bundle supplied with the framework.

The second section is produced by the second validation invocation on the same object.
This time you can see that the 'not-null' error has been changed to one found in my properties file but the second message remained unchanged

The third section is the result of the third validation call. Here you can see that both of the messages have been redefined

Since the messages appear in the set, we can't do any assumptions about their order.

I would also like to provide the full list of the available keys and default values (in english):
=====================================================================================
javax.validation.constraints.AssertFalse.message=must be false
javax.validation.constraints.AssertTrue.message=must be true
javax.validation.constraints.DecimalMax.message=must be less than or equal to {value}
javax.validation.constraints.DecimalMin.message=must be greater than or equal to {value}
javax.validation.constraints.Digits.message=numeric value out of bounds (<{integer} digits>.<{fraction} digits> expected)
javax.validation.constraints.Future.message=must be in the future
javax.validation.constraints.Max.message=must be less than or equal to {value}
javax.validation.constraints.Min.message=must be greater than or equal to {value}
javax.validation.constraints.NotNull.message=may not be null
javax.validation.constraints.Null.message=must be null
javax.validation.constraints.Past.message=must be in the past
javax.validation.constraints.Pattern.message=must match "{regexp}"
javax.validation.constraints.Size.message=size must be between {min} and {max}
org.hibernate.validator.constraints.Email.message=not a well-formed email address
org.hibernate.validator.constraints.Length.message=length must be between {min} and {max}
org.hibernate.validator.constraints.NotEmpty.message=may not be empty
org.hibernate.validator.constraints.Range.message=must be between {min} and {max}
=====================================================================================

I hope this post was helpful, feel free to comment

Best regards, Mark Bramnik

2 comments:

  1. Excellent walkthrough!
    two thumbs up buddy.

    Pavel K.

    ReplyDelete
  2. Hi Mark,

    Very nice tutorial step by step. It is very helpful for me to implement in my project without dig out lot of things in validator.

    Thanks,
    Binod Suman

    ReplyDelete