Multi Level XML Parsing


#1

Hi,

The example in the book is pretty simple, just parsing a particular element. I have an XML file that looks like this:

<myServer>
	<currentConnections>100</currentConnections>
	<acceptedConnections<1453</acceptedConnections>
	<virtualHost>
		<Name>Virtual_Host_1</Name>
		<Uptime>5336633</Uptime>
		<Application>
			<Name>Microsoft_Word</Name>
			<Status>active</Status>
			<Document>
				<Name>Word_Document_1</Name>
				<Size>5436</Size>
			</Document
			<Document>
				<Name>Word_Document_2</Name>
				<Size>6536</Size>
			</Document>
		</Application>
		<Application>
			<Name>Microsoft_Excel</Name>
			<Status>active</Status>
			<Document>
				<Name>Excel_Document_1</Name>
				<Size>5436</Size>
			</Document
			<Document>
				<Name>Excel_Document_2</Name>
				<Size>6536</Size>
			</Document>
		</Application>
	</virtualHost>
	<virtualHost>
		<Name>Virtual_Host_2</Name>
		<Uptime>4535435345</Uptime>
		<Application>
			<Name>Microsoft_Project</Name>
			<Status>active</Status>
			<Document>
				<Name>Project_Document_1</Name>
				<Size>5436</Size>
			</Document
			<Document>
				<Name>Project_Document_2</Name>
				<Size>6536</Size>
			</Document>
		</Application>
		<Application>
			<Name>Microsoft_PowerPoint</Name>
			<Status>active</Status>
			<Document>
				<Name>PowerPoint_Document_1</Name>
				<Size>5436</Size>
			</Document
			<Document>
				<Name>PowerPoint_Document_2</Name>
				<Size>6536</Size>
			</Document>
		</Application>
	</virtualHost>
</myServer>

What I want to accomplish is a tableViewController that shows me the virtualhosts in the XML, when I select one, it should push a new tableViewController showing me all the applications running on that particular host, then when I select an application it should push a final tableViewController showing me the documents of that application, and finally, when I select a particular document, I would reach a windows with the details of the document.

The XML could have multiple virtualhosts, multiple applications, multiple documents, the example XML posted only has 2 of each, but there couple be more, or none.

I am going nuts in just thinking how to accomplish this. I have though of using objects to represent each level, and also thought of using arrays within arrays, and finally arrays that contain objects, but I can’t seem to get the logic right.

Any help would be GREATLY appreciated! BTW, I am a complete newbie in XML parsing and have very limited experience with Objective-C and the iPhone SDK (what I’ve learned with this book is as far as I’ve gotten).

Kind Regards,

Manuel.-


#2

Hi,

This seemed like a fun challenge so here you go.

It assumes you’ve copied your xml file into the main bundle (called “VirtualHosts.xml”)and everything is done using grouped tableViews (not because I think it’s the best way but it was easier than having to explain how to wire up nibs).

I’ve used objects for each level but you could also use an NSDictionary - see the Nayshunz chapter for a similar solution.

By the way, there are a few missing “>”’ in your xml file.

HTH

Gareth

//
//  VirtualHostTableViewController.h
//  VirtualHosts
//

#import <UIKit/UIKit.h>

@class ApplicationTableViewController;
@class VirtualHost;
@class Application;
@class Document;


@interface VirtualHostTableViewController : UITableViewController <NSXMLParserDelegate>{

	BOOL storingCharacters;
	NSMutableData *xmlData;
	NSURLConnection *connectionInProgress;
	NSURL *url;
	ApplicationTableViewController *applicationTableViewController;
}

@property (nonatomic, retain) NSMutableArray *virtualHosts;
@property (nonatomic, retain) NSMutableString *currentString;
@property (nonatomic, retain) VirtualHost *currentHost;
@property (nonatomic, retain) Application *currentApplication;
@property (nonatomic, retain) Document *currentDocument;
@property (nonatomic, assign) NSString *context;
@property (nonatomic, retain) NSNumber *currentConnections;
@property (nonatomic, retain) NSNumber *acceptedConnections;

