2016-02-24 16:56:39 +01:00

708 lines
17 KiB
Objective-C

#import "XMPPRoomMemoryStorage.h"
#import "XMPPRoomPrivate.h"
#import "XMPP.h"
#import "NSXMLElement+XEP_0203.h"
#import "XMPPLogging.h"
#if ! __has_feature(objc_arc)
#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC).
#endif
// Log levels: off, error, warn, info, verbose
#if DEBUG
static const int xmppLogLevel = XMPP_LOG_LEVEL_WARN; // | XMPP_LOG_FLAG_TRACE;
#else
static const int xmppLogLevel = XMPP_LOG_LEVEL_WARN;
#endif
#define AssertPrivateQueue() \
NSAssert(dispatch_get_specific(parentQueueTag), @"Private method: MUST run on parentQueue");
#define AssertParentQueue() \
NSAssert(dispatch_get_specific(parentQueueTag), @"Private protocol method: MUST run on parentQueue");
@interface XMPPRoomMemoryStorage ()
{
#if __has_feature(objc_arc_weak)
__weak XMPPRoom *parent;
#else
__unsafe_unretained XMPPRoom *parent;
#endif
dispatch_queue_t parentQueue;
void *parentQueueTag;
NSMutableArray * messages;
NSMutableArray * occupantsArray;
NSMutableDictionary * occupantsDict;
Class messageClass;
Class occupantClass;
}
@property (readonly) dispatch_queue_t parentQueue;
@end
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark -
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
@implementation XMPPRoomMemoryStorage
- (id)init
{
if ((self = [super init]))
{
messages = [[NSMutableArray alloc] init];
occupantsArray = [[NSMutableArray alloc] init];
occupantsDict = [[NSMutableDictionary alloc] init];
messageClass = [XMPPRoomMessageMemoryStorageObject class];
occupantClass = [XMPPRoomOccupantMemoryStorageObject class];
}
return self;
}
- (BOOL)configureWithParent:(XMPPRoom *)aParent queue:(dispatch_queue_t)queue
{
NSParameterAssert(aParent != nil);
NSParameterAssert(queue != NULL);
BOOL result = NO;
@synchronized(self)
{
if ((parent == nil) && (parentQueue == NULL))
{
parent = aParent;
parentQueue = queue;
parentQueueTag = &parentQueueTag;
dispatch_queue_set_specific(parentQueue, parentQueueTag, parentQueueTag, NULL);
#if !OS_OBJECT_USE_OBJC
dispatch_retain(parentQueue);
#endif
result = YES;
}
}
return result;
}
- (GCDMulticastDelegate <XMPPRoomMemoryStorageDelegate> *)multicastDelegate
{
return (GCDMulticastDelegate <XMPPRoomMemoryStorageDelegate> *)[parent multicastDelegate];
}
- (void)dealloc
{
#if !OS_OBJECT_USE_OBJC
if (parentQueue)
dispatch_release(parentQueue);
#endif
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark Properties
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
@synthesize messageClass;
@synthesize occupantClass;
- (XMPPRoom *)parent
{
XMPPRoom *result = nil;
@synchronized(self) // synchronized with configureWithParent:queue:
{
result = parent;
}
return result;
}
- (dispatch_queue_t)parentQueue
{
dispatch_queue_t result = NULL;
@synchronized(self) // synchronized with configureWithParent:queue:
{
result = parentQueue;
}
return result;
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark Internal API
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
- (BOOL)existsMessage:(XMPPMessage *)message
{
NSDate *remoteTimestamp = [message delayedDeliveryDate];
if (remoteTimestamp == nil)
{
// When the xmpp server sends us a room message, it will always timestamp delayed messages.
// For example, when retrieving the discussion history, all messages will include the original timestamp.
// If a message doesn't include such timestamp, then we know we're getting it in "real time".
return NO;
}
if ([messages count] == 0)
{
// Safety net for binary search algorithm used below
return NO;
}
// Does this message already exist in the messages array?
// How can we tell if two XMPPRoomMessages are the same?
//
// 1. Same jid
// 2. Same text
// 3. Same remoteTimestamp
// 4. Approximately the same localTimestamps (if existing message doesn't have set remoteTimestamp)
//
// This is actually a rather difficult question.
// What if the same user sends the exact same message multiple times?
//
// If we first received the message while already in the room, it won't contain a remoteTimestamp.
// Returning to the room later and downloading the discussion history will return the same message,
// this time with a remote timestamp.
//
// So if the message doesn't have a remoteTimestamp,
// but it's localTimestamp is approximately the same as the remoteTimestamp,
// then this is enough evidence to consider the messages the same.
// Algorithm overview:
//
// Since the clock of the client and server may be out of sync,
// a localTimestamp and remoteTimestamp may be off by several seconds.
// So we're going to search a range of messages, bounded by a min and max localTimestamp.
//
// We find the first message that has a localTimestamp >= minLocalTimestamp.
// We then search from there to the first message that has a localTimestamp > maxLocalTimestamp.
//
// This represents our range of messages to search.
// Then we can simply iterate over these messages to see if any have the same jid and text.
NSDate *minLocalTimestamp = [remoteTimestamp dateByAddingTimeInterval:-60];
NSDate *maxLocalTimestamp = [remoteTimestamp dateByAddingTimeInterval: 60];
// Use binary search to locate first message with localTimestamp >= minLocalTimestamp.
NSInteger mid;
NSInteger min = 0;
NSInteger max = [messages count] - 1;
while (YES)
{
mid = (min + max) / 2;
XMPPRoomMessageMemoryStorageObject *currentMessage = messages[mid];
NSComparisonResult cmp = [minLocalTimestamp compare:[currentMessage localTimestamp]];
if (cmp == NSOrderedAscending)
{
// minLocalTimestamp < currentMessage.localTimestamp
if (mid == min)
break;
else
max = mid - 1;
}
else // Descending || Same
{
// minLocalTimestamp >= currentMessage.localTimestamp
if (mid == max) {
mid++;
break;
}
else {
min = mid + 1;
}
}
}
// The 'mid' variable now points to the index of the first message in the sorted messages array
// that has a localTimestamp >= minLocalTimestamp.
//
// We're going to start looking for matching messages here,
// and break if we find a message with localTimestamp <= maxLocalTimestamp.
XMPPJID *messageJid = [message from];
NSString *messageBody = [[message elementForName:@"body"] stringValue];
NSInteger index;
for (index = mid; index < [messages count]; index++)
{
XMPPRoomMessageMemoryStorageObject *currentMessage = messages[index];
NSComparisonResult cmp = [maxLocalTimestamp compare:[currentMessage localTimestamp]];
if (cmp != NSOrderedAscending)
{
// maxLocalTimestamp >= currentMessage.localTimestamp
break;
}
if ([currentMessage.jid isEqualToJID:messageJid])
{
if ([currentMessage.body isEqualToString:messageBody])
{
if (currentMessage.remoteTimestamp)
{
if ([currentMessage.remoteTimestamp isEqualToDate:remoteTimestamp])
{
// 1. jid matches
// 2. body matches
// 3. remoteTimestamp matches
//
// => Incoming message already exists in the array.
return YES;
}
}
else
{
// 1. jid matches
// 2. body matches
// 3. existing message in array doesn't have set remoteTimestamp
// 4. existing message has approximately the same localTimestamp
//
// => Incoming message already exists in the array.
return YES;
}
}
}
}
return NO;
}
- (NSUInteger)insertMessage:(XMPPRoomMessageMemoryStorageObject *)message
{
NSUInteger count = [messages count];
if (count == 0)
{
[messages addObject:message];
return 0;
}
// Shortcut - Most (if not all) messages are inserted at the end
XMPPRoomMessageMemoryStorageObject *lastMessage = messages[count - 1];
if ([message compare:lastMessage] != NSOrderedAscending)
{
[messages addObject:message];
return count;
}
// Shortcut didn't work.
// Find location using binary search algorithm.
NSInteger mid;
NSInteger min = 0;
NSInteger max = count - 1;
while (YES)
{
mid = (min + max) / 2;
XMPPRoomMessageMemoryStorageObject *currentMessage = messages[mid];
NSComparisonResult cmp = [message compare:currentMessage];
if (cmp == NSOrderedAscending)
{
// message < currentMessage
if (mid == min)
break;
else
max = mid - 1;
}
else // Descending || Same
{
// message >= currentMessage
if (mid == max) {
mid++;
break;
}
else {
min = mid + 1;
}
}
}
// Algorithm check:
//
// Insert in array[length] at index (index)
// : min, mid, max (cmp_result)
//
//
// Insert in array[3] at index (0)
// : 0, 1, 2 (asc)
// : 0, 0, 0 (asc) break
//
// Insert in array[3] at index (1)
// : 0, 1, 2 (asc)
// : 0, 0, 0 (dsc) mid++, break
//
// Insert in array[3] at index (2)
// : 0, 1, 2 (dsc)
// : 2, 2, 2 (asc) break
//
// Insert in array[3] at index (3)
// : 0, 1, 2 (dsc)
// : 2, 2, 2 (dsc) mid++, break
[messages insertObject:message atIndex:mid];
return (NSUInteger)mid;
}
- (void)addMessage:(XMPPRoomMessageMemoryStorageObject *)roomMsg
{
NSUInteger index = [self insertMessage:roomMsg];
XMPPRoomOccupantMemoryStorageObject *occupant = occupantsDict[[roomMsg jid]];
XMPPRoomMessageMemoryStorageObject *roomMsgCopy = [roomMsg copy];
XMPPRoomOccupantMemoryStorageObject *occupantCopy = [occupant copy];
NSArray *messagesCopy = [[NSArray alloc] initWithArray:messages copyItems:YES];
[[self multicastDelegate] xmppRoomMemoryStorage:self
didReceiveMessage:roomMsgCopy
fromOccupant:occupantCopy
atIndex:index
inArray:messagesCopy];
}
- (NSUInteger)insertOccupant:(XMPPRoomOccupantMemoryStorageObject *)occupant
{
NSUInteger count = [occupantsArray count];
if (count == 0)
{
[occupantsArray addObject:occupant];
return 0;
}
// Find location using binary search algorithm.
NSInteger mid;
NSInteger min = 0;
NSInteger max = count - 1;
while (YES)
{
mid = (min + max) / 2;
XMPPRoomOccupantMemoryStorageObject *currentOccupant = occupantsArray[mid];
NSComparisonResult cmp = [occupant compare:currentOccupant];
if (cmp == NSOrderedAscending)
{
// occupant < currentOccupant
if (mid == min)
break;
else
max = mid - 1;
}
else // Descending || Same
{
// occupant >= currentOccupant
if (mid == max) {
mid++;
break;
}
else {
min = mid + 1;
}
}
}
[occupantsArray insertObject:occupant atIndex:mid];
return (NSUInteger)mid;
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark Public API
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
- (XMPPRoomOccupantMemoryStorageObject *)occupantForJID:(XMPPJID *)jid
{
XMPPLogTrace();
if (self.parentQueue == NULL)
{
// Haven't been attached to parent yet
return nil;
}
if (dispatch_get_specific(parentQueueTag))
{
return occupantsDict[jid];
}
else
{
__block XMPPRoomOccupantMemoryStorageObject *occupant = nil;
dispatch_sync(parentQueue, ^{ @autoreleasepool {
occupant = [occupantsDict[jid] copy];
}});
return occupant;
}
}
- (NSArray *)messages
{
XMPPLogTrace();
if (self.parentQueue == NULL)
{
// Haven't been attached to parent yet
return nil;
}
if (dispatch_get_specific(parentQueueTag))
{
return messages;
}
else
{
__block NSArray *result = nil;
dispatch_sync(parentQueue, ^{ @autoreleasepool {
result = [[NSArray alloc] initWithArray:messages copyItems:YES];
}});
return result;
}
}
- (NSArray *)occupants
{
XMPPLogTrace();
if (self.parentQueue == NULL)
{
// Haven't been attached to parent yet
return nil;
}
if (dispatch_get_specific(parentQueueTag))
{
return occupantsArray;
}
else
{
__block NSArray *result = nil;
dispatch_sync(parentQueue, ^{ @autoreleasepool {
result = [[NSArray alloc] initWithArray:occupantsArray copyItems:YES];
}});
return result;
}
}
- (NSArray *)resortMessages
{
XMPPLogTrace();
if (self.parentQueue == NULL)
{
// Haven't been attached to parent yet
return nil;
}
if (dispatch_get_specific(parentQueueTag))
{
[messages sortUsingSelector:@selector(compare:)];
return messages;
}
else
{
__block NSArray *result = nil;
dispatch_sync(parentQueue, ^{ @autoreleasepool {
[messages sortUsingSelector:@selector(compare:)];
result = [[NSArray alloc] initWithArray:messages copyItems:YES];
}});
return result;
}
}
- (NSArray *)resortOccupants
{
XMPPLogTrace();
if (self.parentQueue == NULL)
{
// Haven't been attached to parent yet
return nil;
}
if (dispatch_get_specific(parentQueueTag))
{
[occupantsArray sortUsingSelector:@selector(compare:)];
return occupantsArray;
}
else
{
__block NSArray *result = nil;
dispatch_sync(parentQueue, ^{ @autoreleasepool {
[occupantsArray sortUsingSelector:@selector(compare:)];
result = [[NSArray alloc] initWithArray:occupantsArray copyItems:YES];
}});
return result;
}
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark XMPPRoomStorage Protocol
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
- (void)handlePresence:(XMPPPresence *)presence room:(XMPPRoom *)room
{
XMPPLogTrace();
AssertParentQueue();
XMPPJID *from = [presence from];
if ([[presence type] isEqualToString:@"unavailable"])
{
XMPPRoomOccupantMemoryStorageObject *occupant = occupantsDict[from];
if (occupant)
{
// Occupant did leave - remove
NSUInteger index = [occupantsArray indexOfObjectIdenticalTo:occupant];
[occupantsArray removeObjectAtIndex:index];
[occupantsDict removeObjectForKey:from];
// Notify delegate(s)
XMPPRoomOccupantMemoryStorageObject *occupantCopy = [occupant copy];
NSArray *occupantsCopy = [[NSArray alloc] initWithArray:occupantsArray copyItems:YES];
[[self multicastDelegate] xmppRoomMemoryStorage:self
occupantDidLeave:occupantCopy
atIndex:index
fromArray:occupantsCopy];
}
}
else
{
XMPPRoomOccupantMemoryStorageObject *occupant = occupantsDict[from];
if (occupant == nil)
{
// Occupant did join - add
occupant = [[self.occupantClass alloc] initWithPresence:presence];
NSUInteger index = [self insertOccupant:occupant];
occupantsDict[from] = occupant;
// Notify delegate(s)
XMPPRoomOccupantMemoryStorageObject *occupantCopy = [occupant copy];
NSArray *occupantsCopy = [[NSArray alloc] initWithArray:occupantsArray copyItems:YES];
[[self multicastDelegate] xmppRoomMemoryStorage:self
occupantDidJoin:occupantCopy
atIndex:index
inArray:occupantsCopy];
}
else
{
// Occupant did update - move
[occupant updateWithPresence:presence];
NSUInteger oldIndex = [occupantsArray indexOfObjectIdenticalTo:occupant];
[occupantsArray removeObjectAtIndex:oldIndex];
NSUInteger newIndex = [self insertOccupant:occupant];
// Notify delegate(s)
XMPPRoomOccupantMemoryStorageObject *occupantCopy = [occupant copy];
NSArray *occupantsCopy = [[NSArray alloc] initWithArray:occupantsArray copyItems:YES];
[[self multicastDelegate] xmppRoomMemoryStorage:self
occupantDidUpdate:occupantCopy
fromIndex:oldIndex
toIndex:newIndex
inArray:occupantsCopy];
}
}
}
- (void)handleOutgoingMessage:(XMPPMessage *)message room:(XMPPRoom *)room
{
XMPPLogTrace();
AssertParentQueue();
XMPPJID *msgJID = room.myRoomJID;
XMPPRoomMessageMemoryStorageObject *roomMsg;
roomMsg = [[self.messageClass alloc] initWithOutgoingMessage:message jid:msgJID];
[self addMessage:roomMsg];
}
- (void)handleIncomingMessage:(XMPPMessage *)message room:(XMPPRoom *)room
{
XMPPLogTrace();
AssertParentQueue();
XMPPJID *myRoomJID = room.myRoomJID;
XMPPJID *messageJID = [message from];
if ([myRoomJID isEqualToJID:messageJID])
{
if (![message wasDelayed])
{
// Ignore - we already stored message in handleOutgoingMessage:room:
return;
}
}
if ([self existsMessage:message])
{
XMPPLogVerbose(@"%@: %@ - Duplicate message", THIS_FILE, THIS_METHOD);
}
else
{
XMPPRoomMessageMemoryStorageObject *roomMessage = [[self.messageClass alloc] initWithIncomingMessage:message];
[self addMessage:roomMessage];
}
}
- (void)handleDidLeaveRoom:(XMPPRoom *)room
{
XMPPLogTrace();
AssertParentQueue();
[occupantsDict removeAllObjects];
[occupantsArray removeAllObjects];
}
@end