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