-(void)loadHosts;

@end
//
//  VirtualHostTableViewController.m
//  VirtualHosts
//

#import "VirtualHostTableViewController.h"
#import "ApplicationTableViewController.h"
#import "VirtualHost.h"
#import "Application.h"
#import "Document.h"


@implementation VirtualHostTableViewController

@synthesize virtualHosts, currentString, currentHost, currentApplication, currentDocument, context, currentConnections, acceptedConnections;

-(void)loadHosts
{	
	if ([virtualHosts count])
		return;

	//Clear the list in case we add this to an app with multiple view controllers
	[virtualHosts removeAllObjects];
	[[self tableView] reloadData];
	
	url = [[NSURL fileURLWithPath:[[NSBundle mainBundle] pathForResource:@"VirtualHosts" ofType:@"xml" ]] retain];

	//Create a request object with that URL
	NSURLRequest *request =
	[NSURLRequest requestWithURL:url
					 cachePolicy:NSURLRequestReloadIgnoringCacheData
				  timeoutInterval:30];
	//Clear out the existing connectection if there is one
	if (connectionInProgress) {
		[connectionInProgress cancel];
		[connectionInProgress release];
	}
	//Instantiate the object to hold the incoming data
	[xmlData release];
	xmlData = [[NSMutableData alloc] init];
	//Create and initiate the connection - non-blocking
	connectionInProgress = [[NSURLConnection alloc] initWithRequest:request
														   delegate:self
												   startImmediately:YES];
	
}

