Tuesday, February 5, 2008

Groovy DOMBuilder namespace

I'm building some XML in Groovy and it's pretty rad. However, I had the hardest time figuring out how to make namespaces work the way I thought they should. The examples didn't seem to cover the case I needed. I eventually got it, so in case anyone else is trying to do this, here it is:

Update: this didn't actually work. It looks right when serialized, but the in-memory DOM doesn't have the correct Element implementation. I ended up writing a new NSDOMBuilder class.

import groovy.xml.DOMBuilder
import groovy.xml.QName
import javax.xml.XMLConstants

import org.w3c.dom.Element
import org.joda.time.format.DateTimeFormatter
import org.joda.time.format.ISODateTimeFormat

DateTimeFormatter dateTimeFormatter = ISODateTimeFormat.dateTimeNoMillis()
def builder = DOMBuilder.newInstance()
def now = new DateTime()

Element el = builder.'ebiz:GetInventoryConsumption'(releaseID:'9.0',
xmlns:[(new QName(XMLConstants.XMLNS_ATTRIBUTE_NS_URI, 'xmlns')):'http://www.openapplications.org/oagis/9',
(new QName(XMLConstants.XMLNS_ATTRIBUTE_NS_URI, 'xmlns:ebiz')):'http://www.dis-corp.com/xml/ebusiness/2.0']) {
ApplicationArea() {
Sender(InetAddress.getLocalHost().getHostName())
CreationDateTime(dateTimeFormatter.print(now))
BODID(UUID.randomUUID())
}
DataArea() {
Get() {
Expression()
}
InventoryConsumption() {
'ebiz:InventoryConsumptionHeader'() {
// stuff goes here
}
}
}
}


Update 2. Here's the actual code:
HashMapStack.java
package groovy.xml;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.ListIterator;
import java.util.Map;
import java.util.Set;

public class HashMapStack
implements Map
{
private LinkedList stack = new LinkedList();

public HashMapStack()
{
stack.add(new HashMap());
}

/**
* Clears the whole structure.
*/
public void clear()
{
stack.clear();
stack.add(new HashMap());
}

public boolean containsKey(Object key)
{
for (ListIterator iter = stack.listIterator(stack.size()); iter.hasPrevious();)
{
Map map = (Map) iter.previous();
if (map.containsKey(key))
{
return true;
}
}
return false;
}

public boolean containsValue(Object value)
{
for (ListIterator iter = stack.listIterator(stack.size()); iter.hasPrevious();)
{
Map map = (Map) iter.previous();
if (map.containsValue(value))
{
return true;
}
}
return false;
}

public Set entrySet()
{
throw new UnsupportedOperationException();
}

public Object get(Object key)
{
for (ListIterator iter = stack.listIterator(stack.size()); iter.hasPrevious();)
{
Map map = (Map) iter.previous();
if (map.containsKey(key))
{
return map.get(key);
}
}
return null;
}

public boolean isEmpty()
{
return false;
}

public Set keySet()
{
HashSet set = new HashSet();
for (Iterator iter = stack.iterator(); iter.hasNext();)
{
Map map = (Map) iter.next();
set.addAll(map.keySet());
}
return set;
}

/**
* Only returns the value of the item replaced in the current frame, not in any lower frames.
*/
public Object put(Object key, Object value)
{
return ((Map) stack.getLast()).put(key, value);
}

public void putAll(Map t)
{
((Map) stack.getLast()).putAll(t);
}

/**
* Only removes the key from the current frame.
*/
public Object remove(Object key)
{
return ((Map) stack.getLast()).remove(key);
}

public int size()
{
return keySet().size();
}

public Collection values()
{
throw new UnsupportedOperationException();
}

public void push()
{
stack.add(new HashMap());
}

public void pop()
{
if (stack.size() > 1)
{
stack.removeLast();
}
else
{
throw new IndexOutOfBoundsException();
}
}
}


NSDOMBuilder.groovy
package groovy.xml

import javax.xml.namespace.QName
import groovy.xml.FactorySupport

import javax.xml.namespace.QName
import javax.xml.parsers.DocumentBuilder
import javax.xml.parsers.DocumentBuilderFactory
import javax.xml.parsers.ParserConfigurationException

import org.w3c.dom.Document
import org.w3c.dom.Element
import org.w3c.dom.Node
import org.xml.sax.InputSource
import org.xml.sax.SAXException

