Don't deal with autorelease pools on the file level; deal with it in terms of chats. 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 //Iterate over the elements of the log path array.
168 NSEnumerator *pathsEnumerator = [logPaths objectEnumerator];
169 NSString *logPath = nil;
170 while (linesLeftToFind > 0 && (logPath = [pathsEnumerator nextObject])) {
171 //If it's not a .chatlog, ignore it.
172 if (![logPath hasSuffix:@".chatlog"])
175 //Stick the base path on to the beginning
176 logPath = [baseLogPath stringByAppendingPathComponent:logPath];
178 //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.
179 NSString *xmlFilePath = logPath;
182 if ([[NSFileManager defaultManager] fileExistsAtPath:logPath isDirectory:&isDir]) {
183 /* If we have a chatLog bundle, we want to get the text content for the xml file inside */
187 xmlFilePath = [logPath stringByAppendingPathComponent:
188 [[[logPath lastPathComponent] stringByDeletingPathExtension] stringByAppendingPathExtension:@"xml"]];
192 [decoder setBaseURL:baseURL];
195 //Initialize the found messages array and element stack for us-as-delegate
196 foundMessages = [NSMutableArray arrayWithCapacity:linesLeftToFind];
197 elementStack = [NSMutableArray array];
199 //Create the parser and set ourselves as the delegate
200 LMXParser *parser = [LMXParser parser];
201 [parser setDelegate:self];
203 //Set up info needed by elementStarted to create content objects.
204 NSMutableDictionary *contextInfo = nil;
206 //Get the service name from the path name
207 NSString *serviceName = [[[[[logPath stringByDeletingLastPathComponent] stringByDeletingLastPathComponent] lastPathComponent] componentsSeparatedByString:@"."] objectAtIndex:0U];
209 AIListObject *account = chat.account;
210 NSString *accountID = [NSString stringWithFormat:@"%@.%@", account.service.serviceID, account.UID];
212 contextInfo = [NSMutableDictionary dictionaryWithObjectsAndKeys:
213 serviceName, @"Service name",
215 accountID, @"Account ID",
217 decoder, @"AIHTMLDecoder",
218 [NSValue valueWithPointer:&linesLeftToFind], @"LinesLeftToFindValue",
220 [parser setContextInfo:(void *)contextInfo];
223 //Open up the file we need to read from, and seek to the end (this is a *backwards* parser, after all :)
224 NSFileHandle *file = [NSFileHandle fileHandleForReadingAtPath:xmlFilePath];
225 [file seekToEndOfFile];
227 //Set up some more doohickeys and then start the parse loop
228 NSInteger readSize = 4 * getpagesize(); //Read 4 pages at a time.
229 NSMutableData *chunk = [NSMutableData dataWithLength:readSize];
230 NSInteger fd = [file fileDescriptor];
231 char *buf = [chunk mutableBytes];
232 off_t offset = [file offsetInFile];
233 enum LMXParseResult result = LMXParsedIncomplete;
235 parsingAutoreleasePool = [[NSAutoreleasePool alloc] init];
238 //Calculate the new offset
239 offset = (offset <= readSize) ? 0 : offset - readSize;
241 //Seek to it and read greedily until we hit readSize or run out of file.
243 for (ssize_t amountRead = 0; idx < readSize; idx += amountRead) {
244 amountRead = pread(fd, buf + idx, readSize, offset + idx);
245 if (amountRead <= 0) break;
250 result = [parser parseChunk:chunk];
252 //Continue to parse as long as we need more elements, we have data to read, and LMX doesn't think we're done.
253 } while ([foundMessages count] < linesLeftToFind && offset > 0 && result != LMXParsedCompletely);
255 //Pop our autorelease pool.
256 [parsingAutoreleasePool release]; parsingAutoreleasePool = nil;
258 //Be a good citizen and close the file
261 //Add our locals to the outer array; we're probably looping again.
262 [outerFoundContentContexts replaceObjectsInRange:NSMakeRange(0, 0) withObjectsFromArray:foundMessages];
263 linesLeftToFind -= [outerFoundContentContexts count];
266 if (linesLeftToFind > 0) {
267 AILogWithSignature(@"Unable to find %d logs for %@", linesLeftToFind, chat);
270 return outerFoundContentContexts;
273 #pragma mark LMX delegate
275 - (void)parser:(LMXParser *)parser elementEnded:(NSString *)elementName
277 if ([elementName isEqualToString:@"message"]) {
278 [elementStack insertObject:[AIXMLElement elementWithName:elementName] atIndex:0U];
280 else if ([elementStack count]) {
281 AIXMLElement *element = [AIXMLElement elementWithName:elementName];
282 [(AIXMLElement *)[elementStack objectAtIndex:0U] insertObject:element atIndex:0U];
283 [elementStack insertObject:element atIndex:0U];
287 - (void)parser:(LMXParser *)parser foundCharacters:(NSString *)string
289 if ([elementStack count])
290 [(AIXMLElement *)[elementStack objectAtIndex:0U] insertObject:string atIndex:0U];
293 - (void)parser:(LMXParser *)parser elementStarted:(NSString *)elementName attributes:(NSDictionary *)attributes
295 if ([elementStack count]) {
296 AIXMLElement *element = [elementStack objectAtIndex:0U];
298 [element setAttributeNames:[attributes allKeys] values:[attributes allValues]];
301 NSMutableDictionary *contextInfo = [parser contextInfo];
303 if ([elementName isEqualToString:@"message"]) {
304 //A message element has started!
305 //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.
306 //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).
308 NSString *serviceName = [contextInfo objectForKey:@"Service name"];
309 AIListObject *account = [contextInfo objectForKey:@"Account"];
310 NSString *accountID = [contextInfo objectForKey:@"Account ID"];
311 AIChat *chat = [contextInfo objectForKey:@"Chat"];
313 //Set up some doohickers.
314 NSDictionary *attributes = [element attributes];
315 NSString *timeString = [attributes objectForKey:@"time"];
316 //Create the context object
318 NSCalendarDate *time = [NSCalendarDate calendarDateWithString:timeString];
320 NSString *autoreplyAttribute = [attributes objectForKey:@"auto"];
321 NSString *sender = [NSString stringWithFormat:@"%@.%@", serviceName, [attributes objectForKey:@"sender"]];
322 BOOL sentByMe = ([sender isEqualToString:accountID]);
324 /*don't fade the messages if they're within the last 5 minutes
325 *since that will be resuming a conversation, not starting a new one.
326 *Why the class trickery? Less code duplication, clearer what is actually different between the two cases.
328 Class messageClass = (-[time timeIntervalSinceNow] > 300.0) ? [AIContentContext class] : [AIContentMessage class];
330 AIListContact *listContact = nil;
332 if (chat.isGroupChat) {
333 listContact = [chat.account contactWithUID:[attributes objectForKey:@"sender"]];
335 listContact = chat.listObject;
338 AIContentMessage *message = [messageClass messageInChat:chat
339 withSource:(sentByMe ? account : listContact)
340 destination:(sentByMe ? (chat.isGroupChat ? nil : chat.listObject) : account)
342 message:[[contextInfo objectForKey:@"AIHTMLDecoder"] decodeHTML:[element contentsAsXMLString]]
343 autoreply:(autoreplyAttribute && [autoreplyAttribute caseInsensitiveCompare:@"true"] == NSOrderedSame)];
345 //Don't log this object
346 [message setPostProcessContent:NO];
347 [message setTrackContent:NO];
349 //Add it to the array (in front, since we're working backwards, and we want the array in forward order)
350 [foundMessages insertObject:message atIndex:0];
352 NSLog(@"Null message context display time for %@",element);
356 NSInteger *linesLeftToFind = [[contextInfo objectForKey:@"LinesLeftToFindValue"] pointerValue];
358 [elementStack removeObjectAtIndex:0U];
359 if ([foundMessages count] == *linesLeftToFind) {
360 if ([elementStack count]) [elementStack removeAllObjects];
361 [parser abortParsing];