]> err.no Git - sope/blobdiff - sope-ical/versitSaxDriver/VSSaxDriver.m
added support vcard type groupings
[sope] / sope-ical / versitSaxDriver / VSSaxDriver.m
index d0922f19c13f3296a6d0b6955509f0386e3a808b..4f0881be084aa0abb018fa10bce188d7189bd836 100644 (file)
 /*
  Copyright (C) 2003-2004 Max Berger
- Copyright (C) 2004 OpenGroupware.org
+ Copyright (C) 2004-2005 OpenGroupware.org
 
  This file is part of versitSaxDriver, written for the OpenGroupware.org 
  project (OGo).
  
OGo is free software; you can redistribute it and/or modify it under
SOPE is free software; you can redistribute it and/or modify it under
  the terms of the GNU Lesser General Public License as published by the
  Free Software Foundation; either version 2, or (at your option) any
  later version.
  
OGo is distributed in the hope that it will be useful, but WITHOUT ANY
SOPE is distributed in the hope that it will be useful, but WITHOUT ANY
  WARRANTY; without even the implied warranty of MERCHANTABILITY or
  FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public
  License for more details.
  
  You should have received a copy of the GNU Lesser General Public
- License along with OGo; see the file COPYING.  If not, write to the
+ License along with SOPE; see the file COPYING.  If not, write to the
  Free Software Foundation, 59 Temple Place - Suite 330, Boston, MA
  02111-1307, USA.
 */
-// $Id$
 
 #include "VSSaxDriver.h"
