Thursday, September 26, 2013

Overriding toString() in Groovy Using Grails' ExtendedProxy

In Groovy, most of the time you can override the behavior of an object instance's method using the object's metaClass property, like to make the following code print "Yes" instead of "true":

def x = true
x.metaClass.toString { -> delegate ? 'Yes' : 'No' }
println x.toString()

But particularly with toString(), there are some cases (documented in GROOVY_2599) where this doesn't work; for example, the following code will still print "true":

def x = true
x.metaClass.toString { -> delegate ? 'Yes' : 'No' }
println "${x}"

To get around this issue for a project on which I was working recently, I used Grails' ExtendedProxy class to wrap other object instances for which I wanted to override the toString() method. The ExtendedProperty class delegates calls to get and set properties on the wrapped object, as well as method invocations. (It extends Groovy's Proxy class, which delegates method invocations only.)

This allowed me to apply some pretty formatting to a few standard Java objects (like to format Date objects with a US-style date format) without choosing between proxying every property/method explicitly or losing the other aspects of the wrapped objects' functionality. To maintain the functionality I wanted, the only other method (other than toString()) that I found I needed to proxy explicitly was asBoolean() (allowing for wrapped collections to behave as falsey when empty).

This was the wrapper class I ended up creating:

class PrettyToStringWrapper extends grails.util.ExtendedProxy {

    /** Wraps only if it makes a difference for the specified object. */
    static Object wrapMaybe(Object o) {
        (
            o instanceof Collection ||
            o instanceof Date ||
            o instanceof Boolean
        ) ? new PrettyToStringWrapper().wrap(o) : o
    }

    /** Proxies truthy and falsey. */
    boolean asBoolean() {
        getAdaptee().asBoolean()
    }

    /** Overrides toString() with pretty implementation. */
    String toString() {
        def wrapped = getAdaptee()

        if (wrapped instanceof Collection)
            return wrapped.toString().replaceAll(/^\[|\]$/, '')

        if (wrapped instanceof Date)
            return wrapped.format('MM/dd/yyyy')

        if (wrapped instanceof Boolean)
            return wrapped ? 'Yes' : 'No'

        return wrapped.toString()
    }

}

And used it like this:

def emptyList = PrettyToStringWrapper.wrapMaybe([])
println "${emptyList ? 'full' : 'empty'} list contains ${emptyList}"
// prints 'empty list contains '

def fullList = PrettyToStringWrapper.wrapMaybe([1, 2, 3])
println "${fullList ? 'full' : 'empty'} list contains ${fullList}"
// prints 'full list contains 1, 2, 3'

def date = PrettyToStringWrapper.wrapMaybe(new Date(0))
println "epoch begins on ${date}"
// prints 'epoch begins on 12/31/1969' (in US timezones)

def yup = PrettyToStringWrapper.wrapMaybe(true)
println "${yup} this is true"
// prints 'Yes this is true'

I added the static wrapMaybe() method to avoid wrapping objects needlessly — one caveat I found to using ExtendedProxy was that it doesn't proxy the dynamic properties of fancier classes which implement Groovy's special propertyMissing() method (propertyMissing() allows those classes to provide properties without declaring them anywhere).

And one other thing to watch out when using the ExtendedProperty class is that you must reference the wrapped object via the getAdaptee() method instead of simply accessing the adaptee property (the adaptee property is defined by the Proxy class). Using the adaptee property results in a call to the wrapper's getProperty() method for the adaptee property, and is delegated by ExtendedProperty to the wrapped object (raising an IllegalArgumentException as the wrapped object won't have an adaptee property); whereas getAdaptee() accesses the wrapper's adaptee property directly, without a call to getProperty().