An easier way to build XML in UniVerse

The process of creating an XML document in UniVerse BASIC is quite laborious. It takes a number of steps to add a new element node, and similar steps to add attributes to a node.

The sequence is something like this:

  1. Create new XDOM.ELEMENT.NODE
  2. Add new node to existing XML document
  3. If the element is to have a value
    1. Create new XDOM.TEXT.NODE with the element node value
    2. Add text node to new element node
  4. If the element is to have attributes
    1. Create new XDOM.ATTR.NODE with attribute name and value
    2. Add attribute node to new element node
    3. Repeat for all required attributes

All this has create just on element. To create multiple elements requires performing the above sequence multiple times. This makes for a very messy and hard to maintain program.

An Easier way

Ordinarily, such a piece of logic would be placed into an external subroutine. That does work, but there is a way to provide a more intuitive solution: Functions!

UniVerse BASIC supports the creation and use of functions. By creating a function that performs this sequence, the process of building an XML document becomes a lot easier to code, and to read and maintain.

The Add_XMLElement function

The following code is a function that performs all the above steps:

function Add_XMLElement( domHandle, NODE.name, NODE.value, NODE.attr, NODE.ns, nodeHandle)

* This function adds a new element node to the domHandle XML doc.
* It also adds any attributes supplied.
*
* domHandle [IN]  Single Value
*   Handle to an XML node or document
*
* NODE.name [IN]  Single Value
*   Name of the element node to be added
*
* NODE.value [IN]  Single Value
*   The value of the new element node
*
* NODE.attr [IN]  Two attributes, with Multi values
*   Dynamic array of names and values to be used to define the
*   attributes on this node.
*   <1> Contains the attribute name
*   <2> Contains the attribute value
*
* NODE.ns [IN]  Single Value
*   The namespace to use for this node.
*
* nodeHandle [OUT]  Single Value
*   A handle to the added node
*
* Return Value
*   An XML return code: XML.SUCCESS, XML.ERROR, XML.INVALID.HANDLE

$include UNIVERSE.INCLUDE XML.H
xmlStatus = XML.SUCCESS

* Create the new ELEMENT node
xmlStatus = XDOMCreateNode( domHandle, NODE.name, "", XDOM.ELEMENT.NODE, nodeHandle)
if xmlStatus = XML.SUCCESS then
   xmlStatus = XDOMAddChild( domHandle, "", NODE.ns, nodeHandle, XDOM.NODUP)
end

if NODE.value # '' and xmlStatus = XML.SUCCESS then
   * If a value has been supplied, add a nameless child TEXT element
   xmlStatus = XDOMCreateNode(nodeHandle, '', NODE.value, XDOM.TEXT.NODE, newText)
   if xmlStatus = XML.SUCCESS then
      xmlStatus = XDOMAddChild( nodeHandle, "", NODE.ns, newText, XDOM.NODUP)
      xmlStatus = XDOMClose(newText)
   end
end

if NODE.attr # '' and xmlStatus = XML.SUCCESS then
   * If there are attribute to be added, do them now
   qty.ATTR = dcount( NODE.attr<1>, @VM)
   for num.ATTR = 1 to qty.ATTR while xmlStatus = XML.SUCCESS
      name.ATTR = NODE.attr<1,num.ATTR>
      if name.ATTR # '' then
         value.ATTR = NODE.attr<2,num.ATTR>
         xmlStatus = XDOMCreateNode( nodeHandle, name.ATTR, value.ATTR, XDOM.ATTR.NODE, attrHandle)
         if xmlStatus = XML.SUCCESS then
            xmlStatus = XDOMAddChild( nodeHandle, "", NODE.ns, attrHandle, XDOM.NODUP)
            xmlStatus = XDOMClose(attrHandle)
         end
      end
   next num.ATTR
end

return (xmlStatus)

Once compiled and cataloged, this function is available to be used.

A Working Example

This program uses the above function to create a sample XML document:

* manual build of xml using XDOM
$include UNIVERSE.INCLUDE XML.H
* Define the function we are going to use
deffun Add_XMLElement(a,b,c,d,e,f)

* Trying to build xml:
* <PurchaseHistory>
*   <PurchaseItem Ref="ABC123">Learning the Ropes</PurchaseItem>
*   <PurchaseItem Ref="CODE86">Getting Smarter</PurchaseItem>
*   <PurchaseItem Ref="CODE007" Security="High">Bonding the James Way</PurchaseItem>
* </PurchaseHistory>

