Fix a few more issues with context reading; if we're reading less than readSize, we need to decrement readSize too. 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 NSMutableArray *foundMessages = [NSMutableArray arrayWithCapacity:linesLeftToFind];
197 NSMutableArray *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",
219 foundMessages, @"FoundMessages",
220 elementStack, @"ElementStack",
222 [parser setContextInfo:(void *)contextInfo];
225 //Open up the file we need to read from, and seek to the end (this is a *backwards* parser, after all :)
226 NSFileHandle *file = [NSFileHandle fileHandleForReadingAtPath:xmlFilePath];
227 [file seekToEndOfFile];
229 //Set up some more doohickeys and then start the parse loop
230 NSInteger readSize = 4 * getpagesize(); //Read 4 pages at a time.
231 NSMutableData *chunk = [NSMutableData dataWithLength:readSize];
232 NSInteger fd = [file fileDescriptor];
233 off_t offset = [file offsetInFile];
234 enum LMXParseResult result = LMXParsedIncomplete;
236 // These set of file's autorelease pool.
237 NSAutoreleasePool *parsingAutoreleasePool = [[NSAutoreleasePool alloc] init];
240 // The location we're going to read for *this* set of reads.
241 off_t readOffset = offset - readSize;
243 if (readOffset < 0) {
244 // Decrease it by the amount we're over.
245 readSize += readOffset;
246 // Start from the beginning.
250 if (chunk.length != readSize) {
251 // In case we short-read last time, or we're reading a smaller amount this time.
252 [chunk setLength:readSize];
255 char *buf = [chunk mutableBytes];
257 //Seek to it and read greedily until we hit readSize or run out of file.
259 ssize_t amountRead = 0;
260 for (amountRead = 0; idx < readSize; idx += amountRead) {
261 amountRead = pread(fd, buf + idx, readSize, readOffset + idx);
262 if (amountRead <= 0) break;
265 if (idx != readSize) {
266 // If we short read, we don't want to read unknown buffer contents.
267 [chunk setLength:idx];
270 // Adjust the real offset
274 result = [parser parseChunk:chunk];
276 //Continue to parse as long as we need more elements, we have data to read, and LMX doesn't think we're done.
277 } while ([foundMessages count] < linesLeftToFind && offset > 0 && result != LMXParsedCompletely);
279 //Drain our autorelease pool.
280 [parsingAutoreleasePool release];
282 //Be a good citizen and close the file
285 //Add our locals to the outer array; we're probably looping again.
286 [outerFoundContentContexts replaceObjectsInRange:NSMakeRange(0, 0) withObjectsFromArray:foundMessages];
287 linesLeftToFind -= [outerFoundContentContexts count];
290 if (linesLeftToFind > 0) {
291 AILogWithSignature(@"Unable to find %d logs for %@", linesLeftToFind, chat);
294 return outerFoundContentContexts;
297 #pragma mark LMX delegate
299 - (void)parser:(LMXParser *)parser elementEnded:(NSString *)elementName
301 NSMutableDictionary *contextInfo = [parser contextInfo];
302 NSMutableArray *elementStack = [contextInfo objectForKey:@"ElementStack"];
304 if ([elementName isEqualToString:@"message"]) {
305 [elementStack insertObject:[AIXMLElement elementWithName:elementName] atIndex:0U];
307 else if ([elementStack count]) {
308 AIXMLElement *element = [AIXMLElement elementWithName:elementName];
309 [(AIXMLElement *)[elementStack objectAtIndex:0U] insertObject:element atIndex:0U];
310 [elementStack insertObject:element atIndex:0U];
314 - (void)parser:(LMXParser *)parser foundCharacters:(NSString *)string
316 NSMutableDictionary *contextInfo = [parser contextInfo];
317 NSMutableArray *elementStack = [contextInfo objectForKey:@"ElementStack"];
319 if ([elementStack count])
320 [(AIXMLElement *)[elementStack objectAtIndex:0U] insertObject:string atIndex:0U];
323 - (void)parser:(LMXParser *)parser elementStarted:(NSString *)elementName attributes:(NSDictionary *)attributes
325 NSMutableDictionary *contextInfo = [parser contextInfo];
326 NSMutableArray *elementStack = [contextInfo objectForKey:@"ElementStack"];
328 if ([elementStack count]) {
329 AIXMLElement *element = [elementStack objectAtIndex:0U];
331 [element setAttributeNames:[attributes allKeys] values:[attributes allValues]];
334 NSMutableArray *foundMessages = [contextInfo objectForKey:@"FoundMessages"];
335 NSInteger *linesLeftToFind = [[contextInfo objectForKey:@"LinesLeftToFindValue"] pointerValue];
337 if ([elementName isEqualToString:@"message"]) {
338 //A message element has started!
339 //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.
340 //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).
342 NSString *serviceName = [contextInfo objectForKey:@"Service name"];
343 AIListObject *account = [contextInfo objectForKey:@"Account"];
344 NSString *accountID = [contextInfo objectForKey:@"Account ID"];
345 AIChat *chat = [contextInfo objectForKey:@"Chat"];
347 //Set up some doohickers.
348 NSDictionary *attributes = [element attributes];
349 NSString *timeString = [attributes objectForKey:@"time"];
350 //Create the context object
352 NSCalendarDate *time = [NSCalendarDate calendarDateWithString:timeString];
354 NSString *autoreplyAttribute = [attributes objectForKey:@"auto"];
355 NSString *sender = [NSString stringWithFormat:@"%@.%@", serviceName, [attributes objectForKey:@"sender"]];
356 BOOL sentByMe = ([sender isEqualToString:accountID]);
358 /*don't fade the messages if they're within the last 5 minutes
359 *since that will be resuming a conversation, not starting a new one.
360 *Why the class trickery? Less code duplication, clearer what is actually different between the two cases.
362 Class messageClass = (-[time timeIntervalSinceNow] > 300.0) ? [AIContentContext class] : [AIContentMessage class];
364 AIListContact *listContact = nil;
366 if (chat.isGroupChat) {
367 listContact = [chat.account contactWithUID:[attributes objectForKey:@"sender"]];
369 listContact = chat.listObject;
372 AIContentMessage *message = [messageClass messageInChat:chat
373 withSource:(sentByMe ? account : listContact)
374 destination:(sentByMe ? (chat.isGroupChat ? nil : chat.listObject) : account)
376 message:[[contextInfo objectForKey:@"AIHTMLDecoder"] decodeHTML:[element contentsAsXMLString]]
377 autoreply:(autoreplyAttribute && [autoreplyAttribute caseInsensitiveCompare:@"true"] == NSOrderedSame)];
379 //Don't log this object
380 [message setPostProcessContent:NO];
381 [message setTrackContent:NO];
383 //Add it to the array (in front, since we're working backwards, and we want the array in forward order)
384 [foundMessages insertObject:message atIndex:0];
386 NSLog(@"Null message context display time for %@",element);
390 [elementStack removeObjectAtIndex:0U];
391 if ([foundMessages count] == *linesLeftToFind) {
392 if ([elementStack count]) [elementStack removeAllObjects];
393 [parser abortParsing];