Grails custom XML Marshaller

Most Grails XML/JSON marshalling discussions you find on the web (as of July 2011) show mainly simple examples, and therefore most code ends up in BootStrap.groovy. Which is perfect if you just want to tweak the XML marshalling a little bit.

We had the task to migrate a Spring MVC application that used a lot of Spring OXM to Grails. We had a lot of custom XStream converters that needed to be ported, and we wanted to switch to Grails internal marshalling. There are really nice XML/JSON marshalling features in Grails and with this post we want to expose them to the reader.

quoting Luke Daley (Jan 12th 2011)

It turns out that there is a completely undocumented set of features built-in to the converter mechanism that will let you do what you want. You are going to have to read through the source though and work it out.

Most of the great features seem to be implemented by Siegfried Puchbauer around Grails 1.1

This blog post focuses on custom XML marshallers, but Grails XML and JSON converters have very similar implementation patterns.

Different kinds of marshalling

There are many alternatives how to register a custom marshaller in Grails.

You can have it really nice and simple with a closure in BootStrap.groovy like described in  JSON Rendering Your Classes (12 Aug 2009 by Rob Fletcher)

JSON.registerObjectMarshaller(DateTime) {
    return it?.toString("yyyy-MM-dd'T'HH:mm:ss'Z'")
}

You can define your object marshaller right in BootStrap.groovy like replied by Siegfried Puchbauer in this Thread (29th of May 2009)

XML.registerObjectMarshaller Pogo, { pogo, xml ->
  xml.attribute 'id', pogo.id
  xml.build {
    name(pogo.name)
  }
}

It’s possible to use a Map to select which field is exposed in JSON/XML as shown in this post Rendering JSON in Grails: Part 3 Customise your JSON with Object Marshallers (February 15, 2010 by David Bower)

JSON.registerObjectMarshaller(Person) {
  def returnArray = [:]
  returnArray['name'] = it.name
  returnArray['addrs'] = it.addresses
  return returnArray
}

Or you can implement your own ObjectMarshaller classes like described in this post. One reason to make the extra step and implement custom ObjectMarshallers is that it allows for better unit testing, especially with the new upcoming testing improvments in Grails 1.4.0.M1 and 2.0.0.M1

Beside of better unit test support we also required different marshalling for different situations

  1. publicApi – For our public API we did not want to expose to much sensitive information like workflow and internal owners. This is our “publicApi” configuration
  2. contributorApi – For registered content contributors we wanted to expose more, but also reduce the number of Ajax calls, so the user interface can be responsive
  3. default Grails behavior – We wanted to preserve the default DomainClassMarshaller so similar to dynamic HTML scaffolding we would also have a way to always expose a domain object as XML and JSON.

To fulfill this requirement we can use the XML.createNamedConfig() method in BootStrap.groovy

XML.createNamedConfig('publicApi') {
  it.registerObjectMarshaller(new FooPublicMarshaller())
}
XML.createNamedConfig('contributorApi') {
  it.registerObjectMarshaller(new FooContributorMarshaller())
}

and then call them in our controllers be using the XML.use() method. So a simplified controller might look like this:

class FooMarshallController {

  def show = {
    Foo foo = Foo.get(params.id)
    withFormat {
      // ...
      xml {
        XML.use('publicApi') {
          render foo as XML
        }
      }
    }
  }

  // Spring-Security annotation like: @Secured(['IS_AUTHENTICATED_REMEMBERED'])
  def showContributor = {
    Foo foo = Foo.get(params.id)
    withFormat {
      // ...
      xml {
        XML.use('contributorApi') {
          render foo as XML
        }
      }
    }
  }

  // Spring-Security annotation like: @Secured(['ROLE_ADMIN'])
  def showAdmin = {
    Foo foo = Foo.get(params.id)
    withFormat {
      // ...
      xml {
        render foo as XML
      }
    }
  }

  // ... other actions
}

ConverterConfiguration in BootStrap.groovy

