Thursday, November 7, 2013

Parsing XML files in iOS using XPath

Hi all. After a long break I have decided to add another post to my blogger about another interesting post and a technique that found really challenging to me while working on a certain project recently. OK, this time I'm going to explain how I solved a bit of a tough task of parsing an XML file from an iOS Application easily. Keep one thing in your mind that I will not be using 100% of my own code in this post but I will be instructing you to download few header and implementation files to your project in order to perform this task. Also note that you also require a well formatted valid XML file to test your app and ensure that the parsing of XML task is done as you expected.

By the way, let's quickly go through some very simple questions before starting with the code. Try to understand the following and make sure you know the background of what I'm going to perform here.

XML... what's it about?

XML stands for Extensible Markup Language and it is a markup language (everything is marked as a tag) that defines a set of rules for encoding documents in a format that both human-readable and machine-readable. 

Why XML is so popular?

I don't think that anyone in the field of Information Technology will ever tell that they have never heard the word XML since it is so popular because the design goals of XML highlight simplicity, generality and usability over the Internet. XML files are simply text files and no mater of the platform or technology we are using XML can be understood by all of them.

Well Formed Vs Valid XML Documents

A "Well Formed" XML Document has correct XML syntax. For an example, 
  • XML documents must have a root element
  • XML elements have a closing tag
  • XML tags are case sensitive
  • XML elements must be properly nested
  • XML attribute values must be quoted

A "Valid" XML document is a "Well Formed" XML document which is also confirms to the rules of Document Type Definition (DTD)

How to Validate your XML file?

Let's say that you have an XML file with you and you want to validate it to check for it's syntax. you have a number of tools to do this but let's make it simple. Follow the below links to find out how this task can be easily done through online tools within few seconds. All what you have to do is to simply copy paste the content of your XML file or browse your XML file in the given URL and use the validator to check your file.


OK, now let's move to the most interesting part of our post today. As I told you earlier, we need to have a valid and well formed XML file and it is going to be the below shown file.


File content is as below,

<services>
<service name="Service1" url="/utilities/service1.xml">
<type>GET</type>
<parameter-mapping enumvalue="ID">ID</parameter-mapping>
<parameter-mapping enumvalue="NAMEOFDEVICE">DeviceName</parameter-mapping>
<parameter-mapping enumvalue="SERIALOFDEVICE">Serial</parameter-mapping>
<parameter-mapping enumvalue="IPADDRESS">IP_Address</parameter-mapping>
<parameter-mapping enumvalue="STATUS">status</parameter-mapping>
</service>
<service name="Service2" url="/utilities/service2.xml">
<type>POST</type>
<parameter-mapping enumvalue="DEVICEUNIQUEID">UniqueId</parameter-mapping>
<parameter-mapping enumvalue="PASSWORD">password</parameter-mapping>
<parameter-mapping enumvalue="PAIREDDEVICEID">PairedTabletID</parameter-mapping>
<parameter-mapping enumvalue="STATUS">status</parameter-mapping>
</service>
</services>

I have added this xml file to my project resources in the sample XCode project that I've created by just dragging and dropping it to the project file hierarchy as below.


























OK, Now we have simply added the required XML file to our XCode project. Now it's time to get the content of the XML file and start parsing it using our app.

By the way, Do you know how to read an embedded file from an XCode project in Objective-C?


It's simple. Just a matter of adding few lines of code to get the file content to an NSString. Refer the code below.


NSData *configXmlData;
/*
*Function reads the embedded SampleXMLFile.xml file and saves to an instance variable call configXmlData of type NSData
*@return : no data returned
*/

