PDA

View Full Version : NSXMLParser Simple Problem




Soulstorm
Feb 6, 2008, 10:17 AM
I have an xml document:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE addresses SYSTEM "addresses.dtd">
<addresses>
<person>
<lastName>Doe</lastName>
<firstName>John</firstName>
<email>jdoe@foo.com</email>
<address>
<street>100 Main Street</street>
<city>Somewhere</city>
<state>New Jersey</state>
<zip>07670</zip>
</address>
</person>
</addresses>

I have set up a cocoa application for testing purposes. The application consists of a single file. Inside that, this is the code. Header and Implementation file appears together below.



#import <Cocoa/Cocoa.h>
#import "ABPerson.h"

@interface SFXMLParser : NSObject {
NSXMLParser *addressParser;

NSMutableArray *addresses;
ABPerson *currentPerson;

NSMutableString *currentStringValue;

NSMutableArray *dictProperties;

NSString *currentName;
}
- (void)parseXMLFile:(NSString *)pathToFile;

-(IBAction)do:(id)sender;
@end





#import "SFXMLParser.h"


@implementation SFXMLParser
- (id) init
{
self = [super init];
if (self != nil) {

}
return self;
}


-(void)awakeFromNib
{
NSString *str = [[NSString stringWithString:@"~/Desktop/info.xml"]stringByStandardizingPath];
[self parseXMLFile:str];
}


- (void)parseXMLFile:(NSString *)pathToFile {
BOOL success;

NSURL *xmlURL = [NSURL fileURLWithPath:pathToFile];
if (addressParser) // addressParser is an NSXMLParser instance variable
[addressParser release];

addressParser = [[NSXMLParser alloc] initWithContentsOfURL:xmlURL];
[addressParser setDelegate:self];
[addressParser setShouldResolveExternalEntities:YES];

success = [addressParser parse]; // return value not used
// if not successful, delegate is informed of error
NSLog(@"parse!");
}


- (void)parser:(NSXMLParser *)parser didStartElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName attributes:(NSDictionary *)attributeDict
{

if ( [elementName isEqualToString:@"addresses"]) {
// addresses is an NSMutableArray instance variable
NSLog(@"found addresses");
if (!addresses)
addresses = [[NSMutableArray alloc] init];
return;
}

if ( [elementName isEqualToString:@"person"] ) {
NSLog(@"found ABPerson");
// currentPerson is an ABPerson instance variable
currentPerson = [[ABPerson alloc] init];
return;
}
if ( [elementName isEqualToString:@"lastName"] ) {
NSLog(@"found lastName");
// currentPerson is an ABPerson instance variable
currentPerson = [[ABPerson alloc] init];
return;
}

}

- (void)parser:(NSXMLParser *)parser foundCharacters:(NSString *)string {
NSLog(@"found characters:%@",string);
if (!currentStringValue) {
// currentStringValue is an NSMutableString instance variable
currentStringValue = [[NSMutableString alloc] init];
}
NSLog(@"appendingstring...");
[currentStringValue appendString:string];
}

- (void)parser:(NSXMLParser *)parser didEndElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName
{
if (( [elementName isEqualToString:@"addresses"]) ||
( [elementName isEqualToString:@"address"] )) return;

if ([elementName isEqualToString:@"lastName"]) {
NSLog(@"lastname entering...%@",currentStringValue);
[currentPerson setLastName:currentStringValue];
}

if ([elementName isEqualToString:@"firstName"]) {
NSLog(@"first name entering...");
[currentPerson setFirstName:currentStringValue];
}
if ([elementName isEqualToString:@"email"]) {
NSLog(@"email entering...");
[currentPerson setEmail:currentStringValue];
}

if ( [elementName isEqualToString:@"person"] ) {
[addresses addObject:currentPerson];
[currentPerson release];
return;
}
[currentStringValue release];
currentStringValue = nil;
}

-(IBAction)do:(id)sender
{
int i = 0;
NSLog(@"first name:%@\nlast name:%@\nemail:%@",[[addresses objectAtIndex:i]firstName], [[addresses objectAtIndex:i]lastName], [[addresses objectAtIndex:i]email]);
}

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

@end


The problem is that when I press the "do" button (the one that activates the "do" IBAction) I get the following result:

2008-02-06 18:13:04.906 cocoaki2[582:10b] first name:
John
last name:

Doe
email:
jdoe@foo.com


The application appears to give more new lines than those exists inside the file. Why all those '\n' characters?



iSee
Feb 6, 2008, 02:52 PM
I read over the code and it looks fine to me. You aren't inserting that white space in the code, anyway.

It's suspicious because notice that the extra white space includes indentation (spaces or tabs) in addition to newlines.

So either the parser is doing it (that would be a big bug). Or the white space really is in the source .xml file. If you haven't already, open ~/Desktop/info.xml in a plain text editor (not a high-level xml editor that may be hiding or adding white space) to verify its contents.

