Useful XML node functions module

This little gem is based on the cda.xml module from our CDA Solutions section. We made a couple of changes: We added brief comments for each function and removed the interactive help (because it is dependent on the cda.help module).

When reading a TEXT element this module looks for the first TEXT element, and appends it if it does not exist. When setting a TEXT element or an ATTRIBUTE it also looks for the first instance and appends one if it does not exist. This should work well in most cases, however If you need different behaviour then you must to use a different approach. See these pages for some alternative ideas: Setting an XML node to a space ” ” character and Prevent errors reading from an empty XML element.

This module helps resolve some common issues that arise when working with XML node trees:

  1. Use node.text() to resolve the “Index X is out of bounds” error caused by trying to read an empty XML TEXT element.
  2. Use node.setText() to resolve the “Index X is out of bounds” error caused by trying to set an empty XML TEXT element.
  3. Use node.setText() to set a TEXT element to a space, instead of using node.setInner() which ignores the space and sets it to empty.

The module functions are documented in the CDA API guide:

Note: The help documentation has a couple of quirks:

  1. It shows the XML results rather than the function returns (which can confuse).
    For example, this return on the left would be shown as the on XML the right:
  2. It shows the use of “function shortcuts”, we recommend using the colon operator notation instead:
    -- NOT recommended
    local setText = node.setText -- create shortcut
    setText(CD.title, 'Good Health Clinic Consultation Note')
    
    -- Recommended
    CD.title:setText('Good Health Clinic Consultation Note')

Tip: Tip if you want to try the interactive help you can use the cda.xml module from our CDA Solutions section.

  1. Download this project zip file: NIST_Validated_CDA_Example_minimal.zip
  2. Load it into a From HTTPS component
  3. Then add the cda.xml and cda.help modules into your project

You can find the exact same help on the CDA API guide page.

Sample Code [top]

Code for the main module:
Note: You will need to uncomment the code you want to use, see the Using the Code for more information.

require 'xml'

function main(Data)
   local X = xml.parse{data='<SPACETIME></SPACETIME>'}
   local tim = X.SPACETIME:append(xml.ELEMENT,'TIME')
   tim:setInner(os.date())
   local spc = X.SPACETIME:append(xml.ELEMENT,'SPACE'
   
   -- 1) Reading an empty element causes an out of bounds error
   --    Use node.text() to resolve
   --trace(spc)    -- spc[1] TEXT element does not exist
   --trace(spc[1]) -- FAILS - Error: Index 1 is out of bounds.
   --spc:text()    -- creates empty TEXT element spc[1]
   --trace(spc)
   --trace(spc[1]) -- SUCCEEDS (because spc[1] now exists)

   -- 2) Setting an empty element causes an out of bounds error
   --    Use node.setText() to resolve 
   --trace(spc)                         -- spc[1] TEXT element does not exist
   --spc[1]:setInner('Hello World')     -- FAILS - Error: Index 1 is out of bounds.
   --spc[1]='Hello World'               -- FAILS - Error: Index 1 is out of bounds.
   --spc:setText('Hello World')
   --trace(spc)                         -- spc[1] TEXT element now exist
   --spc[1]:setInner('Hello New World') -- SUCCEEDS (because spc[1] now exists)
   --spc[1]='Columbus was here'         -- SUCCEEDS (because spc[1] now exists)

   -- 3) Using setInner() to set an element to a space sets it to blank/empty instead.
   --    Use node.setText() to resolve 
   --    NOTE: You can use setInner() to set the TEXT element directly
   --          but if the element does not exist you will get the out of bounds error
   --          (which setText() resolves by creating a new TEXT element)
   --trace(spc)
   --spc:setInner(' ')
   --spc:setText(' ')
   --spc[1]:setInner(' ') -- unlike setText() only works if the spc[1] TEXT element exists
   --trace(spc)
end

Code for the xml module:

