Jian Yu
Jian Yu

Reputation: 11

XML Parser (Objective C)

I have an xml file, shown below. I'm using NSXMLParser, however I'm not able to parse my author and summary. Because of access rights, I cannot edit the xml file.

Any solution?

XML File:

<book>
<title>Book 1</title>
<author>
<subfield id="a"> Jason </subfield>
<subfield id="b"> Alfonso. </subfield>
</author>
<summary>
<subfield id="a"> Milano </subfield>
<subfield id="b"> Italy </subfield>
</summary>
</book>

My Code:

- (void)parser:(NSXMLParser *)parser didStartElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName attributes:(NSDictionary *)attributeDict{
    currentElement = [elementName copy];
    attributes = [attributeDict copy];

    if ([elementName isEqualToString:@"book"]) {
        item = [[NSMutableDictionary alloc] init];
    } else if ([elementName isEqualToString:@"title"]) {
        self.title = [[NSMutableString alloc] init];
    } else if ([elementName isEqualToString:@"subfield"]) {
        if ([[attributeDict valueForKey:@"id"] isEqualToString:@"a"]) {
            self.authorName1 = [[NSMutableString alloc] init];
        }
    } else if ([elementName isEqualToString:@"subfield"]) {
        if ([[attributeDict valueForKey:@"id"] isEqualToString:@"b"]) {
                        self.authorName2 = [[NSMutableString alloc] init];
        }
    }
}

i'm able to grab subfield. however even summary's subfield is grab during the author which i do not what to. i need both to be separated

Upvotes: 1

Views: 6416

Answers (2)

outis
outis

Reputation: 77400

Note that it might be easier to base your app on Core Data or NSXML (using an appropriate init method of NSXMLDocument) rather than taking responsibility for parsing the XML files. If you wish to do so, read on.

Using different element names for the subfield elements will fix the clobbering problem.

<book>
  <title>Book 1</title>
  <author>
    <first> Jason </first>
    <last> Alfonso. </last>
  </author>
  <summary>
    <city> Milano </city>
    <country> Italy </country>
  </summary>
</book>

However, there are better ways still.

Generally speaking, to properly parse an XML file you'll need to maintain a stack of elements in-process. When parsing of an element starts, you create a new element and add it to the stack. When parsing of an element finishes, you pop an element off the the stack and give it to the element now on the top of the stack. You can create elements of different classes based on the element name by using a factory method (below, -nodeWithTag:attributes:parser:) and a dictionary that maps element names to classes (below, elementClasses).

/* category to return a default object (rather than nil)
   when a key isn't present in a dictionary.
 */
@interface NSDictionary (defaultObject)
-(id)objectForKey:(NSString*)key default:(id)default;
@end

@implementation NSDictionary (defaultObject)
-(id)objectForKey:(NSString*)key default:(id)default {
    id object = [self objectForKey:key];
    if (nil == object) {
        return default;
    }
    return object;
}
@end

/* category to add aliases for stack operations
   to NSMutableArray
 */
@interface NSMutableArray (stack)
-(void)push:(id)object;
-(id)pop;
-(id)top;
@end

@implementation NSMutableArray (stack)
// could also use class_addMethod to alias push & top
-(void)push:(id)object {
    [self addObject:object];
}
-(id)pop {
    id last = [self lastObject];
    [self removeLastObject];
    return last;
}
-(id)top {
    return [self lastObject];
}
@end