It’s possible to configure further setting in BootSptrap.groovy. As a note: if we want to inherit all the nice default marshallers that come with Grails we must pass a delegate into the constructor of DefaultConverterConfiguration. The delegate will be of type ChainedConverterConfiguration and seems to be a performance optimized immutable implementation.

For instance to turn on XML pretty printing we could add configuration like this:

DefaultConverterConfiguration converterConfiguration =
  new DefaultConverterConfiguration<XML>(ConvertersConfigurationHolder.getConverterConfiguration(XML))
converterConfiguration.setPrettyPrint true
ConvertersConfigurationHolder.setDefaultConfiguration XML, converterConfiguration

Domain Model
To demonstrate the inner workings of marshalObject() we will use a very simple domain model like this:

class Foo {
  String name
  Integer age
  Set bars
  static hasMany = [bars:Bar]
}

class Bar {
  String name
  static belongsTo = Foo
}

// in BootStrap.groovy we add something like

def firstBar = Bar.findOrSaveWhere(name: 'firstBar') //Grails 2.0.0.M1
def secondBar = Bar.findOrSaveWhere(name: 'secondBar') //Grails 2.0.0.M1
def foo= Foo.findOrCreateWhere(name: 'someName', age: 99)  //Grails 2.0.0.M1
foo.addToBars(firstBar)
foo.addToBars(secondBar)
foo.save()

The marshalObject() method

Lets start with a very simple marshaller where we decide that we only want to expose the name. No database id, no age and no Bar instances.

class FooContributorMarshaller implements ObjectMarshaller {

  public boolean supports(Object object) {
    return object instanceof Foo
  }

  public void marshalObject(Object object, XML converter) {
    Foo pogo = object as Foo
    converter.chars pogo.name
  }
}

output:

<foo>someName</foo>

NameAwareMarshaller interface

If we like to control the element/tag name from within our custom marshaller we can implement the NameAwareMarshaller interface. This will be more useful later once we use the convertAnother() method

class FooContributorMarshaller implements ObjectMarshaller, NameAwareMarshaller {

  public boolean supports(Object object) {
    return object instanceof Foo
  }

  public void marshalObject(Object object, XML converter) {
    Foo pogo = object as Foo
    converter.chars pogo.name
  }

  String getElementName(Object o) {
    return 'specialTagName'
  }
}

output:

<specialTagName>someName</specialTagName>

startNode(), attribute(), chars(), end()

We can add arbitrary elements and attributes by using some of the methods from the XML converter class

public void marshalObject(Object object, XML converter) {
  Foo foo = object as Foo

  converter.attribute 'id', foo.id.toString()
  converter.attribute 'theAge', foo.age.toString()

  converter.startNode 'name'
  converter.chars foo.name
  converter.end()

  converter.startNode 'guiInstructions'
  converter.chars 'someGuiInstructions'
  converter.end()
}

output:

theAge="99">
  <name>someName</name>
  <guiInstructions>someGuiInstructions</guiInstructions>
</foo>

builder

There is also a small XML builder embeded in grails.converters.XML

public void marshalObject(Object object, XML converter) {
  Foo foo = object as Foo

  converter.build {
    someElement someAttribute: foo.age, foo.name
  }
}

output:

<foo>
  <someElement someAttribute="99">someName
</foo>

converter.convertAnother collection

Grails can handle the marshalling of typical types like List, Set and Map

public void marshalObject(Object object, XML converter) {
  Foo foo = object as Foo

  converter.startNode 'name'
  converter.chars foo.name
  converter.end()

  Map map = [firstKey: 'firstValue', secondKey: 'secondValue']
  converter.startNode 'locallyDefinedMap'
  converter.convertAnother map
  converter.end()
}

output:

<foo>
  <name>someName</name>
  <locallyDefinedMap>
    <entry key="firstKey">firstValue</entry>
    <entry key="secondKey">secondValue</entry>
  </locallyDefinedMap>
</foo>

converter.convertAnother domain relationships

Grails can handle the marshalling of domain relationships by itself

