XML Tree Guide

Introduction

Cinder provides the XmlTree class for processing XML. This class is recursive, meaning an XmlTree can contain other XmlTrees as its children. This design is similar to XML itself, which is a hierarchical format where an element can contain children elements.

For some of the example code in this document, we'll refer to this very basic XML document:


<?xml version="1.0"?&>
<library>
   <owner>
      <name>Andrew Bell</name>
      <city>New York</city>
   </owner>
   <album musician="John Coltrane" year="1989">
      <title>Ole Coltrane</title>
      <track id="0">Ole</track>
      <track id="1">Dahomey Dance</track>
      <track id="2">Aisha</track>
      <track id="3">To Her Ladyship</track>
   </album>
   <album musician="Burial + Four Tet" year="2009">
      <title>Moth/Wolf Club</title>
      <track id="0">Moth</track>
      <track id="1">Wolf</track>
   </album>
</library>

Parsing

To parse a block of XML, construct an XmlTree using a DataSource. For example, to parse an XML file on your disk, use loadFile():

XmlTree doc( loadFile( "/Users/andrewfb/music.xml" ) );

To parse XML on an http server, use loadUrl():

XmlTree doc( loadUrl( "http://rss.news.yahoo.com/rss/tech" ) );

To parse XML contained in a resource (discussed in more depth here), use loadResource():

XmlTree doc( loadResource( RES_MUSIC_LIBRARY ) );

To parse XML contained in a string:


std::string myXmlStr( "<?xml version=\"1.0\"?>\n<library>\n<owne…"
XmlTree doc( myXmlStr );

Iterating XmlTrees

To grab a particular child of a node by name, use the XmlTree::getChild() method:


XmlTree doc( loadFile( "/Users/andrewfb/music.xml" ) );
XmlTree musicLibrary = doc.getChild( "library" );

If the child does not exist, a XmlTree::ExcChildNotFound exception is thrown. To test for the existence of a child use XmlTree::hasChild():


std::string myXmlStr( '<?xml version="1.0"?> <library><owne…' );
XmlTree doc( myXmlStr );

XmlTree also supports finding children by path, where each component of the path is separated by the / character. For example:


console() << musicLibrary.getChild( "owner/city" ) << std::endl;

Output:

<city>New York</city>

The example above shows a convenient way to examine an XML node, which is to pass it to a std::ostream like console(). Another convenient function is XmlTree::getPath(), which returns the path up to and including a given node:


XmlTree ownerCity = doc.getChild( "library/owner/city" );
console() << "Path: " << ownerCity.getPath() << "  Value: " << ownerCity.getValue() <<std::endl;

Output:

Path: library/owner/city  Value: New York

To iterate the children of an XML node, use the XmlTree::Iter class.


XmlTree firstAlbum = doc.getChild( "library/album" );
for( XmlTree::Iter child = firstAlbum.begin(); child != firstAlbum.end(); ++child )
console() << "Tag: " << child->getTag() <<"  Value: " << child->getValue() << endl;

Output:


Path: library/owner/city  Value: New York
Tag: title  Value: Ole Coltrane
Tag: track  Value: Ole
Tag: track  Value: Dahomey Dance
Tag: track  Value: Aisha
Tag: track  Value: To Her Ladyship\endcode

You can also iterate the children of a node with the path syntax. For example, to iterate all the tracks of the music library, we can do something like the code below. Notice that the XmlTree::Iter is smart about finding all the nodes which match the path - there are tracks from both albums, not just the first.


for( XmlTree::Iter track = doc.begin("library/album/track"); track != doc.end(); ++track )
console() << track->getValue() << endl;

Output:


Ole
Dahomey Dance
Aisha
To Her Ladyship
Moth
Wolf

By default paths for the XmlTree are case insensitive. An optional boolean following the path allows you to force case sensitivity. Also for the uncommon case in which your node tags contain the '/' character, you can supply an alternate separator - we use the '.' below:

std::string ownerCity = xmlNode.getChild( "library.owner.city", true, '.' );

Values & Attributes

As we've already seen, you can get the tag (or name) of a node by calling getTag(). To get the value of a node as a string use getValue(). As an additional convenience, you can have XmlTree parse a string for you for any type which supports the istream>> operator. For example, if you know the nodes' values are floats, you might do this:


vector<float> myFloats;
for( XmlTree::Iter item = xml.begin(); item != xml.end(); ++item )
myFloats.push_back( item->getValue<float>() );

The XmlTree also offers facility for walking a node's attributes. To get the value of an attribute as a string, you can call getAttributeValue():


XmlTree firstAlbum = doc.getChild( "library/album" );
console() << "the musician is: " << firstAlbum.getAttributeValue( "musician" ) << endl;

There is also a variant which supports automatic type conversion:


XmlTree firstTrack = doc.getChild( "library/album/track" );
int firstTrackId = myNode.getAttributeValue<int>( "id" );

As an additional convenience, you can supply a default value in the case that a node does not have a particular attribute. If we wanted our default size to be 1 for nodes which do not posses a size attribute, we would do this:


float mySize = myNode.getAttributeValue<float>( "size", 1.0f );

Writing XML

The XmlTree class can be used to build and write XML documents as well. The example below creates a music library with one album and prints it to the console():


XmlTree library( "library", "" );
XmlTree album( "album", "" );
album.setAttribute( "musician", "Sufjan Stevens" );
album.setAttribute( "year", "2004" );
album.push_back( XmlTree( "title", "Seven Swans" ) );
album.push_back( XmlTree( "track", "All the Trees of the Field Will Clap Their Hands" ) );
album.push_back( XmlTree( "track", "The Dress Looks Nice on You" ) );
album.push_back( XmlTree( "track", "In the Devil's Territory" ) );
album.push_back( XmlTree( "track", "To Be Alone With You" ) );
library.push_back( album );
console() << library << std::endl;

Output:


<library>
   <album musician="Sufjan Stevens" year="2004">
      <title>Seven Swans</title>
      <track>All the Trees of the Field Will Clap Their Hands</track>
      <track>The Dress Looks Nice on You</track>
      <track>In the Devil's Territory</track>
      <track>To Be Alone With You</track>
   </album>
</library>

Notice that the node echoed to the console was not treated as an XML document - it lacks the <?xml> declaration of a true XML document. There are a couple of ways of achieving this. The simplest is to use XmlTree::write(), which by default assumes you want a full XML document:


library.write( writeFile( "~/musicOutput.xml" ) );

This routine has an optional second boolean parameter which will create the <?xml> declaration when true, its default value. Another option would be to create a document node ourselves and append the <library> to that:


XmlTree doc = XmlTree::createDoc();
XmlTree library( "library", "" );
…
doc.push_back( library );
console() << doc << std::endl;

Const-Correctness and References

The XmlTree is designed to be const-correct, and supports a ConstIter which mimicks the const_iterator of STL containers:


// Finds the track named \a searchTrack in the music library \a library.
// Throws XmlTree::ExcChildNotFound() if none is found.
const XmlTree& findTrackNamed( const XmlTree &library, const std::string &searchTrack )
{
   for( XmlTree::ConstIter trackIt = library.begin("album/track"); trackIt != library.end(); ++trackIt ) {
      if( trackIt->getValue() == searchTrack )
         return *trackIt;
   }
   
   // failed to find a track named 'searchTrack'
   throw XmlTree::ExcChildNotFound( library, searchTrack );
}

…
XmlTree doc( loadFile( "/Users/andrewfb/library.xml" ) );
console() << findTrackNamed( doc.getChild( "library" ), "Wolf" ) << std::endl;

Output:

<track id="1">Wolf</track>

It's also worth noting the value of passing XmlTrees by reference whenever possible. XmlTrees create a full copy of the XML data tree whenever they are copied, so passing by reference can improve performance significantly. Furthermore, assigning by copy will prevent us from modifying the "original" node of an XmlTree should we so desire. For example:


// Whoops - assignment by value doesn't modifying the original XmlTree
XmlTree firstTrackCopy = doc.getChild( "/library/album/track" );
firstTrackCopy.setValue( "Replacement name" );
console() << doc.getChild( "/library/album/track" ) << std::endl;

Output:

<track id="0">Ole</track>

Instead, use a reference in order to modify the XmlTree:


XmlTree &firstTrackRef = doc.getChild( "/library/album/track" ); // notice the reference
firstTrackRef.setValue( "Replacement name" );
console() << doc.getChild( "/library/album/track" ) << std::endl;

Output:

<track id="0">Replacement name</track>

Implementation Notes

The XmlTree::Iter and XmlTree::ConstIter are designed to be STL-compatible iterators. For example, if you are using a lambdas-aware C++ compiler (currently only VC2010 at the time of this writing) the following code prints the names of the albums in the music library:


std::for_each( doc.begin( "library/album" ), doc.end(), []( const XmlTree &child ) {
   app::console() << child.getChild( "title" ).getValue() << std::endl;
} );

XmlTree is implemented using the RapidXML library. For unusually performance-conscious use cases, it is worth considering using RapidXML directly, as the XmlTree is designed to be convenient more than it is fast. The necessary header files are in cinder/include/rapidxml and can be #included like so:


#include "rapidxml/rapidxml.hpp"
#include "rapidxml/rapidxml_print.hpp"