//This will be called several times as data arrives
-(void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
{
	[xmlData appendData:data];
}

-(void)connectionDidFinishLoading:(NSURLConnection *)connection
{
	
	NSXMLParser *parser = [[NSXMLParser alloc] initWithData:xmlData];
	[parser setDelegate:self];
	currentString = [NSMutableString string];
	[parser parse];
	[parser release];
	currentString = nil;

	[[self tableView] reloadData];
	
}

-(void)parser:(NSXMLParser *)parser
didStartElement:(NSString *)elementName
 namespaceURI:(NSString *)namespaceURI
qualifiedName:(NSString *)qName
   attributes:(NSDictionary *)attributeDict
{	
	if ([elementName isEqual:@"virtualHost"]) {
		currentHost = [[VirtualHost alloc] init];
		[self setContext:@"virtualHost"];
	}
	
	if ([elementName isEqual:@"Application"]) {
		currentApplication = [[Application alloc] init];
		[self setContext:@"application"];

	}
	if ([elementName isEqual:@"Document"]) {
		currentDocument = [[Document alloc] init];
		[self setContext:@"document"];
	}
	
	[currentString setString:@""];
}

-(void)parser:(NSXMLParser *)parser
foundCharacters:(NSString *)string
{
	[currentString appendString:string];
}

-(void) parser:(NSXMLParser *)parser
 didEndElement:(NSString *)elementName
  namespaceURI:(NSString *)namespaceURI
 qualifiedName:(NSString *)qName
{
	if ([elementName isEqual:@"currentConnections"]) {
		[self setCurrentConnections:[NSNumber numberWithInt:[currentString intValue]]];
    } else if ([elementName isEqual:@"acceptedConnections"]) {	
		[self setAcceptedConnections:[NSNumber numberWithInt:[currentString intValue]]];
    } else if ([elementName isEqual:@"virtualHost"]) {	
		[virtualHosts addObject:currentHost];
		[currentHost release];
        currentHost = nil;
    } else if ([elementName isEqual:@"Application"]) {
		[[currentHost applications] addObject:currentApplication];
		[currentApplication release];
        currentApplication = nil;
    } else if ([elementName isEqual:@"Document"]) {
		[[currentApplication documents] addObject:currentDocument];
		[currentDocument release];
        currentDocument = nil;
    } else if ([elementName isEqual:@"Uptime"]) {
        [currentHost setUpTime:[NSNumber numberWithInt:[currentString intValue]]];
	}
	else if ([elementName isEqual:@"Status"]) {
		[currentApplication setStatus:currentString];
	}
	else if ([elementName isEqual:@"Size"]) {
		[currentDocument setSize:[NSNumber numberWithInt:[currentString intValue]]];
	}
	else if ([elementName isEqual:@"Name"]) {
        if ([[self context] isEqualToString:@"virtualHost"]) {
			[currentHost setName:currentString];
		}
		else if ([self.context isEqualToString:@"application"]) {
			[currentApplication setName:currentString];
		}
		else
		{
			[currentDocument setName:currentString];
		}
	}
}

-(void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error
{
	[connectionInProgress release];
	connectionInProgress = nil;
	[xmlData release];
	xmlData = nil;
	
	NSString *errorString = [NSString stringWithFormat:@"Fetch failed: %@",
							 [error localizedDescription]];
	UIActionSheet *actionSheet =
	[[UIActionSheet alloc] initWithTitle:errorString
								delegate:nil
					cancelButtonTitle:@"OK"
				  destructiveButtonTitle:nil
					   otherButtonTitles:nil];
	[actionSheet showInView:[[self view] window]];
	[actionSheet autorelease];
}

-(void)viewWillAppear:(BOOL)animated
{
	[super viewWillAppear];
	[self loadHosts];
}

-(id) initWithStyle:(UITableViewStyle)style
{
	if (self = [super initWithStyle:style]) {
		virtualHosts = [[NSMutableArray alloc] init];
		[[self navigationItem] setTitle:@"Hosts"];

	}
	return self;
}

-(void) tableView:(UITableView *) tableView
didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
	
	if ([indexPath section] < 2)
	{
		[tableView deselectRowAtIndexPath:indexPath animated:YES];
		return;
	}
		
	if (!applicationTableViewController)
	{
		applicationTableViewController = [[ApplicationTableViewController alloc] initWithStyle:UITableViewStyleGrouped];
	}
	
	
	[applicationTableViewController setSelectedHost:[virtualHosts objectAtIndex:[indexPath row]]];
	
	[[self navigationController] pushViewController:applicationTableViewController
										   animated:YES];
}

-(NSInteger)tableView:(UITableView *)tableView
numberOfRowsInSection:(NSInteger)section
{
	if (section < 2) {
		return 1;
	}
	
	return [virtualHosts count];
}

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
	return 3;
}

- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section
{
	if (section == 0)
		return @"Current Connections";
	else if (section == 1)
		return @"Accepted Connections";
	
	return @"Virtual Hosts";
}

-(UITableViewCell *)tableView:(UITableView *)tableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
	UITableViewCell *cell =
	[tableView dequeueReusableCellWithIdentifier:@"UITableViewCell"];
	if (cell == nil) {
		cell = [[[UITableViewCell alloc]
				 initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"UITableViewCell"] autorelease];
	}
	
	NSNumberFormatter *formatter = [[[NSNumberFormatter alloc] init] autorelease];  
	
	[formatter setNumberStyle:NSNumberFormatterDecimalStyle];
	
	if ([indexPath section] == 0) {
		[[cell textLabel] setText:[formatter stringFromNumber:[self currentConnections]]];

	}
	else if ([indexPath section] == 1) {
		[[cell textLabel] setText:[formatter stringFromNumber:[self acceptedConnections]]];
	}
	else if ([indexPath section] == 2) {
		VirtualHost *virtualHost = [virtualHosts objectAtIndex:[indexPath row]];
		[[cell textLabel] setText:[virtualHost name]];
	}
		 
	return cell;
}
		 
- (void)didReceiveMemoryWarning {
    // Releases the view if it doesn't have a superview.
    [super didReceiveMemoryWarning];
    
    // Release any cached data, images, etc that aren't in use.
}

