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

XPath and Namespaces

Namespace handling
UV 10.2 has some quirks when handling namespaces, particularly when processing XPath queries.
Default Namespace
If the XML document contains a default namespace, you need to provide a namespace map to associate the namespace with a prefix, then use the prefix in the XPath queries.
Example
include UNIVERSE.INCLUDE XML.H
xml = ‘<?xml version=”1.0″ ?>’
xml := ‘<TheSearchResults xmlns=”http://theresults >’
xml := ‘<response>’
xml := ‘<exception>Exception Message</exception>’
xml := ‘</response>’
xml := ‘</TheSearchResults>’
nsMAP = ‘xmlns:a=http://theresults’
xpathStr = ‘/a:TheSearchResults/a:response/a:exception’
if XDOMOpen(xml,XML.FROM.STRING,hd) # XML.ERROR then
   if XDOMLocate(hd,xpathStr,nsMAP,hn) # XML.ERROR then
      if not(XDOMGetnodename(hn,name)) and name[9] = ‘exception’ then
         crt ‘Located ‘:name
      end
   end
end
NOTE: The XML document makes use of a default namespace. The nsMAP defines the mapping of the default namespace to a prefix. The XDOMLocate function uses the nsMAP and an appropriate XPath query that uses the prefixes to correctly find the appropriate node.
Namespace Maps
The definition of a namespace map needs to exclude quotes from the URI. The quotes do not get correctly processed by UniVerse.
Example
$include UNIVERSE.INCLUDE XML.H
xml = ‘<?xml version=”1.0″ encoding=”UTF-8″?>’
xml := ‘<Notifications xmlns=”http://www.address.com/ChangeInCircumstance
xml := ‘<Header>’
xml := ‘<BatchId>ab1234</BatchId>’
xml := ‘</Header></Notifications>’
nsMAP = ‘xmlns:a=http://www.address.com/ChangeInCircumstance
nsMAP := ‘xmlns:person=http://www.govtalk.gov.uk/people/AddressAndPersonalDetails ‘
nsMAP := ‘xmlns:pdt=http://www.govtalk.gov.uk/people/PersonDescriptives ‘
nsMAP := ‘xmlns:xsi=http://www.w3.org/2001/XMLSchema-instance’
XPATH = “/a:Notifications/a:Header/a:BatchId/text()
stat = XDOMOpen(xml,XML.FROM.STRING,hd)
if stat # XML.ERROR then
   stat = XDOMLocate(hd,XPATH,nsMAP,TEXTNODEHANDLE)
   if stat # XML.ERROR then
      stat = XDOMGetNodeValue(TEXTNODEHANDLE,NODEVALUE)
      if stat # XML.ERROR then
         CRT NODEVALUE
      end
   end
end
if stat then
   crt “BAD: STATUS() = “:stat
   stat = XMLGetError( xmlERR, xmlDESC)
   crt “[“:xmlERR:”] “:xmlDESC
end
NOTE: The nsMAP defines the prefix-to-namespace mapping without using double-quotes. This is different to how the namespaces are defined within the XML document