class NSDOMBuilder extends BuilderSupport {

private boolean namespaceAware
private HashMapStack nsMap = new HashMapStack()
private Document document
private DocumentBuilder documentBuilder

static NSDOMBuilder newInstance() throws ParserConfigurationException {
return newInstance(false, true)
}

static NSDOMBuilder newInstance(boolean validating, boolean namespaceAware) throws ParserConfigurationException {
DocumentBuilderFactory factory = FactorySupport.createDocumentBuilderFactory()
factory.setNamespaceAware(namespaceAware)
factory.setValidating(validating)
return new NSDOMBuilder(factory.newDocumentBuilder())
}

static Document parse(Reader reader) throws SAXException, IOException, ParserConfigurationException {
return parse(reader, false, true)
}

static Document parse(Reader reader, boolean validating, boolean namespaceAware)
throws SAXException, IOException, ParserConfigurationException {
DocumentBuilderFactory factory = FactorySupport.createDocumentBuilderFactory()
factory.setNamespaceAware(namespaceAware)
factory.setValidating(validating)
DocumentBuilder documentBuilder = factory.newDocumentBuilder()
return documentBuilder.parse(new InputSource(reader))
}

NSDOMBuilder(Document document) {
this(document, true)
}

NSDOMBuilder(Document document, boolean namespaceAware) {
this.document = document
this.namespaceAware = namespaceAware
}

NSDOMBuilder(DocumentBuilder documentBuilder) {
this.documentBuilder = documentBuilder
this.namespaceAware = documentBuilder.namespaceAware
}

protected void setParent(Object parent, Object child) {
Node current = (Node) parent
Node node = (Node) child

current.appendChild(node)
}

protected Object createNode(Object name) {
return createNode(name, [:])
}

protected Document createDocument() {
if (documentBuilder == null) {
throw new IllegalArgumentException("No Document or DOMImplementation available so cannot create Document")
} else {
return documentBuilder.newDocument()
}
}

protected Object createNode(Object name, Object value) {
Element element = (Element) createNode(name, [:])
element.appendChild(document.createTextNode(value.toString()))
return element
}

protected Object createNode(Object name, Map attributes, Object value) {
Element element = (Element) createNode(name, attributes)
element.appendChild(document.createTextNode(value.toString()))
return element
}

protected Object createNode(Object name, Map attributes) {
if (namespaceAware) {
nsMap.push()

for (Iterator iter = attributes.entrySet().iterator(); iter.hasNext();) {
def entry = iter.next()
String attrName = entry.key.toString()
if ("xmlns".equals(attrName) || attrName.startsWith("xmlns:")) {
String prefix = ''
if (attrName.startsWith("xmlns:")) {
prefix = attrName.substring(6)
}
nsMap.put(prefix, entry.value)
iter.remove()
}
}
}

if (document == null) {
document = createDocument()
}

Element element

if (namespaceAware) {
def qname = parseQName(name)
element = document.createElementNS(qname.namespaceURI, name)
} else {
element = document.createElement(name)
}

attributes.each { k, value ->
String valueText = (value != null) ? value.toString() : ""
element.setAttribute(k.toString(), valueText)
}

return element
}

protected void nodeCompleted(Object parent, Object node) {
if (namespaceAware) {
nsMap.pop()
}
}

protected QName parseQName(String name) {
String prefix = ''
String localPart = name
int i = name.indexOf(':')
if (i > -1) {
prefix = name.substring(0, i)
localPart = name.substring(i + 1)
}
String namespaceURI = nsMap.get(prefix)
return new QName(namespaceURI, localPart, prefix)
}
}


Update 3: here's an example of how to actually use it.

    Element createGetSupplierPartyMaster(Dealer dealer, Customer customer = null)
{
assert dealer != null
assert dealer.dealerId != null

def builder = NSDOMBuilder.newInstance()

def el = builder.'ebiz:GetSupplierPartyMaster'(releaseID:'9.0',
xmlns:'http://www.openapplications.org/oagis/9',
'xmlns:ebiz':'http://www.dis-corp.com/xml/ebusiness/2.0') {
ApplicationArea() {
Sender(InetAddress.localHost.hostName)
CreationDateTime(dateTimeFormatter.print(new DateTime()))
BODID(UUID.randomUUID())
}
DataArea() {
Get() {
Expression()
}
SupplierPartyMaster() {
PartyIDs() {
ID(schemeURI:BODHelper.DEALER_SCHEME_URI, dealer.dealerId)
if (customer?.store)
{
ID(schemeURI:BODHelper.STORE_SCHEME_URI, customer.store.storeCode)
}
}
if (customer)
{
AccountIDs() {
ID(customer.customerNumber)
}
}
}
}
}

el.ownerDocument.appendChild(el)
return el
}

No comments: