Extension to MarkupBuilder to Support Dot-Notation

I'm working with a system that accepts XML as input to it's services, and there are times that the XML I need to generate is several layers deep, like so.

<session id="$sessionId">
  <data>
    <policy>
      <line>
        <Type>WorkersCompensation</Type>
        <linestate>
          <exposure>
            <Type>PremiumSelection</Type>
            <sValue>AssignedRisk</sValue>
          </exposure>
        </linestate>
      </line>
    </policy>
  </data>
</session>

Thankfully Groovy provides a nice MarkupBuilder that simplifies generating this XML. It looks like this.

StringWriter writer = new StringWriter()
new MarkupBuilder(writer).session {
  data {
    policy {
      line {
        Type('WorkersCompensation')
        linestate {
          exposure { Type('PremiumSelection'); sValue('AssignedRisk') }
        }
      }
    }
  }
}
println writer.toString()

But what I didn't particular like about this is all of the extra '}' at the end of the function. This is way easier than implementing it in Java, but there's still just too much code for my liking. What I really want to write is this.

println TestDataBuilder.build {
  session.data.policy.line {
    Type('WorkersCompensation')
    linestate.exposure { Type('PremiumSelection'); sValue('AssignedRisk') }
  }
}

The big difference is the use of dot-notation to handle nesting when there's no additional elements contained within the element. I also wrapped the declaration of the StringWriter into a convenience method since in my scenario I'm always looking to get a String back. So, that's the goal, what does the code to do this look like?

Well, I created two classes, the first TestDataBuilder just abstracts the creation of the StringWriter and passing back the string, the more complicated class, the DeepMarkupBuilder (not sure if that's a good name... naming is hard) extends the Groovy MarkupBuilder class and adds functionality to look for 'missing properties', aka, the tags I want to generate that don't have an associated closure.


class TestDataBuilder {
  public static String build(Closure data) {
    Writer writer = new StringWriter()
    DeepMarkupBuilder builder = new DeepMarkupBuilder(writer)
    data.setDelegate(builder)
    data.run()
    return writer.toString()
  }
}


class DeepMarkupBuilder extends MarkupBuilder {
  Stack openTags = [] as Stack
  Map closeableTags = [:]
 
  public DeepMarkupBuilder(Writer writer) {
    super(writer);
  }
  def propertyMissing(String property) {
    openTags.add(property)
    super.invokeMethod(property, null)
    return this
  }
  public Object invokeMethod(String name, Object args) {
    if (args && args[0] instanceof Closure) {
      closeableTags[name] = openTags
      openTags = [] as Stack
    }
    return super.invokeMethod(name, args)
  }
  protected void nodeCompleted(Object parent, Object node) {
    if (!openTags || openTags.peek() != node) {
      super.nodeCompleted(parent, node)
   
      Stack closeable = closeableTags[node]
      while (closeable) {
        super.nodeCompleted(null, closeable.pop())
      }
    }
  }
}

The propertyMissing function is called by Groovy whenever a property is referenced that doesn't exist in the object. The 'session', 'data', and 'policy' parts of the XML will be treated as properties, so the class creates them as open tags. The openTags check in the nodeCompleted method ensures that they are not closed as soon as they are encountered.

The 'line' attribute will be treated as method call since it accepts a Closure as an argument. The invokeMethod method will handle this attribute, and most of the existing super implementation of the method can be used. However, we need to track what tags were opened prior to this tag, so that when this tag is closed, those parents tags can be closed as well. So, the stack of 'closeableTags' is stored off in a map related to this node. Then when the nodeCompleted method is called, those closeable tags, can be closed.

Simple implementation to achieve that concise XML generation I was looking for.

Post a Comment