Looks like my blog is withering a little bit. To help bring it back to life, I thought it would be good to start a series of shorter postings that are quick to post, hopefully meaning less time in between!
These days I spend a considerable amount of time developing applications and forms using Orbeon Forms, which if you are unfamiliar, is one of the leading implementations of the W3C XForms 1.1 recommended specification. For the most part I use this for intelligent data capture and validation, but XForms is a very versatile technology with far reaching potential and is a very useful tool for any developer working with XML technologies. In particular for those already familiar with XPath and XML Schema. The local authority I work for is quite SOAP webservices centric, which is why I advocating XForms as a strategic tool for building user interfaces to our SOA.
I recently had the need to intelligently merge a series of XML documents valid to the same XML Schema. It was important that the merge only merged in elements from the second document that did not already exist in the first document. This required a definition of XML element equality.
How this was achieved using XForms
For the documents I was processing equality was determined by the @name attribute of elements. These were always unique within each individual document, and if the same value appeared in two or more documents, then the XML fragment contained within the parent element could be considered equal. So equality was quite easy to establish in this scenario, and I could have written program code to achieve the desired merge behaviour. But why do this when a small snippet of XForms markup can not only achieve this but also provide scope to introduce manual intervention via a very responsive web user interface in the future!
After achieving this using XForms, I thought I would create a simplified example XForms illustrating the essence of the approach and share it on here. At the end of this post you’ll find a complete XHTML document below which will run successfully on the Orbeon XForms sandbox (you’ll need to install Orbeon Forms). The actual merge logic is implemented by a single XForms action, which I feel quite elegantly illustrates the power of XForms:
origin="instance('copyfrom-instance')[count(instance('copyto-instance')/doc:child[@name eq context()/@name]) eq 0]/doc:child[@name eq context()/@name]
<xforms:insert/> simply iterates over the
<doc:child/> elements of
copyfrom-instance and copies each element into
copyto-instance, unless a
<doc:child/> element exists there with the same value for
@doc:name attribute. This condition is implemented using the “count” condition on the XPath used by the
@xforms:origin attribute. It is quite simple to change how element equality is determined and this may include checking the value of deeply nested elements.
You’ll notice that the example makes use of the
@xxforms:iterate attribute, which is not in the standard XForms 1.1 namespace. It is in fact a proprietary Orbeon extension to the standard specification. Normally I would stay clear of using such extensions, because that introduces lock in to a particular XForms implementation. However, the iterate attribute has actually been incorporated into the new XForms 2.0 specification.
You can read more about Orbeon’s implementation of the iterate attribute here on their XForms Everywhere blog.
Full source code
<xhtml:html xmlns:xhtml="http://www.w3.org/1999/xhtml" xmlns:xforms="http://www.w3.org/2002/xforms" xmlns:ev="http://www.w3.org/2001/xml-events" xmlns:fr="http://orbeon.org/oxf/xml/form-runner" xmlns:xxforms="http://orbeon.org/oxf/xml/xforms" xmlns:doc="http://someuri"> <xhtml:head> <xhtml:title>Merge documents example</xhtml:title> <xforms:model id="mainModel"> <xforms:instance id="copyto-instance"> <doc:document> <doc:child name="child_a" age="10"/> <doc:child name="child_b" age="15"/> <doc:child name="child_z" age="16"/> </doc:document> </xforms:instance> <xforms:instance id="copyfrom-instance"> <doc:document> <doc:child name="child_b" age="15"/> <doc:child name="child_c" age="17"/> <doc:child name="child_d" age="4"/> </doc:document> </xforms:instance> </xforms:model> </xhtml:head> <xhtml:body> <xforms:trigger> <xforms:label>Merge instance documents</xforms:label> <xforms:action ev:event="DOMActivate"> <xforms:insert xxforms:iterate="instance('copyfrom-instance')/doc:child" nodeset="instance('copyto-instance')/doc:child" origin="instance('copyfrom-instance') [count(instance('copyto-instance') /doc:child[@name eq context()/@name]) eq 0] /doc:child[@name eq context()/@name]"/> </xforms:action> </xforms:trigger> <xhtml:table border="1" cellspacing="0" cellpadding="4"> <xhtml:tr> <xhtml:th>Name</xhtml:th> <xhtml:th>Age</xhtml:th> </xhtml:tr> <xforms:repeat nodeset="//doc:child"> <xhtml:tr> <xhtml:td><xforms:output ref="@name"/></xhtml:td> <xhtml:td><xforms:output ref="@age"/></xhtml:td> </xhtml:tr> </xforms:repeat> </xhtml:table> <fr:xforms-inspector xmlns:fr="http://orbeon.org/oxf/xml/form-runner"/> </xhtml:body> </xhtml:html>