Monday, February 15, 2010

Retrieve Grails Domain Errors From the 'messageSource'

As you may know, we can add certain constraints to our Grails domains to ensure the integrity of our priceless data.

Suppose we have a simple domain:


class AppUser{
String login
String password

static constraints = {
login unique:'true', blank:'false'
password blank:'false'
}
}


Here, we have set up our domain so that our login not only needs to be a non-blank value but it also has to be unique.

When saving a new user in something like a service, we can take the following approach:


if (!user.save()) {
log.error "Failed to Save User"
user.errors.allErrors.each {log.error it.defaultMessage}
}


So lets just concentrate on the unique constraint. If that constraint is broken, the 'user.save()' will evaluate to false. We then query the domain for its errors using 'users.errors.allErrors' and iterate over each error using the each{} method that is provided by Groovy to all collections. (We can throw an exception or take another action after this if you wish :) )

OK.... so after checking the log, I was a little confused as my 'log.error it.defaultMessage' was printing something cryptic like:


Property [{0}] of class [{1}] with value [{2}] must be unique


Hmmm.... not very nice... this is where the Grails messageSource bean comes into play. The messageSource can be used to translate our cryptic error message to something more digestible..



class MyService{
def messageSource // inject the messageSource

def saveUser(AppUser user) {
if (!user.save()) {
log.error "Failed to Save User"
user.errors.allErrors.each {log.error messageSource.getMessage(it, null)}
}
}
}

Now our error message will be read from messages.properties in grails-app/i18n and look something like:

Property [login] of class [AppUser] with value [john] must be unique

Huzzah.... readable messages.... (Note that in the call to 'messageSource.getMessage(it, null)', the null refers to the locale. We won't cover that here...)

If we check our messages.properties file mentioned previous we can see where this default message came from:


default.not.unique.message=Property [{0}] of class [{1}] with value [{2}] must be unique


The beauty is... we can provide our own message to make the error message even cleaner for the user... we just need to add the following to message.properties :


com.company.project.AppUser.login.unique=Login must be a unique value!


Basically we use the path to our class, followed by the field then followed by the constraint.

OK, so one final thing was bugging me.... do I need to include code in every service method to iterate over the errors and get the correct messages when validation fails, this wouldn't be very dry and rather .... boring....

Hmmm how could I solve this one???? I know... metaprogramming.... c'mon.. stay with me!!!

The aim: add a method to all my domain classes that would 'collect' all the messages for each error and return them in a readable format. The best place to do this is probably the BootStrap class. So lets give it a try:


class BootStrap{
def messageSource //our old friend
def grailsApplication//even older friend :)

def init = {servletContext ->
grailsApplication.domainClasses.each {domainClass ->//iterate over the domainClasses
if (domainClass.clazz.name.contains("com.company.project")) {//only add it to the domains in my plugin

domainClass.metaClass.retrieveErrors = {
def errorString = delegate?.errors?.allErrors?.collect{messageSource.getMessage(it,null)}?.join(' \n')

return errorString
}
}
}
}//end of init
}



Phew... Taking it one step at a time, we first iterate over the applications domain classes. A check is performed to ensure that we only add the method to our own domains (optional) and the method retrieveErrors() is added to the meta class of each of our domains. We then have a fancy (or egotistical) one liner that collects all the errors of the delegate domain, evaluates all their error messages into a List and joins them with a line break. Let's break up the one liner some what:


def init = {servletContext ->
grailsApplication.domainClasses.each {domainClass ->//iterate over the domainClasses
if (domainClass.clazz.name.contains("com.company.project")) {//only add it to the domains in my plugin

domainClass.metaClass.retrieveErrors = {
def list = delegate?.errors?.allErrors?.collect{messageSource.getMessage(it,null)}
return list?.join('\n')
}
}
}
}//end of init



In the end we have replaced :

if (!user.save()) {
log.error "Failed to Save User"
user.errors.allErrors.each {log.error messageSource.getMessage(it, null)}

with:


if (!user.save()) log.error "Failed to save user : ${user.retrieveErrors())}"


One final tidbit.... If you find yourself needing the messageSource outside of a Grails artefact... you can try the following:


MessageSource messageSource = ApplicationHolder.application.mainContext.getBean('messageSource')


The import requred is 'import org.springframework.context.MessageSource'

Hope this helps!

Info:

http://grails.org/doc/latest/ref/Constraints/validator.html
http://grails.org/doc/latest/ref/Constraints/Usage.html