Tuesday, September 15, 2009

Grails type conversion outside of domain objects

In a recent Grails project, we needed a way to do type conversion outside of merely marshalling values for a domain object (i.e. outside of, say, automatically loading up the properties of a domain object from request parameters, a job which Grails does really well). Specifically, we needed to convert the String representation of a Date into an actual Date object for use in our domain object's findByLastUpdatedGreaterThan() method. Initially, we hoped the method would automatically convert the type for us, but instead it threw exceptions saying "No signature of method . . . is applicable for argument types" because it couldn't handle a String instead of a Date.

When Grails converts types for marshalling request parameters to domain objects, behind the scenes, it uses a GrailsDataBinder instance, converting values in the process of loading up the object's properties. And while the GrailsDataBinder implements Spring's TypeConverter and gives us access to the convertIfNecessary() method, it also requires a domain object instance in its constructor so it has a target to bind values to. Since all we were doing is simple type conversion, we wanted a different way.

Enter Spring's SimpleTypeConverter, a class that really only does one thing: convert from a String type to an instance of a target class. SimpleTypeConverter allows us to register custom property editors, the backbone of the type conversion process, so the first task was to implement a PropertyEditor (extending PropertyEditorSupport) that would let us take a series of date formats and parse them to a Date. Thanks to some very detailed information in a response to this StackOverflow Question, implementing our CustomDateEditor was fairly easy:

import java.beans.PropertyEditorSupport
import java.text.SimpleDateFormat
import java.text.ParseException

* A property editor that allows for handling many different
* date formats at once (as opposed to Spring's default CustomDateEditor
* which only handles one date format configured at instantiation)
* This class is modified from information found at:
* http://stackoverflow.com/questions/963922/grails-date-unmarshalling
class CustomDateEditor extends PropertyEditorSupport {

private final List< String> formats

CustomDateEditor() {
// in our production code, we get the formats from our Grails config
def formats = [ "yyyyMMdd-HHmmss", "yyyy-mm-dd HH:mm:ss.S" ]
List< String> formatList = new ArrayList< String>(formats.size())
formats.each { format ->
formatList << format.toString() // Force String values (eg. for GStrings)
this.formats = Collections.unmodifiableList(formatList)

def String getAsText() {
return this.value.toString()

def void setAsText(String s) throws IllegalArgumentException {
if (s) {
formats.each { format ->
def df = new SimpleDateFormat(format)
try {
this.value = df.parse(s)
} catch (ParseException e) {
// Ignore

Next, we wanted to make this custom Date editor available across the application, so we added it to our resources.groovy Spring config:

beans = {
customDateEditor(CustomDateEditor) { bean ->
bean.singleton = false // editors store state so are not thread-safe

Now anywhere we wanted to do type conversion, all our classes needed to do was create a new SimpleTypeConverter, get a CustomDateEditor instance from Spring, and register that editor with the SimpleTypeConverter:

// grailsApplication auto-wired by Grails
def editor = grailsApplication.mainContext.getBean("customDateEditor")
def converter = new SimpleTypeConverter()
converter.registerCustomEditor( Date.class, editor )

With the converter set up and our custom Date editor registered, it was now a simple matter of doing the conversion:

// NOTE: production code determines type dynamically from field being queried
def type = Date.class
def converted = converter.convertIfNecessary( value, type )

If we had wanted to make our custom editor available for marshalling values to a domain object, we could have easily created an PropertyEditorRegistrar object and configured it via Spring so Grails would automatically use our property editor (as described in the StackOverflow Question above). But, again, all we wanted was fairly straightforward type conversion outside of a domain object. The combination of Spring's SimpleTypeConverter and our CustomDateEditor gave us that.


Mike Miller said...

Thanks, cool entry.

Just a quick question, I just started blogging on blogspot and don't like all the extra space down the left-hand side and it looks like you figured out how to get rid of that, or at least to minimize it. Care to share how you accompolished that?

blog said...

Definitely a good post. Small point, though: once you've set up customDateEditor in resources.groovy, won't it get injected automatically into services and controllers, allowing you to have

def customDateEditor

instead of

def editor = grailsApplication.mainContext.getBean("customDateEditor")


Jeffrey Erikson said...

Absolutely, you could do that! However, I have this personal preference to only inject the "big things" that are used all over the object (like "grailsApplication"). In this case in our production code from which I took the example, the custom date editor is only used in this one place and only if it's required, so I prefer to go fetch it if I need it.

blog said...

Do you prefer selectively injecting stuff because you think it improves readability? Just curious.

Ah, I'd assumed that when you said "Now anywhere we wanted to do type conversion..." you meant it was being used in multiple places. That does make a difference.


Juan said...

Very interesting entry, I have the same problem and I was looking for a solution.

Unfortunately I can't understand the whole process, would you mind to tell us where the last two pieces of code should go?



Jeffrey Erikson said...

They go in whatever class/method you want to do the type conversion in. Wherever you want to do type conversion, you can set up the converter and register the custom editor. If you're going to be using it a lot, you might want to make it available across the application, but in our case, we only needed it in one place, so we kept it local.