+#include "VSStringFormatter.h"
+#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 *whitespaceCharSet = 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 VSStringFormatter *stringFormatter = nil;
 
 + (void)initialize {
   static BOOL didInit = NO;
   NSUserDefaults *ud;
 
-  if(didInit)
+  if (didInit)
     return;
   didInit = YES;
 
   ud      = [NSUserDefaults standardUserDefaults];
-  debugOn = [ud boolForKey:@"OGoDebugVersitSaxDriver"];
+  debugOn = [ud boolForKey:@"VSSaxDriverDebugEnabled"];
 
   dotCharSet =
     [[NSCharacterSet characterSetWithCharactersInString:@"."] retain];
@@ -54,24 +143,29 @@ static NSCharacterSet *whitespaceCharSet = nil;
     [[NSCharacterSet characterSetWithCharactersInString:@","] retain];
   colonAndSemicolonCharSet =
     [[NSCharacterSet characterSetWithCharactersInString:@":;"] retain];
+  colonSemicolonAndDquoteCharSet =
+    [[NSCharacterSet characterSetWithCharactersInString:@":;\""] retain];
   whitespaceCharSet =
     [[NSCharacterSet whitespaceCharacterSet] retain];
+
+  stringFormatter = [VSStringFormatter sharedFormatter];
 }
 
 
 - (id)init {
   if ((self = [super init])) {
     self->prefixURI         = @"";
-    self->cardStack         = [[NSMutableArray alloc]      init];
-    self->elementList       = [[NSMutableArray alloc]      init];
-    self->attributeMapping  = [[NSMutableDictionary alloc] init];
-    self->subItemMapping    = [[NSMutableDictionary alloc] init];
+    self->cardStack         = [[NSMutableArray alloc]      initWithCapacity:4];
+    self->elementList       = [[NSMutableArray alloc]      initWithCapacity:8];
+    self->attributeMapping  = [[NSMutableDictionary alloc] initWithCapacity:8];
+    self->subItemMapping    = [[NSMutableDictionary alloc] initWithCapacity:8];
   }
   return self;
 }
 
 - (void)dealloc {
   [self->contentHandler    release];
+  [self->errorHandler      release];
   [self->prefixURI         release];
   [self->cardStack         release];
   [self->elementList       release];
@@ -99,7 +193,7 @@ static NSCharacterSet *whitespaceCharSet = nil;
 /* handlers */
 
 - (void)setContentHandler:(id<NSObject,SaxContentHandler>)_handler {
-  ASSIGN(self->contentHandler,_handler);
+  ASSIGN(self->contentHandler, _handler);
 }
 
 - (void)setDTDHandler:(id<NSObject,SaxDTDHandler>)_handler {
@@ -107,7 +201,7 @@ static NSCharacterSet *whitespaceCharSet = nil;
 }
 
 - (void)setErrorHandler:(id<NSObject,SaxErrorHandler>)_handler {
-  // FIXME
+  ASSIGN(self->errorHandler, _handler);
 }
 - (void)setEntityResolver:(id<NSObject,SaxEntityResolver>)_handler {
   // FIXME
@@ -119,16 +213,15 @@ static NSCharacterSet *whitespaceCharSet = nil;
 
 - (id<NSObject,SaxDTDHandler>)dtdHandler {
   // FIXME
-  return NULL;
+  return nil;
 }
 
 - (id<NSObject,SaxErrorHandler>)errorHandler {
-  // FIXME
-  return NULL;
+  return self->errorHandler;
 }
 - (id<NSObject,SaxEntityResolver>)entityResolver {
   // FIXME
-  return NULL;
+  return nil;
 }
 
 - (void)setPrefixURI:(NSString *)_uri {
@@ -159,14 +252,12 @@ static NSCharacterSet *whitespaceCharSet = nil;
 - (void)setAttributeMapping:(NSDictionary *)_mapping 
   forElement:(NSString *)_element 
 {
-  if (!_element)
+  if (_element == nil)
     _element = @"";
   [attributeMapping setObject:_mapping forKey:_element];
 }
 
-- (void)setSubItemMapping:(NSArray *)_mapping 
-  forElement:(NSString *)_element 
-{
+- (void)setSubItemMapping:(NSArray *)_mapping forElement:(NSString *)_element {
   [subItemMapping setObject:_mapping forKey:_element];  
 }
 
@@ -174,60 +265,59 @@ static NSCharacterSet *whitespaceCharSet = 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;
 
-  mappedName = [[self->attributeMapping objectForKey:_tagName]
-                                        objectForKey:_attrName];
-  if (!mappedName) {
-    mappedName = [[self->attributeMapping objectForKey:
-                                          [self _mapTagName:_tagName]]
-                                          objectForKey:_attrName];
-  }
-  if (!mappedName) {
-    mappedName = [[self->attributeMapping objectForKey:@""]
-                                          objectForKey:_attrName];
-  }
-  if (!mappedName)
-    mappedName = _attrName;
+  /* 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;
   
-  return mappedName;
+  /* 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 the name as-is */
+  return _attrName;
 }
 
 - (void)_parseAttr:(NSString *)_attr 
@@ -235,48 +325,68 @@ static NSCharacterSet *whitespaceCharSet = nil;
   intoAttr:(NSString **)attr_
   intoValue:(NSString **)value_
 {
-  NSRange         r;
-  NSString        *attrName;
-  NSMutableString *attrValue;
-  NSString        *mappedName;
+  NSRange  r;
+  NSString *attrName, *attrValue, *mappedName;
   
   r = [_attr rangeOfCharacterFromSet:equalSignCharSet];
   if (r.length > 0) {
+    unsigned left, right;
+
     attrName  = [[_attr substringToIndex:r.location] uppercaseString];
-    attrValue = [[_attr substringFromIndex:(r.location + 1)] mutableCopy];
+    left = NSMaxRange(r);
+    right = [_attr length] - 1;
+    if (left < right) {
+      if (([_attr characterAtIndex:left]  == '"') &&
+         ([_attr characterAtIndex:right] == '"'))
+      {
+        left += 1;
+        r = NSMakeRange(left, right - left);
+        attrValue = [_attr substringWithRange:r];
+      }
+      else {
+        attrValue = [_attr substringFromIndex:left];
+      }
+    }
+    else if (left == right) {
+      attrValue = [_attr substringFromIndex:left];
+    }
+    else {
+      attrValue = @"";
+    }
   }
   else {
     attrName  = @"TYPE";
-    attrValue = [[NSMutableString alloc] initWithString:_attr];
+    attrValue = _attr;
   }
   
+#if 0
+  // ZNeK: what's this for?
   r = [attrValue rangeOfCharacterFromSet:commaCharSet];
   while (r.length > 0) {
     [attrValue replaceCharactersInRange:r withString:@" "];
     r = [attrValue rangeOfCharacterFromSet:commaCharSet];
   }
+#endif
 
   mappedName = [self _mapAttrName:attrName forTag:_tagName];
   *attr_ = mappedName;
-  *value_ = [NSString stringWithString:attrValue];
-  [attrValue release];
+  *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
@@ -292,10 +402,9 @@ static NSCharacterSet *whitespaceCharSet = 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];
@@ -303,181 +412,420 @@ static NSCharacterSet *whitespaceCharSet = 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];
-  } 
-  else {
-    [self _beginTag:_tagName withAttrs:_attrs];
-    if ([_content length] > 0) {
-      if ((subItems = [self->subItemMapping objectForKey:_tagName])) {
-        [self _addSubItems:subItems withData:_content];
-      }
-      else {
-        [self->elementList addObject:
-          [NSArray arrayWithObjects:@"DATA", _content, nil]];  
+    /* 
+       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];
+  
+  if ([_content length] > 0) {
+    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];
   }
+
+  [self _endTag:_tagName];
 }
 
-- (void)_eventsForElements {
+/* 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?
+    
+    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;
-  NSArray  *obj;
-  NSString *type;
-  NSString *name;
-  unichar  *chardata;
-  id<NSObject,SaxAttributes> attrs;
-  
-  enu = [elementList objectEnumerator];
-  while ((obj = [enu nextObject])) {
-    type = [obj objectAtIndex:0];
-    name = [obj objectAtIndex:1];
+  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;
+  }
 }
 
-- (void)_parseLine:(NSString *)_line {
-  NSScanner      *scanner;
-  NSString       *tagName, *tagAttribute, *tagValue;
-  NSMutableArray *tagAttributes;
+/* errors */
+
+- (void)error:(NSString *)_text {
+  SaxParseException *e;
 
-  scanner       = [NSScanner scannerWithString:_line];
-  tagAttributes = [[NSMutableArray alloc] init];
+  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];
+}
 
-  tagName = @""; 
-  [scanner scanUpToCharactersFromSet:colonAndSemicolonCharSet
-           intoString:&tagName];
+/* parsing raw string */
 
-  while ([scanner scanString:@";" intoString:NULL]) {
-    [scanner scanUpToCharactersFromSet:colonAndSemicolonCharSet
-                               intoString:&tagAttribute];
-    [tagAttributes addObject:tagAttribute];
+- (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 {
+  NSString       *tagName, *tagValue;
+  NSMutableArray *tagAttributes;
+  NSRange        r, todoRange;
+  unsigned       length;
+
+  length    = [_line length];
+  todoRange = NSMakeRange(0, length);
+  r = [_line rangeOfCharacterFromSet:colonAndSemicolonCharSet
+             options:0
+             range:todoRange];
+  /* is line well-formed? */
+  if (r.length == 0) {
+    [self error:[@"got an improper content line! ->\n" 
+                 stringByAppendingString:_line]];
+    return;
+  }
+  
+  /* 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;
+    BOOL isInDquote = NO;
+    unsigned start;
+    
+    start     = NSMaxRange(r);
+    todoRange = NSMakeRange(start, length - start);
+    while(!isAtEnd) {
+      BOOL skip = YES;
+
+      /* scan for parameters */
+      r = [_line rangeOfCharacterFromSet:colonSemicolonAndDquoteCharSet
+                 options:0
+                 range:todoRange];
+      
+      /* is line well-formed? */
+      if (r.length == 0 || r.location == 0) {
+       [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;
+        NSRange copyRange;
+
+        delimiter = [_line characterAtIndex:r.location];
+        if (delimiter == '\"') {
+          /* not a real delimiter - toggle isInDquote for proper escaping */
+          isInDquote = !isInDquote;
+        }
+        else {
+          if (!isInDquote) {
+            /* is a delimiter, which one? */
+            skip = NO;
+            if (delimiter == ':') {
+              isAtEnd = YES;
+            }
+            copyRange = NSMakeRange(start, r.location - start);
+            [tagAttributes addObject:[_line substringWithRange:copyRange]];
+            if (!isAtEnd) {
+              /* adjust start, todoRange */
+              start     = NSMaxRange(r);
+              todoRange = NSMakeRange(start, length - start);
+            }
+          }
+        }
+      }
+      if (skip) {
+        /* adjust todoRange */
+        unsigned offset = NSMaxRange(r);
+        todoRange = NSMakeRange(offset, length - offset);
+      }
+    }
   }
-  [scanner scanString:@":" intoString:NULL];
-  tagValue = [_line substringFromIndex:[scanner scanLocation]];
+  tagValue = [_line substringFromIndex:NSMaxRange(r)];
   
-  //NSLog (@"%@ %@ %@",tagName,tagAttributes, tagValue);
+  /*
+    At this point we have:
+      name:      'BEGIN', 'TEL', 'EMAIL', 'ITEM1.ADR' etc
+      value:     ';;;Magdeburg;;;Germany'
+      atributes: ("type=INTERNET", "type=HOME", "type=pref")
+  */
   
-  tagName = [tagName uppercaseString];
+  /* 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"]) {
-    [self _endTag:[self _mapTagName:tagValue]];
-    [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;
     
-    if(c == '\r') {
-      if(((length - 1) - pos) >= 1) {
-        if([_rawString characterAtIndex:pos + 1] == '\n') {
+    c = [_rawString characterAtIndex:pos];
+    
+    if (c == '\r') {
+      if (((length - 1) - pos) >= 1) {
+        if ([_rawString characterAtIndex:pos + 1] == '\n') {
           BOOL isAtEndOfLine = YES;
+         
           /* test for folding first */
-          if(((length - 1) - pos) >= 2) {
-            unichar ws = [_rawString characterAtIndex:pos + 2];
-            isAtEndOfLine = [whitespaceCharSet characterIsMember:ws] ? NO
-                                                                     : YES;
-            if(!isAtEndOfLine) {
+          if (((length - 1) - pos) >= 2) {
+            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) {
+              if (r.length > 0) {
                 [line appendString:[_rawString substringWithRange:r]];
               }
               /* unfold */
@@ -485,9 +833,9 @@ static NSCharacterSet *whitespaceCharSet = nil;
               r = NSMakeRange(pos + 1, 0); /* begin new range */
             }
           }
-          if(isAtEndOfLine) {
+          if (isAtEndOfLine) {
             /* assemble part of line up to pos */
-            if(r.length > 0) {
+            if (r.length > 0) {
               [line appendString:[_rawString substringWithRange:r]];
             }
             [self _parseLine:line];
@@ -500,70 +848,197 @@ static NSCharacterSet *whitespaceCharSet = 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;
+       
+       ws = [_rawString characterAtIndex:(pos + 1)];
+       
+        isAtEndOfLine = [whitespaceCharSet characterIsMember:ws] ? NO : YES;
+        if (!isAtEndOfLine) {
+          /* assemble part of line up to pos */
+          if (r.length > 0) {
+            [line appendString:[_rawString substringWithRange:r]];
+          }
+          /* unfold */
+          pos += 1;
+          r = NSMakeRange(pos + 1, 0); /* begin new range */
+        }
+      }
+      if (isAtEndOfLine) {
+        /* assemble part of line up to pos */
+        if (r.length > 0) {
+          [line appendString:[_rawString substringWithRange:r]];
         }
+        [self _parseLine:line];
+        /* reset line */
+        [line deleteCharactersInRange:NSMakeRange(0, [line length])];
+        r = NSMakeRange(pos + 1, 0); /* begin new range */
       }
     }
     else {
       r.length += 1;
     }
   }
-  if(r.length > 0) {
-    if(debugOn) {
-      NSLog(@"%s Last line of iCal string is not properly terminated!",
-            __PRETTY_FUNCTION__);
-    }
+  if (r.length > 0) {
+    [self warn:@"Last line of parse string is not properly terminated!"];
     [line appendString:[_rawString substringWithRange:r]];
     [self _parseLine:line];
   }
+  
+  if ([self->cardStack count] != 0) {
+    [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]; line = nil;
+  
+  [self reportDocEnd];
+}
 
-  [line release];
-  [self->contentHandler endPrefixMapping:@""];
-  [self->contentHandler endDocument];
+/* 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 {
+- (void)parseFromSource:(id)_source systemId:(NSString *)_sysId {
   if (debugOn)
-    NSLog(@"%s: parse: %@", __PRETTY_FUNCTION__, _source);
+    NSLog(@"%s: parse: %@ (sysid=%@)", __PRETTY_FUNCTION__, _source, _sysId);
   
   if ([_source isKindOfClass:[NSURL class]]) {
-    if (debugOn) 
-      NSLog(@"%s: trying to load URL...",__PRETTY_FUNCTION__);
+    if (_sysId == nil) _sysId = [_source absoluteString];
+
+    if (debugOn) {
+      NSLog(@"%s: trying to load URL: %@ (sysid=%@)",__PRETTY_FUNCTION__, 
+           _source, _sysId);
+    }
+    
+    // TODO: remember encoding of source
     _source = [_source resourceDataUsingCache:NO];
   }
   
   if ([_source isKindOfClass:[NSData class]]) {
-    // FIXME: Data is not always utf-8.....
-    if (debugOn) 
-      NSLog(@"%s: trying to decode data...",__PRETTY_FUNCTION__);
-    _source = [[[NSString alloc]
-                initWithData:_source encoding:NSUTF8StringEncoding]
-                autorelease];
+    if (_sysId == nil) _sysId = @"<data>";
+    if ((_source = [self sourceForData:_source systemId:_sysId]) == nil)
+      return;
   }
-  
-  if ([_source isKindOfClass:[NSString class]]) {
-    if (debugOn) 
-      NSLog(@"%s: trying to parse string...",__PRETTY_FUNCTION__);
-    [self _parseString:_source];
-  } 
-  else {
+
+  if (![_source isKindOfClass:[NSString class]]) {
+    SaxParseException *e;
+    NSString *s;
+    
     if (debugOn) 
       NSLog(@"%s: unrecognizable source: %@", __PRETTY_FUNCTION__,_source);
-    // FIXME: Return Error
+    
+    s = [@"cannot handle data-source: " stringByAppendingString:
+           [_source description]];
+    e = (id)[SaxParseException exceptionWithName:@"SaxIOException"
+                               reason:s
+                               userInfo:nil];
+    
+    [self->errorHandler fatalError:e];
+    return;
+  }
+
+  /* ensure consistent state */
+
+  [self->cardStack   removeAllObjects];
+  [self->elementList removeAllObjects];
+  
+  /* start parsing */
+  
+  if (debugOn) {
+    NSLog(@"%s: trying to parse string (0x%08X,len=%d) ...",
+         __PRETTY_FUNCTION__, _source, [_source length]);
   }
+  if (_sysId == nil) _sysId = @"<string>";
+  [self _parseString:_source];
+  
+  /* tear down */
+  
+  [self->cardStack   removeAllObjects];
+  [self->elementList removeAllObjects];
 }
 
-- (void)parseFromSource:(id)_source systemId:(NSString *)_sysId {
-  [self parseFromSource:_source];
+- (void)parseFromSource:(id)_source {
+  [self parseFromSource:_source systemId:nil];
 }
 
 - (void)parseFromSystemId:(NSString *)_sysId {
   NSURL *url;
   
-  if ((url = [NSURL URLWithString:_sysId]))
-    [self parseFromSource:url systemId:_sysId];
+  if ([_sysId rangeOfString:@"://"].length == 0) {
+    /* seems to be a path, path to be a proper URL */
+    url = [NSURL fileURLWithPath:_sysId];
+  }
+  else {
+    /* Note: Cocoa NSURL doesn't complain on "/abc/def" like input! */
+    url = [NSURL URLWithString:_sysId];
+  }
+  
+  if (url == nil) {
+    SaxParseException *e;
+    
+    e = (id)[SaxParseException exceptionWithName:@"SaxIOException"
+                               reason:@"cannot handle system-id"
+                               userInfo:nil];
+    [self->errorHandler fatalError:e];
+    return;
+  }
+  
+  [self parseFromSource:url systemId:_sysId];
 }
 
 /* debugging */