-- find or create first text element
function node.text(X)
 for i=1,#X do
 if X[i]:nodeType() == 'text' then
 return X[i]
 end
 end
 return X:append(xml.TEXT, '')
end

-- set or create+set a text element
-- NOTE: uses node.text() to create element if needed
function node.setText(X, T)
 X:text():setInner(T)
end

-- set or create+set an XML attribute
function node.setAttr(N, K, V)
 if N:nodeType() ~= 'element' then
 error('Must be an element')
 end
 if not N[K] or N[K]:nodeType() ~= 'attribute' then
 N:append(xml.ATTRIBUTE, K)
 end 
 N[K] = V
 return N
end

-- find an XML element by name
function xml.findElement(X, Name)
 if X:nodeName() == Name then
 return X
 end
 for i = 1, #X do
 local Y = xml.findElement(X[i], Name)
 if Y then return Y end
 end
 return nil
end

-- append an XML element
function node.addElement(X, Name)
 return X:append(xml.ELEMENT, Name)
end

Using the code [top]

This section explains how to use the code to solve the three common issues mentioned in the introduction.

1. Use node.text() to prevent errors reading an empty XML TEXT element

If you reference an empty node/element in an XML node tree this will cause an “Index X is out of bounds” error. To prevent this when reading a TEXT element use the node.text() function, which returns the first TEXT element or appends and returns an empty element if one does not exist.

If we try to read the spc[1] element when it does not exist we get an error:

Using node.text() resolves the problem by creating and returning an empty TEXT element (spc[1]):
Note: It also means that we can now reference spc[1] without error.

2. Use node.setText() to prevent errors setting an empty XML TEXT element

If you reference an empty node/element in an XML node tree this will cause an “Index X is out of bounds” error. To prevent this when writing a TEXT element use the node.setText() function, which updates the first TEXT element or appends and updates new element, if one does not exist.

If we try to update the spc[1] element when it does not exist we get an error:

Using node.setText() resolves the problem by creating and returning an empty TEXT element (spc[1]):
Note: It also means that we can now reference spc[1] without error.

3. Use node.setText() to set a TEXT element to a space, instead of node.setInner()

The issue is caused because the builtin node:setInner() parses the XML message and ignores the singleton space ” ” character. To prevent this when writing a TEXT element use the node.setText() function.

Here we can see how the node.setInner() ignores the space and removes the TEXT element, and how node.setText() adds a TEXT element containing a space:

You can use node.setInner() directly on a TEXT element to set it to a space, however if it does not exist you will once again get the “Index X is out of bounds” error:

How it works [top]

This module makes these assumptions:

  1. You always want to read/update the first TEXT element within the specified (parent) element
  2. When you read/set a TEXT element it must exist, therefore if it does not it will be appended to the specified (parent) element
  3. When you set an  ATTRIBUTE it must exist, therefore if it does not it will be appended to the specified (parent) element

If you need different behaviour then see: Setting an XML node to a space ” ” character and Prevent errors reading from an empty XML element for some alternative ideas.

The module contains these functions that are documented in this CDA API guide page:

  • node.text : Iterates through the child elements of the specified (parent) element, and returns the first TEXT element it finds, if no TEXT element is found then it appends an empty TEXT element and returns it
  • node.setText : Uses node.text() to find or create the first TEXT element, updates it to specifed value and returns it
  • node.setAttr : Sets a named XML attribute, if the attribute does not exist it appended, set and returned
  • node.addElement : Append an empty XML element, shorthand for: X:append(xml.ELEMENT, "My new element")
  • xml.findElement : Find an XML element by name and return it, returns nil if the element is not found

Best Practices [top]

  • You need to make sure that the way the functions work matches your requirements
  • Test the code to make sure it works correctly with the type of XML data you receive

What not to do [top]

  • Assume the module will do what you need without understanding how it work
  • Put the module into production without testing against representative sample data

Leave A Comment?