Versions Compared

Key

  • This line was added.
  • This line was removed.
  • Formatting was changed.
Comment: Migrated to Confluence 4.0

Two principles of Agile development are DRY (don't repeat yourself) and merciless refactoring. Thanks to excellent IDE support it isn't too hard to apply these principles to coding Java and Groovy but it's a bit harder with XML.

The good news is that Groovy's Builder notation can help. Whether you are trying to refactor your Ant build file(s) or manage a family of related XML files (e.g. XML request and response files for testing Web Services) you will find that you can make great advances in managing your XML files using builder patterns.

Section
Column
width5%

Column
width90%

Scenario: Consider we have a program to track the sales of copies of GINA (smile) . Books leave a warehouse in trucks. Trucks contain big boxes which are sent off to various countries. The big boxes contain smaller boxes which travel to different states and cities around the world. These boxes may also contain smaller boxes as required. Eventually some of the boxes contain just books. Either GINA or some potential upcoming Groovy titles. Suppose the delivery system produces XML files containing the items in each truck. We are responsible for writing the system which does some fancy reporting.

Column
width5%

If we are a vigilant tester, we will have a family of test files which allow us to test the many possible kinds of XML files we need to deal with. Instead of having to manage a directory full of files which would be hard to maintain if the delivery system changed, we decide to use Groovy to generate the XML files we need. Here is our first attempt:

Code Block
import groovy.xml.MarkupBuilder

def writer = new StringWriter()
def xml = new MarkupBuilder(writer)
xml.truck(id:'ABC123') {
    box(country:'Australia') {
        box(country:'Australia', state:'QLD') {
            book(title:'Groovy in Action', author:'Dierk König et al')
            book(title:'Groovy in Action', author:'Dierk König et al')
            book(title:'Groovy for VBA Macro writers')
        }
        box(country:'Australia', state:'NSW') {
            box(country:'Australia', state:'NSW', city:'Sydney') {
                book(title:'Groovy in Action', author:'Dierk König et al')
                book(title:'Groovy for COBOL Programmers')
            }
            box(country:'Australia', state:'NSW', suburb:'Albury') {
                book(title:'Groovy in Action', author:'Dierk König et al')
                book(title:'Groovy for Fortran Programmers')
            }
        }
    }
    box(country:'USA') {
        box(country:'USA', state:'CA') {
            book(title:'Groovy in Action', author:'Dierk König et al')
            book(title:'Groovy for Ruby programmers')
        }
    }
    box(country:'Germany') {
        box(country:'Germany', city:'Berlin') {
            book(title:'Groovy in Action', author:'Dierk König et al')
            book(title:'Groovy for PHP Programmers')
        }
    }
    box(country:'UK') {
        box(country:'UK', city:'London') {
            book(title:'Groovy in Action', author:'Dierk König et al')
            book(title:'Groovy for Haskel Programmers')
        }
    }
}

println writer.toString()

There is quite a lot of replication in this file. Lets refactor out two helper methods standardBook1 and standardBook2 to remove some of the duplication. We now have something like this:

Code Block
import groovy.xml.MarkupBuilder

// standard book
def standardBook1(builder) { builder.book(title:'Groovy in Action', author:'Dierk König et al') }
// other standard books
def standardBook2(builder, audience) { builder.book(title:"Groovy for ${audience}") }
def writer = new StringWriter()
def xml = new MarkupBuilder(writer)
xml.truck(id:'ABC123') {
    box(country:'Australia') {
        box(country:'Australia', state:'QLD') {
            standardBook1(xml)
            standardBook1(xml)
            standardBook2(xml, 'VBA Macro writers')
        }
        box(country:'Australia', state:'NSW') {
            box(country:'Australia', state:'NSW', city:'Sydney') {
                standardBook1(xml)
                standardBook2(xml, 'COBOL Programmers')
            }
            box(country:'Australia', state:'NSW', suburb:'Albury') {
                standardBook1(xml)
                standardBook2(xml, 'Fortran Programmers')
            }
        }
    }
    box(country:'USA') {
        box(country:'USA', state:'CA') {
            standardBook1(xml)
            standardBook2(xml, 'Ruby Programmers')
        }
    }
    box(country:'Germany') {
        box(country:'Germany', city:'Berlin') {
            standardBook1(xml)
            standardBook2(xml, 'PHP Programmers')
        }
    }
    box(country:'UK') {
        box(country:'UK', city:'London') {
            standardBook1(xml)
            standardBook2(xml, 'Haskel Programmers')
        }
    }
}

println writer.toString()

Next, let's refactor out a few more methods to end up with the following:

Code Block
import groovy.xml.MarkupBuilder

// define standard book and version allowing multiple copies
def standardBook1(builder) { builder.book(title:'Groovy in Action', author:'Dierk König et al') }
def standardBook1(builder, copies) { (0..<copies).each{ standardBook1(builder) } }
// another standard book
def standardBook2(builder, audience) { builder.book(title:"Groovy for ${audience}") }
// define standard box
def standardBox1(builder, args) {
    def other = args.findAll{it.key != 'audience'}
    builder.box(other) { standardBook1(builder); standardBook2(builder, args['audience']) }
}
// define standard country box
def standardBox2(builder, args) {
    builder.box(country:args['country']) {
        if (args.containsKey('language')) {
            args.put('audience', args['language'] + ' programmers')
            args.remove('language')
        }
        standardBox1(builder, args)
}   }


def writer = new StringWriter()
def xml = new MarkupBuilder(writer)
xml.truck(id:'ABC123') {
    box(country:'Australia') {
        box(country:'Australia', state:'QLD') {
            standardBook1(xml, 2)
            standardBook2(xml, 'VBA Macro writers')
        }
        box(country:'Australia', state:'NSW') {
            [Sydney:'COBOL', Albury:'Fortran'].each{ city, language ->
                standardBox1(xml, [country:'Australia', state:'NSW',
                           city:"${city}", audience:"${language} Programmers"])
    }   }   }
    standardBox2(xml, [country:'USA', state:'CA', language:'Ruby'])
    standardBox2(xml, [country:'Germany', city:'Berlin', language:'PHP'])
    standardBox2(xml, [country:'UK', city:'London', language:'Haskel'])
}

println writer.toString()

This is better. If the format of our XML changes, we will minimise the changes required in our builder code. Similarly, if we need to produce multiple XML files, we can add some for loops, closures or if statements to generate all the files from one or a small number of source files.

We could extract out some of our code into a helper method and the code would become:

Code Block
import groovy.xml.MarkupBuilder

def writer = new StringWriter()
def xml = new MarkupBuilder(writer)
def standard = new StandardBookDefinitions(xml)
xml.truck(id:'ABC123') {
    box(country:'Australia') {
        box(country:'Australia', state:'QLD') {
            standard.book1(2)
            standard.book2('VBA Macro writers')
        }
        box(country:'Australia', state:'NSW') {
            [Sydney:'COBOL', Albury:'Fortran'].each{ city, language ->
                standard.box1(country:'Australia', state:'NSW',
                    city:"${city}", audience:"${language} Programmers")
    }   }   }
    standard.box2(country:'USA', state:'CA', language:'Ruby')
    standard.box2(country:'Germany', city:'Berlin', language:'PHP')
    standard.box2(country:'UK', city:'London', language:'Haskel')
}

println writer.toString()

So far we have just produced the one XML file. It would make sense to use similar techniques to produce all the XML files we need. We can take this in several directions at this point including using GStrings, using database contents to help generate the content or making use of templates.

We won't look at any of these, instead we will just augment the previous example just a little more.
First we will slightly expand our helper class. Here is the result:

Code Block
titleBGColor#D0D9BD
borderStylesolid
titleStandardBookDefinitions.groovy
borderColor#D0D9BD
borderWidth1
import groovy.xml.MarkupBuilder

class StandardBookDefinitions {
    private def builder
    StandardBookDefinitions(builder) {
        this.builder = builder
    }
    def removeKey(args, key) { return args.findAll{it.key != key} }
    // define standard book and version allowing multiple copies
    def book1() { builder.book(title:'Groovy in Action', author:'Dierk König et al') }
    def book1(copies) { (0..<copies).each{ book1() } }
    // another standard book
    def book2(audience) { builder.book(title:"Groovy for ${audience}") }
    // define standard box
    def box1(args) {
        def other = removeKey(args, 'audience')
        builder.box(other) { book1(); book2(args['audience']) }
    }
    // define standard country box
    def box2(args) {
        builder.box(country:args['country']) {
            if (args.containsKey('language')) {
                args.put('audience', args['language'] + ' programmers')
                args.remove('language')
            }
            box1(args)
    }   }
    // define deep box
    def box3(args) {
        def depth = args['depth']
        def other = removeKey(args, 'depth')
        if (depth > 1) {
            builder.box(other) {
                other.put('depth', depth - 1)
                box3(other)
            }
        } else {
            box2(other)
    }   }
    // define deep box
    def box4(args) {
        builder.box(country:'South Africa'){
            (0..<args['number']).each{ book1() }
        }
    }
}

And now we will use this helper class to generate a family of related XML files. For illustrative purposes, we will just print out the generated files rather than actually store the files.

Code Block
import groovy.xml.MarkupBuilder

def writer = new StringWriter()
xml = new MarkupBuilder(writer)
standard = new StandardBookDefinitions(xml)
def shortCountry = 'UK'
def longCountry = 'The United Kingdom of Great Britain and Northern Ireland'
def shortState = 'CA'
def longState = 'The State of Rhode Island and Providence Plantations'
def countryForState = 'USA'

def generateWorldOrEuropeXml(world) {
    xml.truck(id:'ABC123') {
        if (world) {
            box(country:'Australia') {
                box(country:'Australia', state:'QLD') {
                    standard.book1(2)
                    standard.book2('VBA Macro writers')
                }
                box(country:'Australia', state:'NSW') {
                    [Sydney:'COBOL', Albury:'Fortran'].each{ city, language ->
                        standard.box1(country:'Australia', state:'NSW',
                            city:"${city}", audience:"${language} Programmers")
            }   }   }
            standard.box2(country:'USA', state:'CA', language:'Ruby')
        }
        standard.box2(country:'Germany', city:'Berlin', language:'PHP')
        standard.box2(country:'UK', city:'London', language:'Haskel')
    }
}

def generateSpecialSizeXml(depth, number) {
    xml.truck(id:'DEF123') {
        standard.box3(country:'UK', city:'London', language:'Haskel', depth:depth)
        standard.box4(country:'UK', city:'London', language:'Haskel', number:number)
        box(country:'UK') {} // empty box
    }
}

def generateSpecialNamesXml(country, state) {
    xml.truck(id:'GHI123') {
        if (state) {
            box(country:country, state:state){ standard.book1() }
        } else {
            box(country:country){ standard.book1() }
        }
    }
}

generateWorldOrEuropeXml(true)
generateWorldOrEuropeXml(false)
generateSpecialSizeXml(10, 10)
generateSpecialNamesXml(shortCountry, '')
generateSpecialNamesXml(longCountry, '')
generateSpecialNamesXml(countryForState, shortState)
generateSpecialNamesXml(countryForState, longState)
println writer.toString()

This will be much more maintainable over time than a directory full of hand-crafted XML files.

Here is what will be produced:

Code Block
xml
xml
<truck id='ABC123'>
  <box country='Australia'>
    <box state='QLD' country='Australia'>
      <book title='Groovy in Action' author='Dierk König et al' />
      <book title='Groovy in Action' author='Dierk König et al' />
      <book title='Groovy for VBA Macro writers' />
    </box>
    <box state='NSW' country='Australia'>
      <box city='Albury' state='NSW' country='Australia'>
        <book title='Groovy in Action' author='Dierk König et al' />
        <book title='Groovy for Fortran Programmers' />
      </box>
      <box city='Sydney' state='NSW' country='Australia'>
        <book title='Groovy in Action' author='Dierk König et al' />
        <book title='Groovy for COBOL Programmers' />
      </box>
    </box>
  </box>
  <box country='USA'>
    <box state='CA' country='USA'>
      <book title='Groovy in Action' author='Dierk König et al' />
      <book title='Groovy for Ruby programmers' />
    </box>
  </box>
  <box country='Germany'>
    <box city='Berlin' country='Germany'>
      <book title='Groovy in Action' author='Dierk König et al' />
      <book title='Groovy for PHP programmers' />
    </box>
  </box>
  <box country='UK'>
    <box city='London' country='UK'>
      <book title='Groovy in Action' author='Dierk König et al' />
      <book title='Groovy for Haskel programmers' />
    </box>
  </box>
</truck>
<truck id='ABC123'>
  <box country='Germany'>
    <box city='Berlin' country='Germany'>
      <book title='Groovy in Action' author='Dierk König et al' />
      <book title='Groovy for PHP programmers' />
    </box>
  </box>
  <box country='UK'>
    <box city='London' country='UK'>
      <book title='Groovy in Action' author='Dierk König et al' />
      <book title='Groovy for Haskel programmers' />
    </box>
  </box>
</truck>
<truck id='DEF123'>
  <box language='Haskel' city='London' country='UK'>
    <box language='Haskel' city='London' country='UK'>
      <box language='Haskel' city='London' country='UK'>
        <box language='Haskel' city='London' country='UK'>
          <box language='Haskel' city='London' country='UK'>
            <box language='Haskel' city='London' country='UK'>
              <box language='Haskel' city='London' country='UK'>
                <box language='Haskel' city='London' country='UK'>
                  <box language='Haskel' city='London' country='UK'>
                    <box country='UK'>
                      <box city='London' country='UK'>
                        <book title='Groovy in Action' author='Dierk König et al' />
                        <book title='Groovy for Haskel programmers' />
                      </box>
                    </box>
                  </box>
                </box>
              </box>
            </box>
          </box>
        </box>
      </box>
    </box>
  </box>
  <box country='South Africa'>
    <book title='Groovy in Action' author='Dierk König et al' />
    <book title='Groovy in Action' author='Dierk König et al' />
    <book title='Groovy in Action' author='Dierk König et al' />
    <book title='Groovy in Action' author='Dierk König et al' />
    <book title='Groovy in Action' author='Dierk König et al' />
    <book title='Groovy in Action' author='Dierk König et al' />
    <book title='Groovy in Action' author='Dierk König et al' />
    <book title='Groovy in Action' author='Dierk König et al' />
    <book title='Groovy in Action' author='Dierk König et al' />
    <book title='Groovy in Action' author='Dierk König et al' />
  </box>
  <box country='UK' />
</truck>
<truck id='GHI123'>
  <box country='UK'>
    <book title='Groovy in Action' author='Dierk König et al' />
  </box>
</truck>
<truck id='GHI123'>
  <box country='The United Kingdom of Great Britain and Northern Ireland'>
    <book title='Groovy in Action' author='Dierk König et al' />
  </box>
</truck>
<truck id='GHI123'>
  <box state='CA' country='USA'>
    <book title='Groovy in Action' author='Dierk König et al' />
  </box>
</truck>
<truck id='GHI123'>
  <box state='The State of Rhode Island and Providence Plantations' country='USA'>
    <book title='Groovy in Action' author='Dierk König et al' />
  </box>
</truck>

Things to be careful about when using markup builders is not to overlap variables you currently have in scope. The following is a good example

Code Block
import groovy.xml.MarkupBuilder



def book = "MyBook"


def writer = new StringWriter()
def xml = new MarkupBuilder(writer)
xml.shelf() {
	book(name:"Fight Club") {
		
	}
}

println writer.toString()

When run this will actually get the error

Code Block
aught: groovy.lang.MissingMethodException: No signature of method: java.lang.String.call() is applicable for argument types: (java.util.LinkedHashMap, HelloWorld$_run_closure1_closure2) values: {["name":"Fight Club"], 

This is because we have a variable above called book, then we are trying to create an element called book using the markup. Markups will always honor for variables/method names in scope first before assuming something should be interpreted as markup. But wait, we want a variable called book AND we want to create an xml element called book! No problem, use delegate variable.

Code Block
import groovy.xml.MarkupBuilder



def book = "MyBook"


def writer = new StringWriter()
def xml = new MarkupBuilder(writer)
xml.shelf() {
	delegate.book(name:"Fight Club") {
		
	}
}

println writer.toString()