-(void)setConfigurationFile
{
    //reading the resources from the mainBundle of where it search for a file named SampleXMLFile with file extension of xml
  NSString *configXmlFilePath=[[NSBundle mainBundle] pathForResource:@"SampleXMLFile" ofType:@"xml"];

  @try
  {
   //if the file exist, then get the contents of the file to the configXmlData variable 
   if([[NSFileManager defaultManager]fileExistsAtPath:configXmlFilePath])
   {
    configXmlData=[NSData dataWithContentsOfFile:configXmlFilePath];
   }

  }

  //if incase of an error occurs (file doesn't exist or user has no privilege to access it etc ), then print this error message in NSLog
  @catch (NSException *exception)
  {
   NSLog(@"Exception Occurred in reading setConfigurationFile: %@ ",exception);
  }
}


Now Let's turn on the XPath related work now. Keep in mind that I am going to use some files for this purpose that are available in GitHub in the Hppl Project. Use the below shown link to access the Hppl project.

Access Hppl Project From Here

download the project above and add these files to your project.
  • TFHppl.h
  • TFHppl.m
  • TFHpplElement.h
  • TFHpplElement.m
  • XPathQuery.h
  • XPathQuery.m
remember that you need to add reference to these header files from your Xcode project.

What Am I going to do now...

Ok, let's come to the main target of this post. Here's what I am going to do. I hope you have noticed the structure of the above shown XML file. All what I m going to do is to extract the data of that file as I wanted. Let's say I don't need the entire file content always but a part of it is sufficient for me to perform a task as shown below.

When I pass a string called 'Service1' then the app should display the output as below.

url=/utilities/service1.xml
type=GET
ID=ID
NAMEOFDEVICE=DeviceName
SERIALOFDEVICE=Serial
IPADDRESS=IP_Address
STATUS=status

when I pass s string 'Service2' then the app should display the output as below.

url=/utilities/service2.xml
type=POST
DEVICEUNIQUEID=UniqueId
PASSWORD=password
PAIREDDEVICEID=PairedTabletID
STATUS=status

My plan is to write a simple XML parser function to read tags, attributes, elements and values of the XML file accordingly with the help of the methods included in Hppl Project that we have just added to the project. It's really easy and all what you have to pay a little more attention is for the XPath query that you are suppose to write to extract the matching XML elements.
For simplicity, I'll explain you the methods one by one and then you can download the sample app and monitor how things are handled as a whole.

Note that MetaConfigurationHandler is the name of the class that I have and you need to add two more reference for it as below.

#import "TFHpple.h"
#import "Discovery.h"
 
 
/*
*Function creates an NSDictionary containing all the data required for a specific http connection as key value pairs
*@return : an NSDictionary containing the required key value pairs
*/