public void marshalObject(Object object, XML converter) {
  Foo foo = object as Foo

  converter.startNode 'name'
  converter.chars foo.name
  converter.end()

  if (foo.bars) {
    converter.startNode 'bars'
    converter.convertAnother foo.bars
    converter.end()
  }
}

output:

<foo>
  <name>someName</name>
  <bars>
    <bar id="2">
      <name>secondBar</name>
    </bar>
    <bar id="1">
      <name>firstBar</name>
    </bar>
  </bars>
</foo>

converter.convertAnother domain relationships (custom)

But we can also customize the way how we want to marshall domain relationships. Instead of line 12 with the delegation to converter.convertAnother(), we could also create custom markup. For instance if we don’t want to expose database ids or other fields. Or if we want to expose more, like public labels so the client can consume the XML easier.

public void marshalObject(Object object, XML converter) {
  Foo foo = object as Foo

  converter.startNode 'name'
  converter.chars foo.name
  converter.end()

  if (foo.bars) {
    converter.startNode 'setOfBars'
    foo.bars.each {
      converter.startNode 'customElementNameOfBar'
      converter.convertAnother it
      converter.end()
    }
    converter.end()
  }
}

output:

<foo>
  <name>someName</name>
  <setOfBars>
    <customElementNameOfBar id="2">
      <name>secondBar</name>
    <!--<span class="hiddenSpellError" pre=""-->customElementNameOfBar>
    <customElementNameOfBar id="1">
      <name>firstBar</name>
    </customElementNameOfBar>
  </setOfBars>
</foo>

New Grails 1.4.0.M1 / 2.0.0.M1 unit tests for controllers

We can now test the controller with the new test features of Grails 1.4.0.M1 / Grails 2.0.0.M1 as introduced by Peter Ledbrook on June 7th 2011 in a real Grails unit test.

so if we decide to end up with a custom marshaller like this:

public void marshalObject(Object object, XML converter) {
  Foo foo = object as Foo

  converter.attribute 'id', foo.id.toString()
  converter.attribute 'theAge', foo.age.toString()

  converter.startNode 'name'
  converter.chars foo.name
  converter.end()

  if (foo.bars) {
    converter.startNode 'customBarsElement'
    converter.convertAnother foo.bars
    converter.end()
  }
}

Through the FooMarshallController it will output XML like this:

<foo id="1" theAge="88">
  <name>nameInUnitTest</name>
  <customBarsElement>
    <bar id="1">
      <name>firstBar</name>
    </bar>
    <bar id="2">
      <name>secondBar</name>
    </bar>
  </customBarsElement>
</foo>

And we can write a Grails unit test like this

@TestFor(FooMarshallController)
@Mock([Foo, Bar])
class FooMarshallControllerTests {

  void testShowContributor() {

    // register the marshaller because BootStrap.groovy is not triggered in a unit test
    XML.createNamedConfig('contributorApi') {
      it.registerObjectMarshaller(new FooContributorMarshaller())
    }

    // set up some mock domain objects
    Foo foo = new Foo(name: 'nameInUnitTest', age: 88)
    foo.addToBars(new Bar(name: 'firstBar'))
    foo.addToBars(new Bar(name: 'secondBar'))
    assert foo.validate()
    foo.save()

    // trigger the controller
    params.id = foo.id
    response.format = 'xml'
    controller.showContributor()

    // some assertions for Foo
    assert response.xml.@id == foo.id
    assert response.xml.@theAge == 88.toString()
    assert response.xml.name.size() == 1
    assert response.xml.name[0] == 'nameInUnitTest'

    // some assertions for the Bar domain relationship
    assert response.xml.customBarsElement.size() == 1
    assert response.xml.customBarsElement.bar.size() == 2

    assert response.xml.customBarsElement.bar.find { it.name == 'firstBar'}
  }
}
About these ads

One response to “Grails custom XML Marshaller

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Follow

Get every new post delivered to your Inbox.

%d bloggers like this: