]> err.no Git - sope/blobdiff - sope-ical/versitSaxDriver/VSSaxDriver.m
added support vcard type groupings
[sope] / sope-ical / versitSaxDriver / VSSaxDriver.m
index e740f5bdfd4248eb7a0983579d01d47c52bd1014..4f0881be084aa0abb018fa10bce188d7189bd836 100644 (file)
 #include <SaxObjC/SaxException.h>
 #include "common.h"
 
+@interface VSSaxTag : NSObject
+{
+@private
+  char          type;
+  NSString      *tagName;
+  NSString      *group;
+@public
+  SaxAttributes *attrs;
+  unichar       *data;
+  unsigned int  datalen;
+}
+
++ (id)beginTag:(NSString *)_tag group:(NSString *)_group
+  attributes:(SaxAttributes *)_attrs;
+- (id)initEndTag:(NSString *)_tag;
+
+- (id)initWithContentString:(NSString *)_data;
+
+- (NSString *)tagName;
+- (BOOL)isStartTag;
+- (BOOL)isEndTag;
+- (BOOL)isTag;
+
+@end
+
+@implementation VSSaxTag
+
++ (id)beginTag:(NSString *)_tag group:(NSString *)_group
+  attributes:(SaxAttributes *)_attrs
+{
+  VSSaxTag *tag;
+
+  tag = [[[self alloc] init] autorelease];
+  tag->type    = 'B';
+  tag->tagName = [_tag copy];
+  tag->group   = [_group copy];
+  tag->attrs   = [_attrs retain];
+  return tag;
+}
+- (id)initEndTag:(NSString *)_tag {
+  self->type    = 'E';
+  self->tagName = [_tag copy];
+  return self;
+}
+- (id)initWithContentString:(NSString *)_data {
+  if (_data == nil) {
+    [self release];
+    return nil;
+  }
+  
+  self->datalen = [_data length];
+  self->data    = calloc(self->datalen + 1, sizeof(unichar));
+  [_data getCharacters:self->data range:NSMakeRange(0, self->datalen)];
+  return self;
+}
+
+- (void)dealloc {
+  if (self->data) free(self->data);
+  [self->group   release];
+  [self->tagName release];
+  [self->attrs   release];
+  [super dealloc];
+}
+
+/* accessors */
+
+- (NSString *)tagName {
+  return self->tagName;
+}
+- (NSString *)group {
+  return self->group;
+}
+
+- (BOOL)isStartTag {
+  return self->type == 'B' ? YES : NO;
+}
+- (BOOL)isEndTag {
+  return self->type == 'E' ? YES : NO;
+}
+- (BOOL)isTag {
+  return (self->type == 'B' || self->type == 'E') ? YES : NO;
+}
+
+@end /* VSSaxTag */
+
 @implementation VSSaxDriver
 
 static BOOL debugOn = NO;
 
-static NSCharacterSet *dotCharSet = nil;
-static NSCharacterSet *equalSignCharSet = nil;
-static NSCharacterSet *commaCharSet = nil;
-static NSCharacterSet *colonAndSemicolonCharSet = nil;
+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 NSCharacterSet *whitespaceCharSet              = nil;
 
 static VSStringFormatter *stringFormatter = nil;
 
@@ -180,60 +265,59 @@ static VSStringFormatter *stringFormatter = nil;
 
 /* parsing */
 
