Source/DCMessageContextDisplayPlugin.m
author Zachary West <zacw@adium.im>
Wed Nov 25 22:41:51 2009 -0500 (2009-11-25)
changeset 2822 2a385da3eace
parent 2821 fa988de96a94
child 2823 24c471ce6c9e
permissions -rw-r--r--
Don't use any instance variables to control parsing, use the parser's context info. Refs #13362.
     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 <Adium/AIContentControllerProtocol.h>
    18 #import "DCMessageContextDisplayPlugin.h"
    19 #import "DCMessageContextDisplayPreferences.h"
    20 #import <AIUtilities/AIDictionaryAdditions.h>
    21 #import <Adium/AIChat.h>
    22 #import <Adium/AIContentContext.h>
    23 #import <Adium/AIService.h>
    24 
    25 //Old school
    26 #import <Adium/AIListContact.h>
    27 #import <AIUtilities/AIAttributedStringAdditions.h>
    28 #import <Adium/AIAccountControllerProtocol.h>
    29 
    30 //omg crawsslinkz
    31 #import "AILoggerPlugin.h"
    32 
    33 //LMX
    34 #import <LMX/LMXParser.h>
    35 #import <Adium/AIXMLElement.h>
    36 #import <AIUtilities/AIStringAdditions.h>
    37 #import "unistd.h"
    38 #import <AIUtilities/NSCalendarDate+ISO8601Parsing.h>
    39 #import <Adium/AIContactControllerProtocol.h>
    40 #import <Adium/AIHTMLDecoder.h>
    41 
    42 #define RESTORED_CHAT_CONTEXT_LINE_NUMBER 50
    43 
    44 /**
    45  * @class DCMessageContextDisplayPlugin
    46  * @brief Component to display in-window message history
    47  *
    48  * The amount of history, and criteria of when to display history, are determined in the Advanced->Message History preferences.
    49  */
    50 @interface DCMessageContextDisplayPlugin ()
    51 - (void)preferencesChangedForGroup:(NSString *)group key:(NSString *)key
    52 							object:(AIListObject *)object preferenceDict:(NSDictionary *)prefDict firstTime:(BOOL)firstTime;
    53 - (NSArray *)contextForChat:(AIChat *)chat;
    54 @end
    55 
    56 @implementation DCMessageContextDisplayPlugin
    57 
    58 /**
    59  * @brief Install
    60  */
    61 - (void)installPlugin
    62 {
    63 	isObserving = NO;
    64 	
    65 	//Setup our preferences
    66     [adium.preferenceController registerDefaults:[NSDictionary dictionaryNamed:CONTEXT_DISPLAY_DEFAULTS
    67 																		forClass:[self class]] 
    68 										  forGroup:PREF_GROUP_CONTEXT_DISPLAY];
    69 	
    70 	//Observe preference changes for whether or not to display message history
    71 	[adium.preferenceController registerPreferenceObserver:self forGroup:PREF_GROUP_CONTEXT_DISPLAY];
    72 }
    73 
    74 /**
    75  * @brief Uninstall
    76  */
    77 - (void)uninstallPlugin
    78 {
    79 	[adium.preferenceController unregisterPreferenceObserver:self];
    80 	[[NSNotificationCenter defaultCenter] removeObserver:self];
    81 }
    82 
    83 - (void)preferencesChangedForGroup:(NSString *)group key:(NSString *)key
    84 								object:(AIListObject *)object preferenceDict:(NSDictionary *)prefDict firstTime:(BOOL)firstTime
    85 {
    86 	if (!object) {		
    87 		shouldDisplay = [[prefDict objectForKey:KEY_DISPLAY_CONTEXT] boolValue];
    88 		linesToDisplay = [[prefDict objectForKey:KEY_DISPLAY_LINES] integerValue];
    89 
    90 		if (shouldDisplay && linesToDisplay > 0 && !isObserving) {
    91 			//Observe new message windows only if we aren't already observing them
    92 			isObserving = YES;
    93 			[[NSNotificationCenter defaultCenter] addObserver:self
    94 										   selector:@selector(addContextDisplayToWindow:)
    95 											   name:Chat_DidOpen 
    96 											 object:nil];
    97 			
    98 		} else if (isObserving && (!shouldDisplay || linesToDisplay <= 0)) {
    99 			//Remove observer
   100 			isObserving = NO;
   101 			[[NSNotificationCenter defaultCenter] removeObserver:self name:Chat_DidOpen object:nil];
   102 			
   103 		}
   104 	}
   105 }
   106 
   107 /**
   108  * @brief Retrieve and display in-window message history
   109  *
   110  * Called in response to the Chat_DidOpen notification
   111  */
   112 - (void)addContextDisplayToWindow:(NSNotification *)notification
   113 {
   114 	AIChat	*chat = (AIChat *)[notification object];
   115 	
   116 	NSArray	*context = [self contextForChat:chat];
   117 
   118 	if (context && [context count] > 0 && shouldDisplay) {
   119 		AIContentContext	*contextMessage;
   120 
   121 		for(contextMessage in context) {
   122 			/* Don't display immediately, so the message view can aggregate multiple message history items.
   123 			 * As required, we post Content_ChatDidFinishAddingUntrackedContent when finished adding. */
   124 			[contextMessage setDisplayContentImmediately:NO];
   125 		
   126 			[adium.contentController displayContentObject:contextMessage
   127 										usingContentFilters:YES
   128 												immediately:YES];
   129 		}
   130 
   131 		//We finished adding untracked content
   132 		[[NSNotificationCenter defaultCenter] postNotificationName:Content_ChatDidFinishAddingUntrackedContent
   133 												  object:chat];
   134 	}
   135 }
   136 /*!
   137  * @brief Retrieve the message history for a particular chat
   138  *
   139  * Asks AILoggerPlugin for the path to the right file, and then uses LMX to parse that file backwards.
   140  */
   141 - (NSArray *)contextForChat:(AIChat *)chat
   142 {
   143 	//If there's no log there, there's no message history. Bail out.
   144 	NSArray *logPaths = [AILoggerPlugin sortedArrayOfLogFilesForChat:chat];
   145 	if(!logPaths) return nil;
   146 	
   147 	NSInteger linesLeftToFind = 0;
   148 
   149 	AIHTMLDecoder *decoder = [AIHTMLDecoder decoder];
   150 
   151 	NSString *logObjectUID = chat.name;
   152 	if (!logObjectUID) logObjectUID = chat.listObject.UID;
   153 	logObjectUID = [logObjectUID safeFilenameString];
   154 
   155 	NSString *baseLogPath = [[AILoggerPlugin logBasePath] stringByAppendingPathComponent:
   156 		[AILoggerPlugin relativePathForLogWithObject:logObjectUID onAccount:chat.account]];	
   157 
   158 	if ([chat boolValueForProperty:@"Restored Chat"] && linesToDisplay < RESTORED_CHAT_CONTEXT_LINE_NUMBER) {
   159 		linesLeftToFind = MAX(linesLeftToFind, RESTORED_CHAT_CONTEXT_LINE_NUMBER);
   160 	} else {
   161 		linesLeftToFind = linesToDisplay;		
   162 	}
   163 			
   164 	//Initialize a place to store found messages
   165 	NSMutableArray *outerFoundContentContexts = [NSMutableArray arrayWithCapacity:linesLeftToFind]; 
   166 
   167 	// These set of file's autorelease pool.
   168 	NSAutoreleasePool *parsingAutoreleasePool = [[NSAutoreleasePool alloc] init];
   169 	
   170 	//Iterate over the elements of the log path array.
   171 	NSEnumerator *pathsEnumerator = [logPaths objectEnumerator];
   172 	NSString *logPath = nil;
   173 	while (linesLeftToFind > 0 && (logPath = [pathsEnumerator nextObject])) {
   174 		//If it's not a .chatlog, ignore it.
   175 		if (![logPath hasSuffix:@".chatlog"])
   176 			continue;
   177 				
   178 		//Stick the base path on to the beginning
   179 		logPath = [baseLogPath stringByAppendingPathComponent:logPath];
   180 
   181 		//By default, the xmlFilePath is the chat log file/bundle... if we find that the chatlog is a bundle, we'll use the xml file inside.
   182 		NSString *xmlFilePath = logPath;
   183 
   184 		BOOL isDir;
   185 		if ([[NSFileManager defaultManager] fileExistsAtPath:logPath isDirectory:&isDir]) {
   186 			/* If we have a chatLog bundle, we want to get the text content for the xml file inside */
   187 			NSString *baseURL;
   188 			if (isDir) {
   189 				baseURL = logPath;
   190 				xmlFilePath = [logPath stringByAppendingPathComponent:
   191 							   [[[logPath lastPathComponent] stringByDeletingPathExtension] stringByAppendingPathExtension:@"xml"]];
   192 			} else {
   193 				baseURL = nil;
   194 			}
   195 			[decoder setBaseURL:baseURL];
   196 		}
   197 
   198 		//Initialize the found messages array and element stack for us-as-delegate
   199 		NSMutableArray *foundMessages = [NSMutableArray arrayWithCapacity:linesLeftToFind];
   200 		NSMutableArray *elementStack = [NSMutableArray array];
   201 
   202 		//Create the parser and set ourselves as the delegate
   203 		LMXParser *parser = [LMXParser parser];
   204 		[parser setDelegate:self];
   205 
   206 		//Set up info needed by elementStarted to create content objects.
   207 		NSMutableDictionary *contextInfo = nil;
   208 		{
   209 			//Get the service name from the path name
   210 			NSString *serviceName = [[[[[logPath stringByDeletingLastPathComponent] stringByDeletingLastPathComponent] lastPathComponent] componentsSeparatedByString:@"."] objectAtIndex:0U];
   211 
   212 			AIListObject *account = chat.account;
   213 			NSString	 *accountID = [NSString stringWithFormat:@"%@.%@", account.service.serviceID, account.UID];
   214 
   215 			contextInfo = [NSMutableDictionary dictionaryWithObjectsAndKeys:
   216 						   serviceName, @"Service name",
   217 						   account, @"Account",
   218 						   accountID, @"Account ID",
   219 						   chat, @"Chat",
   220 						   decoder, @"AIHTMLDecoder",
   221 						   [NSValue valueWithPointer:&linesLeftToFind], @"LinesLeftToFindValue",
   222 						   foundMessages, @"FoundMessages",
   223 						   elementStack, @"ElementStack",
   224 						   nil];
   225 			[parser setContextInfo:(void *)contextInfo];
   226 		}
   227 
   228 		//Open up the file we need to read from, and seek to the end (this is a *backwards* parser, after all :)
   229 		NSFileHandle *file = [NSFileHandle fileHandleForReadingAtPath:xmlFilePath];
   230 		[file seekToEndOfFile];
   231 		
   232 		//Set up some more doohickeys and then start the parse loop
   233 		NSInteger readSize = 4 * getpagesize(); //Read 4 pages at a time.
   234 		NSMutableData *chunk = [NSMutableData dataWithLength:readSize];
   235 		NSInteger fd = [file fileDescriptor];
   236 		char *buf = [chunk mutableBytes];
   237 		off_t offset = [file offsetInFile];
   238 		enum LMXParseResult result = LMXParsedIncomplete;
   239 
   240 		do {
   241 			//Calculate the new offset
   242 			offset = (offset <= readSize) ? 0 : offset - readSize;
   243 			
   244 			//Seek to it and read greedily until we hit readSize or run out of file.
   245 			NSInteger idx = 0;
   246 			for (ssize_t amountRead = 0; idx < readSize; idx += amountRead) { 
   247 				amountRead = pread(fd, buf + idx, readSize, offset + idx); 
   248 			   if (amountRead <= 0) break;
   249 			}
   250 			offset -= idx;
   251 			
   252 			//Parse
   253 			result = [parser parseChunk:chunk];
   254 			
   255 		//Continue to parse as long as we need more elements, we have data to read, and LMX doesn't think we're done.
   256 		} while ([foundMessages count] < linesLeftToFind && offset > 0 && result != LMXParsedCompletely);
   257 
   258 		//Drain our autorelease pool.
   259 		[parsingAutoreleasePool drain];
   260 
   261 		//Be a good citizen and close the file
   262 		[file closeFile];
   263 
   264 		//Add our locals to the outer array; we're probably looping again.
   265 		[outerFoundContentContexts replaceObjectsInRange:NSMakeRange(0, 0) withObjectsFromArray:foundMessages];
   266 		linesLeftToFind -= [outerFoundContentContexts count];
   267 	}
   268 	
   269 	[parsingAutoreleasePool release];
   270 	
   271 	if (linesLeftToFind > 0) {
   272 		AILogWithSignature(@"Unable to find %d logs for %@", linesLeftToFind, chat);
   273 	}
   274 	
   275 	return outerFoundContentContexts;
   276 }
   277 
   278 #pragma mark LMX delegate
   279 
   280 - (void)parser:(LMXParser *)parser elementEnded:(NSString *)elementName
   281 {
   282 	NSMutableDictionary *contextInfo = [parser contextInfo];
   283 	NSMutableArray *elementStack = [contextInfo objectForKey:@"ElementStack"];
   284 	
   285 	if ([elementName isEqualToString:@"message"]) {
   286 		[elementStack insertObject:[AIXMLElement elementWithName:elementName] atIndex:0U];
   287 	}
   288 	else if ([elementStack count]) {
   289 		AIXMLElement *element = [AIXMLElement elementWithName:elementName];
   290 		[(AIXMLElement *)[elementStack objectAtIndex:0U] insertObject:element atIndex:0U];
   291 		[elementStack insertObject:element atIndex:0U];
   292 	}
   293 }
   294 
   295 - (void)parser:(LMXParser *)parser foundCharacters:(NSString *)string
   296 {
   297 	NSMutableDictionary *contextInfo = [parser contextInfo];
   298 	NSMutableArray *elementStack = [contextInfo objectForKey:@"ElementStack"];
   299 	
   300 	if ([elementStack count])
   301 		[(AIXMLElement *)[elementStack objectAtIndex:0U] insertObject:string atIndex:0U];
   302 }
   303 
   304 - (void)parser:(LMXParser *)parser elementStarted:(NSString *)elementName attributes:(NSDictionary *)attributes
   305 {
   306 	NSMutableDictionary *contextInfo = [parser contextInfo];
   307 	NSMutableArray *elementStack = [contextInfo objectForKey:@"ElementStack"];
   308 	
   309 	if ([elementStack count]) {
   310 		AIXMLElement *element = [elementStack objectAtIndex:0U];
   311 		if (attributes) {
   312 			[element setAttributeNames:[attributes allKeys] values:[attributes allValues]];
   313 		}
   314 		
   315 		NSMutableArray	*foundMessages = [contextInfo objectForKey:@"FoundMessagesValue"];
   316 		NSInteger	 *linesLeftToFind = [[contextInfo objectForKey:@"LinesLeftToFindValue"] pointerValue];
   317 		
   318 		if ([elementName isEqualToString:@"message"]) {
   319 			//A message element has started!
   320 			//This means that we have all of this message now, and therefore can create a single content object from the AIXMLElement tree and then throw away that tree.
   321 			//This saves memory when a message element contains many elements (since each one is represented by an AIXMLElement sub-tree in the AIXMLElement tree, as opposed to a simple NSAttributeRun in the NSAttributedString of the content object).
   322 
   323 			NSString     *serviceName = [contextInfo objectForKey:@"Service name"];
   324 			AIListObject *account     = [contextInfo objectForKey:@"Account"];
   325 			NSString     *accountID   = [contextInfo objectForKey:@"Account ID"];
   326 			AIChat       *chat        = [contextInfo objectForKey:@"Chat"];
   327 			
   328 			//Set up some doohickers.
   329 			NSDictionary	*attributes = [element attributes];
   330 			NSString		*timeString = [attributes objectForKey:@"time"];
   331 			//Create the context object
   332 			if (timeString) {
   333 				NSCalendarDate *time = [NSCalendarDate calendarDateWithString:timeString];
   334 
   335 				NSString		*autoreplyAttribute = [attributes objectForKey:@"auto"];
   336 				NSString		*sender = [NSString stringWithFormat:@"%@.%@", serviceName, [attributes objectForKey:@"sender"]];
   337 				BOOL			sentByMe = ([sender isEqualToString:accountID]);
   338 				
   339 				/*don't fade the messages if they're within the last 5 minutes
   340 				 *since that will be resuming a conversation, not starting a new one.
   341 				 *Why the class trickery? Less code duplication, clearer what is actually different between the two cases.
   342 				 */
   343 				Class messageClass = (-[time timeIntervalSinceNow] > 300.0) ? [AIContentContext class] : [AIContentMessage class];
   344 				
   345 				AIListContact *listContact = nil;
   346 				
   347 				if (chat.isGroupChat) {
   348 					listContact = [chat.account contactWithUID:[attributes objectForKey:@"sender"]];
   349 				} else {
   350 					listContact = chat.listObject;
   351 				}
   352 				
   353 				AIContentMessage *message = [messageClass messageInChat:chat 
   354 															 withSource:(sentByMe ? account : listContact)
   355 															destination:(sentByMe ? (chat.isGroupChat ? nil : chat.listObject) : account)
   356 																   date:time
   357 																message:[[contextInfo objectForKey:@"AIHTMLDecoder"] decodeHTML:[element contentsAsXMLString]]
   358 															  autoreply:(autoreplyAttribute && [autoreplyAttribute caseInsensitiveCompare:@"true"] == NSOrderedSame)];
   359 				
   360 				//Don't log this object
   361 				[message setPostProcessContent:NO];
   362 				[message setTrackContent:NO];
   363 
   364 				//Add it to the array (in front, since we're working backwards, and we want the array in forward order)
   365 				[foundMessages insertObject:message atIndex:0];
   366 			} else {
   367 				NSLog(@"Null message context display time for %@",element);
   368 			}
   369 		}
   370 		
   371 		[elementStack removeObjectAtIndex:0U];
   372 		if ([foundMessages count] == *linesLeftToFind) {
   373 			if ([elementStack count]) [elementStack removeAllObjects];
   374 			[parser abortParsing];
   375 		}
   376 	}
   377 }
   378 
   379 @end