Written by Dariusz Nowak
Software Engineer
Published April 19, 2017

How to make XML-generating code more readable with Kotlin extensions and lambdas

XML-generating code in Kotlin may not be readable enough by default. Thankfully, using Kotlin extension functions and lambdas with receiver, we can improve it significantly.

more-readable-xml-generating-code

Recently, during Kotlin 1.1 Launch Meetup, I gave a short speech about how we used Kotlin’s extension functions and lambda with receivers to make XML generating code more readable. I want to share with you a few ideas I’ve came up with, using Kotlin.

First of all, we want to create XML document from a very big collection of objects. Because of that we want use XML stream API instead of creating a document in memory. We will use javax.xml.XMLStreamWriter.

We want to serialize the following objects:

data class Name(val first: String, val last: String)

data class Account(val bank: String, val number: String, val currency: Currency)

data class Person(val name: Name, val accounts: List<Account>)

We will operate on the tiny data set for the tests:

val USD: Currency = Currency.getInstance("USD")
val EUR: Currency = Currency.getInstance("EUR")

val persons = listOf(
        Person(Name("John", "Doe"),
                listOf(
                        Account("JP Morgan Chase & Co", "000-111-222-333", USD),
                        Account("Goldman Sachs", "000-111-222-444", EUR))),
        Person(Name("Jane", "Doe"),
                listOf(
                        Account("JP Morgan Chase & Co", "000-777-222-333", USD),
                        Account("Goldman Sachs", "000-777-222-444", EUR)))

By doing serialization, this should be your final result:

<?xml version="1.0" ?>
<persons version="1.1" created="2017-04-10T11:03:18.608">
  <person>
    <name>
      <first>John</first>
      <last>Doe</last>
    </name>
    <accounts>
      <account>
        <bank>JP Morgan Chase &amp; Co</bank>
        <number>000-111-222-333</number>
        <currency>USD</currency>
      </account>
      <account>
        <bank>Goldman Sachs</bank>
        <number>000-111-222-444</number>
        <currency>EUR</currency>
      </account>
    </accounts>
  </person>
  <person>
    <name>
      <first>Jane</first>
      <last>Doe</last>
    </name>
    <accounts>
      <account>
        <bank>JP Morgan Chase &amp; Co</bank>
        <number>000-777-222-333</number>
        <currency>USD</currency>
      </account>
      <account>
        <bank>Goldman Sachs</bank>
        <number>000-777-222-444</number>
        <currency>EUR</currency>
      </account>
    </accounts>
  </person>
</persons>

XMLStreamWriter API is rather verbose, as you can see below.

writer.writeStartDocument()
    writer.writeStartElement("persons")
    writer.writeAttribute("version", "1.1")
    writer.writeAttribute("created", LocalDateTime.now().toString())
    persons.forEach { (name, accounts) ->
        writer.writeStartElement("person")
        writer.writeStartElement("name")
        writer.writeStartElement("first")
        writer.writeCharacters(name.first)
        writer.writeEndElement()
        writer.writeStartElement("last")
        writer.writeCharacters(name.last)
        writer.writeEndElement()
        writer.writeEndElement()
        writer.writeStartElement("accounts")
        accounts.forEach { (bank, number, currency) ->
            writer.writeStartElement("account")
            writer.writeStartElement("bank")
            writer.writeCharacters(bank)
            writer.writeEndElement()
            writer.writeStartElement("number")
            writer.writeCharacters(number)
            writer.writeEndElement()
            writer.writeStartElement("currency")
            writer.writeCharacters(currency.currencyCode)
            writer.writeEndElement()
            writer.writeEndElement()
        }
        writer.writeEndElement()
        writer.writeEndElement()
    }
    writer.writeEndElement()
    writer.writeEndDocument()

Thankfully, with a few extension functions accepting lambda with receivers, we can make the code much more readable:

writer.document {
        element("persons") {
            attribute("version", "1.1")
            attribute("created", LocalDateTime.now().toString())
            persons.forEach { (name, accounts) ->
                element("person") {
                    element("name") {
                        element("first", name.first)
                        element("last", name.last)
                    }
                    element("accounts") {
                        accounts.forEach { (bank, number, currency) ->
                            element("account") {
                                element("bank", bank)
                                element("number", number)
                                element("currency", currency.currencyCode)
                            }
                        }
                    }
                }
            }
        }
    }

Interestingly, the code above does the same as the first sample! It calls methods on the writer in the same order. But at the same time it is much easier to read and modify.

Time to see what kind of code let that magic happen!

fun XMLStreamWriter.document(init: XMLStreamWriter.() -> Unit): XMLStreamWriter {
    this.writeStartDocument()
    this.init()
    this.writeEndDocument()
    return this
}

fun XMLStreamWriter.element(name: String, init: XMLStreamWriter.() -> Unit): XMLStreamWriter {
    this.writeStartElement(name)
    this.init()
    this.writeEndElement()
    return this
}

fun XMLStreamWriter.element(name: String, content: String) {
    element(name) {
        writeCharacters(content)
    }
}

fun XMLStreamWriter.attribute(name: String, value: String) = writeAttribute(name, value)

If you need more details on how extension functions and lambda with receiver work, check out the Kotlin reference:

Want to play with the code for yourself? No problem, just click here to find in on my GitHub profile. It contains a few incremental steps leading to the final solution.

Written by Dariusz Nowak
Software Engineer
Published April 19, 2017