March 10, 2009
Enhancing the standard NSXMLParser class
Cocoa offers a "complete" XML parser with the NSXML family of classes. This tree-based parser is fairly sophisticated but it is not included in Cocoa-Touch. In the spirit of small and simple, iPhone developers have the NSXMLParser class to use. This is an event-driven parser, which calls methods on a delegate to handle "events" as it parses through the XML.
The basic operation of the NSXMLParser class will not be covered here. This is well documented in the Apple Programming Guide "Introduction to Event-Driven XML Programming Guide for Cocoa". What I would like to introduce is an enhancement to this parser.
For very simple XML parsing purposes, NSXMLParser is probably adequate, but it is not going to handle all your XML parsing needs. The biggest problem with this type of streaming parser is that you lose the structure of your data; your call-backs do not provide any context or hierarchy. Thus your parser:didStartElement and parser:foundCharacters methods tend to be very messy with lots of if/else if statements, and you have to keep track of your own context via instance variables.
A better way: a Tree
One solution is to use the parser to build a tree first, then your application can access the tree afterwards. Even the above referenced document alludes to this solution. Below is such a solution, an implementation of the XMLTreeParser and XMLTreeNode classes.
The tree that is built uses nodes of type XMLTreeNode. The node is defined this way:
@interface XMLTreeNode : NSObject { XMLTreeNode* parent; NSString* name; // element name NSMutableDictionary* attributes; NSMutableString* text; // from foundCharacters() NSMutableDictionary* children; // key is child's element name, value is NSMutableArray of tree nodes}
Most of these properties are obvious. The text property is a concatenation of all the free-floating text that were inside an element. For example, the text "Hello World" would be stored in the text property:
<elementName>Hello World</elementName>
The choice of using a dictionary for the children property might be a curious one. The key of the dictionary is an NSString, which is the element name. The value of the dictionary is an NSArray of XMLTreeNode objects. To illustrate, here is some sample XML:
<stuff><item attr="value1"/><item attr="value2"/><note>TEXT</note></stuff>
Parsing this would create a root node with one child whose name is "stuff". The "stuff" node has two entries in its children dictionary: one entry for "item" and one for "note". The value for these entries is an NSArray of XMLTreeNode objects. The "item" array will have two nodes, and the "note" array will have only one.
The use of a dictionary allows us to quickly search for children, as we'll see later. The downside of this is that the order of the "item" and "note" child nodes for "stuff" will not be retained. This is a side-effect of dictionaries not preserving the order. However, proper XML should not care about the order of the elements, only that the hierarchy is correct.
I should note that the order of children of the same element is preserved. For example, when you query the tree, you have no idea if the "item" children will come before the "next" child or not. What you do know, though, is that you will get the item with "value1" before the item with "value2". This is important for the indexing scheme described below.
Building the tree
To create a parser, simply instantiate the XMLTreeParser class:
XMLTreeParser* parser = [[XMLTreeParser alloc] init];
This is a very simple implementation, so as it currently stands, an instance can only parse one XML input. There is no way to reuse a parser instance to parse some other XML. But that would be an easy exercise for the reader to address.
Now to start parsing, call the parse method and provide the XML data:
XMLTreeNode* root = [parser parse:xmlData];
Again, this simple implementation only handles XML passed directly to it, not indirectly through a filename. When this returns, your XML has been completely parsed. The beautiful part is you don't have to write all those event handlers. What you get back is the root node of the tree. This node does not have any useful data other than its children. This return will be nil if there is any problem parsing the supplied XML.
Querying the tree
Now you could traverse through the tree manually looking for things. A good demonstration of how to do this is in the XMLTreeNode method "description". This method will recreate the XML from the tree; unparse it, if you will. It will dump out the element name and any attributes. Then it will iterate over all the children and recursively call description on all of them. Please take a look at the implementation of -(NSString*) description in the XMLTreeNode class to see this.
For simpler queries, the XMLTreeNode class offers methods to find child nodes. These are the find* methods.
Do you know where your children are?
The most basic find method is findChildren. This simply returns an array of children that match the given element name. Referring back to the simple XML above, the statements below will pull out a list of XMLTreeNodes for the "item" elements:
NSArray* stuffs = [root findChildren:@"stuff"];XMLTreeNode* stuff = [stuffs objectAtIndex:0];NSArray* items = [stuff findChildren:@"item"];
OK, this is great, but a little cumbersome. If you know that there is only one "stuff" element, it's a shame you have to get back an array of them. So there is the findChild method, which returns a single XMLTreeNode object:
XMLTreeNode stuff = [root findChild:@"stuff"];
What if there are more than one "stuff" elements? This version of findChild: always takes the first element in the array. If you want a different element, you must use findChild:at:
XMLTreeNode* item2 = [stuff findChild:@"item" at:1];
This will return the second "item" node.
Getting deeper
Pretty cool, but this is still a little cumbersome. What if you have XML with elements 10 layers deep? That means at least 10 separate calls to findChild:. The findChild: method supports paths. Here is an example to get the first "item" node in one call:
XMLTreeNode* item1 = [root findChild:@"stuff/item"];
This basically combines two searches into 1. Now, for your 10-deep XML, you could use something like this:
XMLTreeNode* deep = [root findChild:@"stuff/items/item/something/lists/list/test"];
That's 7 searches in 1.
Pinpoint accuracy
Note that for every step through the path in the above example, if there are more than one element with the same name, this will traverse the first one. If you want a different element besides the first one, then you would have to fall back on the single-step findChild:at: method. Or, you can specify an index:
// I want the 3rd doodad in the 4th thingamajig of the 2nd whatchacallitXMLTreeNode* doodad3 = [root findChild:@"whatchacallit[1]/thingamajig[3]/doodad[2]"];
Remember that these indeces are 0-based, so they are one less than what you'd expect.
Now you can dig around in your XML to your heart's content. Be aware that if the query fails anywhere along the path, it will return nil. So if you get nil back from a find method, you can assume that it didn't find what you were looking for.
Optmization
If you find yourself traversing over the same ground over and over, you can move your starting point. There is no need to always start at the root node. Each node has these find methods, so you can get a new starting point and search from there:
XMLTreeNode* searchRoot = [root findChild:@"stuff/same/old/path"];XMLTreeNode* newThing = [searchRoot findChild:@"cool/new/thing"];
Think of this as changing your current directory so you don't always have to type in the full path.
Room for improvement
That's it for now. This simple implementation co