// the parser delegate.
@interface ... <NSXMLParserDelegate> {
    NSMutableArray activeElements;
    id item;
   ...
@property (nonatomic,retain) item;
@end

@implementation ...
@synthesize item;

#pragma mark Class members
// map element names to classes
static NSDictionary *elementClasses;

+(void)initialize {
    nodeTypes=[[NSDictionary alloc] initWithObjectsAndKeys:
      // Just an illustrative example of a custom class.
      // You don't necessarily need a Book class.
      [Book class],@"book",
      nil];
}

// if you have other init methods, make sure activeElements is created.
-(id)init {
    if ((self = [super init])) {
        activeElements = [[NSMutableArray alloc] init];
        ...
    }
    return self;
}

-(void)parserDidStartDocument:(NSXMLParser *)parser {
    // add sentinel element so stack isn't empty at start.
    [activeElements push:[self nodeWithTag:@"root" attributes:nil parser:parser]];
}

-(void)parserDidEndDocument:(NSXMLParser *)parser {
    // The parser should ensure only case 1 is reachable, but still...
    switch ([activeElements count]) {
    case 0:
        NSLog(@"Root element removed from stack early.");
        break;
    default:
        NSLog(@"Extra elements in stack at parse end.");
        [activeElements removeObjectsInRange:NSMakeRange(1, activeElements.count-1)];
        // FALLTHRU
    case 1:
        // top item should be the sentinel
        self.item = [activeElements pop];
        if ([item.children count] == 1) {
            // sentinel can safely be discarded if 
            self.item = [item.children objectAtIndex:0];    
        }
        break;
    }
}

#pragma mark Instance methods    
-(void)  parser:(NSXMLParser *)parser 
didStartElement:(NSString *)elementName
   namespaceURI:(NSString *)namespaceURI 
  qualifiedName:(NSString *)qName 
     attributes:(NSDictionary *)attributeDict
{
    [activeElements push:[self nodeWithTag:elementName 
                               attributes:attributeDict 
                                   parser:parser]];
}

- (void)parser:(NSXMLParser *)parser 
 didEndElement:(NSString *)elementName
  namespaceURI:(NSString *)namespaceURI 
 qualifiedName:(NSString *)qName
{
    id element = [activeElements pop];
    if (element.attributes.count == 0 && element.children.count == 0) {
        // simple leafs don't need to be Nodes.
        [activeElements.top setValue:element.value forKey:elementName];
    } else {
        [activeElements.top setValue:element forKey:elementName];
    }
}

-(void)parser:(NSXMLParser*)parser foundCharacters:(NSString*)string {
    activeElements.top.value = string;
}

/* Factory method. Depending on elementName,  create an 
   object of the appropriate type.
 */
-(id)nodeWithTag:elementName attributes:attrs parser:(NSXMLParser*)parser {
    id node =[[[elementClasses objectForKey:elementName 
                                    default:[NSMutableDictionary class]] 
                        alloc] init];
    for (id key in attrs) {
        @try {
            [node setValue:[attrs objectForKey:key] forKey:key];
        }
        @catch (NSException *exc) {
            // TODO: warn user of invalid attribute(s) when parsing is finished
    if ([exc name] == NSUndefinedKeyException) {
                NSLog(@"%d,%d: Set attribute '%@' on a %@, but it doesn't have that property.", 
                      [parser columnNumber], [parser lineNumber],
                      key, elementName);
    } else {
                NSLog(@"%d,%d: Caught %@ when setting %@ on a %@.",
                      [parser columnNumber], [parser lineNumber],
                      [exc name], key, elementName);
            }
        }
    }
    return [node autorelease];
}

-(void)parser:(NSXMLParser *)parser parseErrorOccurred:(NSError *)parseError {
    NSLog(@"parse error: %@", parseError);
    [self abort];
}
-(void)parser:(NSXMLParser *)parser validationErrorOccurred:(NSError *)validError {
    NSLog(@"validation error: %@", validError);
    [self abort];
}

-(void)abort {
    [activeElements removeAllObjects];
}

Another bug

Take a close look at:

} else if ([elementName isEqualToString:@"subfield"]) {
    if ([[attributeDict valueForKey:@"id"] isEqualToString:@"a"]) {
        self.authorName1 = [[NSMutableString alloc] init];
    }
} else if ([elementName isEqualToString:@"subfield"]) {
    if ([[attributeDict valueForKey:@"id"] isEqualToString:@"b"]) {
                    self.authorName2 = [[NSMutableString alloc] init];
    }
}

If the first test succeeds, you'll never reach the second. The trivial fix here is to combine the blocks, though this will still have the clobber issue:

} else if ([elementName isEqualToString:@"subfield"]) {
    if ([[attributeDict valueForKey:@"id"] isEqualToString:@"a"]) {
        self.authorName1 = [[NSMutableString alloc] init];
    } else if ([[attributeDict valueForKey:@"id"] isEqualToString:@"b"]) {
        self.authorName2 = [[NSMutableString alloc] init];
    }
}

Upvotes: 4

Cœur
Cœur

Reputation: 161

Download: https://github.com/Insert-Witty-Name/XML-to-NSDictionary

Then you simply do :

NSDictionary *dic = [XMLReader dictionaryForPath:filepath error:nil];

Result is a NSDictionary *dic with dictionaries, arrays and strings inside, depending of the XML:

{
    book =     {
        author =         {
            first = Jason;
            last = "Alfonso.";
        };
        summary =         {
            city = Milano;
            country = Italy;
        };
        title = "Book 1";
    };
}

Upvotes: 2

Related Questions