* Create the doc
xmlStatus = XDOMCreateRoot(domHandle)

* Add the root element
xmlStatus = Add_XMLElement( domHandle, "PurchaseHistory", "", "", "", newnode)

* add the PurchaseItem elements
attrNode = "Ref": @AM: "ABC123"
xmlStatus = Add_XMLElement( newnode, "PurchaseItem", "Learning the Ropes", attrNode, "", subnode)
xmlStatus = XDOMClose(subnode)
attrNode = "Ref": @AM: "CODE86"
xmlStatus = Add_XMLElement( newnode, "PurchaseItem", "Getting Smarter", attrNode, "", subnode)
xmlStatus = XDOMClose(subnode)
attrNode = "Ref":@VM:"Security": @AM: "CODE007": @VM: "High"
xmlStatus = Add_XMLElement( newnode, "PurchaseItem", "Bonding the James Way", attrNode, "", subnode)
xmlStatus = XDOMClose(subnode)

* Output the XML
xmlStatus = XDOMWrite( domHandle, doc.XML, XML.TO.STRING)
crt "XML = "
crt "[":doc.XML:"]"

xmlStatus = XDOMClose(newnode)
xmlStatus = XDOMClose(domHandle)

As you can see, this is a much easier way of coding the manual build of XML, whilst maintaining the XDOM API-type function interface.

Compile and run this program, and it will produce the following output:

XML =
[<PurchaseHistory>
  <PurchaseItem Ref=”ABC123″>Learning the Ropes</PurchaseItem>
  <PurchaseItem Ref=”CODE86″>Getting Smarter</PurchaseItem>
  <PurchaseItem Ref=”CODE007″ Security=”High”>Bonding the James Way</PurchaseItem>
</PurchaseHistory>
]

Creating Processing Instruction nodes

The Processing Instruction nodes created via the XDOM API do NOT allow children to be added. This makes it impossible to add Attribute nodes to define the data for the node.

So, how does the data component of a Processing Instruction node get populated?

The XDOMCreateNode() function has a parameter for the nodeValue. This parameter is not used for all nodeType nodes.

Fortunately, it is used for XDOM.PROC.INST.NODE (Processing Instruction) nodes. Simply provide the data, formatted exactly as required for the node, in the nodeValue parameter.

Example

$include UNIVERSE.INCLUDE XML.H
XSL = 'cd_catalog.xsl'
theValue = 'type=':dquote('text/xsl'):' href=':dquote(XSL)
domHandle = ''
stat = XDOMCreateRoot(domHandle)
if stat # XML.ERROR then
   stat=XDOMCreateNode(domHandle, "xml-stylesheet", theValue, XDOM.PROC.INST.NODE, NEW.NODE)
   if stat # XML.ERROR then
      stat = XDOMAddChild(domHandle, "", "", NEW.NODE, XDOM.NODUP)
      if stat # XML.ERROR then
         doc.XML = ''
         stat = XDOMWrite( domHandle, doc.XML, XML.TO.STRING)
         if stat # XML.ERROR then
            crt "XML = [":doc.XML:"]"
         end
      end
   end
end
if stat then
   crt "BAD: STATUS() = ":stat
   stat = XMLGetError( xmlERR, xmlDESC)
   crt "[":xmlERR:"] ":xmlDESC
end

NOTE

The theValue defines the data portion of the Processing Instruction node. The XDOMCreateNode() function uses this to populate the node in the XML document.

Output

XML = [<?xml-stylesheet type=”text/xsl” href=”cd_catalog.xsl”?>
]

Traversing an XML node’s attributes

UV 10.2

The XDOM API in UniVerse provides the XDOMLocateNode() to traverse up and down, and iterate across the nodes in an XML document.

This works well, until you need to iterate through the attributes of an XML document. The XDOMLocateNode() will find the first child attribute, but there is no way to use it to iterate through all the attributes. This is because attributes are NOT child nodes of elements.

How to traverse a node’s attributes?

Using XPath 1.0 syntax and the XDOMEvaluate() function (which is in the U2 Basic Extensions manual – it is just not in the index!), it is possible to query the quantity of attributes on a node. It is then possible to query the name of each of the numbered attributes.

