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>
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.
/*
*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
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
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