+- (NSString *)_groupFromTagName:(NSString *)_tagName {
+  NSRange  r;
+  
+  r = [_tagName rangeOfCharacterFromSet:dotCharSet];
+  if (r.length == 0)
+    return nil;
+  
+  return [_tagName substringToIndex:r.location];
+}
+
 - (NSString *)_mapTagName:(NSString *)_tagName {
   NSString *ret;
   NSRange  r;
 
-  if ((ret = [self->elementMapping objectForKey:_tagName]) == nil) {
-    //NSLog(@"Unknown Key: %@ in %@",_tagName,self->elementMapping);
-    ret = _tagName;
+  if ((ret = [self->elementMapping objectForKey:_tagName]) != nil)
+    return ret;
 
-    /* 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)]];
-    }
-  }
+  //NSLog(@"Unknown Key: %@ in %@",_tagName,self->elementMapping);
+  ret = _tagName;
+  
+  /*
+    This is to allow parsing of vCards produced by Apple
+    Addressbook.
+    The dot-notation is described as 'grouping' in RFC 2425, section 5.8.2.
+  */
+  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 {
+  NSDictionary *tagMap;
   NSString *mappedName;
+
+  /* check whether we have a attr-map stored under the element-name */
+  tagMap = [self->attributeMapping objectForKey:_tagName];
+  if ((mappedName = [tagMap objectForKey:_attrName]) != nil)
+    return mappedName;
   
-  mappedName = [(NSDictionary *)[self->attributeMapping objectForKey:_tagName]
-                                        objectForKey:_attrName];
-  if (mappedName == nil) {
-    mappedName = [(NSDictionary *)[self->attributeMapping objectForKey:
-                                          [self _mapTagName:_tagName]]
-                                          objectForKey:_attrName];
-  }
-  if (mappedName == nil) {
-    mappedName = [(NSDictionary *)[self->attributeMapping objectForKey:@""]
-                                          objectForKey:_attrName];
-  }
-  if (mappedName == nil)
-    mappedName = _attrName;
+  /* check whether we have a attr-map stored under the mapped element-name */
+  tagMap = [self->attributeMapping objectForKey:[self _mapTagName:_tagName]];
+  if ((mappedName = [tagMap objectForKey:_attrName]) != nil)
+    return mappedName;
+
+  /* check whether we have a global attr-map */
+  tagMap = [self->attributeMapping objectForKey:@""];
+  if ((mappedName = [tagMap objectForKey:_attrName]) != nil)
+    return mappedName;
   
-  return mappedName;
+  /* return the name as-is */
+  return _attrName;
 }
 
 - (void)_parseAttr:(NSString *)_attr 
@@ -289,21 +373,20 @@ static VSStringFormatter *stringFormatter = nil;
   *value_ = [stringFormatter stringByUnescapingRFC2445Text:attrValue];
 }
 
-- (id<NSObject,SaxAttributes>)_mapAttrs:(NSArray *)_attrs 
-  forTag:(NSString *)_tagName 
-{
+- (SaxAttributes *)_mapAttrs:(NSArray *)_attrs forTag:(NSString *)_tagName {
   SaxAttributes       *retAttrs;
   NSEnumerator        *attrEnum;
   NSString            *curAttr, *mappedAttr, *mappedValue, *oldValue;
   NSMutableDictionary *attributes;
 
-  if (!_attrs || [_attrs count] == 0)
+  if (_attrs == nil || [_attrs count] == 0)
     return nil;
 
   attributes = [[NSMutableDictionary alloc] init];
-  retAttrs = [[[SaxAttributes alloc] init] autorelease];
+  retAttrs   = [[[SaxAttributes alloc] init] autorelease];
+  
   attrEnum = [_attrs objectEnumerator];
-  while ((curAttr = [attrEnum nextObject])) {
+  while ((curAttr = [attrEnum nextObject]) != nil) {
     [self _parseAttr:curAttr
           forTag:_tagName
           intoAttr:&mappedAttr
@@ -319,10 +402,9 @@ static VSStringFormatter *stringFormatter = nil;
   }
 
   attrEnum = [attributes keyEnumerator];
-  while ((curAttr = [attrEnum nextObject])) {
-    [self _addAttribute:curAttr
-          value:[attributes objectForKey:curAttr]
-          toAttrs:retAttrs];
+  while ((curAttr = [attrEnum nextObject]) != nil) {
+    [retAttrs addAttribute:curAttr uri:self->prefixURI rawName:curAttr
+             type:@"CDATA" value:[attributes objectForKey:curAttr]];
   }
   
   [attributes release];
@@ -330,103 +412,251 @@ static VSStringFormatter *stringFormatter = nil;
   return retAttrs;
 }
 
-- (NSArray *)_beginTag:(NSString *)_tagName 
-  withAttrs:(id<NSObject,SaxAttributes>)_attrs 
+- (VSSaxTag *)_beginTag:(NSString *)_tagName group:(NSString *)_group
+  withAttrs:(SaxAttributes *)_attrs
 {
-  NSArray *tag = [NSArray arrayWithObjects:@"BEGIN",_tagName,_attrs,NULL];
+  VSSaxTag *tag;
+  
+  tag = [VSSaxTag beginTag:_tagName group:_group attributes:_attrs];
   [self->elementList addObject:tag];
   return tag;
 }
 
 - (void)_endTag:(NSString *)_tagName {
-  [self->elementList addObject:
-        [NSArray arrayWithObjects:@"END",_tagName,NULL]];
+  VSSaxTag *tag;
+  
+  tag = [[VSSaxTag alloc] initEndTag:_tagName];
+  [self->elementList addObject:tag];
+  [tag release]; tag = nil;
 }
 
-- (void)_addSubItems:(NSArray *)_items withData:(NSString *)_content {
+- (void)_addSubItems:(NSArray *)_items group:(NSString *)_group
+  withData:(NSString *)_content
+{
   NSEnumerator *itemEnum, *contentEnum;
   NSString *subTag;
-  NSString *subContent;
   
   itemEnum    = [_items objectEnumerator];
   contentEnum = [[_content componentsSeparatedByString:@";"] objectEnumerator];
   
-  while ((subTag=[itemEnum nextObject])) {
+  while ((subTag = [itemEnum nextObject]) != nil) {
+    NSString *subContent;
+    
     subContent = [contentEnum nextObject];
     
-    [self _beginTag:subTag withAttrs:nil];
-    if ([subContent length]>0) 
-      [self->elementList addObject:
-        [NSArray arrayWithObjects:@"DATA", subContent, nil]];  
+    [self _beginTag:subTag group:_group withAttrs:nil];
+    if ([subContent length] > 0) {
+      VSSaxTag *a;
+      
+      a = [(VSSaxTag*)[VSSaxTag alloc] initWithContentString:subContent];
+      if (a != nil) {
+       [self->elementList addObject:a];
+       [a release];
+      }
+    }
     [self _endTag:subTag];
   }
 }
 
-- (void)_dataTag:(NSString *)_tagName 
-  withAttrs:(id<NSObject,SaxAttributes>)_attrs 
+- (void)_reportContentAsTag:(NSString *)_tagName
+  group:(NSString *)_group
+  withAttrs:(SaxAttributes *)_attrs 
   andContent:(NSString *)_content 
 {
+  /*
+    This is called for all non-BEGIN|END types.
+  */
   NSArray *subItems;
   
   _content = [stringFormatter stringByUnescapingRFC2445Text:_content];
+
+  /* check whether type should be reported as an attribute in XML */
+  
   if ([self->attributeElements containsObject:_tagName]) {
-    [self _addAttribute:_tagName value:_content];
+    /* 
+       Add tag as an attribute to last component in the cardstack. This is
+       stuff like the "VERSION" type contained in a "BEGIN:VCARD" which will
+       be reported as <vcard version=""> (as an attribute of the container).
+    */
+    VSSaxTag *element;
+    
+    element = [self->cardStack lastObject];
+    [element->attrs addAttribute:_tagName uri:self->prefixURI
+                   rawName:_tagName type:@"CDATA" value:_content];
     return;
-  } 
+  }
+
+  /* report type as an XML tag */
+  
+  [self _beginTag:_tagName group:_group withAttrs:_attrs];
   
-  [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]];  
+    if ((subItems = [self->subItemMapping objectForKey:_tagName]) != nil) {
+      [self _addSubItems:subItems group:_group withData:_content];
+    }
+    else {
+      VSSaxTag *a;
+      
+      a = [(VSSaxTag *)[VSSaxTag alloc] initWithContentString:_content];
+      if (a != nil) {
+       [self->elementList addObject:a];
+       [a release];
       }
+    }
   }
+
   [self _endTag:_tagName];
 }
 
-- (void)_eventsForElements {
-  NSEnumerator *enu;
-  NSArray  *obj;
-  
-  enu = [elementList objectEnumerator];
-  while ((obj = [enu nextObject]) != nil) {
-    id<NSObject,SaxAttributes> attrs;
-    NSString *type;
-    NSString *name;
-    unichar  *chardata;
+/* report events for collected elements */
+
+- (void)reportStartGroup:(NSString *)_group {
+  SaxAttributes *attrs;
+  
+  attrs = [[SaxAttributes alloc] init];
+  [attrs addAttribute:@"name" uri:self->prefixURI rawName:@"name"
+        type:@"CDATA" value:_group];
+  
+  [self->contentHandler startElement:@"group" namespace:self->prefixURI
+                        rawName:@"group" attributes:attrs];
+  [attrs release];
+}
+- (void)reportEndGroup {
+  [self->contentHandler endElement:@"group" namespace:self->prefixURI
+                        rawName:@"group"];
+}
+
+- (void)reportQueuedTags {
+  /*
+    Why does the parser need the list instead of reporting the events
+    straight away?
     
-    type = [obj objectAtIndex:0];
-    name = [obj objectAtIndex:1];
+    Because some vCard tags like the 'version' are reported as attributes
+    on the container tag. So we have a sequence like:
+      BEGIN:VCARD
+      ...
+      VERSION:3.0
+    which will get reported as:
+      <vcard version="3.0">
+  */
+  NSEnumerator *enu;
+  VSSaxTag *tagToReport;
+  NSString *lastGroup;
+  
+  lastGroup = nil;
+  enu = [self->elementList objectEnumerator];
+  while ((tagToReport = [enu nextObject]) != nil) {
+    if ([tagToReport isStartTag]) {
+      NSString *tg;
+      
+      tg = [tagToReport group];
+      if (![lastGroup isEqualToString:tg] && lastGroup != tg) {
+       if (lastGroup != nil) [self reportEndGroup];
+       ASSIGNCOPY(lastGroup, tg);
+       if (lastGroup != nil) [self reportStartGroup:lastGroup];
+      }
+    }
     
-    if ([obj count] > 2) 
-      attrs = [obj objectAtIndex:2];
-    else
-      attrs = nil;
-
-    if ([type isEqualToString:@"BEGIN"]) {
-      [self->contentHandler startElement:name
+    if ([tagToReport isStartTag]) {
+      [self->contentHandler startElement:[tagToReport tagName]
                             namespace:self->prefixURI
-                            rawName:name
-                            attributes:attrs];
-    } 
-    else if ([type isEqualToString:@"END"]) {
-      [self->contentHandler endElement:name
+                            rawName:[tagToReport tagName]
+                            attributes:tagToReport->attrs];
+    }
+    else if ([tagToReport isEndTag]) {
+      [self->contentHandler endElement:[tagToReport tagName]
                             namespace:self->prefixURI
-                            rawName:name];
+                            rawName:[tagToReport tagName]];
     }
     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);
+      [self->contentHandler characters:tagToReport->data
+                            length:tagToReport->datalen];
     }
   }
-  [elementList removeAllObjects];
+  
+  /* flush event group */
+  [self->elementList removeAllObjects];
+  
+  /* close open groups */
+  if (lastGroup != nil) {
+    [self reportEndGroup];
+    [lastGroup release]; lastGroup = nil;
+  }
+}
+
+/* errors */
+
+- (void)error:(NSString *)_text {
+  SaxParseException *e;
+
+  e = (id)[SaxParseException exceptionWithName:@"SaxParseException"
+                            reason:_text
+                            userInfo:nil];
+  [self->errorHandler error:e];
+}
+- (void)warn:(NSString *)_warn {
+  SaxParseException *e;
+
+  e = (id)[SaxParseException exceptionWithName:@"SaxParseException"
+                            reason:_warn
+                            userInfo:nil];
+  [self->errorHandler warning:e];
+}
+
+/* parsing raw string */
+
+- (void)_beginComponentWithValue:(NSString *)tagValue {
+  VSSaxTag *tag;
+  
+  tag = [self _beginTag:[self _mapTagName:tagValue]
+             group:nil
+             withAttrs:[[[SaxAttributes alloc] init] autorelease]];
+  [self->cardStack addObject:tag];
+}
+
+- (void)_endComponent:(NSString *)tagName value:(NSString *)tagValue {
+  NSString *mtName;
+    
+  mtName = [self _mapTagName:tagValue];
+  if ([self->cardStack count] > 0) {
+      NSString *expectedName;
+      
+      expectedName = [(VSSaxTag *)[self->cardStack lastObject] tagName];
+      if (![expectedName isEqualToString:mtName]) {
+       NSString *s;
+       
+       // TODO: rather report an error?
+       // TODO: setup userinfo dict with details
+       s = [NSString stringWithFormat:
+                       @"Found end tag '%@' which does not match expected "
+                       @"name '%@'!"
+                       @" Tag '%@' has not been closed properly. Given "
+                       @"document contains errors!",
+                       mtName, expectedName, expectedName];
+       [self error:s];
+       
+        /* 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 {
+      // TOOD: generate error?
+      [self error:[@"found end tag without any open tags left: "
+                  stringByAppendingString:mtName]];
+  }
+  [self _endTag:mtName];
+  [self->cardStack removeLastObject];
+    
+  /* report parsed elements */
+    
+  if ([self->cardStack count] == 0)
+    [self reportQueuedTags];
 }
 
 - (void)_parseLine:(NSString *)_line {
@@ -435,31 +665,32 @@ static VSStringFormatter *stringFormatter = nil;
   NSRange        r, todoRange;
   unsigned       length;
 
-  length = [_line 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);
-    }
+    [self error:[@"got an improper content line! ->\n" 
+                 stringByAppendingString:_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"
+  
+  /* tagname is everything up to a ':' or  ';' (value or parameter) */
+  tagName       = [[_line substringToIndex:r.location] uppercaseString];
+  tagAttributes = [[NSMutableArray alloc] initWithCapacity:16];
+  
+  /* 
+     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);
-
+    BOOL isAtEnd    = NO;
+    BOOL isInDquote = NO;
+    unsigned start;
+    
+    start     = NSMaxRange(r);
     todoRange = NSMakeRange(start, length - start);
     while(!isAtEnd) {
       BOOL skip = YES;
@@ -468,16 +699,15 @@ static VSStringFormatter *stringFormatter = nil;
       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];
+       [self error:[@"got an improper content line! ->\n" 
+                     stringByAppendingString:_line]];
+        [tagAttributes release]; tagAttributes = nil;
         return;
       }
+      
       /* first check if delimiter candidate is escaped */
       if ([_line characterAtIndex:(r.location - 1)] != '\\') {
         unichar delimiter;
@@ -513,92 +743,86 @@ static VSStringFormatter *stringFormatter = nil;
     }
   }
   tagValue = [_line substringFromIndex:NSMaxRange(r)];
-
+  
+  /*
+    At this point we have:
+      name:      'BEGIN', 'TEL', 'EMAIL', 'ITEM1.ADR' etc
+      value:     ';;;Magdeburg;;;Germany'
+      atributes: ("type=INTERNET", "type=HOME", "type=pref")
+  */
+  
+  /* process tag */
+  
   if ([tagName isEqualToString:@"BEGIN"]) {
-    id tag;
-    
-    tag = [self _beginTag:[self _mapTagName:tagValue] 
-                withAttrs:[[[SaxAttributes alloc] init] autorelease]];
-    [self->cardStack addObject:tag];
-  } 
+    if ([tagAttributes count] > 0)
+      [self warn:@"Losing unexpected parameters of BEGIN line."];
+    [self _beginComponentWithValue:tagValue];
+  }
   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];
+    if ([tagAttributes count] > 0)
+      [self warn:@"Losing unexpected parameters of END line."];
+    [self _endComponent:tagName value:tagValue];
   }
   else {
-    [self _dataTag:[self _mapTagName:tagName]
-         withAttrs:[self _mapAttrs:tagAttributes forTag:tagName] 
-        andContent:tagValue];
+    [self _reportContentAsTag:[self _mapTagName:tagName]
+         group:[self _groupFromTagName:tagName]
+         withAttrs:[self _mapAttrs:tagAttributes forTag:tagName] 
+         andContent:tagValue];
   }
+  
   [tagAttributes release];
 }
 
-- (void)_parseString:(NSString *)_rawString {
-  unsigned pos, length;
-  NSMutableString *line;
-  NSRange r;
 
+/* top level parsing method */
+
+- (void)reportDocStart {
   [self->contentHandler startDocument];
   [self->contentHandler startPrefixMapping:@"" uri:self->prefixURI];
-  
-  length = [_rawString length];
-  /* RFC2445:
+}
+- (void)reportDocEnd {
+  [self->contentHandler endPrefixMapping:@""];
+  [self->contentHandler endDocument];
+}
+
+- (void)_parseString:(NSString *)_rawString {
+  /*
+    This method split the string into content lines for actual vCard
+    parsing.
+
+    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];
+  NSMutableString *line;
+  unsigned pos, length;
+  NSRange  r;
 
-  for(pos = 0; pos < length; pos++) {
-    unichar c = [_rawString characterAtIndex:pos];
+  [self reportDocStart];
+  
+  /* start parsing */
+  
+  length = [_rawString length];
+  r      = NSMakeRange(0, 0);
+  line   = [[NSMutableString alloc] initWithCapacity:75 + 2];
+  
+  for (pos = 0; pos < length; pos++) {
+    unichar c;
+    
+    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;
+            unichar ws;
+           
+           ws = [_rawString characterAtIndex:pos + 2];
+            isAtEndOfLine = [whitespaceCharSet characterIsMember:ws] ? NO :YES;
             if (!isAtEndOfLine) {
               /* assemble part of line up to pos */
               if (r.length > 0) {
@@ -624,19 +848,19 @@ static VSStringFormatter *stringFormatter = nil;
       }
       else {
         /* garbled last line! */
-        if (debugOn) {
-          NSLog(@"%s Last line is truncated, trying to parse anyways!",
-                __PRETTY_FUNCTION__);
-        }
+       [self warn:@"last line is truncated, trying to parse anyways!"];
       }
     }
     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;
+        unichar ws;
+       
+       ws = [_rawString characterAtIndex:(pos + 1)];
+       
+        isAtEndOfLine = [whitespaceCharSet characterIsMember:ws] ? NO : YES;
         if (!isAtEndOfLine) {
           /* assemble part of line up to pos */
           if (r.length > 0) {
@@ -663,30 +887,72 @@ static VSStringFormatter *stringFormatter = nil;
     }
   }
   if (r.length > 0) {
-    if (debugOn) {
-      NSLog(@"%s Last line of iCal string is not properly terminated!",
-            __PRETTY_FUNCTION__);
-    }
+    [self warn:@"Last line of parse string is not properly terminated!"];
     [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__);
-    }
+    [self warn:@"found elements on cardStack. This indicates an improper "
+            @"nesting structure! Not all required events will have been "
+            @"generated, leading to unpredictable results!"];
+    [self->cardStack removeAllObjects]; // clean up
   }
-
-  [line release];
-  [self->contentHandler endPrefixMapping:@""];
-  [self->contentHandler endDocument];
+  
+  [line release]; line = nil;
+  
+  [self reportDocEnd];
 }
 
 /* main entry functions */
 
+- (id)sourceForData:(NSData *)_data systemId:(NSString *)_sysId {
+  SaxParseException *e = nil;
+  NSStringEncoding encoding;
+  unsigned len;
+  const unsigned char *bytes;
+  id source;
+  
+  if (debugOn) {
+    NSLog(@"%s: trying to decode data (0x%08X,len=%d) ...",
+           __PRETTY_FUNCTION__, _data, [_data length]);
+  }
+  
+  if ((len = [_data length]) == 0) {
+    e = (id)[SaxParseException exceptionWithName:@"SaxIOException"
+                              reason:@"Got no parsing data!"
+                              userInfo:nil];
+    [self->errorHandler fatalError:e];
+    return nil;
+  }
+  if (len < 10) {
+    e = (id)[SaxParseException exceptionWithName:@"SaxIOException"
+                              reason:@"Input data to short for vCard!"
+                              userInfo:nil];
+    [self->errorHandler fatalError:e];
+    return nil;
+  }
+  
+  bytes = [_data bytes];
+  if ((bytes[0] == 0xFF && bytes[1] == 0xFE) ||
+      (bytes[0] == 0xFE && bytes[1] == 0xFF)) {
+    encoding = NSUnicodeStringEncoding;
+  }
+  else
+    encoding = NSUTF8StringEncoding;
+  
+  // FIXME: Data is not always utf-8.....
+  source = [[[NSString alloc] initWithData:_data encoding:encoding]
+            autorelease];
+  if (source == nil) {
+    e = (id)[SaxParseException exceptionWithName:@"SaxIOException"
+                              reason:@"Could not convert input to string!"
+                              userInfo:nil];
+    [self->errorHandler fatalError:e];
+  }
+  return source;
+}
+
 - (void)parseFromSource:(id)_source systemId:(NSString *)_sysId {
   if (debugOn)
     NSLog(@"%s: parse: %@ (sysid=%@)", __PRETTY_FUNCTION__, _source, _sysId);
@@ -704,31 +970,32 @@ static VSStringFormatter *stringFormatter = nil;
   }
   
   if ([_source isKindOfClass:[NSData class]]) {
-    if (debugOn) {
-      NSLog(@"%s: trying to decode data (0x%08X,len=%d) ...",
-           __PRETTY_FUNCTION__, _source, [_source length]);
-    }
     if (_sysId == nil) _sysId = @"<data>";
-    
-    // FIXME: Data is not always utf-8.....
-    _source = [[[NSString alloc]
-                initWithData:_source encoding:NSUTF8StringEncoding]
-                autorelease];
+    if ((_source = [self sourceForData:_source systemId:_sysId]) == nil)
+      return;
   }
 
   if (![_source isKindOfClass:[NSString class]]) {
     SaxParseException *e;
+    NSString *s;
     
     if (debugOn) 
       NSLog(@"%s: unrecognizable source: %@", __PRETTY_FUNCTION__,_source);
     
+    s = [@"cannot handle data-source: " stringByAppendingString:
+           [_source description]];
     e = (id)[SaxParseException exceptionWithName:@"SaxIOException"
-                               reason:@"cannot handle data-source"
+                               reason:s
                                userInfo:nil];
     
     [self->errorHandler fatalError:e];
     return;
   }
+
+  /* ensure consistent state */
+
+  [self->cardStack   removeAllObjects];
+  [self->elementList removeAllObjects];
   
   /* start parsing */
   
@@ -738,6 +1005,11 @@ static VSStringFormatter *stringFormatter = nil;
   }
   if (_sysId == nil) _sysId = @"<string>";
   [self _parseString:_source];
+  
+  /* tear down */
+  
+  [self->cardStack   removeAllObjects];
+  [self->elementList removeAllObjects];
 }
 
 - (void)parseFromSource:(id)_source {