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

19 comments:

  1. Hey John,
    Is there a simple way to make this work inside a unit test?

    ReplyDelete
  2. Well, I should add,
    that you have already figured out. I am trying to do this same thing inside my unit tests and am having trouble getting access to the messageSource. I was hoping that possibly you had already accomplished this? Am I lucky?

    ReplyDelete
  3. Hey

    sorry haven't actually done that in a unit test yet... Just integration tests, I imagine this has been done though... Check the mailing list archive on nabble... The grails user list.
    John

    ReplyDelete
  4. MessageSource is not injected in Unit Test. See http://grails.1312388.n4.nabble.com/How-do-I-access-inject-messageSource-from-unit-test-td1695170.html#a1695170

    ReplyDelete
  5. Thanks buddy, you really helped us!

    ReplyDelete
  6. Is there a way to set the errors programmatically rather have set up the constraints ? In other words could I do something like users.errors.add "Passwords dont match"
    ?

    Thanks,

    ReplyDelete
  7. Hey,

    Looking at the MessageSource interface, I do not see any set methods but I imagine this has to be possible.

    I have never set an error message outside of the internationalization file so maybe you can ask the user list??? It is a pretty response list!

    Sorry I cant be of more help!

    John

    ReplyDelete
  8. Wow, this was so useful! I want to buy you a beer.

    ReplyDelete
  9. Found a much simpler way:

    user.errors.allErrors.each {
    println message(error: it)
    }

    ReplyDelete
  10. pretty sure that only prints the Property [{0}] of class [{1}] with value [{2}] must be unique type message and not the i18n defined messages

    Maybe grails 2.0 has some magic?

    ReplyDelete
  11. I have tried

    user.errors.allErrors.each {
    println message(error: it)
    }

    It works in grails 1.3.7 without any additional manipulations

    ReplyDelete
  12. nice one, it does.

    Must be a shortcut method that was added, so you can subsitute

    log.error messageSource.getMessage(it, null)

    with log.error message(error: it)

    Sweet. I will try some more tests and update.

    Thanks!

    ReplyDelete
  13. Thanks for posting this. You'd think Grails would make some of this a little easier.

    ReplyDelete
    Replies
    1. no worries greg, it might be worth trying

      user.errors.allErrors.each {log.error message(error : it)}

      as opposed to

      user.errors.allErrors.each {log.error messageSource.getMessage(it, null)}

      to save on some code clutter, I haven't tested this with the meta programming yet, but if you feel adventurous, might be worth a shot!

      Delete
  14. Thanks, I was wondering this exact thing and this did the trick!

    ReplyDelete
  15. Perfect - just what I needed!

    Thanks...

    ReplyDelete