- (void)viewDidUnload {
    [super viewDidUnload];
    // Release any retained subviews of the main view.
    // e.g. self.myOutlet = nil;
}


- (void)dealloc {
	[applicationTableViewController release];
	[virtualHosts release];
	[url release];
    [super dealloc];
}


@end
//
//  ApplicationTableViewController.h
//  VirtualHosts
//

#import <UIKit/UIKit.h>
@class DocumentTableViewController;

@class VirtualHost;
@class Application;

@interface ApplicationTableViewController : UITableViewController {

	NSMutableArray *applications;
	
	DocumentTableViewController *documentTableViewController;
}

@property (nonatomic, retain) VirtualHost *selectedHost;

@end
//
//  ApplicationTableViewController.m
//  VirtualHosts
//

#import "ApplicationTableViewController.h"
#import "DocumentTableViewController.h"
#import "VirtualHost.h"
#import "Application.h"

@implementation ApplicationTableViewController

@synthesize selectedHost;

-(void)viewWillAppear:(BOOL)animated
{
	[super viewWillAppear];
	[[self navigationItem] setTitle:[selectedHost name]];
	[[self tableView] reloadData];
}

-(id) initWithStyle:(UITableViewStyle)style
{
	if (self = [super initWithStyle:style]) {
		return self;
	}
	return nil;
}

-(void) tableView:(UITableView *) tableView
didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
	if ([indexPath section] < 1)
	{
		[tableView deselectRowAtIndexPath:indexPath animated:YES];
		return;
	}

	if (!documentTableViewController)
	{
		documentTableViewController = [[DocumentTableViewController alloc] initWithStyle:UITableViewStyleGrouped];
	}
	
	[documentTableViewController setSelectedApplication:[[selectedHost applications] objectAtIndex:[indexPath row]]];
	
	[[self navigationController] pushViewController:documentTableViewController
										   animated:YES];
}

-(NSInteger)tableView:(UITableView *)tableView
numberOfRowsInSection:(NSInteger)section
{
	if (section < 1) {
		return 1;
	}
		
	return [[selectedHost applications] count];
}

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
	return 2;
}

- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section
{
	if (section == 0)
		return @"Up Time";

	return @"Applications";	
} 


-(UITableViewCell *)tableView:(UITableView *)tableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
	UITableViewCell *cell =
	[tableView dequeueReusableCellWithIdentifier:@"UITableViewCell"];
	if (cell == nil) {
		cell = [[[UITableViewCell alloc]
				 initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"UITableViewCell"] autorelease];
	}
	if ([indexPath section] == 0) {
		NSNumberFormatter *formatter = [[[NSNumberFormatter alloc] init] autorelease];
		[formatter setNumberStyle:NSNumberFormatterDecimalStyle];
		[[cell textLabel] setText:[formatter stringFromNumber:[selectedHost upTime]]];
	}
	else {
		Application *application = [[selectedHost applications] objectAtIndex:[indexPath row]];
		[[cell textLabel] setText:[NSString stringWithFormat:@"%@ (%@)",[application name],[application status]]];		
	}
	
	return cell;
	  
}
- (void)didReceiveMemoryWarning {
    // Releases the view if it doesn't have a superview.
    [super didReceiveMemoryWarning];
    
    // Release any cached data, images, etc that aren't in use.
}

- (void)viewDidUnload {
    [super viewDidUnload];
    // Release any retained subviews of the main view.
    // e.g. self.myOutlet = nil;
}


- (void)dealloc {
	[documentTableViewController release];
    [super dealloc];
}


@end
//
//  DocumentTableViewController.h
//  VirtualHosts
//

#import <UIKit/UIKit.h>
@class DocumentDetailTableViewController;

@class Application;

@interface DocumentTableViewController : UITableViewController {

	NSMutableArray *documents;
	
	DocumentDetailTableViewController *documentDetailTableViewController;
}

