/* Copyright (C) 2003-2004 Max Berger Copyright (C) 2004 OpenGroupware.org This file is part of versitSaxDriver, written for the OpenGroupware.org project (OGo). SOPE is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2, or (at your option) any later version. SOPE is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with SOPE; see the file COPYING. If not, write to the Free Software Foundation, 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */ #include "VSSaxDriver.h" #include "VSStringFormatter.h" #include "common.h" @implementation VSSaxDriver static BOOL debugOn = NO; static NSCharacterSet *dotCharSet = nil; static NSCharacterSet *equalSignCharSet = nil; static NSCharacterSet *commaCharSet = nil; static NSCharacterSet *colonAndSemicolonCharSet = nil; static NSCharacterSet *colonSemicolonAndDquoteCharSet = nil; static NSCharacterSet *whitespaceCharSet = nil; static VSStringFormatter *stringFormatter = nil; + (void)initialize { static BOOL didInit = NO; NSUserDefaults *ud; if(didInit) return; didInit = YES; ud = [NSUserDefaults standardUserDefaults]; debugOn = [ud boolForKey:@"VSSaxDriverDebugEnabled"]; dotCharSet = [[NSCharacterSet characterSetWithCharactersInString:@"."] retain]; equalSignCharSet = [[NSCharacterSet characterSetWithCharactersInString:@"="] retain]; commaCharSet = [[NSCharacterSet characterSetWithCharactersInString:@","] retain]; colonAndSemicolonCharSet = [[NSCharacterSet characterSetWithCharactersInString:@":;"] retain]; colonSemicolonAndDquoteCharSet = [[NSCharacterSet characterSetWithCharactersInString:@":;\""] retain]; whitespaceCharSet = [[NSCharacterSet whitespaceCharacterSet] retain]; stringFormatter = [VSStringFormatter sharedFormatter]; } - (id)init { if ((self = [super init])) { self->prefixURI = @""; self->cardStack = [[NSMutableArray alloc] init]; self->elementList = [[NSMutableArray alloc] init]; self->attributeMapping = [[NSMutableDictionary alloc] init]; self->subItemMapping = [[NSMutableDictionary alloc] init]; } return self; } - (void)dealloc { [self->contentHandler release]; [self->prefixURI release]; [self->cardStack release]; [self->elementList release]; [self->attributeElements release]; [self->elementMapping release]; [self->attributeMapping release]; [self->subItemMapping release]; [super dealloc]; } /* accessors */ - (void)setFeature:(NSString *)_name to:(BOOL)_value { } - (BOOL)feature:(NSString *)_name { return NO; } - (void)setProperty:(NSString *)_name to:(id)_value { } - (id)property:(NSString *)_name { return nil; } /* handlers */ - (void)setContentHandler:(id)_handler { ASSIGN(self->contentHandler,_handler); } - (void)setDTDHandler:(id)_handler { // FIXME } - (void)setErrorHandler:(id)_handler { // FIXME } - (void)setEntityResolver:(id)_handler { // FIXME } - (id)contentHandler { return self->contentHandler; } - (id)dtdHandler { // FIXME return NULL; } - (id)errorHandler { // FIXME return NULL; } - (id)entityResolver { // FIXME return NULL; } - (void)setPrefixURI:(NSString *)_uri { ASSIGNCOPY(self->prefixURI, _uri); } - (NSString *)prefixURI { return self->prefixURI; } - (void)setAttributeElements:(NSSet *)_elements { ASSIGNCOPY(self->attributeElements, _elements); } - (NSSet *)attributeElements { return self->attributeElements; } - (void)setElementMapping:(NSDictionary *)_mapping { ASSIGNCOPY(self->elementMapping, _mapping); } - (NSDictionary *)elementMapping { return self->elementMapping; } - (void)setAttributeMapping:(NSDictionary *)_mapping { [self setAttributeMapping:_mapping forElement:@""]; } - (void)setAttributeMapping:(NSDictionary *)_mapping forElement:(NSString *)_element { if (!_element) _element = @""; [attributeMapping setObject:_mapping forKey:_element]; } - (void)setSubItemMapping:(NSArray *)_mapping forElement:(NSString *)_element { [subItemMapping setObject:_mapping forKey:_element]; } /* parsing */ - (NSString *)_mapTagName:(NSString *)_tagName { NSString *ret; NSRange r; if ((ret = [self->elementMapping objectForKey:_tagName]) == nil) { //NSLog(@"Unknown Key: %@ in %@",_tagName,self->elementMapping); ret = _tagName; /* This is to allow parsing of vCards produced by Apple Addressbook. AFAIK the .dot notation is a non-standard extension */ r = [_tagName rangeOfCharacterFromSet:dotCharSet]; if (r.length > 0) { ret = [self _mapTagName:[_tagName substringFromIndex:(r.location + 1)]]; } } return ret; } - (void)_addAttribute:(NSString *)_attribute value:(NSString *)_value toAttrs:(SaxAttributes *)_attrs { [_attrs addAttribute:_attribute uri:self->prefixURI rawName:_attribute type:@"CDATA" value:_value]; } - (void)_addAttribute:(NSString *)_attribute value:(NSString *)_value { NSArray *element = [cardStack lastObject]; SaxAttributes *attrs = [element objectAtIndex:2]; [self _addAttribute:_attribute value:_value toAttrs:attrs]; } - (NSString *)_mapAttrName:(NSString *)_attrName forTag:(NSString *)_tagName { NSString *mappedName; mappedName = [[self->attributeMapping objectForKey:_tagName] objectForKey:_attrName]; if (!mappedName) { mappedName = [[self->attributeMapping objectForKey: [self _mapTagName:_tagName]] objectForKey:_attrName]; } if (!mappedName) { mappedName = [[self->attributeMapping objectForKey:@""] objectForKey:_attrName]; } if (!mappedName) mappedName = _attrName; return mappedName; } - (void)_parseAttr:(NSString *)_attr forTag:(NSString *)_tagName intoAttr:(NSString **)attr_ intoValue:(NSString **)value_ { NSRange r; NSString *attrName, *attrValue, *mappedName; r = [_attr rangeOfCharacterFromSet:equalSignCharSet]; if (r.length > 0) { unsigned left, right; attrName = [[_attr substringToIndex:r.location] uppercaseString]; left = NSMaxRange(r); right = [_attr length] - 1; if(left < right) { if(([_attr characterAtIndex:left] == '"') && ([_attr characterAtIndex:right] == '"')) { left += 1; r = NSMakeRange(left, right - left); attrValue = [_attr substringWithRange:r]; } else { attrValue = [_attr substringFromIndex:left]; } } else if(left == right) { attrValue = [_attr substringFromIndex:left]; } else { attrValue = @""; } } else { attrName = @"TYPE"; attrValue = _attr; } #if 0 // ZNeK: what's this for? r = [attrValue rangeOfCharacterFromSet:commaCharSet]; while (r.length > 0) { [attrValue replaceCharactersInRange:r withString:@" "]; r = [attrValue rangeOfCharacterFromSet:commaCharSet]; } #endif mappedName = [self _mapAttrName:attrName forTag:_tagName]; *attr_ = mappedName; *value_ = [stringFormatter stringByUnescapingRFC2445Text:attrValue]; } - (id)_mapAttrs:(NSArray *)_attrs forTag:(NSString *)_tagName { SaxAttributes *retAttrs; NSEnumerator *attrEnum; NSString *curAttr, *mappedAttr, *mappedValue, *oldValue; NSMutableDictionary *attributes; if (!_attrs || [_attrs count] == 0) return nil; attributes = [[NSMutableDictionary alloc] init]; retAttrs = [[[SaxAttributes alloc] init] autorelease]; attrEnum = [_attrs objectEnumerator]; while ((curAttr = [attrEnum nextObject])) { [self _parseAttr:curAttr forTag:_tagName intoAttr:&mappedAttr intoValue:&mappedValue]; if ((oldValue = [attributes objectForKey:mappedAttr])) { NSString *val; /* ZNeK: duh! */ val = [NSString stringWithFormat:@"%@ %@",oldValue, mappedValue]; [attributes setObject:val forKey:mappedAttr]; } else [attributes setObject:mappedValue forKey:mappedAttr]; } attrEnum = [attributes keyEnumerator]; while ((curAttr = [attrEnum nextObject])) { [self _addAttribute:curAttr value:[attributes objectForKey:curAttr] toAttrs:retAttrs]; } [attributes release]; return retAttrs; } - (NSArray *)_beginTag:(NSString *)_tagName withAttrs:(id)_attrs { NSArray *tag = [NSArray arrayWithObjects:@"BEGIN",_tagName,_attrs,NULL]; [self->elementList addObject:tag]; return tag; } - (void)_endTag:(NSString *)_tagName { [self->elementList addObject: [NSArray arrayWithObjects:@"END",_tagName,NULL]]; } - (void)_addSubItems:(NSArray *)_items withData:(NSString *)_content { NSEnumerator *itemEnum, *contentEnum; NSString *subTag; NSString *subContent; itemEnum = [_items objectEnumerator]; contentEnum = [[_content componentsSeparatedByString:@";"] objectEnumerator]; while ((subTag=[itemEnum nextObject])) { subContent = [contentEnum nextObject]; [self _beginTag:subTag withAttrs:nil]; if ([subContent length]>0) [self->elementList addObject: [NSArray arrayWithObjects:@"DATA", subContent, nil]]; [self _endTag:subTag]; } } - (void)_dataTag:(NSString *)_tagName withAttrs:(id)_attrs andContent:(NSString *)_content { NSArray *subItems; _content = [stringFormatter stringByUnescapingRFC2445Text:_content]; if ([self->attributeElements containsObject:_tagName]) { [self _addAttribute:_tagName value:_content]; } else { [self _beginTag:_tagName withAttrs:_attrs]; if ([_content length] > 0) { if ((subItems = [self->subItemMapping objectForKey:_tagName])) { [self _addSubItems:subItems withData:_content]; } else { [self->elementList addObject: [NSArray arrayWithObjects:@"DATA", _content, nil]]; } } [self _endTag:_tagName]; } } - (void)_eventsForElements { NSEnumerator *enu; NSArray *obj; NSString *type; NSString *name; unichar *chardata; id attrs; enu = [elementList objectEnumerator]; while ((obj = [enu nextObject])) { type = [obj objectAtIndex:0]; name = [obj objectAtIndex:1]; if ([obj count] > 2) attrs = [obj objectAtIndex:2]; else attrs = nil; if ([type isEqualToString:@"BEGIN"]) { [self->contentHandler startElement:name namespace:self->prefixURI rawName:name attributes:attrs]; } else if ([type isEqualToString:@"END"]) { [self->contentHandler endElement:name namespace:self->prefixURI rawName:name]; } else { unsigned len = [name length]; chardata = malloc(len * sizeof(unichar)); [name getCharacters:chardata range:NSMakeRange(0, len)]; [self->contentHandler characters:chardata length:len]; if (chardata) free(chardata); } } [elementList removeAllObjects]; } - (void)_parseLine:(NSString *)_line { NSString *tagName, *tagValue; NSMutableArray *tagAttributes; NSRange r, todoRange; unsigned length; length = [_line length]; todoRange = NSMakeRange(0, length); r = [_line rangeOfCharacterFromSet:colonAndSemicolonCharSet options:0 range:todoRange]; /* is line well-formed? */ if(r.length == 0) { if(debugOn) { NSLog(@"%s got an improper content line! ->\n%@", __PRETTY_FUNCTION__, _line); } return; } tagName = [[_line substringToIndex:r.location] uppercaseString]; tagAttributes = [[NSMutableArray alloc] init]; /* possible shortcut: if we spotted a ':', we don't have to do "expensive" argument scanning/processing. */ if([_line characterAtIndex:r.location] != ':') { BOOL isAtEnd = NO, isInDquote = NO; unsigned start = NSMaxRange(r); todoRange = NSMakeRange(start, length - start); while(!isAtEnd) { BOOL skip = YES; /* scan for parameters */ r = [_line rangeOfCharacterFromSet:colonSemicolonAndDquoteCharSet options:0 range:todoRange]; /* is line well-formed? */ if(r.length == 0 || r.location == 0) { if(debugOn) { NSLog(@"%s got an improper content line! ->\n%@", __PRETTY_FUNCTION__, _line); } [tagAttributes release]; return; } /* first check if delimiter candidate is escaped */ if([_line characterAtIndex:(r.location - 1)] != '\\') { unichar delimiter; NSRange copyRange; delimiter = [_line characterAtIndex:r.location]; if(delimiter == '\"') { /* not a real delimiter - toggle isInDquote for proper escaping */ isInDquote = !isInDquote; } else { if(!isInDquote) { /* is a delimiter, which one? */ skip = NO; if(delimiter == ':') { isAtEnd = YES; } copyRange = NSMakeRange(start, r.location - start); [tagAttributes addObject:[_line substringWithRange:copyRange]]; if(!isAtEnd) { /* adjust start, todoRange */ start = NSMaxRange(r); todoRange = NSMakeRange(start, length - start); } } } } if(skip) { /* adjust todoRange */ unsigned offset = NSMaxRange(r); todoRange = NSMakeRange(offset, length - offset); } } } tagValue = [_line substringFromIndex:NSMaxRange(r)]; if ([tagName isEqualToString:@"BEGIN"]) { id tag; tag = [self _beginTag:[self _mapTagName:tagValue] withAttrs:[[[SaxAttributes alloc] init] autorelease]]; [self->cardStack addObject:tag]; } else if ([tagName isEqualToString:@"END"]) { NSString *mtName; mtName = [self _mapTagName:tagValue]; if([self->cardStack count] > 0) { NSString *expectedName; expectedName = [[self->cardStack lastObject] objectAtIndex:1]; if(![expectedName isEqualToString:mtName]) { if(debugOn) { NSLog(@"%s found end tag '%@' which doesn't match expected name " @"'%@'! Tag '%@' hasn't been closed properly. Given iCal " @"document contains errors!", __PRETTY_FUNCTION__, mtName, expectedName, expectedName); } /* probably futile attempt to parse anyways */ if(debugOn) { NSLog(@"%s trying to fix previous error by inserting bogus end " @"tag.", __PRETTY_FUNCTION__); } [self _endTag:expectedName]; [self->cardStack removeLastObject]; } } else { if(debugOn) { NSLog(@"%s found end tag '%@' without any open tags left?!", __PRETTY_FUNCTION__, mtName); } } [self _endTag:mtName]; [self->cardStack removeLastObject]; if ([self->cardStack count] == 0) [self _eventsForElements]; } else { [self _dataTag:[self _mapTagName:tagName] withAttrs:[self _mapAttrs:tagAttributes forTag:tagName] andContent:tagValue]; } [tagAttributes release]; } - (void)_parseString:(NSString *)_rawString { unsigned pos, length; NSMutableString *line; NSRange r; [self->contentHandler startDocument]; [self->contentHandler startPrefixMapping:@"" uri:self->prefixURI]; length = [_rawString length]; /* RFC2445: contentline = name *(";" param ) ":" value CRLF ; When parsing a content line, folded lines MUST first ; be unfolded */ r = NSMakeRange(0, 0); /* probably too optimistic */ line = [[NSMutableString alloc] initWithCapacity:75 + 2]; for(pos = 0; pos < length; pos++) { unichar c = [_rawString characterAtIndex:pos]; if(c == '\r') { if(((length - 1) - pos) >= 1) { if([_rawString characterAtIndex:pos + 1] == '\n') { BOOL isAtEndOfLine = YES; /* test for folding first */ if(((length - 1) - pos) >= 2) { unichar ws = [_rawString characterAtIndex:pos + 2]; isAtEndOfLine = [whitespaceCharSet characterIsMember:ws] ? NO : YES; if(!isAtEndOfLine) { /* assemble part of line up to pos */ if(r.length > 0) { [line appendString:[_rawString substringWithRange:r]]; } /* unfold */ pos += 2; r = NSMakeRange(pos + 1, 0); /* begin new range */ } } if(isAtEndOfLine) { /* assemble part of line up to pos */ if(r.length > 0) { [line appendString:[_rawString substringWithRange:r]]; } [self _parseLine:line]; /* reset line */ [line deleteCharactersInRange:NSMakeRange(0, [line length])]; pos += 1; r = NSMakeRange(pos + 1, 0); /* begin new range */ } } } else { /* garbled last line! */ if(debugOn) { NSLog(@"%s Last line is truncated, trying to parse anyways!", __PRETTY_FUNCTION__); } } } else if(c == '\n') { /* broken, non-standard */ BOOL isAtEndOfLine = YES; /* test for folding first */ if(((length - 1) - pos) >= 1) { unichar ws = [_rawString characterAtIndex:pos + 1]; isAtEndOfLine = [whitespaceCharSet characterIsMember:ws] ? NO : YES; if(!isAtEndOfLine) { /* assemble part of line up to pos */ if(r.length > 0) { [line appendString:[_rawString substringWithRange:r]]; } /* unfold */ pos += 1; r = NSMakeRange(pos + 1, 0); /* begin new range */ } } if(isAtEndOfLine) { /* assemble part of line up to pos */ if(r.length > 0) { [line appendString:[_rawString substringWithRange:r]]; } [self _parseLine:line]; /* reset line */ [line deleteCharactersInRange:NSMakeRange(0, [line length])]; r = NSMakeRange(pos + 1, 0); /* begin new range */ } } else { r.length += 1; } } if(r.length > 0) { if(debugOn) { NSLog(@"%s Last line of iCal string is not properly terminated!", __PRETTY_FUNCTION__); } [line appendString:[_rawString substringWithRange:r]]; [self _parseLine:line]; } if([self->cardStack count] != 0) { if(debugOn) { NSLog(@"%s found elements on cardStack. This indicates an improper " @"iCal structure! Not all required events will have been " @"generated, leading to unpredictable results!", __PRETTY_FUNCTION__); } } [line release]; [self->contentHandler endPrefixMapping:@""]; [self->contentHandler endDocument]; } - (void)parseFromSource:(id)_source { if (debugOn) NSLog(@"%s: parse: %@", __PRETTY_FUNCTION__, _source); if ([_source isKindOfClass:[NSURL class]]) { if (debugOn) NSLog(@"%s: trying to load URL...",__PRETTY_FUNCTION__); _source = [_source resourceDataUsingCache:NO]; } if ([_source isKindOfClass:[NSData class]]) { // FIXME: Data is not always utf-8..... if (debugOn) NSLog(@"%s: trying to decode data...",__PRETTY_FUNCTION__); _source = [[[NSString alloc] initWithData:_source encoding:NSUTF8StringEncoding] autorelease]; } if ([_source isKindOfClass:[NSString class]]) { if (debugOn) NSLog(@"%s: trying to parse string...",__PRETTY_FUNCTION__); [self _parseString:_source]; } else { if (debugOn) NSLog(@"%s: unrecognizable source: %@", __PRETTY_FUNCTION__,_source); // FIXME: Return Error } } - (void)parseFromSource:(id)_source systemId:(NSString *)_sysId { [self parseFromSource:_source]; } - (void)parseFromSystemId:(NSString *)_sysId { NSURL *url; if ((url = [NSURL URLWithString:_sysId])) [self parseFromSource:url systemId:_sysId]; } /* debugging */ - (BOOL)isDebuggingEnabled { return debugOn; } @end /* VersitSaxDriver */