In the example below, the show.attribs subroutine uses two XPath 1.0 queries:

  1. The first (assigned to xpath.ATTR.COUNT) is passed to the XDOMEvaluate() function, and returns the quantity of attributes in the node.
  2. The second (assigned to xpath.ATTR.NODE) is passed to the XDOMEvaluate() function, and returns the name of the specified attribute number.

Once the name of the attribute is known, the XDOMGetAttribute() function returns a XDOM handle to the attribute node, and XDOMGetNodeValue() returns the value of the attribute node.

Example

$include UNIVERSE.INCLUDE XML.H  sample.XML =  '<?xml version="1.0" encoding="UTF-8"?>'
sample.XML := '<animals>'
sample.XML := '        <animal species="giraffe" size="large" horns="yes">'
sample.XML := '                <legs>4</legs>'
sample.XML := '                <color>tan/orange</color>'
sample.XML := '                <tail>Y</tail>'
sample.XML := '        </animal>'
sample.XML := '        <animal species="rhino" size="large" horns="yes" dangerlevel="extreme">'
sample.XML := '                <legs>4</legs>'
sample.XML := '                <color>mud</color>'
sample.XML := '                <tail>Y</tail>'
sample.XML := '        </animal>'
sample.XML := '        <animal species="rodent" size="tiny" cutenesslevel="medium">'
sample.XML := '                <legs>4</legs>'
sample.XML := '                <color>fawn</color>'
sample.XML := '                <tail>Y</tail>'
sample.XML := '        </animal>'
sample.XML := '</animals>'

nsMAP = ""
doc.ID = "Sample2.xml"
path.LOOK = "//animals"

stat = XDOMOpen(sample.XML,XML.FROM.STRING,dom.HANDLE)
if stat # XML.ERROR then
    stat = XDOMLocate(dom.HANDLE,path.LOOK,nsMAP,dom.ANIMALS)
    if stat # XML.ERROR then
       * Found the root node. Use XDomLocateNode to step down to first child
       stat = XDOMLocateNode( dom.ANIMALS, XDOM.CHILD, XDOM.FIRST.CHILD, XDOM.ELEMENT.NODE, dom.ANIMALS.CHILD)
       if stat # XML.ERROR then
          * Found first child element
          loop
             stat = XDOMGetNodeName(dom.ANIMALS.CHILD, name.NODE)
             if stat # XML.ERROR then
                crt "Name = ":squote(name.NODE)
                gosub show.attribs
             end
             * Find the next node
             nstat = XDOMLocateNode( dom.ANIMALS.CHILD, XDOM.NEXT.SIBLING, 1, XDOM.ELEMENT.NODE, dom.ANIMALS.CHILD)
          while stat # XML.ERROR and nstat # XML.ERROR do
          repeat
          cstat = XDOMClose(dom.ANIMALS.CHILD)
       end
       cstat = XDOMClose(dom.ANIMALS)
    end
    cstat = XDOMClose(dom.HANDLE)
end

if stat then
    crt "BAD: STATUS() = ":stat
    stat = XMLGetError( xmlERR, xmlDESC)
    crt "[":xmlERR:"] ":xmlDESC
end

stop

* This is where we show all the node's attributes
show.attribs:

qty.ATTRIB = 0

* Get the count of attributes for this node
xpath.ATTR.COUNT = "count(@*)"
stat = XDOMEvaluate( dom.ANIMALS.CHILD, xpath.ATTR.COUNT, nsMAP, qty.ATTRIB)
if stat # XML.ERROR and qty.ATTRIB then
    * There ARE attributes for this node
    for num.ATTRIB = 1 to qty.ATTRIB while stat # XML.ERROR
       * Use XPath 1.0 syntax to get the name of each attribute
       xpath.ATTR.NAME = "name(@*[position()=":num.ATTRIB:"])"
       name.ATTRIB = ''
       stat = XDOMEvaluate(dom.ANIMALS.CHILD,xpath.ATTR.NAME,nsMAP, name.ATTRIB)
       if stat # XML.ERROR then
          * Now we have the attirbute name, we can get the value
          crt "  ":name.ATTRIB:" = ":
          stat = XDOMGetAttribute(dom.ANIMALS.CHILD, name.ATTRIB, node.ATTRIB)
          if stat # XML.ERROR then
             * Got the attribute node, get the value
             stat = XDOMGetNodeValue(node.ATTRIB, val.ATTRIB)
             crt dquote(val.ATTRIB):
             cstat = XDOMClose(node.ATTRIB)
          end
          crt
       end
    next num.ATTRIB
end

return