Loading...

Monday, January 23, 2012

Groovy Goodness: Solve Naming Conflicts with Builders

With Groovy we can use for example the MarkupBuilder or JSONBuilder to create XML or JSON content. The builders are a very elegant way to create the content. Most builders in Groovy use the invokeMethod and getProperty and setProperty methods to dynamically build the contents. But this also means that if we have builder node names that are the same as method or property names in the local context of our code running the builder, that we have a naming conflict. Let's see this with a simple sample:

import groovy.xml.*

def body = []

def writer = new StringWriter()
def builder = new MarkupBuilder(writer)
builder.message {
    body(contentType: 'plain') {
        text 'Simple message'
    }
}

def contents = writer.toString()
println contents

When we run this code we get an error message:

groovy.lang.MissingMethodException: No signature of method: java.util.ArrayList.call() is applicable for argument types: (java.util.LinkedHashMap, xmlmessage$_run_closure1_closure2) values: [[contentType:plain], xmlmessage$_run_closure1_closure2@50502819]
Possible solutions: tail(), wait(), last(), any(), max(), clear()
 at xmlmessage$_run_closure1.doCall(xmlmessage.groovy:7)
 at xmlmessage$_run_closure1.doCall(xmlmessage.groovy)
 at xmlmessage.run(xmlmessage.groovy:6)

Groovy tries to use the ArrayList class of our books variable to execute a call method with two parameters of type LinkedHashMap and closure. So our local variable is found by the builder and the builder tries to use this variable, which results in the shown error.

The following sample shows what happens if we have a local method with the same name as a node in the builder:

import groovy.xml.*

def body(value) {
    println "body contents is $value"
}

def writer = new StringWriter()
def builder = new MarkupBuilder(writer)
builder.message {
    body {
        text 'Simple message'
    }
}

def contents = writer.toString()
println contents

We get the following output if we run this script:

body contents is 
<message>
  <text>Simple message</text>
</message>

Our method with the name body has the same signature and name as the body node in the builder. Our local method is invoked and we see the output of the println statement.

To solve this naming conflict we can change the name of our local variable or method. But this is not always possible or desired. Imagine the method or variable is dynamically added to our class than we cannot change the name. But we can also change our builder syntax slightly to get what we want.

To force our builder to use the builder's code to create the contents we can prepend the node name with delegate. Delegate is the closure context of our builder. This way our builder will not use any already defined variable or method names to create the content.

import groovy.xml.*

def body = []

def writer = new StringWriter()
def builder = new MarkupBuilder(writer)
builder.message {
    delegate.body(contentType: 'plain') {
        text 'Simple message'
    }
}

def contents = writer.toString()
println contents

When we run the script we get the following output:

<message>
  <body contentType='plain'>
    <text>Simple message</text>
  </body>
</message>

And that is the output we want.

If we are using Grails and use the render method to create XML or JSON we must be aware that methods are also dynamically available in a controller. For example the message method from the Grails tag libraries is a method in a controller:

package builder.naming

class SampleController {

    def index() {
        render(contenType: 'text/xml') {
            message {
                content 'Contents'
            }
        }
    }
}

If we invoke this controller we get the following XML output:

<content>Contents</content>

But if we change the code to:

package builder.naming

class SampleController {

    def index() {
        render(contenType: 'text/xml') {
            delegate.message {
                content 'Contents'
            }
        }
    }
}

We get the following output:

<message><content>Contents</content></message>

1 comments:

DB said...

The line:
Groovy tries to use the ArrayList class of our books variable
should read:
Groovy tries to use the ArrayList class of our 'body' variable

Post a Comment