Default namespace handling in 11.2.5

In UniVerse 11.2 the XML libraries used by UV were updated. This updating appears to have broken the namespace handling when adding new nodes to a DOM with a default namespace.

The problem

If an XML document has a default namespace, such as this:

<TheBeginning xmlns="http://www.wordpress.com/webpath/20160222">
</TheBeginning>

the addition of nodes that do not explicitly reference a namespace produces an unexpected result:

<TheBeginning xmlns="http://www.wordpress.com/webpath/20160222">
  <Header xmlns=""/>
</TheBeginning>

The Header node should look like this:

 <TheBeginning xmlns="http://www.wordpress.com/webpath/20160222">
  <Header/>
</TheBeginning>

My thinking is that under the 11.2 XML module, the XDOMCreateNode() and XDOMAddChild() functions are expecting you to know exactly what you are doing. So when you add a node that has no namespace to a DOM that has a default the new node is added with no namespace.

The work-around

There is a way to work with the 11.2 XDOM functions to get nodes to have the same default namespace as the DOM to which they are added. It involves changing how the node name is structured.

The code used to create the Header node was:

$include UNIVERSE.INCLUDE XML.H
nsPATH="http://www.wordpress.com/webpath/20160222"
rootXML = '<TheBeginning '
rootXML := ' xmlns="':nsPATH:'"'
rootXML := '></TheBeginning>'
xmlStatus = XDOMOpen(rootXML,XML.FROM.STRING,domHandle)
if (xmlStatus = XML.ERROR) then
   gosub HANDLE.ERROR