-(NSDictionary *)generateConfigurationDictionary:(NSString *)request
{
//calls the above mentioned function to read the embedded xml

//file contents to the instance variable configXmlData
  [self setConfigurationFile];

//holds each value for each key of the dictionary
  NSString *valueForKey=nil;

//holds each key of the dictionary
  NSString *key=nil;

//holds the name of the tag
  NSString *tagName=nil;

//holds the xPath query (what matching string we are searching for)
  NSString *xPathQuery=nil;

//holds all the elements of the Services tag
  NSArray *configurationServiceElements=[NSMutableArray array];

//this will be returned as an output containing the aforementioned
//NSMutableDictionary with keys and values respectively
  NSMutableDictionary *configurationDictionary =[NSMutableDictionary dictionary];

@try
{
//xpath query string to search for the children of service tag

//where the service's name= request (here, request is a string
//as Service1 or Service2 that is passed to this function as
//a parameter
  xPathQuery=[NSString stringWithFormat:@"/services/service[@name='%@']",request];

//passing the xPath query, retrieving all the tags that are

//matching with the above xPath query
//configurationServiceElements array will hold elements like url,
//type,ID,NAMEOFDEVICE,SERIALOFDEVICE,IPADDRESS and STATUS

//when request=Service1

  configurationServiceElements=[self getConfigurationElementArray:xPathQuery];

 //valueForKey is going to hold /utilities/service1.xml

//(when request=Service1)
  valueForKey=[[configurationServiceElements objectAtIndex:0] objectForKey:@"url"];

//add value=/utilities/service1.xml where key=url

//(when request=Service1) which
//means configurationDictionary will hold one entry with a key-value pair
  [configurationDictionary setObject:valueForKey forKey:@"url"];

//clear the xPathQuery
  xPathQuery=nil;

//xpath query string to search for the value under the tag named as type and
//which is a sub element od Services/service where service name is equal to
//request
  xPathQuery=[NSString stringWithFormat:@"/services/service[@name='%@']/type",request];

//pass the query to get the value
  configurationServiceElements=[self getConfigurationElementArray:xPathQuery];

//how to get the value of the tag which is type
  valueForKey=[[[configurationServiceElements objectAtIndex:0] firstChild] content];

//add this key value pair to the dictionary,  configurationDictionary
  [configurationDictionary setObject:valueForKey forKey:@"type"];


/*Now, I'm trying to get all the tags under /services/service that are named as
parameter-mapping, then get(element) each of its attribute values and element values to form the key-value pairs for the dictionary as
ID: ID
NAMEOFDEVICE=DeviceName
SERIALOFDEVICE:Serial etc
*/


//first the xPath query search for parameter-mapping elements
//under services/service
  xPathQuery=[NSString stringWithFormat:@"/services/service[@name='%@']/parameter-mapping", request];

//get all those elements that matches the xpath query above to an array
configurationServiceElements=[self getConfigurationElementArray: xPathQuery];
 

//now for each element in the configurationServiceElements array...
  for (TFHppleElement *element in configurationServiceElements)
  {

         //get the name of the tag and assign to tagName variable
     tagName=element.tagName;
       

     //check if that tagName is equal to parameter-mapping
     if[tagName isEqualToString:@"parameter-mapping"])

     //if true, assign it to key          
      key=element.tagName;

     //get the attribute of parameter-mapping which is enumvalue and
     //make it a key
     key=[element objectForKey:@"enumvalue"];


          //get the value of the attribute named key (which means enumvalue)and
     //assign it to valueForKey
     valueForKey=[[element firstChild]content];
     

     //now add those key-value pair to the configurationDictionary as
     //another entry 
     [configurationDictionary setObject:valueForKey forKey:key];          
  }
}

  //if incase an exception occurs then handle it by printing the exception
  //in NSLog
  @catch (NSException *exception)
  {
    NSLog(@"Exception Occurred in generateConfigurationDictionary:%@ ",exception);  
  }

 //finally, return the configurationDictionary NDmutableDictionary
  @finally
  {
    return configurationDictionary;
  }

}
 

/*
*Function executes an xpath query and returns the matching nodes as an NSArray to the callee
*@xPathQuery : xpath query as a string should be passed
*@return : an NSArray containing the matching elements will be returned
*/

-(NSArray *)getConfigurationElementArray:(NSString *)xPathQuery
{
  NSArray *servicesNodes;
  TFHpple *servicesParser = [TFHpple hppleWithXMLData:configXmlData];
  NSString *servicesXpathQueryString = xPathQuery;               
  servicesNodes = [servicesParser searchWithXPathQuery:servicesXpathQueryString];
  return servicesNodes;
}


Now, the most required or I would rather say most difficult part of our XML parsing is done. All what you have to do is to make relevant function calls passing proper parameters to get the output as you want.


for an example, I can call the parser functions as shown below. Assume that I am going to call these functions from another class

MetaConfigurationHandler *meta=[[MetaConfigurationHandler alloc]init];

NSDictionary  *configDic=[NSDictionary  dictionary];

*configDic=[meta generateConfigurationDictionary:@"Service1"];


Now you can simply print the configDic contents in NSLog or as you preferred. 

Hope this post was interesting to you guys  :)

Further reading :

http://www.w3schools.com/xml/default.asp
http://www.raywenderlich.com/14172/how-to-parse-html-on-ios

No comments:

Post a Comment