kainjow
Feb 6, 2008, 03:07 PM
I think parser:foundCharacters: is getting called for every element, and you're appending the element value to your variable. I believe whitespace is an XML element and so the whitespace is being appended to your string. You should set a flag so that in that delegate method you're only appending to your variable when the flag is on.

Also, if you're targeting 10.4+, you can use NSXMLDocument and related classes and read the XML with XPath and other DOM related methods. I find that a much easier way for simple XML reading.

iSee
Feb 6, 2008, 04:08 PM
Ah, I see what you mean kainjow.

the currentStringValue used to set firstName, for example, includes the whitespace from the end of </lastName> to the start of <firstName>: a newline plus eight spaces.

So the OP could modify the code so that:
1. currentStringValue is not allocated in -foundCharacters[]. Instead -foundCharacters[] would do nothing when currentStringValue is nil.
2. currentStringValue is allocated and init'ed in didStartElement when an appropriate element name is passed in. It would be good to double-check that currentStringValue wasn't already non-nil.

(My solution uses whether or not currentStringValue is nil as the flag you mention in your post.)

Soulstorm
Feb 10, 2008, 10:23 AM
Seems the NSXMLParser fails to load nested variables. Can anyone help me with this? Those are the files:

ABPerson Definition File:
#import <Cocoa/Cocoa.h>
#import "ABAddress.h"

@interface ABPerson : NSObject {
NSString *lastName;
NSString *firstName;
NSString *email;

ABAddress *address;
}

- (NSString *)lastName;
- (NSString *)firstName;
- (NSString *)email;
- (ABAddress *)address;

- (void)setAddress:(ABAddress *)newAddress;
- (void)setLastName:(NSString *)newLastName;
- (void)setFirstName:(NSString *)newFirstName;
- (void)setEmail:(NSString *)newEmail;

@end


ABAddress definition file:
#import <Cocoa/Cocoa.h>


@interface ABAddress : NSObject {
NSString *street;
NSString *city;
NSString *state;
NSString *zipCode;
}

- (void)setStreet:(NSString *)aStreet;
- (void)setCity:(NSString *)aCity;
- (void)setState:(NSString *)aState;
- (void)setZipCode:(NSString *)aZipCode;

- (NSString *)street;
- (NSString *)city;
- (NSString *)state;
- (NSString *)zipCode;

@end


SFXMLParser definition and Implementation:
#import <Cocoa/Cocoa.h>
#import "ABPerson.h"

@interface SFXMLParser : NSObject {
NSXMLParser *addressParser;

NSMutableArray *addresses;
ABPerson *currentPerson;

NSMutableString *currentStringValue;

NSMutableArray *dictProperties;
NSString *currentName;

ABAddress *currentAddress;
}
- (void)parseXMLFile:(NSString *)pathToFile;

-(IBAction)do:(id)sender;
@end



#import "SFXMLParser.h"


@implementation SFXMLParser
- (id) init
{
self = [super init];
if (self != nil) {

}
return self;
}


-(void)awakeFromNib
{
NSString *str = [[NSString stringWithString:@"~/Desktop/info.xml"]stringByStandardizingPath];
[self parseXMLFile:str];
}


- (void)parseXMLFile:(NSString *)pathToFile {
BOOL success;

NSURL *xmlURL = [NSURL fileURLWithPath:pathToFile];
if (addressParser) // addressParser is an NSXMLParser instance variable
[addressParser release];

addressParser = [[NSXMLParser alloc] initWithContentsOfURL:xmlURL];
[addressParser setDelegate:self];
[addressParser setShouldResolveExternalEntities:YES];

success = [addressParser parse]; // return value not used
// if not successful, delegate is informed of error
NSLog(@"parse!");
}


- (void)parser:(NSXMLParser *)parser didStartElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName attributes:(NSDictionary *)attributeDict
{
currentStringValue = [[NSMutableString alloc] init];
if ( [elementName isEqualToString:@"addresses"]) {
// addresses is an NSMutableArray instance variable
NSLog(@"found addresses");
if (!addresses)
addresses = [[NSMutableArray alloc] init];
return;
}

if ( [elementName isEqualToString:@"address"]) {
// addresses is an NSMutableArray instance variable
NSLog(@"found single address");
if (!currentAddress)
currentAddress = [[ABAddress alloc] init];
return;
}

if ( [elementName isEqualToString:@"person"] ) {
NSLog(@"found ABPerson");
// currentPerson is an ABPerson instance variable
currentPerson = [[ABPerson alloc] init];
return;
}


}

- (void)parser:(NSXMLParser *)parser foundCharacters:(NSString *)string
{
//NSLog(@"found characters:%@",string);
if (!currentStringValue) {
// currentStringValue is an NSMutableString instance variable
//currentStringValue = [[NSMutableString alloc] init];
}
//NSLog(@"appendingstring...");
[currentStringValue appendString:string];
}

