]> err.no Git - sope/blob - sope-ical/versitSaxDriver/VSSaxDriver.m
git-svn-id: http://svn.opengroupware.org/SOPE/trunk@1195 e4a50df8-12e2-0310-a44c...
[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 #if 0
696     NSLog(@"todo-range: %i-%i, range: %i-%i, length %i, str-class %@",
697           todoRange.location, todoRange.length,
698           r.location, r.length,
699           length, NSStringFromClass([_line class]));
700 #endif
701
702     [self reportError:
703             [@"got an improper content line! (did not find colon) ->\n" 
704               stringByAppendingString:_line]];
705     return;
706   }
707   
708   /* tagname is everything up to a ':' or  ';' (value or parameter) */
709   tagName       = [[_line substringToIndex:r.location] uppercaseString];
710   tagAttributes = [[NSMutableArray alloc] initWithCapacity:16];
711   
712   if (debugOn && ([tagName length] == 0)) {
713     [self reportError:[@"got an improper content line! ->\n" 
714                   stringByAppendingString:_line]];
715     return;
716   }
717   
718   /* 
719      possible shortcut: if we spotted a ':', we don't have to do "expensive"
720      argument scanning/processing.
721   */
722   if ([_line characterAtIndex:r.location] != ':') {
723     BOOL isAtEnd    = NO;
724     BOOL isInDquote = NO;
725     unsigned start;
726     
727     start     = NSMaxRange(r);
728     todoRange = NSMakeRange(start, length - start);
729     while(!isAtEnd) {
730       BOOL skip = YES;
731
732       /* scan for parameters */
733       r = [_line rangeOfCharacterFromSet:colonSemicolonAndDquoteCharSet
734                  options:0
735                  range:todoRange];
736       
737       /* is line well-formed? */
738       if (r.length == 0 || r.location == 0) {
739         [self reportError:[@"got an improper content line! ->\n" 
740                       stringByAppendingString:_line]];
741         [tagAttributes release]; tagAttributes = nil;
742         return;
743       }
744       
745       /* first check if delimiter candidate is escaped */
746       if ([_line characterAtIndex:(r.location - 1)] != '\\') {
747         unichar delimiter;
748         NSRange copyRange;
749
750         delimiter = [_line characterAtIndex:r.location];
751         if (delimiter == '\"') {
752           /* not a real delimiter - toggle isInDquote for proper escaping */
753           isInDquote = !isInDquote;
754         }
755         else {
756           if (!isInDquote) {
757             /* is a delimiter, which one? */
758             skip = NO;
759             if (delimiter == ':') {
760               isAtEnd = YES;
761             }
762             copyRange = NSMakeRange(start, r.location - start);
763             [tagAttributes addObject:[_line substringWithRange:copyRange]];
764             if (!isAtEnd) {
765               /* adjust start, todoRange */
766               start     = NSMaxRange(r);
767               todoRange = NSMakeRange(start, length - start);
768             }
769           }
770         }
771       }
772       if (skip) {
773         /* adjust todoRange */
774         unsigned offset = NSMaxRange(r);
775         todoRange = NSMakeRange(offset, length - offset);
776       }
777     }
778   }
779   tagValue = [_line substringFromIndex:NSMaxRange(r)];
780   
781   if (debugOn && ([tagName length] == 0)) {
782     NSLog(@"%s: missing tagname in line: '%@'", 
783           __PRETTY_FUNCTION__, _line);
784   }
785   
786   /*
787     At this point we have:
788       name:       'BEGIN', 'TEL', 'EMAIL', 'ITEM1.ADR' etc
789       value:      ';;;Magdeburg;;;Germany'
790       attributes: ("type=INTERNET", "type=HOME", "type=pref")
791   */
792
793 #if 0
794 #  warning DEBUG LOG ENABLED
795   NSLog(@"TAG: %@, value %@ attrs %@",
796         tagName, tagValue, tagAttributes);
797 #endif
798   
799   /* process tag */
800   
801   if ([tagName isEqualToString:@"BEGIN"]) {
802     if ([tagAttributes count] > 0)
803       [self warn:@"Losing unexpected parameters of BEGIN line."];
804     [self _beginComponentWithValue:tagValue];
805   }
806   else if ([tagName isEqualToString:@"END"]) {
807     if ([tagAttributes count] > 0)
808       [self warn:@"Losing unexpected parameters of END line."];
809     [self _endComponent:tagName value:tagValue];
810   }
811   else {
812     /* a regular content tag */
813     
814     /* 
815        check whether the tga value is encoded in quoted printable,
816        this one is used with Outlook vCards (see data/ for examples)
817     */
818     // TODO: make the encoding check more generic
819     if ([tagAttributes containsObject:@"ENCODING=QUOTED-PRINTABLE"]) {
820       // TODO: QP is charset specific! The one below decodes in Unicode!
821       tagValue = [tagValue stringByDecodingQuotedPrintable];
822       [tagAttributes removeObject:@"ENCODING=QUOTED-PRINTABLE"];
823     }
824     
825     [self _reportContentAsTag:[self _mapTagName:tagName]
826           group:[self _groupFromTagName:tagName]
827           withAttrs:[self _mapAttrs:tagAttributes forTag:tagName]
828           andContent:tagValue];
829   }
830   
831   [tagAttributes release];
832 }
833
834
835 /* top level parsing method */
836
837 - (void)reportDocStart {
838   [self->contentHandler startDocument];
839   [self->contentHandler startPrefixMapping:@"" uri:self->prefixURI];
840 }
841 - (void)reportDocEnd {
842   [self->contentHandler endPrefixMapping:@""];
843   [self->contentHandler endDocument];
844 }
845
846 - (void)_parseString:(NSString *)_rawString {
847   /*
848     This method split the string into content lines for actual vCard
849     parsing.
850
851     RFC2445:
852      contentline        = name *(";" param ) ":" value CRLF
853      ; When parsing a content line, folded lines MUST first
854      ; be unfolded
855   */
856   NSMutableString *line;
857   unsigned pos, length;
858   NSRange  r;
859
860   [self reportDocStart];
861   
862   /* start parsing */
863   
864   length = [_rawString length];
865   r      = NSMakeRange(0, 0);
866   line   = [[NSMutableString alloc] initWithCapacity:75 + 2];
867   
868   for (pos = 0; pos < length; pos++) {
869     unichar c;
870     
871     c = [_rawString characterAtIndex:pos];
872     
873     if (c == '\r') {
874       if (((length - 1) - pos) >= 1) {
875         if ([_rawString characterAtIndex:pos + 1] == '\n') {
876           BOOL isAtEndOfLine = YES;
877           
878           /* test for folding first */
879           if (((length - 1) - pos) >= 2) {
880             unichar ws;
881             
882             ws = [_rawString characterAtIndex:pos + 2];
883             isAtEndOfLine = [whitespaceCharSet characterIsMember:ws] ? NO :YES;
884             if (!isAtEndOfLine) {
885               /* assemble part of line up to pos */
886               if (r.length > 0) {
887                 [line appendString:[_rawString substringWithRange:r]];
888               }
889               /* unfold */
890               pos += 2;
891               r = NSMakeRange(pos + 1, 0); /* begin new range */
892             }
893           }
894           if (isAtEndOfLine) {
895             /* assemble part of line up to pos */
896             if (r.length > 0) {
897               [line appendString:[_rawString substringWithRange:r]];
898             }
899             [self _parseLine:line];
900             /* reset line */
901             [line deleteCharactersInRange:NSMakeRange(0, [line length])];
902             pos += 1;
903             r = NSMakeRange(pos + 1, 0); /* begin new range */
904           }
905         }
906       }
907       else {
908         /* garbled last line! */
909         [self warn:@"last line is truncated, trying to parse anyways!"];
910       }
911     }
912     else if (c == '\n') { /* broken, non-standard */
913       BOOL isAtEndOfLine = YES;
914       
915       /* test for folding first */
916       if (((length - 1) - pos) >= 1) {
917         unichar ws;
918         
919         ws = [_rawString characterAtIndex:(pos + 1)];
920         
921         isAtEndOfLine = [whitespaceCharSet characterIsMember:ws] ? NO : YES;
922         if (!isAtEndOfLine) {
923           /* assemble part of line up to pos */
924           if (r.length > 0) {
925             [line appendString:[_rawString substringWithRange:r]];
926           }
927           /* unfold */
928           pos += 1;
929           r = NSMakeRange(pos + 1, 0); /* begin new range */
930         }
931       }
932       if (isAtEndOfLine) {
933         /* assemble part of line up to pos */
934         if (r.length > 0) {
935           [line appendString:[_rawString substringWithRange:r]];
936         }
937         [self _parseLine:line];
938         /* reset line */
939         [line deleteCharactersInRange:NSMakeRange(0, [line length])];
940         r = NSMakeRange(pos + 1, 0); /* begin new range */
941       }
942     }
943     else {
944       r.length += 1;
945     }
946   }
947   if (r.length > 0) {
948     [self warn:@"Last line of parse string is not properly terminated!"];
949     [line appendString:[_rawString substringWithRange:r]];
950     [self _parseLine:line];
951   }
952   
953   if ([self->cardStack count] != 0) {
954     [self warn:@"found elements on cardStack. This indicates an improper "
955             @"nesting structure! Not all required events will have been "
956             @"generated, leading to unpredictable results!"];
957     [self->cardStack removeAllObjects]; // clean up
958   }
959   
960   [line release]; line = nil;
961   
962   [self reportDocEnd];
963 }
964
965 /* main entry functions */
966
967 - (id)sourceForData:(NSData *)_data systemId:(NSString *)_sysId {
968   SaxParseException *e = nil;
969   NSStringEncoding encoding;
970   unsigned len;
971   const unsigned char *bytes;
972   id source;
973   
974   if (debugOn) {
975     NSLog(@"%s: trying to decode data (0x%08X,len=%d) ...",
976             __PRETTY_FUNCTION__, _data, [_data length]);
977   }
978   
979   if ((len = [_data length]) == 0) {
980     e = (id)[SaxParseException exceptionWithName:@"SaxIOException"
981                                reason:@"Got no parsing data!"
982                                userInfo:nil];
983     [self->errorHandler fatalError:e];
984     return nil;
985   }
986   if (len < 10) {
987     e = (id)[SaxParseException exceptionWithName:@"SaxIOException"
988                                reason:@"Input data to short for vCard!"
989                                userInfo:nil];
990     [self->errorHandler fatalError:e];
991     return nil;
992   }
993   
994   bytes = [_data bytes];
995   if ((bytes[0] == 0xFF && bytes[1] == 0xFE) ||
996       (bytes[0] == 0xFE && bytes[1] == 0xFF)) {
997     encoding = NSUnicodeStringEncoding;
998   }
999   else
1000     encoding = NSUTF8StringEncoding;
1001   
1002   // FIXME: Data is not always utf-8.....
1003   source = [[[NSString alloc] initWithData:_data encoding:encoding]
1004              autorelease];
1005   if (source == nil) {
1006     e = (id)[SaxParseException exceptionWithName:@"SaxIOException"
1007                                reason:@"Could not convert input to string!"
1008                                userInfo:nil];
1009     [self->errorHandler fatalError:e];
1010   }
1011   return source;
1012 }
1013
1014 - (void)parseFromSource:(id)_source systemId:(NSString *)_sysId {
1015   if (debugOn)
1016     NSLog(@"%s: parse: %@ (sysid=%@)", __PRETTY_FUNCTION__, _source, _sysId);
1017   
1018   if ([_source isKindOfClass:[NSURL class]]) {
1019     if (_sysId == nil) _sysId = [_source absoluteString];
1020
1021     if (debugOn) {
1022       NSLog(@"%s: trying to load URL: %@ (sysid=%@)",__PRETTY_FUNCTION__, 
1023             _source, _sysId);
1024     }
1025     
1026     // TODO: remember encoding of source
1027     _source = [_source resourceDataUsingCache:NO];
1028   }
1029   
1030   if ([_source isKindOfClass:[NSData class]]) {
1031     if (_sysId == nil) _sysId = @"<data>";
1032     if ((_source = [self sourceForData:_source systemId:_sysId]) == nil)
1033       return;
1034   }
1035
1036   if (![_source isKindOfClass:[NSString class]]) {
1037     SaxParseException *e;
1038     NSString *s;
1039     
1040     if (debugOn) 
1041       NSLog(@"%s: unrecognizable source: %@", __PRETTY_FUNCTION__,_source);
1042     
1043     s = [@"cannot handle data-source: " stringByAppendingString:
1044             [_source description]];
1045     e = (id)[SaxParseException exceptionWithName:@"SaxIOException"
1046                                reason:s
1047                                userInfo:nil];
1048     
1049     [self->errorHandler fatalError:e];
1050     return;
1051   }
1052
1053   /* ensure consistent state */
1054
1055   [self->cardStack   removeAllObjects];
1056   [self->elementList removeAllObjects];
1057   
1058   /* start parsing */
1059   
1060   if (debugOn) {
1061     NSLog(@"%s: trying to parse string (0x%08X,len=%d) ...",
1062           __PRETTY_FUNCTION__, _source, [_source length]);
1063   }
1064   if (_sysId == nil) _sysId = @"<string>";
1065   [self _parseString:_source];
1066   
1067   /* tear down */
1068   
1069   [self->cardStack   removeAllObjects];
1070   [self->elementList removeAllObjects];
1071 }
1072
1073 - (void)parseFromSource:(id)_source {
1074   [self parseFromSource:_source systemId:nil];
1075 }
1076
1077 - (void)parseFromSystemId:(NSString *)_sysId {
1078   NSURL *url;
1079   
1080   if ([_sysId rangeOfString:@"://"].length == 0) {
1081     /* seems to be a path, path to be a proper URL */
1082     url = [NSURL fileURLWithPath:_sysId];
1083   }
1084   else {
1085     /* Note: Cocoa NSURL doesn't complain on "/abc/def" like input! */
1086     url = [NSURL URLWithString:_sysId];
1087   }
1088   
1089   if (url == nil) {
1090     SaxParseException *e;
1091     
1092     e = (id)[SaxParseException exceptionWithName:@"SaxIOException"
1093                                reason:@"cannot handle system-id"
1094                                userInfo:nil];
1095     [self->errorHandler fatalError:e];
1096     return;
1097   }
1098   
1099   [self parseFromSource:url systemId:_sysId];
1100 }
1101
1102 /* debugging */
1103
1104 - (BOOL)isDebuggingEnabled {
1105   return debugOn;
1106 }
1107
1108 @end /* VersitSaxDriver */