2 Copyright (C) 2003-2004 Max Berger
3 Copyright (C) 2004-2005 OpenGroupware.org
5 This file is part of versitSaxDriver, written for the OpenGroupware.org
8 SOPE is free software; you can redistribute it and/or modify it under
9 the terms of the GNU Lesser General Public License as published by the
10 Free Software Foundation; either version 2, or (at your option) any
13 SOPE is distributed in the hope that it will be useful, but WITHOUT ANY
14 WARRANTY; without even the implied warranty of MERCHANTABILITY or
15 FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
16 License for more details.
18 You should have received a copy of the GNU Lesser General Public
19 License along with SOPE; see the file COPYING. If not, write to the
20 Free Software Foundation, 59 Temple Place - Suite 330, Boston, MA
24 #include "VSSaxDriver.h"
25 #include "VSStringFormatter.h"
26 #include <SaxObjC/SaxException.h>
29 @implementation VSSaxDriver
31 static BOOL debugOn = NO;
33 static NSCharacterSet *dotCharSet = nil;
34 static NSCharacterSet *equalSignCharSet = nil;
35 static NSCharacterSet *commaCharSet = nil;
36 static NSCharacterSet *colonAndSemicolonCharSet = nil;
37 static NSCharacterSet *colonSemicolonAndDquoteCharSet = nil;
38 static NSCharacterSet *whitespaceCharSet = nil;
40 static VSStringFormatter *stringFormatter = nil;
43 static BOOL didInit = NO;
50 ud = [NSUserDefaults standardUserDefaults];
51 debugOn = [ud boolForKey:@"VSSaxDriverDebugEnabled"];
54 [[NSCharacterSet characterSetWithCharactersInString:@"."] retain];
56 [[NSCharacterSet characterSetWithCharactersInString:@"="] retain];
58 [[NSCharacterSet characterSetWithCharactersInString:@","] retain];
59 colonAndSemicolonCharSet =
60 [[NSCharacterSet characterSetWithCharactersInString:@":;"] retain];
61 colonSemicolonAndDquoteCharSet =
62 [[NSCharacterSet characterSetWithCharactersInString:@":;\""] retain];
64 [[NSCharacterSet whitespaceCharacterSet] retain];
66 stringFormatter = [VSStringFormatter sharedFormatter];
71 if ((self = [super init])) {
72 self->prefixURI = @"";
73 self->cardStack = [[NSMutableArray alloc] initWithCapacity:4];
74 self->elementList = [[NSMutableArray alloc] initWithCapacity:8];
75 self->attributeMapping = [[NSMutableDictionary alloc] initWithCapacity:8];
76 self->subItemMapping = [[NSMutableDictionary alloc] initWithCapacity:8];
82 [self->contentHandler release];
83 [self->errorHandler release];
84 [self->prefixURI release];
85 [self->cardStack release];
86 [self->elementList release];
87 [self->attributeElements release];
88 [self->elementMapping release];
89 [self->attributeMapping release];
90 [self->subItemMapping release];
96 - (void)setFeature:(NSString *)_name to:(BOOL)_value {
98 - (BOOL)feature:(NSString *)_name {
102 - (void)setProperty:(NSString *)_name to:(id)_value {
104 - (id)property:(NSString *)_name {
110 - (void)setContentHandler:(id<NSObject,SaxContentHandler>)_handler {
111 ASSIGN(self->contentHandler, _handler);
114 - (void)setDTDHandler:(id<NSObject,SaxDTDHandler>)_handler {
118 - (void)setErrorHandler:(id<NSObject,SaxErrorHandler>)_handler {
119 ASSIGN(self->errorHandler, _handler);
121 - (void)setEntityResolver:(id<NSObject,SaxEntityResolver>)_handler {
125 - (id<NSObject,SaxContentHandler>)contentHandler {
126 return self->contentHandler;
129 - (id<NSObject,SaxDTDHandler>)dtdHandler {
134 - (id<NSObject,SaxErrorHandler>)errorHandler {
135 return self->errorHandler;
137 - (id<NSObject,SaxEntityResolver>)entityResolver {
142 - (void)setPrefixURI:(NSString *)_uri {
143 ASSIGNCOPY(self->prefixURI, _uri);
145 - (NSString *)prefixURI {
146 return self->prefixURI;
149 - (void)setAttributeElements:(NSSet *)_elements {
150 ASSIGNCOPY(self->attributeElements, _elements);
152 - (NSSet *)attributeElements {
153 return self->attributeElements;
156 - (void)setElementMapping:(NSDictionary *)_mapping {
157 ASSIGNCOPY(self->elementMapping, _mapping);
159 - (NSDictionary *)elementMapping {
160 return self->elementMapping;
163 - (void)setAttributeMapping:(NSDictionary *)_mapping {
164 [self setAttributeMapping:_mapping forElement:@""];
167 - (void)setAttributeMapping:(NSDictionary *)_mapping
168 forElement:(NSString *)_element
172 [attributeMapping setObject:_mapping forKey:_element];
175 - (void)setSubItemMapping:(NSArray *)_mapping forElement:(NSString *)_element {
176 [subItemMapping setObject:_mapping forKey:_element];
183 - (NSString *)_mapTagName:(NSString *)_tagName {
187 if ((ret = [self->elementMapping objectForKey:_tagName]) == nil) {
188 //NSLog(@"Unknown Key: %@ in %@",_tagName,self->elementMapping);
191 /* This is to allow parsing of vCards produced by Apple
192 Addressbook. AFAIK the .dot notation is a non-standard
194 r = [_tagName rangeOfCharacterFromSet:dotCharSet];
196 ret = [self _mapTagName:[_tagName substringFromIndex:(r.location + 1)]];
202 - (void)_addAttribute:(NSString *)_attribute
203 value:(NSString *)_value
204 toAttrs:(SaxAttributes *)_attrs
206 [_attrs addAttribute:_attribute
213 - (void)_addAttribute:(NSString *)_attribute value:(NSString *)_value {
214 NSArray *element = [cardStack lastObject];
215 SaxAttributes *attrs = [element objectAtIndex:2];
216 [self _addAttribute:_attribute value:_value toAttrs:attrs];
219 - (NSString *)_mapAttrName:(NSString *)_attrName forTag:(NSString *)_tagName {
220 NSString *mappedName;
222 mappedName = [(NSDictionary *)[self->attributeMapping objectForKey:_tagName]
223 objectForKey:_attrName];
224 if (mappedName == nil) {
225 mappedName = [(NSDictionary *)[self->attributeMapping objectForKey:
226 [self _mapTagName:_tagName]]
227 objectForKey:_attrName];
229 if (mappedName == nil) {
230 mappedName = [(NSDictionary *)[self->attributeMapping objectForKey:@""]
231 objectForKey:_attrName];
233 if (mappedName == nil)
234 mappedName = _attrName;
239 - (void)_parseAttr:(NSString *)_attr
240 forTag:(NSString *)_tagName
241 intoAttr:(NSString **)attr_
242 intoValue:(NSString **)value_
245 NSString *attrName, *attrValue, *mappedName;
247 r = [_attr rangeOfCharacterFromSet:equalSignCharSet];
249 unsigned left, right;
251 attrName = [[_attr substringToIndex:r.location] uppercaseString];
252 left = NSMaxRange(r);
253 right = [_attr length] - 1;
255 if (([_attr characterAtIndex:left] == '"') &&
256 ([_attr characterAtIndex:right] == '"'))
259 r = NSMakeRange(left, right - left);
260 attrValue = [_attr substringWithRange:r];
263 attrValue = [_attr substringFromIndex:left];
266 else if (left == right) {
267 attrValue = [_attr substringFromIndex:left];
279 // ZNeK: what's this for?
280 r = [attrValue rangeOfCharacterFromSet:commaCharSet];
281 while (r.length > 0) {
282 [attrValue replaceCharactersInRange:r withString:@" "];
283 r = [attrValue rangeOfCharacterFromSet:commaCharSet];
287 mappedName = [self _mapAttrName:attrName forTag:_tagName];
289 *value_ = [stringFormatter stringByUnescapingRFC2445Text:attrValue];
292 - (id<NSObject,SaxAttributes>)_mapAttrs:(NSArray *)_attrs
293 forTag:(NSString *)_tagName
295 SaxAttributes *retAttrs;
296 NSEnumerator *attrEnum;
297 NSString *curAttr, *mappedAttr, *mappedValue, *oldValue;
298 NSMutableDictionary *attributes;
300 if (!_attrs || [_attrs count] == 0)
303 attributes = [[NSMutableDictionary alloc] init];
304 retAttrs = [[[SaxAttributes alloc] init] autorelease];
305 attrEnum = [_attrs objectEnumerator];
306 while ((curAttr = [attrEnum nextObject])) {
307 [self _parseAttr:curAttr
310 intoValue:&mappedValue];
311 if ((oldValue = [attributes objectForKey:mappedAttr])) {
314 val = [NSString stringWithFormat:@"%@ %@",oldValue, mappedValue];
315 [attributes setObject:val forKey:mappedAttr];
318 [attributes setObject:mappedValue forKey:mappedAttr];
321 attrEnum = [attributes keyEnumerator];
322 while ((curAttr = [attrEnum nextObject])) {
323 [self _addAttribute:curAttr
324 value:[attributes objectForKey:curAttr]
328 [attributes release];
333 - (NSArray *)_beginTag:(NSString *)_tagName
334 withAttrs:(id<NSObject,SaxAttributes>)_attrs
336 NSArray *tag = [NSArray arrayWithObjects:@"BEGIN",_tagName,_attrs,NULL];
337 [self->elementList addObject:tag];
341 - (void)_endTag:(NSString *)_tagName {
342 [self->elementList addObject:
343 [NSArray arrayWithObjects:@"END",_tagName,NULL]];
346 - (void)_addSubItems:(NSArray *)_items withData:(NSString *)_content {
347 NSEnumerator *itemEnum, *contentEnum;
349 NSString *subContent;
351 itemEnum = [_items objectEnumerator];
352 contentEnum = [[_content componentsSeparatedByString:@";"] objectEnumerator];
354 while ((subTag=[itemEnum nextObject])) {
355 subContent = [contentEnum nextObject];
357 [self _beginTag:subTag withAttrs:nil];
358 if ([subContent length]>0)
359 [self->elementList addObject:
360 [NSArray arrayWithObjects:@"DATA", subContent, nil]];
361 [self _endTag:subTag];
365 - (void)_dataTag:(NSString *)_tagName
366 withAttrs:(id<NSObject,SaxAttributes>)_attrs
367 andContent:(NSString *)_content
371 _content = [stringFormatter stringByUnescapingRFC2445Text:_content];
372 if ([self->attributeElements containsObject:_tagName]) {
373 [self _addAttribute:_tagName value:_content];
377 [self _beginTag:_tagName withAttrs:_attrs];
378 if ([_content length] > 0) {
379 if ((subItems = [self->subItemMapping objectForKey:_tagName])) {
380 [self _addSubItems:subItems withData:_content];
383 [self->elementList addObject:
384 [NSArray arrayWithObjects:@"DATA", _content, nil]];
387 [self _endTag:_tagName];
390 - (void)_eventsForElements {
394 enu = [elementList objectEnumerator];
395 while ((obj = [enu nextObject]) != nil) {
396 id<NSObject,SaxAttributes> attrs;
401 type = [obj objectAtIndex:0];
402 name = [obj objectAtIndex:1];
405 attrs = [obj objectAtIndex:2];
409 if ([type isEqualToString:@"BEGIN"]) {
410 [self->contentHandler startElement:name
411 namespace:self->prefixURI
415 else if ([type isEqualToString:@"END"]) {
416 [self->contentHandler endElement:name
417 namespace:self->prefixURI
421 unsigned len = [name length];
422 chardata = malloc(len * sizeof(unichar));
423 [name getCharacters:chardata range:NSMakeRange(0, len)];
424 [self->contentHandler characters:chardata length:len];
429 [elementList removeAllObjects];
432 - (void)_parseLine:(NSString *)_line {
433 NSString *tagName, *tagValue;
434 NSMutableArray *tagAttributes;
435 NSRange r, todoRange;
438 length = [_line length];
439 todoRange = NSMakeRange(0, length);
440 r = [_line rangeOfCharacterFromSet:colonAndSemicolonCharSet
443 /* is line well-formed? */
446 NSLog(@"%s got an improper content line! ->\n%@",
453 tagName = [[_line substringToIndex:r.location] uppercaseString];
454 tagAttributes = [[NSMutableArray alloc] init];
456 /* possible shortcut: if we spotted a ':', we don't have to do "expensive"
457 argument scanning/processing.
459 if ([_line characterAtIndex:r.location] != ':') {
460 BOOL isAtEnd = NO, isInDquote = NO;
461 unsigned start = NSMaxRange(r);
463 todoRange = NSMakeRange(start, length - start);
467 /* scan for parameters */
468 r = [_line rangeOfCharacterFromSet:colonSemicolonAndDquoteCharSet
471 /* is line well-formed? */
472 if (r.length == 0 || r.location == 0) {
474 NSLog(@"%s got an improper content line! ->\n%@",
478 [tagAttributes release];
481 /* first check if delimiter candidate is escaped */
482 if ([_line characterAtIndex:(r.location - 1)] != '\\') {
486 delimiter = [_line characterAtIndex:r.location];
487 if (delimiter == '\"') {
488 /* not a real delimiter - toggle isInDquote for proper escaping */
489 isInDquote = !isInDquote;
493 /* is a delimiter, which one? */
495 if (delimiter == ':') {
498 copyRange = NSMakeRange(start, r.location - start);
499 [tagAttributes addObject:[_line substringWithRange:copyRange]];
501 /* adjust start, todoRange */
502 start = NSMaxRange(r);
503 todoRange = NSMakeRange(start, length - start);
509 /* adjust todoRange */
510 unsigned offset = NSMaxRange(r);
511 todoRange = NSMakeRange(offset, length - offset);
515 tagValue = [_line substringFromIndex:NSMaxRange(r)];
517 if ([tagName isEqualToString:@"BEGIN"]) {
520 tag = [self _beginTag:[self _mapTagName:tagValue]
521 withAttrs:[[[SaxAttributes alloc] init] autorelease]];
522 [self->cardStack addObject:tag];
524 else if ([tagName isEqualToString:@"END"]) {
527 mtName = [self _mapTagName:tagValue];
528 if ([self->cardStack count] > 0) {
529 NSString *expectedName;
531 expectedName = [[self->cardStack lastObject] objectAtIndex:1];
532 if (![expectedName isEqualToString:mtName]) {
534 NSLog(@"%s found end tag '%@' which doesn't match expected name "
535 @"'%@'! Tag '%@' hasn't been closed properly. Given iCal "
536 @"document contains errors!",
542 /* probably futile attempt to parse anyways */
544 NSLog(@"%s trying to fix previous error by inserting bogus end "
546 __PRETTY_FUNCTION__);
548 [self _endTag:expectedName];
549 [self->cardStack removeLastObject];
554 NSLog(@"%s found end tag '%@' without any open tags left?!",
559 [self _endTag:mtName];
560 [self->cardStack removeLastObject];
561 if ([self->cardStack count] == 0)
562 [self _eventsForElements];
565 [self _dataTag:[self _mapTagName:tagName]
566 withAttrs:[self _mapAttrs:tagAttributes forTag:tagName]
567 andContent:tagValue];
569 [tagAttributes release];
572 - (void)_parseString:(NSString *)_rawString {
573 unsigned pos, length;
574 NSMutableString *line;
577 [self->contentHandler startDocument];
578 [self->contentHandler startPrefixMapping:@"" uri:self->prefixURI];
580 length = [_rawString length];
582 contentline = name *(";" param ) ":" value CRLF
583 ; When parsing a content line, folded lines MUST first
586 r = NSMakeRange(0, 0);
587 /* probably too optimistic */
588 line = [[NSMutableString alloc] initWithCapacity:75 + 2];
590 for(pos = 0; pos < length; pos++) {
591 unichar c = [_rawString characterAtIndex:pos];
594 if (((length - 1) - pos) >= 1) {
595 if ([_rawString characterAtIndex:pos + 1] == '\n') {
596 BOOL isAtEndOfLine = YES;
597 /* test for folding first */
598 if (((length - 1) - pos) >= 2) {
599 unichar ws = [_rawString characterAtIndex:pos + 2];
600 isAtEndOfLine = [whitespaceCharSet characterIsMember:ws] ? NO
602 if (!isAtEndOfLine) {
603 /* assemble part of line up to pos */
605 [line appendString:[_rawString substringWithRange:r]];
609 r = NSMakeRange(pos + 1, 0); /* begin new range */
613 /* assemble part of line up to pos */
615 [line appendString:[_rawString substringWithRange:r]];
617 [self _parseLine:line];
619 [line deleteCharactersInRange:NSMakeRange(0, [line length])];
621 r = NSMakeRange(pos + 1, 0); /* begin new range */
626 /* garbled last line! */
628 NSLog(@"%s Last line is truncated, trying to parse anyways!",
629 __PRETTY_FUNCTION__);
633 else if (c == '\n') { /* broken, non-standard */
634 BOOL isAtEndOfLine = YES;
635 /* test for folding first */
636 if (((length - 1) - pos) >= 1) {
637 unichar ws = [_rawString characterAtIndex:pos + 1];
638 isAtEndOfLine = [whitespaceCharSet characterIsMember:ws] ? NO
640 if (!isAtEndOfLine) {
641 /* assemble part of line up to pos */
643 [line appendString:[_rawString substringWithRange:r]];
647 r = NSMakeRange(pos + 1, 0); /* begin new range */
651 /* assemble part of line up to pos */
653 [line appendString:[_rawString substringWithRange:r]];
655 [self _parseLine:line];
657 [line deleteCharactersInRange:NSMakeRange(0, [line length])];
658 r = NSMakeRange(pos + 1, 0); /* begin new range */
667 NSLog(@"%s Last line of iCal string is not properly terminated!",
668 __PRETTY_FUNCTION__);
670 [line appendString:[_rawString substringWithRange:r]];
671 [self _parseLine:line];
674 if ([self->cardStack count] != 0) {
676 NSLog(@"%s found elements on cardStack. This indicates an improper "
677 @"iCal structure! Not all required events will have been "
678 @"generated, leading to unpredictable results!",
679 __PRETTY_FUNCTION__);
684 [self->contentHandler endPrefixMapping:@""];
685 [self->contentHandler endDocument];
688 /* main entry functions */
690 - (void)parseFromSource:(id)_source systemId:(NSString *)_sysId {
692 NSLog(@"%s: parse: %@ (sysid=%@)", __PRETTY_FUNCTION__, _source, _sysId);
694 if ([_source isKindOfClass:[NSURL class]]) {
695 if (_sysId == nil) _sysId = [_source absoluteString];
698 NSLog(@"%s: trying to load URL: %@ (sysid=%@)",__PRETTY_FUNCTION__,
702 // TODO: remember encoding of source
703 _source = [_source resourceDataUsingCache:NO];
706 if ([_source isKindOfClass:[NSData class]]) {
708 NSLog(@"%s: trying to decode data (0x%08X,len=%d) ...",
709 __PRETTY_FUNCTION__, _source, [_source length]);
711 if (_sysId == nil) _sysId = @"<data>";
713 // FIXME: Data is not always utf-8.....
714 _source = [[[NSString alloc]
715 initWithData:_source encoding:NSUTF8StringEncoding]
719 if (![_source isKindOfClass:[NSString class]]) {
720 SaxParseException *e;
723 NSLog(@"%s: unrecognizable source: %@", __PRETTY_FUNCTION__,_source);
725 e = (id)[SaxParseException exceptionWithName:@"SaxIOException"
726 reason:@"cannot handle data-source"
729 [self->errorHandler fatalError:e];
736 NSLog(@"%s: trying to parse string (0x%08X,len=%d) ...",
737 __PRETTY_FUNCTION__, _source, [_source length]);
739 if (_sysId == nil) _sysId = @"<string>";
740 [self _parseString:_source];
743 - (void)parseFromSource:(id)_source {
744 [self parseFromSource:_source systemId:nil];
747 - (void)parseFromSystemId:(NSString *)_sysId {
750 if ([_sysId rangeOfString:@"://"].length == 0) {
751 /* seems to be a path, path to be a proper URL */
752 url = [NSURL fileURLWithPath:_sysId];
755 /* Note: Cocoa NSURL doesn't complain on "/abc/def" like input! */
756 url = [NSURL URLWithString:_sysId];
760 SaxParseException *e;
762 e = (id)[SaxParseException exceptionWithName:@"SaxIOException"
763 reason:@"cannot handle system-id"
765 [self->errorHandler fatalError:e];
769 [self parseFromSource:url systemId:_sysId];
774 - (BOOL)isDebuggingEnabled {
778 @end /* VersitSaxDriver */