]> err.no Git - sope/blob - sope-ical/versitSaxDriver/VSSaxDriver.m
0866c87a0d70f43598e710fb2741f1d24eb5499c
[sope] / sope-ical / versitSaxDriver / VSSaxDriver.m
1 /*
2   Copyright (C) 2003-2004 Max Berger
3   Copyright (C) 2004-2005 OpenGroupware.org
4  
5   This file is part of versitSaxDriver, written for the OpenGroupware.org 
6   project (OGo).
7   
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
11   later version.
12   
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.
17   
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
21   02111-1307, USA.
22 */
23
24 #include "VSSaxDriver.h"
25 #include "VSStringFormatter.h"
26 #include <SaxObjC/SaxException.h>
27 #include <NGExtensions/NGQuotedPrintableCoding.h>
28 #include "common.h"
29
30 @interface VSSaxTag : NSObject
31 {
32 @private
33   char          type;
34   NSString      *tagName;
35   NSString      *group;
36 @public
37   SaxAttributes *attrs;
38   unichar       *data;
39   unsigned int  datalen;
40 }
41
42 + (id)beginTag:(NSString *)_tag group:(NSString *)_group
43   attributes:(SaxAttributes *)_attrs;
44 - (id)initEndTag:(NSString *)_tag;
45
46 - (id)initWithContentString:(NSString *)_data;
47
48 - (NSString *)tagName;
49 - (NSString *)group;
50 - (BOOL)isStartTag;
51 - (BOOL)isEndTag;
52 - (BOOL)isTag;
53
54 @end
55
56 @implementation VSSaxTag
57
58 + (id)beginTag:(NSString *)_tag group:(NSString *)_group
59   attributes:(SaxAttributes *)_attrs
60 {
61   VSSaxTag *tag;
62
63   tag = [[[self alloc] init] autorelease];
64   tag->type    = 'B';
65   tag->tagName = [_tag copy];
66   tag->group   = [_group copy];
67   tag->attrs   = [_attrs retain];
68   return tag;
69 }
70 - (id)initEndTag:(NSString *)_tag {
71   self->type    = 'E';
72   self->tagName = [_tag copy];
73   return self;
74 }
75 - (id)initWithContentString:(NSString *)_data {
76   if (_data == nil) {
77     [self release];
78     return nil;
79   }
80   
81   self->datalen = [_data length];
82   self->data    = calloc(self->datalen + 1, sizeof(unichar));
83   [_data getCharacters:self->data range:NSMakeRange(0, self->datalen)];
84   return self;
85 }
86
87 - (void)dealloc {
88   if (self->data) free(self->data);
89   [self->group   release];
90   [self->tagName release];
91   [self->attrs   release];
92   [super dealloc];
93 }
94
95 /* accessors */
96
97 - (NSString *)tagName {
98   return self->tagName;
99 }
100 - (NSString *)group {
101   return self->group;
102 }
103
104 - (BOOL)isStartTag {
105   return self->type == 'B' ? YES : NO;
106 }
107 - (BOOL)isEndTag {
108   return self->type == 'E' ? YES : NO;
109 }
110 - (BOOL)isTag {
111   return (self->type == 'B' || self->type == 'E') ? YES : NO;
112 }
113
114 @end /* VSSaxTag */
115
116 @implementation VSSaxDriver
117
118 static BOOL debugOn = NO;
119
120 static NSCharacterSet *dotCharSet                     = nil;
121 static NSCharacterSet *equalSignCharSet               = nil;
122 static NSCharacterSet *commaCharSet                   = nil;
123 static NSCharacterSet *colonAndSemicolonCharSet       = nil;
124 static NSCharacterSet *colonSemicolonAndDquoteCharSet = nil;
125 static NSCharacterSet *whitespaceCharSet              = nil;
126
127 static VSStringFormatter *stringFormatter = nil;
128
129 + (void)initialize {
130   static BOOL didInit = NO;
131   NSUserDefaults *ud;
132
133   if (didInit)
134     return;
135   didInit = YES;
136
137   ud      = [NSUserDefaults standardUserDefaults];
138   debugOn = [ud boolForKey:@"VSSaxDriverDebugEnabled"];
139
140   dotCharSet =
141     [[NSCharacterSet characterSetWithCharactersInString:@"."] retain];
142   equalSignCharSet =
143     [[NSCharacterSet characterSetWithCharactersInString:@"="] retain];
144   commaCharSet =
145     [[NSCharacterSet characterSetWithCharactersInString:@","] retain];
146   colonAndSemicolonCharSet =
147     [[NSCharacterSet characterSetWithCharactersInString:@":;"] retain];
148   colonSemicolonAndDquoteCharSet =
149     [[NSCharacterSet characterSetWithCharactersInString:@":;\""] retain];
150   whitespaceCharSet =
151     [[NSCharacterSet whitespaceCharacterSet] retain];
152
153   stringFormatter = [VSStringFormatter sharedFormatter];
154 }
155
156
157 - (id)init {
158   if ((self = [super init])) {
159     self->prefixURI         = @"";
160     self->cardStack         = [[NSMutableArray alloc]      initWithCapacity:4];
161     self->elementList       = [[NSMutableArray alloc]      initWithCapacity:8];
162     self->attributeMapping  = [[NSMutableDictionary alloc] initWithCapacity:8];
163     self->subItemMapping    = [[NSMutableDictionary alloc] initWithCapacity:8];
164   }
165   return self;
166 }
167
168 - (void)dealloc {
169   [self->contentHandler    release];
170   [self->errorHandler      release];
171   [self->prefixURI         release];
172   [self->cardStack         release];
173   [self->elementList       release];
174   [self->attributeElements release];
175   [self->elementMapping    release];
176   [self->attributeMapping  release];
177   [self->subItemMapping    release];
178   [super dealloc];
179 }
180
181 /* accessors */
182
183 - (void)setFeature:(NSString *)_name to:(BOOL)_value {
184 }
185 - (BOOL)feature:(NSString *)_name {
186   return NO;
187 }
188
189 - (void)setProperty:(NSString *)_name to:(id)_value {
190 }
191 - (id)property:(NSString *)_name {
192   return nil;
193 }
194
195 /* handlers */
196
197 - (void)setContentHandler:(id<NSObject,SaxContentHandler>)_handler {
198   ASSIGN(self->contentHandler, _handler);
199 }
200
201 - (void)setDTDHandler:(id<NSObject,SaxDTDHandler>)_handler {
202   // FIXME
203 }
204
205 - (void)setErrorHandler:(id<NSObject,SaxErrorHandler>)_handler {
206   ASSIGN(self->errorHandler, _handler);
207 }
208 - (void)setEntityResolver:(id<NSObject,SaxEntityResolver>)_handler {
209   // FIXME
210 }
211
212 - (id<NSObject,SaxContentHandler>)contentHandler {
213   return self->contentHandler;
214 }
215
216 - (id<NSObject,SaxDTDHandler>)dtdHandler {
217   // FIXME
218   return nil;
219 }
220
221 - (id<NSObject,SaxErrorHandler>)errorHandler {
222   return self->errorHandler;
223 }
224 - (id<NSObject,SaxEntityResolver>)entityResolver {
225   // FIXME
226   return nil;
227 }
228
229 - (void)setPrefixURI:(NSString *)_uri {
230   ASSIGNCOPY(self->prefixURI, _uri);
231 }
232 - (NSString *)prefixURI {
233   return self->prefixURI;
234 }
235
236 - (void)setAttributeElements:(NSSet *)_elements {
237   ASSIGNCOPY(self->attributeElements, _elements);
238 }
239 - (NSSet *)attributeElements {
240   return self->attributeElements;
241 }
242
243 - (void)setElementMapping:(NSDictionary *)_mapping {
244   ASSIGNCOPY(self->elementMapping, _mapping);
245 }
246 - (NSDictionary *)elementMapping {
247   return self->elementMapping;
248 }
249
250 - (void)setAttributeMapping:(NSDictionary *)_mapping {
251   [self setAttributeMapping:_mapping forElement:@""];
252 }
253
254 - (void)setAttributeMapping:(NSDictionary *)_mapping 
255   forElement:(NSString *)_element 
256 {
257   if (_element == nil)
258     _element = @"";
259   [attributeMapping setObject:_mapping forKey:_element];
260 }
261
262 - (void)setSubItemMapping:(NSArray *)_mapping forElement:(NSString *)_element {
263   [subItemMapping setObject:_mapping forKey:_element];  
264 }
265
266
267
268 /* parsing */
269
270 - (NSString *)_groupFromTagName:(NSString *)_tagName {
271   NSRange  r;
272   
273   r = [_tagName rangeOfCharacterFromSet:dotCharSet];
274   if (r.length == 0)
275     return nil;
276   
277   return [_tagName substringToIndex:r.location];
278 }
279
280 - (NSString *)_mapTagName:(NSString *)_tagName {
281   NSString *ret;
282   NSRange  r;
283
284   if ((ret = [self->elementMapping objectForKey:_tagName]) != nil)
285     return ret;
286
287   //NSLog(@"Unknown Key: %@ in %@",_tagName,self->elementMapping);
288   ret = _tagName;
289   
290   /*
291     This is to allow parsing of vCards produced by Apple
292     Addressbook.
293     The dot-notation is described as 'grouping' in RFC 2425, section 5.8.2.
294   */
295   r = [_tagName rangeOfCharacterFromSet:dotCharSet];
296   if (r.length > 0)
297     ret = [self _mapTagName:[_tagName substringFromIndex:(r.location + 1)]];
298   
299   return ret;
300 }
301
302 - (NSString *)_mapAttrName:(NSString *)_attrName forTag:(NSString *)_tagName {
303   NSDictionary *tagMap;
304   NSString *mappedName;
305
306   /* check whether we have a attr-map stored under the element-name */
307   tagMap = [self->attributeMapping objectForKey:_tagName];
308   if ((mappedName = [tagMap objectForKey:_attrName]) != nil)
309     return mappedName;
310   
311   /* check whether we have a attr-map stored under the mapped element-name */
312   tagMap = [self->attributeMapping objectForKey:[self _mapTagName:_tagName]];
313   if ((mappedName = [tagMap objectForKey:_attrName]) != nil)
314     return mappedName;
315
316   /* check whether we have a global attr-map */
317   tagMap = [self->attributeMapping objectForKey:@""];
318   if ((mappedName = [tagMap objectForKey:_attrName]) != nil)
319     return mappedName;
320   
321   /* return the name as-is */
322   return _attrName;
323 }
324
325 - (void)_parseAttr:(NSString *)_attr 
326   forTag:(NSString *)_tagName
327   intoAttr:(NSString **)attr_
328   intoValue:(NSString **)value_
329 {
330   NSRange  r;
331   NSString *attrName, *attrValue, *mappedName;
332   
333   r = [_attr rangeOfCharacterFromSet:equalSignCharSet];
334   if (r.length > 0) {
335     unsigned left, right;
336
337     attrName  = [[_attr substringToIndex:r.location] uppercaseString];
338     left = NSMaxRange(r);
339     right = [_attr length] - 1;
340     if (left < right) {
341       if (([_attr characterAtIndex:left]  == '"') &&
342          ([_attr characterAtIndex:right] == '"'))
343       {
344         left += 1;
345         r = NSMakeRange(left, right - left);
346         attrValue = [_attr substringWithRange:r];
347       }
348       else {
349         attrValue = [_attr substringFromIndex:left];
350       }
351     }
352     else if (left == right) {
353       attrValue = [_attr substringFromIndex:left];
354     }
355     else {
356       attrValue = @"";
357     }
358   }
359   else {
360     attrName  = @"TYPE";
361     attrValue = _attr;
362   }
363   
364 #if 0
365   // ZNeK: what's this for?
366   r = [attrValue rangeOfCharacterFromSet:commaCharSet];
367   while (r.length > 0) {
368     [attrValue replaceCharactersInRange:r withString:@" "];
369     r = [attrValue rangeOfCharacterFromSet:commaCharSet];
370   }
371 #endif
372
373   mappedName = [self _mapAttrName:attrName forTag:_tagName];
374   *attr_ = mappedName;
375   *value_ = [stringFormatter stringByUnescapingRFC2445Text:attrValue];
376 }
377
378 - (SaxAttributes *)_mapAttrs:(NSArray *)_attrs forTag:(NSString *)_tagName {
379   SaxAttributes       *retAttrs;
380   NSEnumerator        *attrEnum;
381   NSString            *curAttr, *mappedAttr, *mappedValue, *oldValue;
382   NSMutableDictionary *attributes;
383
384   if (_attrs == nil || [_attrs count] == 0)
385     return nil;
386   
387   attributes = [[NSMutableDictionary alloc] initWithCapacity:4];
388   retAttrs   = [[[SaxAttributes alloc] init] autorelease];
389   
390   attrEnum = [_attrs objectEnumerator];
391   while ((curAttr = [attrEnum nextObject]) != nil) {
392     [self _parseAttr:curAttr
393           forTag:_tagName
394           intoAttr:&mappedAttr
395           intoValue:&mappedValue];
396     if ((oldValue = [attributes objectForKey:mappedAttr]) != nil) {
397       NSString *val;
398       
399       /* ZNeK: duh! */
400       // TODO: hh asks: what does 'duh' is supposed to mean?
401       val = [[NSString alloc] initWithFormat:@"%@ %@",oldValue, mappedValue];
402       [attributes setObject:val forKey:mappedAttr];
403       [val release];
404     }
405     else  
406       [attributes setObject:mappedValue forKey:mappedAttr];
407   }
408
409   attrEnum = [attributes keyEnumerator];
410   while ((curAttr = [attrEnum nextObject]) != nil) {
411     /*
412       TODO: values are not always mapped to CDATA! Eg in the dawson draft:
413         | TYPE for TEL   | tel.type   | NMTOKENS  | 'VOICE'         |
414         | TYPE for EMAIL | email.type | NMTOKENS  | 'INTERNET'      |
415         | TYPE for PHOTO,| img.type   | CDATA     | REQUIRED        |
416         |  and LOGO      |            |           |                 |
417         | TYPE for SOUND | aud.type   | CDATA     | REQUIRED        |
418         | VALUE          | value      | NOTATION  | See elements    |
419     */
420     
421     [retAttrs addAttribute:curAttr uri:self->prefixURI rawName:curAttr
422               type:@"CDATA" value:[attributes objectForKey:curAttr]];
423   }
424   
425   [attributes release];
426   
427   return retAttrs;
428 }
429
430 - (VSSaxTag *)_beginTag:(NSString *)_tagName group:(NSString *)_group
431   withAttrs:(SaxAttributes *)_attrs
432 {
433   VSSaxTag *tag;
434   
435   tag = [VSSaxTag beginTag:_tagName group:_group attributes:_attrs];
436   [self->elementList addObject:tag];
437   return tag;
438 }
439
440 - (void)_endTag:(NSString *)_tagName {
441   VSSaxTag *tag;
442   
443   tag = [[VSSaxTag alloc] initEndTag:_tagName];
444   [self->elementList addObject:tag];
445   [tag release]; tag = nil;
446 }
447
448 - (void)_addSubItems:(NSArray *)_items group:(NSString *)_group
449   withData:(NSString *)_content
450 {
451   NSEnumerator *itemEnum, *contentEnum;
452   NSString *subTag;
453   
454   itemEnum    = [_items objectEnumerator];
455   contentEnum = [[_content componentsSeparatedByString:@";"] objectEnumerator];
456   
457   while ((subTag = [itemEnum nextObject]) != nil) {
458     NSString *subContent;
459     
460     subContent = [contentEnum nextObject];
461     
462     [self _beginTag:subTag group:_group withAttrs:nil];
463     if ([subContent length] > 0) {
464       VSSaxTag *a;
465       
466       a = [(VSSaxTag*)[VSSaxTag alloc] initWithContentString:subContent];
467       if (a != nil) {
468         [self->elementList addObject:a];
469         [a release];
470       }
471     }
472     [self _endTag:subTag];
473   }
474 }
475
476 - (void)_reportContentAsTag:(NSString *)_tagName
477   group:(NSString *)_group
478   withAttrs:(SaxAttributes *)_attrs 
479   andContent:(NSString *)_content 
480 {
481   /*
482     This is called for all non-BEGIN|END types.
483   */
484   NSArray *subItems;
485   
486   _content = [stringFormatter stringByUnescapingRFC2445Text:_content];
487
488   /* check whether type should be reported as an attribute in XML */
489   
490   if ([self->attributeElements containsObject:_tagName]) {
491     /* 
492        Add tag as an attribute to last component in the cardstack. This is
493        stuff like the "VERSION" type contained in a "BEGIN:VCARD" which will
494        be reported as <vcard version=""> (as an attribute of the container).
495     */
496     VSSaxTag *element;
497     
498     element = [self->cardStack lastObject];
499     [element->attrs addAttribute:_tagName uri:self->prefixURI
500                     rawName:_tagName type:@"CDATA" value:_content];
501     return;
502   }
503
504   /* report type as an XML tag */
505   
506   [self _beginTag:_tagName group:_group withAttrs:_attrs];
507   
508   if ([_content length] > 0) {
509     if ((subItems = [self->subItemMapping objectForKey:_tagName]) != nil) {
510       [self _addSubItems:subItems group:_group withData:_content];
511     }
512     else {
513       VSSaxTag *a;
514       
515       a = [(VSSaxTag *)[VSSaxTag alloc] initWithContentString:_content];
516       if (a != nil) {
517         [self->elementList addObject:a];
518         [a release];
519       }
520     }
521   }
522
523   [self _endTag:_tagName];
524 }
525
526 /* report events for collected elements */
527
528 - (void)reportStartGroup:(NSString *)_group {
529   SaxAttributes *attrs;
530   
531   attrs = [[SaxAttributes alloc] init];
532   [attrs addAttribute:@"name" uri:self->prefixURI rawName:@"name"
533          type:@"CDATA" value:_group];
534   
535   [self->contentHandler startElement:@"group" namespace:self->prefixURI
536                         rawName:@"group" attributes:attrs];
537   [attrs release];
538 }
539 - (void)reportEndGroup {
540   [self->contentHandler endElement:@"group" namespace:self->prefixURI
541                         rawName:@"group"];
542 }
543
544 - (void)reportQueuedTags {
545   /*
546     Why does the parser need the list instead of reporting the events
547     straight away?
548     
549     Because some vCard tags like the 'version' are reported as attributes
550     on the container tag. So we have a sequence like:
551       BEGIN:VCARD
552       ...
553       VERSION:3.0
554     which will get reported as:
555       <vcard version="3.0">
556   */
557   NSEnumerator *enu;
558   VSSaxTag *tagToReport;
559   NSString *lastGroup;
560   
561   lastGroup = nil;
562   enu = [self->elementList objectEnumerator];
563   while ((tagToReport = [enu nextObject]) != nil) {
564     if ([tagToReport isStartTag]) {
565       NSString *tg;
566       
567       tg = [tagToReport group];
568       if (![lastGroup isEqualToString:tg] && lastGroup != tg) {
569         if (lastGroup != nil) [self reportEndGroup];
570         ASSIGNCOPY(lastGroup, tg);
571         if (lastGroup != nil) [self reportStartGroup:lastGroup];
572       }
573     }
574     
575     if ([tagToReport isStartTag]) {
576       [self->contentHandler startElement:[tagToReport tagName]
577                             namespace:self->prefixURI
578                             rawName:[tagToReport tagName]
579                             attributes:tagToReport->attrs];
580     }
581     else if ([tagToReport isEndTag]) {
582       [self->contentHandler endElement:[tagToReport tagName]
583                             namespace:self->prefixURI
584                             rawName:[tagToReport tagName]];
585     }
586     else {
587       [self->contentHandler characters:tagToReport->data
588                             length:tagToReport->datalen];
589     }
590   }
591   
592   /* flush event group */
593   [self->elementList removeAllObjects];
594   
595   /* close open groups */
596   if (lastGroup != nil) {
597     [self reportEndGroup];
598     [lastGroup release]; lastGroup = nil;
599   }
600 }
601
602 /* errors */
603
604 - (void)reportError:(NSString *)_text {
605   SaxParseException *e;
606
607   e = (id)[SaxParseException exceptionWithName:@"SaxParseException"
608                              reason:_text
609                              userInfo:nil];
610   [self->errorHandler error:e];
611 }
612 - (void)warn:(NSString *)_warn {
613   SaxParseException *e;
614
615   e = (id)[SaxParseException exceptionWithName:@"SaxParseException"
616                              reason:_warn
617                              userInfo:nil];
618   [self->errorHandler warning:e];
619 }
620
621 /* parsing raw string */
622
623 - (void)_beginComponentWithValue:(NSString *)tagValue {
624   VSSaxTag *tag;
625   
626   tag = [self _beginTag:[self _mapTagName:tagValue]
627               group:nil
628               withAttrs:[[[SaxAttributes alloc] init] autorelease]];
629   [self->cardStack addObject:tag];
630 }
631
632 - (void)_endComponent:(NSString *)tagName value:(NSString *)tagValue {
633   NSString *mtName;
634     
635   mtName = [self _mapTagName:tagValue];
636   if ([self->cardStack count] > 0) {
637       NSString *expectedName;
638       
639       expectedName = [(VSSaxTag *)[self->cardStack lastObject] tagName];
640       if (![expectedName isEqualToString:mtName]) {
641         NSString *s;
642         
643         // TODO: rather report an error?
644         // TODO: setup userinfo dict with details
645         s = [NSString stringWithFormat:
646                         @"Found end tag '%@' which does not match expected "
647                         @"name '%@'!"
648                         @" Tag '%@' has not been closed properly. Given "
649                         @"document contains errors!",
650                         mtName, expectedName, expectedName];
651         [self reportError:s];
652         
653         /* probably futile attempt to parse anyways */
654         if (debugOn) {
655           NSLog(@"%s trying to fix previous error by inserting bogus end "
656                 @"tag.",
657                 __PRETTY_FUNCTION__);
658         }
659         [self _endTag:expectedName];
660         [self->cardStack removeLastObject];
661       }
662   }
663   else {
664       // TOOD: generate error?
665       [self reportError:[@"found end tag without any open tags left: "
666                    stringByAppendingString:mtName]];
667   }
668   [self _endTag:mtName];
669   [self->cardStack removeLastObject];
670     
671   /* report parsed elements */
672     
673   if ([self->cardStack count] == 0)
674     [self reportQueuedTags];
675 }
676
677 - (void)_parseLine:(NSString *)_line {
678   NSString       *tagName, *tagValue;
679   NSMutableArray *tagAttributes;
680   NSRange        r, todoRange;
681   unsigned       length;
682   
683 #if 0
684   if (debugOn)
685     NSLog(@"%s: parse line: '%@'", __PRETTY_FUNCTION__, _line);
686 #endif
687
688   length    = [_line length];
689   todoRange = NSMakeRange(0, length);
690   r = [_line rangeOfCharacterFromSet:colonAndSemicolonCharSet
691              options:0
692              range:todoRange];
693   /* is line well-formed? */
694   if (r.length == 0 || r.location == 0) {
695     NSLog(@"todo-range: %i-%i, range: %i-%i, length %i, str-class %@",
696           todoRange.location, todoRange.length,
697           r.location, r.length,
698           length, NSStringFromClass([_line class]));
699
700     [self reportError:
701             [@"got an improper content line! (did not find colon) ->\n" 
702               stringByAppendingString:_line]];
703     return;
704   }
705   
706   /* tagname is everything up to a ':' or  ';' (value or parameter) */
707   tagName       = [[_line substringToIndex:r.location] uppercaseString];
708   tagAttributes = [[NSMutableArray alloc] initWithCapacity:16];
709   
710   if (debugOn && ([tagName length] == 0)) {
711     [self reportError:[@"got an improper content line! ->\n" 
712                   stringByAppendingString:_line]];
713     return;
714   }
715   
716   /* 
717      possible shortcut: if we spotted a ':', we don't have to do "expensive"
718      argument scanning/processing.
719   */
720   if ([_line characterAtIndex:r.location] != ':') {
721     BOOL isAtEnd    = NO;
722     BOOL isInDquote = NO;
723     unsigned start;
724     
725     start     = NSMaxRange(r);
726     todoRange = NSMakeRange(start, length - start);
727     while(!isAtEnd) {
728       BOOL skip = YES;
729
730       /* scan for parameters */
731       r = [_line rangeOfCharacterFromSet:colonSemicolonAndDquoteCharSet
732                  options:0
733                  range:todoRange];
734       
735       /* is line well-formed? */
736       if (r.length == 0 || r.location == 0) {
737         [self reportError:[@"got an improper content line! ->\n" 
738                       stringByAppendingString:_line]];
739         [tagAttributes release]; tagAttributes = nil;
740         return;
741       }
742       
743       /* first check if delimiter candidate is escaped */
744       if ([_line characterAtIndex:(r.location - 1)] != '\\') {
745         unichar delimiter;
746         NSRange copyRange;
747
748         delimiter = [_line characterAtIndex:r.location];
749         if (delimiter == '\"') {
750           /* not a real delimiter - toggle isInDquote for proper escaping */
751           isInDquote = !isInDquote;
752         }
753         else {
754           if (!isInDquote) {
755             /* is a delimiter, which one? */
756             skip = NO;
757             if (delimiter == ':') {
758               isAtEnd = YES;
759             }
760             copyRange = NSMakeRange(start, r.location - start);
761             [tagAttributes addObject:[_line substringWithRange:copyRange]];
762             if (!isAtEnd) {
763               /* adjust start, todoRange */
764               start     = NSMaxRange(r);
765               todoRange = NSMakeRange(start, length - start);
766             }
767           }
768         }
769       }
770       if (skip) {
771         /* adjust todoRange */
772         unsigned offset = NSMaxRange(r);
773         todoRange = NSMakeRange(offset, length - offset);
774       }
775     }
776   }
777   tagValue = [_line substringFromIndex:NSMaxRange(r)];
778   
779   if (debugOn && ([tagName length] == 0)) {
780     NSLog(@"%s: missing tagname in line: '%@'", 
781           __PRETTY_FUNCTION__, _line);
782   }
783   
784   /*
785     At this point we have:
786       name:       'BEGIN', 'TEL', 'EMAIL', 'ITEM1.ADR' etc
787       value:      ';;;Magdeburg;;;Germany'
788       attributes: ("type=INTERNET", "type=HOME", "type=pref")
789   */
790
791 #if 0
792 #  warning DEBUG LOG ENABLED
793   NSLog(@"TAG: %@, value %@ attrs %@",
794         tagName, tagValue, tagAttributes);
795 #endif
796   
797   /* process tag */
798   
799   if ([tagName isEqualToString:@"BEGIN"]) {
800     if ([tagAttributes count] > 0)
801       [self warn:@"Losing unexpected parameters of BEGIN line."];
802     [self _beginComponentWithValue:tagValue];
803   }
804   else if ([tagName isEqualToString:@"END"]) {
805     if ([tagAttributes count] > 0)
806       [self warn:@"Losing unexpected parameters of END line."];
807     [self _endComponent:tagName value:tagValue];
808   }
809   else {
810     /* a regular content tag */
811     
812     /* 
813        check whether the tga value is encoded in quoted printable,
814        this one is used with Outlook vCards (see data/ for examples)
815     */
816     // TODO: make the encoding check more generic
817     if ([tagAttributes containsObject:@"ENCODING=QUOTED-PRINTABLE"]) {
818       // TODO: QP is charset specific! The one below decodes in Unicode!
819       tagValue = [tagValue stringByDecodingQuotedPrintable];
820       [tagAttributes removeObject:@"ENCODING=QUOTED-PRINTABLE"];
821     }
822     
823     [self _reportContentAsTag:[self _mapTagName:tagName]
824           group:[self _groupFromTagName:tagName]
825           withAttrs:[self _mapAttrs:tagAttributes forTag:tagName]
826           andContent:tagValue];
827   }
828   
829   [tagAttributes release];
830 }
831
832
833 /* top level parsing method */
834
835 - (void)reportDocStart {
836   [self->contentHandler startDocument];
837   [self->contentHandler startPrefixMapping:@"" uri:self->prefixURI];
838 }
839 - (void)reportDocEnd {
840   [self->contentHandler endPrefixMapping:@""];
841   [self->contentHandler endDocument];
842 }
843
844 - (void)_parseString:(NSString *)_rawString {
845   /*
846     This method split the string into content lines for actual vCard
847     parsing.
848
849     RFC2445:
850      contentline        = name *(";" param ) ":" value CRLF
851      ; When parsing a content line, folded lines MUST first
852      ; be unfolded
853   */
854   NSMutableString *line;
855   unsigned pos, length;
856   NSRange  r;
857
858   [self reportDocStart];
859   
860   /* start parsing */
861   
862   length = [_rawString length];
863   r      = NSMakeRange(0, 0);
864   line   = [[NSMutableString alloc] initWithCapacity:75 + 2];
865   
866   for (pos = 0; pos < length; pos++) {
867     unichar c;
868     
869     c = [_rawString characterAtIndex:pos];
870     
871     if (c == '\r') {
872       if (((length - 1) - pos) >= 1) {
873         if ([_rawString characterAtIndex:pos + 1] == '\n') {
874           BOOL isAtEndOfLine = YES;
875           
876           /* test for folding first */
877           if (((length - 1) - pos) >= 2) {
878             unichar ws;
879             
880             ws = [_rawString characterAtIndex:pos + 2];
881             isAtEndOfLine = [whitespaceCharSet characterIsMember:ws] ? NO :YES;
882             if (!isAtEndOfLine) {
883               /* assemble part of line up to pos */
884               if (r.length > 0) {
885                 [line appendString:[_rawString substringWithRange:r]];
886               }
887               /* unfold */
888               pos += 2;
889               r = NSMakeRange(pos + 1, 0); /* begin new range */
890             }
891           }
892           if (isAtEndOfLine) {
893             /* assemble part of line up to pos */
894             if (r.length > 0) {
895               [line appendString:[_rawString substringWithRange:r]];
896             }
897             [self _parseLine:line];
898             /* reset line */
899             [line deleteCharactersInRange:NSMakeRange(0, [line length])];
900             pos += 1;
901             r = NSMakeRange(pos + 1, 0); /* begin new range */
902           }
903         }
904       }
905       else {
906         /* garbled last line! */
907         [self warn:@"last line is truncated, trying to parse anyways!"];
908       }
909     }
910     else if (c == '\n') { /* broken, non-standard */
911       BOOL isAtEndOfLine = YES;
912       
913       /* test for folding first */
914       if (((length - 1) - pos) >= 1) {
915         unichar ws;
916         
917         ws = [_rawString characterAtIndex:(pos + 1)];
918         
919         isAtEndOfLine = [whitespaceCharSet characterIsMember:ws] ? NO : YES;
920         if (!isAtEndOfLine) {
921           /* assemble part of line up to pos */
922           if (r.length > 0) {
923             [line appendString:[_rawString substringWithRange:r]];
924           }
925           /* unfold */
926           pos += 1;
927           r = NSMakeRange(pos + 1, 0); /* begin new range */
928         }
929       }
930       if (isAtEndOfLine) {
931         /* assemble part of line up to pos */
932         if (r.length > 0) {
933           [line appendString:[_rawString substringWithRange:r]];
934         }
935         [self _parseLine:line];
936         /* reset line */
937         [line deleteCharactersInRange:NSMakeRange(0, [line length])];
938         r = NSMakeRange(pos + 1, 0); /* begin new range */
939       }
940     }
941     else {
942       r.length += 1;
943     }
944   }
945   if (r.length > 0) {
946     [self warn:@"Last line of parse string is not properly terminated!"];
947     [line appendString:[_rawString substringWithRange:r]];
948     [self _parseLine:line];
949   }
950   
951   if ([self->cardStack count] != 0) {
952     [self warn:@"found elements on cardStack. This indicates an improper "
953             @"nesting structure! Not all required events will have been "
954             @"generated, leading to unpredictable results!"];
955     [self->cardStack removeAllObjects]; // clean up
956   }
957   
958   [line release]; line = nil;
959   
960   [self reportDocEnd];
961 }
962
963 /* main entry functions */
964
965 - (id)sourceForData:(NSData *)_data systemId:(NSString *)_sysId {
966   SaxParseException *e = nil;
967   NSStringEncoding encoding;
968   unsigned len;
969   const unsigned char *bytes;
970   id source;
971   
972   if (debugOn) {
973     NSLog(@"%s: trying to decode data (0x%08X,len=%d) ...",
974             __PRETTY_FUNCTION__, _data, [_data length]);
975   }
976   
977   if ((len = [_data length]) == 0) {
978     e = (id)[SaxParseException exceptionWithName:@"SaxIOException"
979                                reason:@"Got no parsing data!"
980                                userInfo:nil];
981     [self->errorHandler fatalError:e];
982     return nil;
983   }
984   if (len < 10) {
985     e = (id)[SaxParseException exceptionWithName:@"SaxIOException"
986                                reason:@"Input data to short for vCard!"
987                                userInfo:nil];
988     [self->errorHandler fatalError:e];
989     return nil;
990   }
991   
992   bytes = [_data bytes];
993   if ((bytes[0] == 0xFF && bytes[1] == 0xFE) ||
994       (bytes[0] == 0xFE && bytes[1] == 0xFF)) {
995     encoding = NSUnicodeStringEncoding;
996   }
997   else
998     encoding = NSUTF8StringEncoding;
999   
1000   // FIXME: Data is not always utf-8.....
1001   source = [[[NSString alloc] initWithData:_data encoding:encoding]
1002              autorelease];
1003   if (source == nil) {
1004     e = (id)[SaxParseException exceptionWithName:@"SaxIOException"
1005                                reason:@"Could not convert input to string!"
1006                                userInfo:nil];
1007     [self->errorHandler fatalError:e];
1008   }
1009   return source;
1010 }
1011
1012 - (void)parseFromSource:(id)_source systemId:(NSString *)_sysId {
1013   if (debugOn)
1014     NSLog(@"%s: parse: %@ (sysid=%@)", __PRETTY_FUNCTION__, _source, _sysId);
1015   
1016   if ([_source isKindOfClass:[NSURL class]]) {
1017     if (_sysId == nil) _sysId = [_source absoluteString];
1018
1019     if (debugOn) {
1020       NSLog(@"%s: trying to load URL: %@ (sysid=%@)",__PRETTY_FUNCTION__, 
1021             _source, _sysId);
1022     }
1023     
1024     // TODO: remember encoding of source
1025     _source = [_source resourceDataUsingCache:NO];
1026   }
1027   
1028   if ([_source isKindOfClass:[NSData class]]) {
1029     if (_sysId == nil) _sysId = @"<data>";
1030     if ((_source = [self sourceForData:_source systemId:_sysId]) == nil)
1031       return;
1032   }
1033
1034   if (![_source isKindOfClass:[NSString class]]) {
1035     SaxParseException *e;
1036     NSString *s;
1037     
1038     if (debugOn) 
1039       NSLog(@"%s: unrecognizable source: %@", __PRETTY_FUNCTION__,_source);
1040     
1041     s = [@"cannot handle data-source: " stringByAppendingString:
1042             [_source description]];
1043     e = (id)[SaxParseException exceptionWithName:@"SaxIOException"
1044                                reason:s
1045                                userInfo:nil];
1046     
1047     [self->errorHandler fatalError:e];
1048     return;
1049   }
1050
1051   /* ensure consistent state */
1052
1053   [self->cardStack   removeAllObjects];
1054   [self->elementList removeAllObjects];
1055   
1056   /* start parsing */
1057   
1058   if (debugOn) {
1059     NSLog(@"%s: trying to parse string (0x%08X,len=%d) ...",
1060           __PRETTY_FUNCTION__, _source, [_source length]);
1061   }
1062   if (_sysId == nil) _sysId = @"<string>";
1063   [self _parseString:_source];
1064   
1065   /* tear down */
1066   
1067   [self->cardStack   removeAllObjects];
1068   [self->elementList removeAllObjects];
1069 }
1070
1071 - (void)parseFromSource:(id)_source {
1072   [self parseFromSource:_source systemId:nil];
1073 }
1074
1075 - (void)parseFromSystemId:(NSString *)_sysId {
1076   NSURL *url;
1077   
1078   if ([_sysId rangeOfString:@"://"].length == 0) {
1079     /* seems to be a path, path to be a proper URL */
1080     url = [NSURL fileURLWithPath:_sysId];
1081   }
1082   else {
1083     /* Note: Cocoa NSURL doesn't complain on "/abc/def" like input! */
1084     url = [NSURL URLWithString:_sysId];
1085   }
1086   
1087   if (url == nil) {
1088     SaxParseException *e;
1089     
1090     e = (id)[SaxParseException exceptionWithName:@"SaxIOException"
1091                                reason:@"cannot handle system-id"
1092                                userInfo:nil];
1093     [self->errorHandler fatalError:e];
1094     return;
1095   }
1096   
1097   [self parseFromSource:url systemId:_sysId];
1098 }
1099
1100 /* debugging */
1101
1102 - (BOOL)isDebuggingEnabled {
1103   return debugOn;
1104 }
1105
1106 @end /* VersitSaxDriver */