Showing posts with label XML. Show all posts
Showing posts with label XML. Show all posts

Sunday, March 22, 2009

Serializing Domain Objects To XML Using Groovy

This post is a continuation from my prior post in which we took serialized data from XML and constructed a complex domain object graph.  In this post, we’ll take these populated in memory object instances of the Customer class and we will serialize them back out to XML format using Groovy and XMLSlurper.  If you haven’t read the previous post on this topic I’d encourage you to stop now and do so before continuing on in order to set the context for this example.

Just as a quick refresher, here are the domain objects for our test (in order to more easily and cleanly support the approach I wanted to take, I converted the Customer domain class to a Groovy class):

Customer class

   1: package org.demo.xmldomain;
   2:  
   3: import groovy.xml.StreamingMarkupBuilder;
   4: import java.util.ArrayList;
   5: import java.util.List;
   6:  
   7: /**
   8:  * A sample customer domain class implemented as a groovy class
   9:  * @author twalters
  10:  */
  11: public class Customer {
  12:  
  13:     int id;
  14:     String firstName;
  15:     String lastName;
  16:     List addresses;
  17:  
  18:     def Customer(){
  19:         addresses = new ArrayList()
  20:     }
  21:  
  22:     /**
  23:      * This method returns a fully formatted name for this customer.
  24:      * @return - a string of the format 'firstName lastName'
  25:      */
  26:     def String returnFullName(){
  27:         if ("".equals(this.getFirstName()) || this.getFirstName() == null)
  28:             return this.getLastName();
  29:  
  30:         if ("".equals(this.getLastName()) || this.getLastName() == null)
  31:             return this.getFirstName();
  32:  
  33:         return this.getFirstName() + " " + this.getLastName();
  34:     }
  35:  
  36:     def String toXML() {
  37:         StreamingMarkupBuilder builder = new StreamingMarkupBuilder();
  38:         def customer = {
  39:             customer(id:this.id){
  40:                 firstName(this.firstName)
  41:                 lastName(this.lastName)
  42:                 addresses {
  43:                     unescaped << this.buildAddressesXML()
  44:                 }
  45:             }
  46:         }
  47:         builder.bind(customer)
  48:     }
  49:  
  50:     def String buildAddressesXML() {
  51:         def result = ""
  52:         this.addresses.each{address ->
  53:             result += address.toXML()
  54:         }
  55:         return result
  56:     }
  57:  
  58: }

Address class

   1: package org.demo.xmldomain
   2: import groovy.xml.StreamingMarkupBuilder
   3:  
   4: /**
   5:  * This is a simple domain model class for a snail mail address.
   6:  * @author twalters
   7:  */
   8: class Address {
   9:     int id
  10:     String addressLine1
  11:     String addressLine2
  12:     String city
  13:     String state
  14:     String postalCode
  15:  
  16:     /**
  17:     * Returns the address formatted as a single line string.  If line1, city, state, or postal code are null,
  18:     * the entire string will return null
  19:     **/
  20:     def String formattedAddress() {
  21:         if (addressLine1 == null || addressLine1 == "" ||
  22:             city == null || city == "" || state == null || state == "" ||
  23:             postalCode == null || postalCode == "")
  24:             return null;
  25:  
  26:         if (addressLine2 != null && addressLine2 != "")
  27:             return addressLine1 + " " + addressLine2 + " " + city + ", " + state + " " + postalCode
  28:         else
  29:             return addressLine1 + " " + city + ", " + state + " " + postalCode
  30:     }
  31:  
  32:     def String toXML() {
  33:         def builder = new StreamingMarkupBuilder()
  34:         def address = {
  35:             address(id:this.id){
  36:                 "line-1"(this.addressLine1)
  37:                 "line-2"(this.addressLine2)
  38:                 city(this.city)
  39:                 state(this.state)
  40:                 "postal-code"(this.postalCode)
  41:             }
  42:         }
  43:         builder.bind(address)
  44:     }
  45: }
  46:  

The other code of interest in supporting this example is the method which drives the whole process (the class it’s in is really not relevant):

   1: public void serializeCustomersToXMLFile() {
   2:     def writer = new FileWriter("D:\\temp\\SerializedAsXML.xml")
   3:     List<Customer> customerList = this.buildCustomersFromXMLFile()
   4:     
   5:     def builder = new StreamingMarkupBuilder().bind {
   6:         unescaped << "<?xml version='1.0' encoding='UTF-8'?>"
   7:         unescaped << '<customers>'
   8:         customerList.each {cust ->
   9:             unescaped << cust.toXML()
  10:         }
  11:         unescaped << '</customers>'
  12:     }
  13:     writer << builder
  14: }

My primary objective here is to take a couple of fully populated customers (including multiple addresses for each), and to serialize them out to produce the same xml format as the xml file I’ve been reading in from my last few posts.  If you haven’t been following along up til now, I tried to make the sample a bit more realistic by ensuring some key differences between my classes and the xml file (difference in names, the use of attributes, element names containing hyphens). 

To serialize the objects, I’m using Groovy’s StreamingMarkupBuilder class.  This class has a bind method which takes a special closure used to specify the format for your output markup.  So, without further ado, the solution I present here works as follows:

For each domain class I added a toXML method which essentially knows how to generate the xml markup to represent itself.  Since a customer domain class also contains zero to many address objects, the customer’s toXML method loops through each address associated with it and calls the toXML method on each address (I use a small helper method to accomplish this).  Finally, the XML I’m striving to generate has some additional tags around the serialized data (<customers></customers> and some general xml information), we need another method to wrap calls to the domain classes toXML method.  This method is presented above (serializeCustomersToXMLFile).  Let’s walk through this method now to see what it’s doing:

On line #3 above, we generate a list of customers fully populated using a method I wrote as part of my last few posts named (buildCustomersFromXMLFile).  To summarize, this method reads data from an XML file and generates a list of fully populated customers in memory using the data in the XML file.  We next create an instance of the StreamingMarkupBuilder and invoke it’s bind method.

The bind method accepts a closure which is used to specify the format of the xml data.  Within this bind method we loop through the list of customers and call the toXML method for each.  In addition to the customer loop within the closure there are a few other things about the builder class that you should be aware of:

If you want to generate an element named ‘customer’ e.g. <customer></customer> you simply add to the closure customer.  If you want to add sub-elements under ‘customer’ you do this by specifying another closure under the primary one e.g. customer {firstName addresses{line-1}}.  If you want to add attributes to your customer tag this is done using syntax which looks like a method constructor.  For example, to generate an id attribute for your customer you could do something like this: customer(id:variable containing the id to write out).

What’s the significance of the name ‘unescaped’ in the example.  As you’ve probably guessed by now, by using this special name, we’re instructing the builder not to escape the values of the string being provided.  Another special name the builder supports is ‘out’.  The purpose of ‘out’ is to tell the builder that you’re passing a closure as opposed to markup to render.

In summary the StreamingMarkupBuilder class provides a simple flexible mechanism for generating xml markup.  It can support highly customized xml and is fairly intuitive to use (not as easy as XmlSlurper but not too hard to wrap your head around).

==================================================================

Based on some feedback from a reader (see comment from John below) I refactored this example a bit to simplify things through the use of the builder interface.  Here’s the same sample application using the new approach:

 

Address.Groovy

   1: package org.demo.xmldomain
   2: import groovy.xml.StreamingMarkupBuilder
   3:  
   4: /**
   5:  * This is a simple domain model class for a snail mail address.
   6:  * @author twalters
   7:  */
   8: class Address implements Buildable {
   9:     int id
  10:     String addressLine1
  11:     String addressLine2
  12:     String city
  13:     String state
  14:     String postalCode
  15:  
  16:     /**
  17:     * Returns the address formated as a single line string.  If line1, city, state, or postal code are null,
  18:     * the entire string will return null
  19:     **/
  20:     def String formattedAddress() {
  21:         if (addressLine1 == null || addressLine1 == "" ||
  22:             city == null || city == "" || state == null || state == "" ||
  23:             postalCode == null || postalCode == "")
  24:             return null;
  25:  
  26:         if (addressLine2 != null && addressLine2 != "")
  27:             return addressLine1 + " " + addressLine2 + " " + city + ", " + state + " " + postalCode
  28:         else
  29:             return addressLine1 + " " + city + ", " + state + " " + postalCode
  30:     }
  31:  
  32:     def void build(GroovyObject builder){
  33:         def address = {
  34:             address(id:this.id){
  35:                 "line-1"(this.addressLine1)
  36:                 "line-2"(this.addressLine2)
  37:                 city(this.city)
  38:                 state(this.state)
  39:                 "postal-code"(this.postalCode)
  40:             }
  41:         }
  42:         address.delegate = builder
  43:         address()
  44:     }
  45:  
  46: }

Customer.Groovy

   1: package org.demo.xmldomain;
   2:  
   3: import groovy.xml.StreamingMarkupBuilder;
   4: import java.util.ArrayList;
   5: import java.util.List;
   6:  
   7: /**
   8:  * A sample customer domain class implemented as a groovy class
   9:  * @author twalters
  10:  */
  11: public class Customer implements Buildable {
  12:  
  13:     int id;
  14:     String firstName;
  15:     String lastName;
  16:     List addresses;
  17:  
  18:     def Customer(){
  19:         addresses = new ArrayList()
  20:     }
  21:  
  22:     /**
  23:      * This method returns a fully formatted name for this customer.
  24:      * @return - a string of the format 'firstName lastName'
  25:      */
  26:     def String returnFullName(){
  27:         if ("".equals(this.getFirstName()) || this.getFirstName() == null)
  28:             return this.getLastName();
  29:  
  30:         if ("".equals(this.getLastName()) || this.getLastName() == null)
  31:             return this.getFirstName();
  32:  
  33:         return this.getFirstName() + " " + this.getLastName();
  34:     }
  35:  
  36:     def void build(GroovyObject builder){
  37:         def customer = {
  38:             customer(id:this.id){
  39:                 firstName(this.firstName)
  40:                 lastName(this.lastName)
  41:                 addresses {
  42:                     this.addresses.each{address ->
  43:                         out << address
  44:                     }
  45:                 }
  46:             }
  47:         }
  48:         customer.delegate = builder
  49:         customer()
  50:     }
  51: }

 

In both of these domain objects, note they both now implement the Buildable interface and also the required build method.

Here’s the revised driver method which generates the XML file and wraps the domain objects with a customers element.

   1: public void serializeCustomersToXMLFile() {
   2:     def writer = new FileWriter("D:\\temp\\SerializedAsXML.xml")
   3:     List<Customer> customerList = this.buildCustomersFromXMLFile()
   4:     
   5:     def builder = new StreamingMarkupBuilder().bind {
   6:         mkp.xmlDeclaration()
   7:         unescaped << '<customers>'
   8:         customerList.each {cust ->
   9:             out << cust
  10:         }
  11:         unescaped << '</customers>'
  12:     }
  13:     writer << builder
  14: }

Also, I updated this code to remove the hard coded xml declaration and replaced it with a call to mkp.xmlDeclaration().  Depending on the consumer of this generated xml you may or may not want to set the encoding type directly rather than letting it default to your local encoding.  I ran this particular example on my windows based laptop and it sets the encoding to: "windows-1252".