@property (nonatomic, retain) Application *selectedApplication;

@end
//
//  DocumentTableViewController.m
//  VirtualHosts
//

#import "DocumentTableViewController.h"
#import "DocumentDetailTableViewController.h"
#import "Application.h"

@implementation DocumentTableViewController

@synthesize selectedApplication;

-(void)viewWillAppear:(BOOL)animated
{
	[super viewWillAppear];
	[[self navigationItem] setTitle:[selectedApplication name]];
	[[self tableView] reloadData];
}

-(id) initWithStyle:(UITableViewStyle)style
{
	if (self = [super initWithStyle:style]) {

	}
	return self;
}

-(void) tableView:(UITableView *) tableView
didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
	if (!documentDetailTableViewController)
	{
		documentDetailTableViewController = [[DocumentDetailTableViewController alloc] initWithStyle:UITableViewStyleGrouped];
	}
	
	[documentDetailTableViewController setSelectedDocument:[[selectedApplication documents] objectAtIndex:[indexPath row]]];
	
	[[self navigationController] pushViewController:documentDetailTableViewController
										   animated:YES];
}

-(NSInteger)tableView:(UITableView *)tableView
numberOfRowsInSection:(NSInteger)section
{
	return [[selectedApplication documents] count];
}

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
	return 1;
}

- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section
{	
	return @"Documents";	
} 

-(UITableViewCell *)tableView:(UITableView *)tableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
	UITableViewCell *cell =
	[tableView dequeueReusableCellWithIdentifier:@"UITableViewCell"];
	if (cell == nil) {
		cell = [[[UITableViewCell alloc]
				 initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"UITableViewCell"] autorelease];
	}
	
	Document *document = [[selectedApplication documents] objectAtIndex:[indexPath row]];
	[[cell textLabel] setText:[document name]];
	return cell;
	  
}
- (void)didReceiveMemoryWarning {
    // Releases the view if it doesn't have a superview.
    [super didReceiveMemoryWarning];
    
    // Release any cached data, images, etc that aren't in use.
}

- (void)viewDidUnload {
    [super viewDidUnload];
    // Release any retained subviews of the main view.
    // e.g. self.myOutlet = nil;
}


- (void)dealloc {
	[documentDetailTableViewController release];
    [super dealloc];
}


@end
//
//  DocumentDetailTableViewController.h
//  VirtualHosts
//

#import <UIKit/UIKit.h>
@class DocumentDetailViewController;
@class Document;

@interface DocumentDetailTableViewController : UITableViewController {
}

@property (nonatomic, assign) Document *selectedDocument;

@end
//
//  DocumentDetailTableViewController.m
//  VirtualHosts
//

#import "DocumentDetailTableViewController.h"
#import "Document.h"

@implementation DocumentDetailTableViewController

@synthesize selectedDocument;

-(void)viewWillAppear:(BOOL)animated
{
	[super viewWillAppear];
	[[self navigationItem] setTitle:[selectedDocument name]];
	[[self tableView] reloadData];
}

-(id) initWithStyle:(UITableViewStyle)style
{
	if (self = [super initWithStyle:style]) {
		return self;
	}
	return nil;
}


-(NSInteger)tableView:(UITableView *)tableView
numberOfRowsInSection:(NSInteger)section
{
	return 1;
}

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
	return 2;
}

- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section
{
	if (section == 0)
		return @"Document Name";
	
	return @"Size";
} 

-(void) tableView:(UITableView *) tableView
didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
	[tableView deselectRowAtIndexPath:indexPath animated:YES];
}	