- (void)parser:(NSXMLParser *)parser didEndElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName
{
if (( [elementName isEqualToString:@"addresses"]) ||
( [elementName isEqualToString:@"address"] )) return;

if ([elementName isEqualToString:@"lastName"]) {
NSLog(@"lastname entering...%@",currentStringValue);
[currentPerson setLastName:currentStringValue];
}

if ([elementName isEqualToString:@"firstName"]) {
NSLog(@"first name entering...");
[currentPerson setFirstName:currentStringValue];
}
if ([elementName isEqualToString:@"email"]) {
NSLog(@"email entering...");
[currentPerson setEmail:currentStringValue];
}

if ([elementName isEqualToString:@"street"]) {
NSLog(@"street entering...");
[currentAddress setStreet:currentStringValue];
}

if ([elementName isEqualToString:@"city"]) {
NSLog(@"city entering...");
[currentAddress setCity:currentStringValue];
}

if ([elementName isEqualToString:@"state"]) {
NSLog(@"state entering...");
[currentAddress setState:currentStringValue];
}

if ([elementName isEqualToString:@"zip"]) {
NSLog(@"zipcode entering...");
[currentAddress setZipCode:currentStringValue];
}

if ([elementName isEqualToString:@"address"]) {
NSLog(@"address finishing...");
[currentPerson setAddress:currentAddress];
[currentAddress release];
return;
}

if ( [elementName isEqualToString:@"person"] ) {
[addresses addObject:currentPerson];
[currentPerson release];
return;
}
[currentStringValue release];
currentStringValue = nil;
}

-(IBAction)do:(id)sender
{
int i = 0;
//NSLog(@"first name:%@\nlast name:%@\nemail:%@",[[addresses objectAtIndex:i]firstName], [[addresses objectAtIndex:i]lastName], [[addresses objectAtIndex:i]email]);
ABPerson *person = [addresses objectAtIndex:i];

if ([[person address]state] == nil) {
NSLog(@"fdssdfsd nooooooooooooooooooooooooooo!");
}

NSLog(@"it is: %@",[[person address]zipCode]);
}

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

@end

The problem is that the parser although it read correctly every ABPerson attribute, it won't give me any result for its ABAddress variable.

Any idea why this happens?

iSee
Feb 10, 2008, 10:42 AM
Ah ha, I see the problem. It's just a little copy & paste error.

Look at didStartElement, where you are allocating currentAddress.
Notice you are checking if addresses is nil, not currentAddress...

Soulstorm
Feb 10, 2008, 10:55 AM
Ah ha, I see the problem. It's just a little copy & paste error.

Look at didStartElement, where you are allocating currentAddress.
Notice you are checking if addresses is nil, not currentAddress...

That was correct, but the problem remains the same (I changed my original post to avoid confusing anyone):

Strangely, this is what I get from the result

2008-02-10 18:52:36.977 cocoaki2[1155:10b] found addresses
2008-02-10 18:52:36.983 cocoaki2[1155:10b] found ABPerson
2008-02-10 18:52:36.984 cocoaki2[1155:10b] lastname entering...Doe
2008-02-10 18:52:36.984 cocoaki2[1155:10b] first name entering...
2008-02-10 18:52:36.985 cocoaki2[1155:10b] email entering...
2008-02-10 18:52:36.985 cocoaki2[1155:10b] found single address
2008-02-10 18:52:36.986 cocoaki2[1155:10b] street entering...
2008-02-10 18:52:36.986 cocoaki2[1155:10b] city entering...
2008-02-10 18:52:36.986 cocoaki2[1155:10b] state entering...
2008-02-10 18:52:36.987 cocoaki2[1155:10b] zipcode entering...
2008-02-10 18:52:36.987 cocoaki2[1155:10b] person ending
2008-02-10 18:52:36.988 cocoaki2[1155:10b] parse!
2008-02-10 18:52:38.109 cocoaki2[1155:10b] it is:

Notice that the didEndElement function is NOT called when addresses end! Or, is it called and something messy is going on?

EDIT: Actually, this is getting weird. The didEndElement function is indeed called for the "address" field. However, it fails to go into this branch:

if ([elementName isEqualToString:@"address"]) {
NSLog(@"address finishing... %@", [currentAddress state]);
[currentPerson setAddress:currentAddress];
[currentAddress release];
return;
}

It's as if the "if" function has something wrong, only I can't figure out what!
EDIT2: I am stupid. this line was the problem:

if (( [elementName isEqualToString:@"addresses"]) ||
( [elementName isEqualToString:@"address"] )) return;

I changed it to:
if ( [elementName isEqualToString:@"addresses"])
return;
and it worked,only I am experiencing a crash. It's a memory management thingie, since when I enable garbage collection, it works just fine.