Source/AIXMLChatlogConverter.m
author Colin Barrett <colin@springsandstruts.com>
Mon Aug 16 23:07:04 2010 -0700 (21 months ago)
changeset 3271 4dc4e5bceaa9
parent 3087 30703336e5b6
child 3679 f4294bb53b0f
permissions -rw-r--r--
Move us off of deprecated CFXMLParser and onto NSXMLDocument for parsing chat transcripts.
     1 /*
     2  * Adium is the legal property of its developers, whose names are listed in the copyright file included
     3  * with this source distribution.
     4  * 
     5  * This program is free software; you can redistribute it and/or modify it under the terms of the GNU
     6  * General Public License as published by the Free Software Foundation; either version 2 of the License,
     7  * or (at your option) any later version.
     8  * 
     9  * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
    10  * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General
    11  * Public License for more details.
    12  * 
    13  * You should have received a copy of the GNU General Public License along with this program; if not,
    14  * write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
    15  */
    16 
    17 #import "AIXMLChatlogConverter.h"
    18 #import "AIStandardListWindowController.h"
    19 #import <Adium/AIHTMLDecoder.h>
    20 #import <Adium/AIListContact.h>
    21 #import <Adium/AIService.h>
    22 #import <Adium/AIAccountControllerProtocol.h>
    23 #import <Adium/AIContactControllerProtocol.h>
    24 #import <Adium/AIContentControllerProtocol.h>
    25 #import <Adium/AIStatusControllerProtocol.h>
    26 #import <AIUtilities/NSCalendarDate+ISO8601Parsing.h>
    27 #import <AIUtilities/AIDateFormatterAdditions.h>
    28 #import <AIUtilities/AIStringAdditions.h>
    29 
    30 #define PREF_GROUP_WEBKIT_MESSAGE_DISPLAY		@"WebKit Message Display"
    31 #define KEY_WEBKIT_USE_NAME_FORMAT				@"Use Custom Name Format"
    32 #define KEY_WEBKIT_NAME_FORMAT					@"Name Format"
    33 
    34 @interface NSMutableString (XMLMethods)
    35 - (void)stripInvalidCharacters;
    36 @end
    37 
    38 @implementation NSMutableString (XMLMethods)
    39 
    40 //Strip invalid XML characters
    41 - (void)stripInvalidCharacters
    42 {
    43     static NSCharacterSet *invalidXMLCharacterSet;
    44     
    45     if (invalidXMLCharacterSet == nil)
    46     {
    47         // First, create a character set containing all valid UTF8 characters.
    48         NSMutableCharacterSet *xmlCharacterSet = [[NSMutableCharacterSet alloc] init];        
    49         [xmlCharacterSet addCharactersInRange:NSMakeRange(0x9, 1)];       
    50         [xmlCharacterSet addCharactersInRange:NSMakeRange(0xA, 1)];        
    51         [xmlCharacterSet addCharactersInRange:NSMakeRange(0xD, 1)];        
    52         [xmlCharacterSet addCharactersInRange:NSMakeRange(0x20, 0xD7FF - 0x20)];
    53         [xmlCharacterSet addCharactersInRange:NSMakeRange(0xE000, 0xFFFD - 0xE000)];        
    54         [xmlCharacterSet addCharactersInRange:NSMakeRange(0x10000, 0x10FFFF - 0x10000)];        
    55         // Then create and retain an inverted set, which will thus contain all invalid XML characters.        
    56         invalidXMLCharacterSet = [[xmlCharacterSet invertedSet] retain];        
    57         [xmlCharacterSet release];        
    58     }
    59     
    60     // Are there any invalid characters in this string?    
    61     NSRange range = [self rangeOfCharacterFromSet:invalidXMLCharacterSet];
    62     
    63     // Otherwise go through and remove any illegal XML characters from a copy of the string.
    64     while (range.length > 0)
    65     {        
    66         [self deleteCharactersInRange:range];
    67         range = [self rangeOfCharacterFromSet:invalidXMLCharacterSet                 
    68                                       options:0                 
    69                                         range:NSMakeRange(range.location,[self length]-range.location)];        
    70     }    
    71 }
    72 
    73 @end
    74 
    75 @interface NSXMLElement (AIAttributeDict)
    76 - (NSDictionary *)AIAttributesAsDictionary;
    77 @end
    78 
    79 @implementation NSXMLElement (AIAttributeDict)
    80 
    81 - (NSDictionary *)AIAttributesAsDictionary 
    82 {
    83     NSArray *attrArray = [self attributes];
    84     NSMutableDictionary *attributes = [NSMutableDictionary dictionary];
    85     for (NSXMLNode *attr in attrArray) {
    86         [attributes setObject:attr forKey:[attr name]];
    87     }
    88     return attributes;
    89 }
    90 
    91 @end
    92 
    93 @interface AIXMLChatlogConverter()
    94 - (NSAttributedString *)readData:(NSData *)xmlData withOptions:(NSDictionary *)options retrying:(BOOL)reentrancyFlag;
    95 @end
    96 
    97 @implementation AIXMLChatlogConverter
    98 
    99 + (NSAttributedString *)readFile:(NSString *)filePath withOptions:(NSDictionary *)options
   100 {
   101 	static AIXMLChatlogConverter *converter;
   102     if (!converter) {
   103         converter = [[AIXMLChatlogConverter alloc] init];
   104 	}
   105     NSData *xmlData = [NSData dataWithContentsOfURL:[NSURL fileURLWithPath:filePath]];
   106     [converter->htmlDecoder setBaseURL:[filePath stringByDeletingLastPathComponent]];
   107     NSAttributedString *result = nil;
   108     @try {
   109         result = [converter readData:xmlData withOptions:options retrying:NO];
   110     } @catch (NSException *e) {
   111         NSLog(@"Error \"%@\" parsing log file at %@.", e, filePath);
   112         return [[[NSAttributedString alloc] initWithString:@"Sorry, there was an error parsing this transcript. It may be corrupt."] autorelease];
   113     }
   114     return result;
   115 }
   116 
   117 - (id)init
   118 {
   119 	if ((self = [super init])) {
   120         if (!newlineAttributedString) {
   121             newlineAttributedString = [[NSAttributedString alloc] initWithString:@"\n" attributes:nil];
   122         }
   123         
   124         htmlDecoder = [[AIHTMLDecoder alloc] init];
   125 		dateFormatter = [[NSDateFormatter localizedDateFormatterShowingSeconds:YES showingAMorPM:YES] retain];
   126 
   127 		statusLookup = [[NSDictionary alloc] initWithObjectsAndKeys:
   128 			AILocalizedString(@"Online", nil), @"online",
   129 			AILocalizedString(@"Idle", nil), @"idle",
   130 			[adium.statusController localizedDescriptionForCoreStatusName:STATUS_NAME_OFFLINE], @"offline",
   131 			[adium.statusController localizedDescriptionForCoreStatusName:STATUS_NAME_AWAY], @"away",
   132 			[adium.statusController localizedDescriptionForCoreStatusName:STATUS_NAME_AVAILABLE], @"available",
   133 			[adium.statusController localizedDescriptionForCoreStatusName:STATUS_NAME_BUSY], @"busy",
   134 			[adium.statusController localizedDescriptionForCoreStatusName:STATUS_NAME_NOT_AT_HOME], @"notAtHome",
   135 			[adium.statusController localizedDescriptionForCoreStatusName:STATUS_NAME_PHONE], @"onThePhone",
   136 			[adium.statusController localizedDescriptionForCoreStatusName:STATUS_NAME_VACATION], @"onVacation",
   137 			[adium.statusController localizedDescriptionForCoreStatusName:STATUS_NAME_DND], @"doNotDisturb",
   138 			[adium.statusController localizedDescriptionForCoreStatusName:STATUS_NAME_EXTENDED_AWAY], @"extendedAway",
   139 			[adium.statusController localizedDescriptionForCoreStatusName:STATUS_NAME_BRB], @"beRightBack",
   140 			[adium.statusController localizedDescriptionForCoreStatusName:STATUS_NAME_NOT_AVAILABLE], @"notAvailable",
   141 			[adium.statusController localizedDescriptionForCoreStatusName:STATUS_NAME_NOT_AT_DESK], @"notAtMyDesk",
   142 			[adium.statusController localizedDescriptionForCoreStatusName:STATUS_NAME_NOT_IN_OFFICE], @"notInTheOffice",
   143 			[adium.statusController localizedDescriptionForCoreStatusName:STATUS_NAME_STEPPED_OUT], @"steppedOut",
   144 			nil];			
   145 	}
   146 
   147 	return self;
   148 }
   149 
   150 - (void)dealloc
   151 {
   152 	[dateFormatter release];
   153 	[statusLookup release];
   154 	[htmlDecoder release];
   155 	[super dealloc];
   156 }
   157 
   158 - (NSAttributedString *)readData:(NSData *)xmlData withOptions:(NSDictionary *)options retrying:(BOOL)reentrancyFlag
   159 {
   160     if (!xmlData) {
   161         return [[[NSAttributedString alloc] initWithString:@""] autorelease];
   162     }
   163     AINameFormat nameFormat;
   164     if ([[adium.preferenceController preferenceForKey:KEY_WEBKIT_USE_NAME_FORMAT
   165                                                 group:PREF_GROUP_WEBKIT_MESSAGE_DISPLAY] boolValue]) {
   166         nameFormat = [[adium.preferenceController preferenceForKey:KEY_WEBKIT_NAME_FORMAT
   167                                                              group:PREF_GROUP_WEBKIT_MESSAGE_DISPLAY] intValue];
   168     } else {
   169         nameFormat = AIDefaultName;
   170     }
   171     NSMutableAttributedString *output = [[[NSMutableAttributedString alloc] init] autorelease];
   172     
   173     NSError *err=nil;
   174 	NSXMLDocument *xmlDoc = [[[NSXMLDocument alloc] initWithData:xmlData
   175                                                          options:NSXMLNodePreserveCDATA
   176                                                            error:&err] autorelease];    
   177 	
   178 	if (!xmlDoc)
   179 	{    
   180         goto ohno;
   181     }
   182     
   183     BOOL showTimestamps = [[options objectForKey:@"showTimestamps"] boolValue];
   184 	BOOL showEmoticons = [[options objectForKey:@"showEmoticons"] boolValue];
   185     
   186     NSXMLElement *chatElement = [[xmlDoc nodesForXPath:@"//chat" error:&err] lastObject];
   187     
   188     NSDictionary *chatAttributes = [chatElement AIAttributesAsDictionary];
   189     NSString *mySN = [[chatAttributes objectForKey:@"account"] stringValue];
   190     NSString *service = [[chatAttributes objectForKey:@"service"] stringValue];
   191     
   192     NSString *myDisplayName = nil;
   193     
   194     for (AIAccount *account in adium.accountController.accounts) {
   195         if ([[account.UID compactedString] isEqualToString:[mySN compactedString]] &&
   196             [account.service.serviceID isEqualToString:service]) {
   197             myDisplayName = [account.displayName retain];
   198             break;
   199         }
   200     }    
   201         
   202     NSArray *elements = [xmlDoc nodesForXPath:@"//message | //status" error:&err];
   203     if (!elements) {
   204         goto ohno;
   205     }
   206     
   207     for (NSXMLElement *element in elements) {
   208         NSString *type = [element name];
   209      
   210         NSDictionary *attributes = [element AIAttributesAsDictionary];
   211         
   212         if ([type isEqualToString:@"message"]) {
   213             NSString *senderAlias = [[attributes objectForKey:@"alias"] stringValue];
   214             NSString *dateStr = [[attributes objectForKey:@"time"] stringValue];
   215             NSDate *date = dateStr ? [NSCalendarDate calendarDateWithString:dateStr] : nil;
   216             NSString *sender = [[attributes objectForKey:@"sender"] stringValue];
   217             NSString *shownSender = (senderAlias ? senderAlias : sender);
   218             BOOL autoResponse = [[[attributes objectForKey:@"auto"] stringValue] isEqualToString:@"true"];
   219 
   220             NSMutableString *messageXML = [NSMutableString string];
   221             for (NSXMLNode *node in [element children]) {
   222                 [messageXML appendString:[node XMLString]];
   223             }
   224     
   225             NSString *displayName = nil, *longDisplayName = nil;
   226             
   227             BOOL sentMessage = [mySN isEqualToString:sender];
   228 
   229             
   230             if (sentMessage) {
   231                 //Find an account if one exists, and use its name
   232                 displayName = (myDisplayName ? myDisplayName : sender);
   233             } else {
   234                 AIListObject *listObject = [adium.contactController existingListObjectWithUniqueID:[AIListObject internalObjectIDForServiceID:service UID:sender]];
   235                 
   236                 displayName = listObject.displayName;
   237                 longDisplayName = [listObject longDisplayName];
   238             }
   239                 
   240             if (displayName && !sentMessage) {
   241                 switch (nameFormat) {
   242                     case AIDefaultName:
   243                         shownSender = (longDisplayName ? longDisplayName : displayName);
   244                         break;
   245                         
   246                     case AIDisplayName:
   247                         shownSender = displayName;
   248                         break;
   249                         
   250                     case AIDisplayName_ScreenName:
   251                         shownSender = [NSString stringWithFormat:@"%@ (%@)",displayName,sender];
   252                         break;
   253                         
   254                     case AIScreenName_DisplayName:
   255                         shownSender = [NSString stringWithFormat:@"%@ (%@)",sender,displayName];
   256                         break;
   257                         
   258                     case AIScreenName:
   259                         shownSender = sender;
   260                         break;	
   261                 }
   262             }
   263 				
   264             NSString *timestampStr = [dateFormatter stringFromDate:date];
   265 				
   266             [output appendAttributedString:[htmlDecoder decodeHTML:[NSString stringWithFormat:
   267                                                                     @"<div class=\"%@\">%@<span class=\"sender\">%@%@:</span></div> ",
   268                                                                     (sentMessage ? @"send" : @"receive"),
   269                                                                     (showTimestamps ? [NSString stringWithFormat:@"<span class=\"timestamp\">%@</span> ", timestampStr] : @""),
   270                                                                     shownSender, (autoResponse ? AILocalizedString(@" (Autoreply)", nil) : @"")]]];
   271 				
   272             NSAttributedString *attributedMessage = [htmlDecoder decodeHTML:messageXML];
   273             if (showEmoticons) {
   274                 attributedMessage = [adium.contentController filterAttributedString:attributedMessage
   275                                                                     usingFilterType:AIFilterMessageDisplay
   276                                                                           direction:(sentMessage ? AIFilterOutgoing : AIFilterIncoming)
   277                                                                             context:nil];				
   278             }
   279             [output appendAttributedString:attributedMessage];
   280             [output appendAttributedString:newlineAttributedString];
   281         } else if ([type isEqualToString:@"status"]) {
   282             NSString *dateStr = [[attributes objectForKey:@"time"] stringValue];
   283             NSDate *date = dateStr ? [NSCalendarDate calendarDateWithString:dateStr] : nil;
   284             NSString *status = [[attributes objectForKey:@"type"] stringValue];
   285             
   286             NSMutableString *messageXML = [NSMutableString string];
   287             for (NSXMLNode *node in [element children]) {
   288                 [messageXML appendString:[node XMLString]];
   289             }            
   290             
   291             NSString *displayMessage = nil;
   292             //Note: I am diverging from what the AILoggerPlugin logs in this case.  It can't handle every case we can have here
   293             if([messageXML length]) {
   294                 if([statusLookup objectForKey:status]) {
   295                     displayMessage = [NSString stringWithFormat:AILocalizedString(@"Changed status to %@: %@", nil), [statusLookup objectForKey:status], messageXML];
   296                 } else {
   297                     displayMessage = [NSString stringWithFormat:AILocalizedString(@"%@", nil), messageXML];
   298                 }
   299             } else if([status length] && [statusLookup objectForKey:status]) {
   300                 displayMessage = [NSString stringWithFormat:AILocalizedString(@"Changed status to %@", nil), [statusLookup objectForKey:status]];
   301             }
   302             
   303             if([displayMessage length]) {
   304                 [output appendAttributedString:[htmlDecoder decodeHTML:[NSString stringWithFormat:@"<div class=\"status\">%@ (%@)</div>\n",
   305                                                                         displayMessage,
   306                                                                         [dateFormatter stringFromDate:date]]]];
   307             }
   308         }
   309     }
   310     
   311     return output;
   312     
   313 ohno:
   314     if (!reentrancyFlag) {
   315         NSMutableString *xmlString = [NSMutableString stringWithUTF8String:[xmlData bytes]];
   316         [xmlString stripInvalidCharacters];
   317         return [self readData:[xmlString dataUsingEncoding:NSUTF8StringEncoding] withOptions:options retrying:YES];
   318     }
   319     @throw [NSException exceptionWithName:@"Log File Parsing Error" reason:[err description] userInfo:nil];
   320 }
   321 
   322 @end