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
Hey John,
ReplyDeleteIs there a simple way to make this work inside a unit test?
Well, I should add,
ReplyDeletethat 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?
Hey
ReplyDeletesorry 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
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
ReplyDeleteThanks buddy, you really helped us!
ReplyDeleteIs 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"
ReplyDelete?
Thanks,
Hey,
ReplyDeleteLooking 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
Wow, this was so useful! I want to buy you a beer.
ReplyDeleteDuvel please... :)
ReplyDeleteFound a much simpler way:
ReplyDeleteuser.errors.allErrors.each {
println message(error: it)
}
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
ReplyDeleteMaybe grails 2.0 has some magic?
I have tried
ReplyDeleteuser.errors.allErrors.each {
println message(error: it)
}
It works in grails 1.3.7 without any additional manipulations
nice one, it does.
ReplyDeleteMust 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!
Thanks for posting this. You'd think Grails would make some of this a little easier.
ReplyDeleteno worries greg, it might be worth trying
Deleteuser.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!
Love you J
ReplyDeletethanks , nice blog
ReplyDeleteThanks, I was wondering this exact thing and this did the trick!
ReplyDeletePerfect - just what I needed!
ReplyDeleteThanks...
It’s very useful blog your site thanks provide blog comments
ReplyDeleteMyindiatourism
All visitor use to check domain
Thanks! This looks great!
ReplyDeleteHello John, I am not sure if the instructions shown above work with Grails 2.4.4. I tried to follow this but couldn't get it to work. I figured it out via another blog. I have created my own blog entry with more details about how validation messages work and how to setup custom validation messages for your domain object: http://www.javawithravi.com/custom-validation-message-in-grails/
ReplyDeleteThank you,
Ravi Hasija
Awesome! I'm not surprised that its no longer working as this post is very old! Thanks for posting your link! Excellent blog post btw.
DeleteThank you John for the kind words! I appreciate it! :)
ReplyDeleteI appreciate your effort for providing such detail information with screen shot. Thank you for sharing it with us
ReplyDeleteVPS Hosting India | VPS Hosting Plans | VPS Hosting companies India
AM SANDRA FROM CANADA, THANKS TO DR ONIHA WHO HELP ME BRING MY HUSBAND BACK, MY HUSBAND LEFT ME WITH THREE KIDS, FOR ANOTHER YOUNG GIRL, FOR OVER TWO YEARS, I TRIED ALL I COULD TO SETTLED OUR DIFFRENCES, BUT IT YIELDED NO RESULT, I WAS THE ONE TAKING CARE OF THE CHILDREN ALONE, UNTIL ONE DAY, I CAME IN CONTACT WITH SOME ARTICLES ONLINE, CONTAINING HOW DR ONIHA HAS HELP SO MANY LOVERS AND FAMILY REUNION AND REUNIT AGAIN, AND I DECIDED TO CONTACT HIM, AND HE CAST HIS SPELL ON MY HUSBAND, WITHIN FIVE DAYS, MY HUSBAND RAN BACK HOME, AND WAS BEGGING ME AND THE KIDS FOR FORGIVENESS, IN CASE YOU ARE PASSING THROUGH SIMILAR PROBLEMS, AND YOU WANTS TO CONTACT DR ONIHA, YOU CAN REACH HIM VIA HIS CONTACT NUMBER, ON CALL OR WHATSAP +2347089275769 OR EMAIL DRONIHASPELL@YAHOO.COM
ReplyDelete