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".

15 comments:

  1. Hi Travis!

    Nice example. Just a couple of comments which might make the process a little simpler.

    Do you know about the Buildable interface?

    This is analogous to the Writeable interface, if an object implements Buildable it tells StreamingMarkupBuilder that it knows how to write itself out as XML. The Buildable interface has a single method "void build(GroovyObject builder)". When you give such an object to StreamingMarkupBuilder it calls build passing the "magic' builder object as a parameter. Typically, if the Buildable object is written in Groovy, the build method executes a closure with builder as its delegate and the closure just uses the normal StreamingMarkupBuilder mechanisms to serialise the object.

    So you would just do out << cust in your example and your build() implementation would just execute your address closure with the delegate set to builder.

    One other small point:

    You use unescaped to generate the XML header and you explicitly set the encoding to UTF-8. This is not best practice. StreamingMarkupBuilder provides the mkp.xmlDeclaration() method which "snifs" the Writer to determine the actual encoding being used and puts the right encoding in the header. It's lots safer to use this method

    Bets Wishes

    John

    ReplyDelete
  2. John,
    Thanks so much for the great feedback! I'll look into your suggestions to learn a bit more about the Buildable interface.

    ReplyDelete
  3. Great articles I know only the basic information of domain object . You have described very clearly. Thanks for sharing.it helping more
    Domain registration

    ReplyDelete
  4. Awesome article. It is so detailed and well formatted that i enjoyed reading it as well as get some new information too.
    Microsoft azure training in Bangalore
    Power bi training in Chennai

    ReplyDelete
  5. Attend The Python training in bangalore From ExcelR. Practical Python training in bangalore Sessions With Assured Placement Support From Experienced Faculty. ExcelR Offers The Python training in bangalore.
    python training in bangalore

    ReplyDelete
  6. Took me time to read all the comments, but I really enjoyed the article. It proved to be Very helpful to me and I am sure to all the commenters here! It’s always nice when you can not only be informed, but also entertained!Digital Marketing Course in vizag

    ReplyDelete
  7. digital marketing course by 360digiTMG is the best one in Hyderabad with quality training and best support, more than 9,000 Students Enrolled and
    you get 9 global certifications from Google, Facebook, YouTube, etc. and guaranteed placement assistance.
    Are You A Non-Technical Person Without Programming Skills, But Still Want To Pursue A Career In IT?
    Digital Marketing doesn’t need any programming skills and familiarity with the internet is sufficient to master the concepts to pursue a successful career
    The demand for Digital Marketing professionals is exceeding the supply and this void is creating ample job opportunities for non-technical graduates.
    We help you to crack a job by doing the handholding from training to placement
    https://360digitmg.com/digital-marketing-training-in-hyderabad

    ReplyDelete
  8. cool stuff you have and you keep overhaul every one of us.

    arttificial intelligence course in faridabad

    ReplyDelete

  9. hello sir,
    thanks for giving that type of information. I am really happy to visit your blog.Leading Solar company in Andhra Pradesh

    ReplyDelete

  10. hello sir,
    thanks for giving that type of information. I am really happy to visit your blog.Leading Solar company in Andhra Pradesh

    ReplyDelete
  11. AI-generated captions and real-time human interpretations are important when it comes to consuming content online as they help make event content more accessible to both diverse audiences and attendees. events looking for vendors and formal invitation letter sample for an event

    ReplyDelete