-(UITableViewCell *)tableView:(UITableView *)tableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
	UITableViewCell *cell =
	[tableView dequeueReusableCellWithIdentifier:@"UITableViewCell"];
	if (cell == nil) {
		cell = [[[UITableViewCell alloc]
				 initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"UITableViewCell"] autorelease];
	}
	if ([indexPath section] == 0) {
		[[cell textLabel] setText:[selectedDocument name]];
	}
	else {
		NSNumberFormatter *formatter = [[[NSNumberFormatter alloc] init] autorelease];
		[formatter setNumberStyle:NSNumberFormatterDecimalStyle];
		[[cell textLabel] setText:[formatter stringFromNumber:[selectedDocument size]]];
	}

	return cell;
	  
}
- (void)didReceiveMemoryWarning {
    // Releases the view if it doesn't have a superview.
    [super didReceiveMemoryWarning];
    
    // Release any cached data, images, etc that aren't in use.
}

- (void)viewDidUnload {
    [super viewDidUnload];
    // Release any retained subviews of the main view.
    // e.g. self.myOutlet = nil;
}


- (void)dealloc {
    [super dealloc];
}


@end
//
//  VirtualHostsAppDelegate.h
//  VirtualHosts
//

#import <UIKit/UIKit.h>

@class VirtualHostTableViewController;

@interface VirtualHostsAppDelegate : NSObject <UIApplicationDelegate> {
    UIWindow *window;
	VirtualHostTableViewController *tvc;
	UINavigationController *navController;
}

@property (nonatomic, retain) IBOutlet UIWindow *window;

@end
//
//  VirtualHostsAppDelegate.m
//  VirtualHosts
//

#import "VirtualHostsAppDelegate.h"
#import "VirtualHostTableViewController.h"

@implementation VirtualHostsAppDelegate

@synthesize window;


- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {    

	tvc = [[VirtualHostTableViewController alloc]
								   initWithStyle:UITableViewStyleGrouped];
	
	navController = [[UINavigationController alloc]
											 initWithRootViewController:tvc];
	[window addSubview:[navController view]];
	
    // Override point for customization after application launch
	
    [window makeKeyAndVisible];
	
	return YES;
}


- (void)dealloc {
    [window release];
	[tvc release];
	[navController release];
    [super dealloc];
}


@end
//
//  Document.h
//  VirtualHosts
//

#import <Foundation/Foundation.h>

@interface Document : NSObject {
	
}

@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSNumber *size;

@end
//
//  Document.m
//  VirtualHosts
//

#import "Document.h"


@implementation Document

@synthesize name, size;

-(void) dealloc
{
	[name release];
	[size release];

	[super dealloc];
}

@end
//
//  Application.h
//  VirtualHosts
//

#import <Foundation/Foundation.h>


@interface Application : NSObject {

}

@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *status;
@property (nonatomic, copy) NSMutableArray *documents;

@end
//
//  Application.m
//  VirtualHosts
//

#import "Application.h"


@implementation Application

@synthesize name, status, documents;

-(id) init
{
	[super init];
	documents = [[NSMutableArray alloc] init];
	return self;	
}


-(void) dealloc
{
	[name release];
	[status release];
	[documents release];
	
	[super dealloc];
}


@end
//
//  VirtualHost.h
//  VirtualHosts
//

#import <Foundation/Foundation.h>


@interface VirtualHost : NSObject {

}
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSNumber *upTime;
@property (nonatomic, copy) NSMutableArray *applications;


@end
//
//  VirtualHost.m
//  VirtualHosts
//

#import "VirtualHost.h"


@implementation VirtualHost

@synthesize name, upTime, applications;

-(id) init
{
	[super init];
	applications = [[NSMutableArray alloc] init];
	return self;	
}


-(void) dealloc
{
	[name release];
	[upTime release];
	[applications release];
	
	[super dealloc];
}


@end

#3

Many thanks! I will play with this and report back. About the missing “>”, it’s a sample XML that I wrote on the fly, not the “production” xml.

XML is actually pulled realtime from the web, but I know how to do that part.

Cheers and happy holidays,

Manuel.-


#4

Great! As promised, I looked at your code and applied the logic to my real XML file and it is working great!

Thank you,

Manuel.-


#5

Hi,

My pleasure - Glad it worked out.

Gareth