end else
   xmlStatus = XDOMLocateNode(domHandle, XDOM.CHILD, XDOM.FIRST.CHILD, XDOM.ELEMENT.NODE, rootNode)
   if (xmlStatus = XML.ERROR) then
      gosub HANDLE.ERROR
   end else
      name.NODE = "Header"
      map.NS = nsPATH
      xmlStatus = XDOMCreateNode( rootNode, name.NODE, "", XDOM.ELEMENT.NODE, headerNode)
      if xmlStatus = XML.SUCCESS then
         xmlStatus = XDOMAddChild( rootNode, "", map.NS, headerNode, XDOM.NODUP)
         if (xmlStatus = XML.ERROR) then
            gosub HANDLE.ERROR
         end
      end
      XmlStatus= XDOMWrite(domHandle, recXml, XML.TO.STRING)
      if (xmlStatus # XML.ERROR) then crt recXml else crt "failed to build XML!"
   end
end
STOP

HANDLE.ERROR:
CRT 'HANDLE.ERROR'
DEBUG
return

In order to add a node that uses the default name space the name.NODE variable needs to be defined much the same as when adding a node with a namespace prefix.

In Creating XML Elements with a namespace prefix I cover how to add nodes with a namespace prefix. The work-around is very similar, so we will work through adding the Footer node as an example.

There is no namespace prefix involved in this DOM, so the structure of the name.NODE variable looks like this:

:” {node_Name} “:” {namespace_PATH}

As we are dealing with the default namespace the map.NS variable, important for the namespace prefix handling, needs to be empty for this work-around to work. So the code to add the Footer node looks like this:

      name.NODE = ":Footer:":nsPATH
      map.NS = ""
      xmlStatus = XDOMCreateNode( rootNode, name.NODE, "", XDOM.ELEMENT.NODE, headerNode)
      if xmlStatus = XML.SUCCESS then
         xmlStatus = XDOMAddChild( rootNode, "", map.NS, headerNode, XDOM.NODUP)
         if (xmlStatus = XML.ERROR) then
            gosub HANDLE.ERROR
         end
      end

Insert this above the XDOMWrite() function in the above program, and the output should look like this on an 11.2.x database:

<TheBeginning xmlns="http://www.wordpress.com/webpath/20160222">
  <Header xmlns=""/>
  <Footer/>
</TheBeginning>

This is a UV11.2.x only Solution

Beware that his logic only works on UV 11.2.x. As the XML libraries integrated in other version of UV behave differently, this work-around is only functional on UV 11.2.x servers.

To get this example to work on multiple UV versions you will need to leverage the compiler directives to test the UV version when compiling the code. The alternative is to code in a test of the UV version at runtime, but as there is no BASIC function that returns the current UV version this is a bit more cumbersome.

*******************************************************************************
*
*       Program TEST_UV11XML_NAMESPACE
*
*****************************************************************************
$include UNIVERSE.INCLUDE XML.H
nsPATH="http://www.wordpress.com/webpath/20160222"
rootXML = '<TheBeginning '
rootXML := ' xmlns="':nsPATH:'"'
rootXML := '></TheBeginning>'
xmlStatus = XDOMOpen(rootXML,XML.FROM.STRING,domHandle)
if (xmlStatus = XML.ERROR) then
   gosub HANDLE.ERROR
end else
   xmlStatus = XDOMLocateNode(domHandle, XDOM.CHILD, XDOM.FIRST.CHILD, XDOM.ELEMENT.NODE, rootNode)
   if (xmlStatus = XML.ERROR) then
      gosub HANDLE.ERROR
   end else
      name.NODE = "Header"
      map.NS = nsPATH
      xmlStatus = XDOMCreateNode( rootNode, name.NODE, "", XDOM.ELEMENT.NODE, headerNode)
      if xmlStatus = XML.SUCCESS then
         xmlStatus = XDOMAddChild( rootNode, "", map.NS, headerNode, XDOM.NODUP)
         if (xmlStatus = XML.ERROR) then
            gosub HANDLE.ERROR
         end
      end
$IFDEF U2__UNIVERSEv11
      name.NODE = ":Footer:":nsPATH
      map.NS = ""
$ELSE
      name.NODE = "Footer"
$ENDIF      
      xmlStatus = XDOMCreateNode( rootNode, name.NODE, "", XDOM.ELEMENT.NODE, headerNode)
      if xmlStatus = XML.SUCCESS then
         xmlStatus = XDOMAddChild( rootNode, "", map.NS, headerNode, XDOM.NODUP)
         if (xmlStatus = XML.ERROR) then
            gosub HANDLE.ERROR
         end
      end
      XmlStatus= XDOMWrite(domHandle, recXml, XML.TO.STRING)
      if (xmlStatus # XML.ERROR) then crt recXml else crt "failed to build XML!"
   end
end
STOP
HANDLE.ERROR:
CRT 'HANDLE.ERROR'
DEBUG
return

 

 

Advertisement

Syncronization Locks

The task syncronization locks within UV are a very handy, but often not well understood, facility. It does not help that there are no metrics on their usage or collision rates, there is no system function to call to query the state of each lock and the BASIC LOCK command does not report wich process/port has the lock.

The hardest thing to come to grips with is the output from the LIST.LOCKS command that reports the current usage of the locks.

Example Output

>LIST.LOCKS
 0:------  1:------  2:------  3:------  4:------  5:------  6:------  7:------
 8:------  9:-21709 10:------ 11:------ 12:------ 13:------ 14:------ 15:------
16:------ 17:------ 18:------ 19:------ 20:------ 21:------ 22:------ 23:------
24:------ 25:------ 26:------ 27:------ 28:13     29:------ 30:------ 31:------
32:------ 33:------ 34:------ 35:------ 36:------ 37:------ 38:------ 39:------
40:------ 41:------ 42:------ 43:------ 44:------ 45:------ 46:------ 47:------
48:------ 49:------ 50:------ 51:------ 52:------ 53:------ 54:------ 55:------
56:------ 57:------ 58:------ 59:------ 60:------ 61:------ 62:------ 63:------

>

There are two active locks in the above report: 9 and 28, but the values next to each lock are quite different – Why?

The rules are simple

If the value next to the execution lock is…

  1. Left-justified and Positive – it represents the PORT number of the UV process that is holding the lock. Typically these will be interactive users.
    Port 13 is holding lock 28 in the above example.
  2. Right-justified and Negative – it represents the First 5 digits of the PID of the UV process that is holding the lock. Typically these will be phantom processes.
    Lock 9 is held by a process whose PID starts with 21709.
    On the majority of RedHat servers running UV, 5 digits is likely to be the full PID. On AIX, the PID can be 8 digits which makes finding the PID a little more cumbersome.

The invisible @TMP table

I have been working with UV for a few years now but I still occasionally uncover some functionality I never knew existed that is really cool and useful.

Such as the invisible @TMP table.

It is documented in the “BASIC SQL Client Interface Guide” and the “UCI Developers Guide” for those needing documentary evidence that it does exist.

The premise of it is quite simple:

  1. Create a dynamic array of data to be reported on.
  2. Assign the array to a numbered select list
  3. Use the SQL SELECT syntax to populate the @TMP table with the select list contents and produce an appropriate report/recordset.

An example always helps to understand.

The Test program:  TEST.@TMP

CATEG = "ACBDAADBCB"
INFO = ''
FOR II = 1 TO 10
   ROW = II
   ROW<2> = "ROW ":II
   ROW<3> = TIME()-(II*(if mod(II,2) then -1 else 1))
   ROW<4> = CATEG[RIGHT(II,1)+1,1]
   INFO<II> = CONVERT(@AM,@TM,ROW)
NEXT II
SELECTN INFO TO 9

Note that each row’s data is separated by @TM instead of an @VM. This is important for the SQL interface.

Compile and run the above program.

Using the active select list…

Try this:

SELECT F1 FMT "4R" AS "Row", F2 FMT "20L" AS "Descr", F3 FMT "10R" CONV "MTS" AS "Time", F4 FMT "3L" AS "Code" FROM @TMP SLIST 9;

This produces a simple output of the contents of select list 9 in a nicely formatted result.

Row. Descr............... Time...... Code
   1 ROW 1                  10:45:54 C
   2 ROW 2                  10:45:51 B
   3 ROW 3                  10:45:56 D
   4 ROW 4                  10:45:49 A
   5 ROW 5                  10:45:58 A
   6 ROW 6                  10:45:47 D
   7 ROW 7                  10:46:00 B
   8 ROW 8                  10:45:45 C
   9 ROW 9                  10:46:02 B
  10 ROW 10                 10:45:43 A

10 records listed.
>

For a more complex sort sequence, try this one:

SELECT F1 FMT "4R" AS "Row", F4 FMT "3L" AS "Category", F3 FMT "10R" CONV "MTS" AS "RowTime" FROM @TMP SLIST 9 ORDER BY Category DESC, RowTime ASC;
Row. Category RowTime...
   6 D          11:20:15
   3 D          11:20:24
   8 C          11:20:13
   1 C          11:20:22
   2 B          11:20:19
   7 B          11:20:28
   9 B          11:20:30
  10 A          11:20:11
   4 A          11:20:17
   5 A          11:20:26

10 records listed.
>

Gotchas to be aware of

  1. You cannot use the WHERE clause in the select statement. All the rows (attributes) in the select list will be output
  2. You cannot use the “TO SLIST {n}” option to create another select list from the re-ordered output.
  3. To order the output using non-default formatting (i.e. “10L”) you must use the ‘F1 FMT “{xx}” AS “{ColName}”‘ syntax in the output and then use {ColName} in the ORDER BY section
  4. Only the SQL form of the SELECT verb works with the @TMP table – it does not exist for UV LIST & SELECT statements

Creating XML Elements with a namespace prefix

The Problem

Recently we had a need to create an XML document from UniVerse where the nodes needed to have a specific namespace prefix, like this:

<wsse:Security xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd">
   <wsse:Username>myUID</wsse:Username>
</wsse:Security>

“That’s easy”, I thought and promptly set to creating many an example of it not being easy. Nothing we tried would create the desired output.

The Solution

Rocket U2 Support were very helpful when I explained the requirement. Their solution, whilst not being the most intuitive option, certainly achieves the desired result.

There are two things that need to be done to properly build XDOM elements with a namespace prefix:

  1. Adjust the name declaration passed in to XDOMCreateNode()
  2. Adjust the namespace map passed in to XDOMAddChild()

The change to the name declaration sets up the new XDOM node internally, so that when the node is added to the main XDOM object it is integrated correctly. The change to the namespace map ensures that the node being added is added correctly into the existing namespace definitions of the main XDOM object.

The name declaration for XDOMCreateNode()

Instead of simply defining the name of the element as “Username”, we need to define it using the following syntax:

{prefix} “:” {node_name} “:” {path}

For the example xml above, our components are:

{prefix} = “wsse
{node_name} = “Username
{path} = “http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd

Using these components, our new name declaration for our new XML element needs to be defined as:

wsse:Username:http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd

To see this in context, we would use create a node such as this with the following UV code snippet:

PREFIX = "wsse"
PATH = "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"
name.NODE = PREFIX: ":": "Username": ":": PATH
hstat = XDOMCreateNode( hd, name.NODE, "", XDOM.ELEMENT.NODE, nhd)

The namespace map for XDOMAddChild()

Now that we have created the node, we now need to add the node to our XML document. To properly add an element created with the above name definition, we need to supply a namespace map that will instruct UV to place the node into the right place within the XML document.

The namespace map that needs to be supplied to the XDOMAddChild() function needs to be defined using this syntax:

“xmlns:” {prefix} “=”

Simple really. So, using our components from above, to add our newly created node to an existing XML document we need to define our namespace map and supply it to XDOMAddChild():

snMAP = "xmlns:wsse="
hstat = XDOMAddChild( hd, "", map.NS, nhd, XDOM.NODUP)

An Example

This is an example program showing an implementation of the above solution:

$include UNIVERSE.INCLUDE XML.H
* The aim is to create the "Username" element in the following:
* <wsse:Security xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd">
*    <wsse:Username>myUID</wsse:Username>
* </wsse:Security>

PREFIX = "wsse"
PATH = "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"
src.XML = '<wsse:Security xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"></wsse:Security>'
if XDOMOpen(src.XML, XML.FROM.STRING, rdom) # XML.ERROR then

   * Show the basic XML
   str.XML = ""
   hstat = XDOMWrite( rdom, str.XML, XML.TO.STRING)
   crt "XML-Before:"
   crt "[":str.XML:"]"

   * Locate our starting position
   if XDOMLocate( rdom, PREFIX:":Security", "", hd) # XML.ERROR then

      * Add new node with namespace prefix
      name.NODE = PREFIX: ":": "UserName": ":": PATH
      hstat = XDOMCreateNode( hd, name.NODE, "", XDOM.ELEMENT.NODE, nhd)
      map.NS = "xmlns:": PREFIX: "="
      hstat = XDOMAddChild( hd, "", map.NS, nhd, XDOM.NODUP)

      * Add a text node with the value
      hstat = XDOMCreateNode( nhd, '', "myUID", XDOM.TEXT.NODE, thd)
      hstat = XDOMAddChild( nhd, "", map.NS, thd, XDOM.NODUP)

      * Show final XML
      str.XML = ""
      hstat = XDOMWrite( hd, str.XML, XML.TO.STRING)
      crt "XML-After:"
      crt "[":str.XML:"]"

      * Clean-up
      hstat = XDOMClose(thd)
      hstat = XDOMClose(nhd)
      hstat = XDOMClose(hd)
   end
   hstat = XDOMClose(rdom)
end

Example output

This example code produces the following output:

XML-Before:
[<wsse:Security xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"/>]
XML-After:
[<wsse:Security xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"><wsse:Username>myUID</wsse:Username></wsse:Security>]

The truth about uvrestore

After finding there were differences in the behaviour of uvrestore  between AIX and RedHat Enterprise Linux platforms (see Facts about uvbackup and uvrestore) I performed some tracing on the uvrestore command (using the strace command in Linux).

Undocumented behaviour

The first read  of the backup image attempted by uvrestore is a 512-byte block size. It makes no attempt to detect the current block size of the backup image. This 512-byte read is obtaining the tape header written by uvbackup, and the header contains the block size used by uvbackup to create the backup image. Once the block size of the backup image is determined, all future reads of the backup image use the detected block size.

This initial read of a 512-byte block is what is causing all the issues, as it is up to the operating system to deal with an attempt to read a block size that might not match the backup image device block size (tape drive block size).

AIX

On AIX, if the backup image was created on a tape device with a block size larger than 512 then uvrestore needs the tape device block size to be set to automatic (i.e. zero) to be able to succesfully read it. AIX’s support for reading from a device with an automatic block size setting makes this the most flexible approach.

Recommendation

Set AIX tape device block size to automatic before running uvbackup and uvrestore. This will allow you to maximise the block size used, helping to maximise the performance of uvbackup and uvrestore.

Linux (RHEL)

On RHEL, the support for reading from a tape device with an automatic block size seems to be flawed.

If the block size of the tape device was automatic (i.e. zero) when uvbackup was run to create the backup image, then uvrestore will only be able to read the backup image if one of the following conditions exist:

  • The block size of the tape device is still automatic and the block size used by uvbackup is 512
  • The tape device block size is adjusted to match the block size used by uvbackup

If the block size of the tape device was larger than 512 when uvbackup was run then it must be set to the same value when uvrestore is run.

Beware of tape device driver versions

The behaviour of the tape device driver is a critical factor in the behaviour of uvrestore on RHEL (my tests where done using an LTO-3 or LTO-4 tape drive, which uses the st device driver).

I found that RHEL4 has a st tape device driver problem that can cause a running system to panic and seize up if the tape device block size is set to a value larger than 512 and uvrestore is used to access a backup image. The st driver has a bug when driect I/o is enabled and a read of less than a full block is performed. The fix was to turn off direct I/O for reading.

The point is, RHEL does not have the same support for a tape device block size of automatic when reading, as AIX does. Some device drivers do have flaws, so test the block size permutations on your hardware.

Recommendation

  1. Set the tape device block size to match the uvbackup block size before running uvrestore
  2. Test tape device hardware using both uvbackup and uvrestore with a block size larger than 512 to ensure there are no issues with the hardware or device drivers

Incorrect documentation

All versions of the uvrestore documentation I have seen indicate that the command can be provided a block size parameter using the syntax:

-b <block_size>

Having seen the 512-byte read, followed by the use of the block size in the backup image header, I suspected that this command-line parameter was being ignored. I confirmed this with Rocket Software U2 support.

The uvrestore command completely ignores the -b <block_size> parameter, so there is no need to supply it to the command.

XDOMOpen() fails on RedHat

The Issue

The default installation of UniVerse 10.3.x on RedHat does not correctly setup the server so that UniVerse can find it’s shared libraries.

This shows up when code attempts to use the XDOMOpen() function by generating the error:

Program "xyz": Line nn, Can't load "/usr/ibm/uv/bin/libxml.so": libxslt4c.so.111:
cannot open shared object file: No such file or directory

Once a UV session has generated this error, it will not generate it again, which is sort of nice. Except that the XDOMOpen() function continues to fail!

There are a couple of technotes on the Rocket Software website that explain this issue, but their suggested solution is to assign the environment variable LD_LIBRARY_PATH with the path to the bin directory in the uv installation directory (i.e. LD_LIBRARY_PATH=”`cat /.uvhome`/bin”) prior to using uv.

Whilst this solution does work, it is not recommended for production environments (at least that is the feedback I get from reearching this environment variable). Add to that the effort needed to setup the assignment of the variable for all logins that use uv. Now you are getting a messy install & configuration effort just to be able to use the XML functionality in UV.

The Solution

There is a more robust alternative to setting the environment variable. It is a cache of links to the most recent shared libraries found on a system, which is used by ld.so – the RH run-time linker. This cache is built and maintained by the ldconfig command.

To update the shared library cache with UniVerse libraries perform the following as user ‘root’:

      echo `cat /.uvhome`/bin > /etc/ld.so.conf.d/UniVerse.conf
      ldconfig

To check that the cache has been updated, run the  following and confirm there are files in the UV installatin directory:

      ldconfig -p|grep "/uv/"

Now that this is done, every time you start UniVerse it will be able to load the necessary shared library modules needed to support the XML functionality.

HEX conversion limit

There is a limit to the HEX value that can be correctly processed by the MCDX conversion within UniVerse.

Conversion Result Expected  
OCONV(‘15849015887′,’MCDX’) B0ACCA4F 3B0ACCA4F Error
OCONV(‘2147483648′,’MCDX’) 80000000 80000000 ok
ICONV(‘80000000′,’MCDX’) -2147483648 80000000 Error
OCONV(‘2147483647′,’MCDX’) 7FFFFFFF 7FFFFFFF ok
ICONV(‘7FFFFFFF’,’MCDX’) 2147483647 2147483647 ok

This is because the HEX conversion uses a single signed 32-bit integer. These have a maximum value of 2,147,483,647.

Just in case you were wondering why 2147483648 would not convert to HEX correctly!

Facts about uvbackup and uvrestore

After doing extensive testing of uvbackup and uvrestore on both AIX and RedHat Enterprise Linux, a number of facts have emerged that are not obvious or have not been widely reported/documented.

Some might find these interesting, others might already know them. At least they are here for you to see.

If you think you might be able to make some changes based on these results, just make sure you test, test and test again on your hardware so you know what your system does under different configurations.

Matching the UVbackup/UVrestore block size to unix tape device block size improves performance!

This might seem an obvious one to many who know their unix tape device management inside out, but there is little in any of the UV manuals to push you toward investigating this.

On AIX, a simple backup/verify test of three UV accounts was conducted under three different configurations:

Configuration Unix tape device block size UVbackup/UVrestore block size Timing Diff
1 512 8192 03:43:47  
2 8192 8192 00:56:25 75%
3 0 (automatic) 8192 00:56:02 75%

Configurations 2 & 3 produced improvements of 75% over Configuration 1.

That is a massive improvement on AIX, and achievable using the “-b” option on the uvbackup and uvrestore commands.

The 8192 block size is also the UV default maximum so no UV configuration changes are needed.

Block size DOES matter

Using configuration 3 from above, further testing was done on the impact of different block sizes on the performance.

Block Size Timing Diff
32768 00:45:15 20%
65536 00:43:19 23%
1048576 00:42:42 24%

These differences are to the 00:56:02 timing for configuration 3 from above.

To achieve these block sizes, the BLKMAX uvconfig setting needs to be adjusted, otherwise uvbackup and uvrestore will not be able to use the block size provided with the “-b” command-line option.

UVrestore on AIX has a slight problem

During testing of variations of tape device block sizes it was found that if the current block size of the tape device was set to a value other than 0 or 512, then uvrestore would fail, setting a return code of 7!

This made it difficult to have the tape device block size set to 65536, for example, and have both the uvbackup and uvrestore work correctly.

There are two options to deal with this:

  1. Use automatic block sizes for the tape device (i.e. block size = 0)
  2. If using a tape device block size larger than 512 for uvbackup, set the tape device block size to zero (0) before running uvrestore.

UVrestore on Linux (RHEL) has a big problem

On RHEL, uvrestore is even more limited than it is on AIX.

If the backup tape was created with a block size that is not 512, uvrestore will not work. End-of-story!

All the above performance gains available to AIX cannot be achieved on Linux. The tape device block size MUST be set to 512 when uvbackup is run otherwise uvrestore will not work with the tape.

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”?>
]