Don't use any instance variables to control parsing, use the parser's context info. Refs #13362.
2 * Adium is the legal property of its developers, whose names are listed in the copyright file included
3 * with this source distribution.
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.
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.
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.
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>
26 #import <Adium/AIListContact.h>
27 #import <AIUtilities/AIAttributedStringAdditions.h>
28 #import <Adium/AIAccountControllerProtocol.h>
31 #import "AILoggerPlugin.h"
34 #import <LMX/LMXParser.h>
35 #import <Adium/AIXMLElement.h>
36 #import <AIUtilities/AIStringAdditions.h>
38 #import <AIUtilities/NSCalendarDate+ISO8601Parsing.h>
39 #import <Adium/AIContactControllerProtocol.h>
40 #import <Adium/AIHTMLDecoder.h>
42 #define RESTORED_CHAT_CONTEXT_LINE_NUMBER 50
45 * @class DCMessageContextDisplayPlugin
46 * @brief Component to display in-window message history
48 * The amount of history, and criteria of when to display history, are determined in the Advanced->Message History preferences.
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;
56 @implementation DCMessageContextDisplayPlugin
65 //Setup our preferences
66 [adium.preferenceController registerDefaults:[NSDictionary dictionaryNamed:CONTEXT_DISPLAY_DEFAULTS
67 forClass:[self class]]
68 forGroup:PREF_GROUP_CONTEXT_DISPLAY];
70 //Observe preference changes for whether or not to display message history
71 [adium.preferenceController registerPreferenceObserver:self forGroup:PREF_GROUP_CONTEXT_DISPLAY];
77 - (void)uninstallPlugin
79 [adium.preferenceController unregisterPreferenceObserver:self];
80 [[NSNotificationCenter defaultCenter] removeObserver:self];
83 - (void)preferencesChangedForGroup:(NSString *)group key:(NSString *)key
84 object:(AIListObject *)object preferenceDict:(NSDictionary *)prefDict firstTime:(BOOL)firstTime
87 shouldDisplay = [[prefDict objectForKey:KEY_DISPLAY_CONTEXT] boolValue];
88 linesToDisplay = [[prefDict objectForKey:KEY_DISPLAY_LINES] integerValue];
90 if (shouldDisplay && linesToDisplay > 0 && !isObserving) {
91 //Observe new message windows only if we aren't already observing them
93 [[NSNotificationCenter defaultCenter] addObserver:self
94 selector:@selector(addContextDisplayToWindow:)
98 } else if (isObserving && (!shouldDisplay || linesToDisplay <= 0)) {
101 [[NSNotificationCenter defaultCenter] removeObserver:self name:Chat_DidOpen object:nil];
108 * @brief Retrieve and display in-window message history
110 * Called in response to the Chat_DidOpen notification
112 - (void)addContextDisplayToWindow:(NSNotification *)notification
114 AIChat *chat = (AIChat *)[notification object];
116 NSArray *context = [self contextForChat:chat];
118 if (context && [context count] > 0 && shouldDisplay) {
119 AIContentContext *contextMessage;
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];
126 [adium.contentController displayContentObject:contextMessage
127 usingContentFilters:YES
131 //We finished adding untracked content
132 [[NSNotificationCenter defaultCenter] postNotificationName:Content_ChatDidFinishAddingUntrackedContent
137 * @brief Retrieve the message history for a particular chat
139 * Asks AILoggerPlugin for the path to the right file, and then uses LMX to parse that file backwards.
141 - (NSArray *)contextForChat:(AIChat *)chat
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;
147 NSInteger linesLeftToFind = 0;
149 AIHTMLDecoder *decoder = [AIHTMLDecoder decoder];
151 NSString *logObjectUID = chat.name;
152 if (!logObjectUID) logObjectUID = chat.listObject.UID;
153 logObjectUID = [logObjectUID safeFilenameString];
155 NSString *baseLogPath = [[AILoggerPlugin logBasePath] stringByAppendingPathComponent:
156 [AILoggerPlugin relativePathForLogWithObject:logObjectUID onAccount:chat.account]];
158 if ([chat boolValueForProperty:@"Restored Chat"] && linesToDisplay < RESTORED_CHAT_CONTEXT_LINE_NUMBER) {
159 linesLeftToFind = MAX(linesLeftToFind, RESTORED_CHAT_CONTEXT_LINE_NUMBER);
161 linesLeftToFind = linesToDisplay;
164 //Initialize a place to store found messages
165 NSMutableArray *outerFoundContentContexts = [NSMutableArray arrayWithCapacity:linesLeftToFind];
167 // These set of file's autorelease pool.
168 NSAutoreleasePool *parsingAutoreleasePool = [[NSAutoreleasePool alloc] init];
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"])
178 //Stick the base path on to the beginning
179 logPath = [baseLogPath stringByAppendingPathComponent:logPath];
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;
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 */
190 xmlFilePath = [logPath stringByAppendingPathComponent:
191 [[[logPath lastPathComponent] stringByDeletingPathExtension] stringByAppendingPathExtension:@"xml"]];
195 [decoder setBaseURL:baseURL];
198 //Initialize the found messages array and element stack for us-as-delegate
199 NSMutableArray *foundMessages = [NSMutableArray arrayWithCapacity:linesLeftToFind];
200 NSMutableArray *elementStack = [NSMutableArray array];
202 //Create the parser and set ourselves as the delegate
203 LMXParser *parser = [LMXParser parser];
204 [parser setDelegate:self];
206 //Set up info needed by elementStarted to create content objects.
207 NSMutableDictionary *contextInfo = nil;
209 //Get the service name from the path name
210 NSString *serviceName = [[[[[logPath stringByDeletingLastPathComponent] stringByDeletingLastPathComponent] lastPathComponent] componentsSeparatedByString:@"."] objectAtIndex:0U];
212 AIListObject *account = chat.account;
213 NSString *accountID = [NSString stringWithFormat:@"%@.%@", account.service.serviceID, account.UID];
215 contextInfo = [NSMutableDictionary dictionaryWithObjectsAndKeys:
216 serviceName, @"Service name",
218 accountID, @"Account ID",
220 decoder, @"AIHTMLDecoder",
221 [NSValue valueWithPointer:&linesLeftToFind], @"LinesLeftToFindValue",
222 foundMessages, @"FoundMessages",
223 elementStack, @"ElementStack",
225 [parser setContextInfo:(void *)contextInfo];
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];
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;
241 //Calculate the new offset
242 offset = (offset <= readSize) ? 0 : offset - readSize;
244 //Seek to it and read greedily until we hit readSize or run out of file.
246 for (ssize_t amountRead = 0; idx < readSize; idx += amountRead) {
247 amountRead = pread(fd, buf + idx, readSize, offset + idx);
248 if (amountRead <= 0) break;
253 result = [parser parseChunk:chunk];
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);
258 //Drain our autorelease pool.
259 [parsingAutoreleasePool drain];
261 //Be a good citizen and close the file
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];
269 [parsingAutoreleasePool release];
271 if (linesLeftToFind > 0) {
272 AILogWithSignature(@"Unable to find %d logs for %@", linesLeftToFind, chat);
275 return outerFoundContentContexts;
278 #pragma mark LMX delegate
280 - (void)parser:(LMXParser *)parser elementEnded:(NSString *)elementName
282 NSMutableDictionary *contextInfo = [parser contextInfo];
283 NSMutableArray *elementStack = [contextInfo objectForKey:@"ElementStack"];
285 if ([elementName isEqualToString:@"message"]) {
286 [elementStack insertObject:[AIXMLElement elementWithName:elementName] atIndex:0U];
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];
295 - (void)parser:(LMXParser *)parser foundCharacters:(NSString *)string
297 NSMutableDictionary *contextInfo = [parser contextInfo];
298 NSMutableArray *elementStack = [contextInfo objectForKey:@"ElementStack"];
300 if ([elementStack count])
301 [(AIXMLElement *)[elementStack objectAtIndex:0U] insertObject:string atIndex:0U];
304 - (void)parser:(LMXParser *)parser elementStarted:(NSString *)elementName attributes:(NSDictionary *)attributes
306 NSMutableDictionary *contextInfo = [parser contextInfo];
307 NSMutableArray *elementStack = [contextInfo objectForKey:@"ElementStack"];
309 if ([elementStack count]) {
310 AIXMLElement *element = [elementStack objectAtIndex:0U];
312 [element setAttributeNames:[attributes allKeys] values:[attributes allValues]];
315 NSMutableArray *foundMessages = [contextInfo objectForKey:@"FoundMessagesValue"];
316 NSInteger *linesLeftToFind = [[contextInfo objectForKey:@"LinesLeftToFindValue"] pointerValue];
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).
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"];
328 //Set up some doohickers.
329 NSDictionary *attributes = [element attributes];
330 NSString *timeString = [attributes objectForKey:@"time"];
331 //Create the context object
333 NSCalendarDate *time = [NSCalendarDate calendarDateWithString:timeString];
335 NSString *autoreplyAttribute = [attributes objectForKey:@"auto"];
336 NSString *sender = [NSString stringWithFormat:@"%@.%@", serviceName, [attributes objectForKey:@"sender"]];
337 BOOL sentByMe = ([sender isEqualToString:accountID]);
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.
343 Class messageClass = (-[time timeIntervalSinceNow] > 300.0) ? [AIContentContext class] : [AIContentMessage class];
345 AIListContact *listContact = nil;
347 if (chat.isGroupChat) {
348 listContact = [chat.account contactWithUID:[attributes objectForKey:@"sender"]];
350 listContact = chat.listObject;
353 AIContentMessage *message = [messageClass messageInChat:chat
354 withSource:(sentByMe ? account : listContact)
355 destination:(sentByMe ? (chat.isGroupChat ? nil : chat.listObject) : account)
357 message:[[contextInfo objectForKey:@"AIHTMLDecoder"] decodeHTML:[element contentsAsXMLString]]
358 autoreply:(autoreplyAttribute && [autoreplyAttribute caseInsensitiveCompare:@"true"] == NSOrderedSame)];
360 //Don't log this object
361 [message setPostProcessContent:NO];
362 [message setTrackContent:NO];
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];
367 NSLog(@"Null message context display time for %@",element);
371 [elementStack removeObjectAtIndex:0U];
372 if ([foundMessages count] == *linesLeftToFind) {
373 if ([elementStack count]) [elementStack removeAllObjects];
374 [parser abortParsing];