Loading...

Thursday, February 6, 2014

Grails Goodness: Customize Root Element Name Collections for XML Marshalling

When we convert a List or Set to XML using the Grails XML marshalling support the name of the root element is either <list> or <set>. We can change this name by extending the org.codehaus.groovy.grails.web.converters.marshaller.xml.CollectionMarshaller. We must override the method supports() to denote the type of collection we want to customize the root element name for. And we must override the method getElementName() which returns the actual name of the root element for the List or Set.

Let's first see the default output of a collection of Book domain classes. In a controller we have the following code:

package com.mrhaki.grails.sample

class SampleController {
    def list() {
        // To force 404 when no results are found
        // we must return null, because respond method
        // checks explicitly for null value and 
        // not Groovy truth.
        respond Book.list() ?: null
    }
}

The XML output is:

<?xml version="1.0" encoding="UTF-8"?>
<list>
  <book id="1" version="0">
    <author id="1" />
    <isbn>
      0451169514
    </isbn>
    <numberOfPages>
      1104
    </numberOfPages>
    <title>
      It
    </title>
  </book>
  <book id="2" version="0">
    <author id="1" />
    <isbn>
      0307743683
    </isbn>
    <numberOfPages>
      1472
    </numberOfPages>
    <title>
      The stand
    </title>
  </book>
</list>

To change the element name list to books we add the following code to the init closure in grails-app/app/BootStrap.groovy:

// File: grails-app/conf/BootStrap.groovy
import com.mrhaki.grails.sample.Book
import grails.converters.XML
import org.codehaus.groovy.grails.web.converters.marshaller.xml.CollectionMarshaller

class BootStrap {

    def init = { servletContext ->
        // Register custom collection marshaller for List with Book instances.
        // The root element name is set to books.
        XML.registerObjectMarshaller(new CollectionMarshaller() {
            @Override
            public boolean supports(Object object) {
                // We know there is at least one result, 
                // otherwise the controller respond method
                // would have returned a 404 response code.
                object instance of List && object.first() instance of Book
            }

            @Override
            String getElementName(final Object o) {
                'books'
            }
        })
    }
}

Now when we render a list of Book instances we get the following XML:

<?xml version="1.0" encoding="UTF-8"?>
<books>
  <book id="1" version="0">
    <author id="1" />
    <isbn>
      0451169514
    </isbn>
    <numberOfPages>
      1104
    </numberOfPages>
    <title>
      It
    </title>
  </book>
  <book id="2" version="0">
    <author id="1" />
    <isbn>
      0307743683
    </isbn>
    <numberOfPages>
      1472
    </numberOfPages>
    <title>
      The stand
    </title>
  </book>
</books>

To customize the XML marshaling output for Map collections we must subclass org.codehaus.groovy.grails.web.converters.marshaller.xml.MapMarshaller.

Code written with Grails 2.3.2

4 comments:

Vahid Pazirandeh said...

Nice post as usual Hubert, thank you. Here's what I'm using to create these marshallers for all my domain classes:

// Render XML List with root name of the entity type (e.g., "") rather than default "".
// TODO: Does not handle empty list case (still renders as "").
grailsApplication.domainClasses.each { dc ->
XML.registerObjectMarshaller(new CollectionMarshaller() {
@Override
public boolean supports(Object object) {
return object instanceof List && object.size() && dc.clazz.isInstance(object.first())
}

@Override
String getElementName(final Object o) {
toPlural(dc.logicalPropertyName)
}
})
}

static String toPlural(String singularNoun) {
if (singularNoun.endsWith("y")) {
return "$singularNoun[0..-2]ies"
} else if (singularNoun.endsWith("s")) {
return singularNoun
}
return "${singularNoun}s"
}

Vahid Pazirandeh said...

Formatting got messed up. Here's a link: http://pastebin.com/9xamJr9s

Vahid Pazirandeh said...

Better yet this one handles all Collection types (List, Set, etc): http://pastebin.com/uddpegzG

Vahid Pazirandeh said...

Here's one that handles all element types (String, Integer, etc) not just domain classes. Creates only one marshaller: http://pastebin.com/DHhYmXMh

Post a Comment