Showing posts with label groovy. Show all posts
Showing posts with label groovy. Show all posts

Friday, June 20, 2008

Groovy DOMBuilder update

I finally got around to submitting my NSDOMBuilder code to the Groovy dev list. I got the "why didn't you just fix the existing class instead of creating a new one" response that I sort of expected, which made me go back and figure out what that didn't work when I tried it. Someone in that thread did point out StreamingDOMBuilder, which would probably work for the situations I need, except default namespace handling is currently broken in the releases, but now fixed in svn. It's not documented anywhere that I could find, but it's not too hard to figure out.

Very un-groovy

I really like Groovy as a language, but it does have its flaws. My colleague and I came across some very strange syntax and weird behavior surrounding GPathResult. GPathResult is fine and dandy for a read-only view of an object graph, but it does support some write operations. The documentation says you should only use it for read operations and then gives examples of how to write.

Unfortunately, those write operations are a bit broken. You can add nodes but the changes won't be reflected by the iterators or size() method. There is not method to remove a node, but you can effectively do so by replacing it with an empty closure. Unfortunately there are some downsides that as well because in some cases the nodes act like a linked list and if you replace one in the middle, you lose all the nodes after it.

Probably the worst thing about GPathResult is the overridden plus operator. I have never seen the plus operator overridden to perform a mutation on the object, but that's what GPathResult does. Take this from a mock Grails plugin for example:

class AGrailsPlugin {
def doWithWebDescriptor = { xml ->
def mappingElement = xml.'servlet'
mappingElement[mappingElement.size() - 1] + {
'servlet' {
'servlet-name'('a')
'servlet-class'('com.discorp.web.AServlet')
}
}
}
}

That actually adds a new servlet node right after the last servlet node in the tree. That has to be to most un-groovy syntax I have seen in Groovy. It's not at all obvious what going on and it's very hackish. Unfortunately I don't think there is any other way to perform that function in Grails, though I have a ticket open for it.

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
}