From 6b773bf416a63cb5c10ad3a8d9980d6c76a76fe5 Mon Sep 17 00:00:00 2001 From: Piotr Wegrzynek Date: Fri, 11 Aug 2017 08:35:34 +0200 Subject: [PATCH] Universal message storage core implementation --- Core/XMPPFramework.h | 10 +- .../NSManagedObject+XMPPCoreDataStorage.h | 33 + .../NSManagedObject+XMPPCoreDataStorage.m | 65 ++ .../CoreDataStorage/XMPPCoreDataStorage.m | 2 +- .../XMPPMessage.xcdatamodel/elements | Bin 0 -> 134828 bytes .../XMPPMessage.xcdatamodel/layout | Bin 0 -> 17560 bytes ...geContextCoreDataStorageObject+Protected.h | 50 ++ .../XMPPMessageContextCoreDataStorageObject.h | 15 + .../XMPPMessageContextCoreDataStorageObject.m | 18 + ...ntextItemCoreDataStorageObject+Protected.h | 91 +++ ...PMessageContextItemCoreDataStorageObject.h | 151 +++++ ...PMessageContextItemCoreDataStorageObject.m | 194 ++++++ .../XMPPMessageCoreDataStorage.h | 15 + .../XMPPMessageCoreDataStorage.m | 5 + ...sageCoreDataStorageObject+ContextHelpers.h | 78 +++ ...sageCoreDataStorageObject+ContextHelpers.m | 221 +++++++ ...PPMessageCoreDataStorageObject+Protected.h | 78 +++ .../XMPPMessageCoreDataStorageObject.h | 86 +++ .../XMPPMessageCoreDataStorageObject.m | 527 ++++++++++++++++ XMPPFramework.xcodeproj/project.pbxproj | 146 +++++ .../project.pbxproj | 8 + .../XMPPMessageCoreDataStorageTests.m | 586 ++++++++++++++++++ .../project.pbxproj | 4 + .../project.pbxproj | 4 + 24 files changed, 2385 insertions(+), 2 deletions(-) create mode 100644 Extensions/CoreDataStorage/NSManagedObject+XMPPCoreDataStorage.h create mode 100644 Extensions/CoreDataStorage/NSManagedObject+XMPPCoreDataStorage.m create mode 100644 Extensions/MessageStorage/XMPPMessage.xcdatamodeld/XMPPMessage.xcdatamodel/elements create mode 100644 Extensions/MessageStorage/XMPPMessage.xcdatamodeld/XMPPMessage.xcdatamodel/layout create mode 100644 Extensions/MessageStorage/XMPPMessageContextCoreDataStorageObject+Protected.h create mode 100644 Extensions/MessageStorage/XMPPMessageContextCoreDataStorageObject.h create mode 100644 Extensions/MessageStorage/XMPPMessageContextCoreDataStorageObject.m create mode 100644 Extensions/MessageStorage/XMPPMessageContextItemCoreDataStorageObject+Protected.h create mode 100644 Extensions/MessageStorage/XMPPMessageContextItemCoreDataStorageObject.h create mode 100644 Extensions/MessageStorage/XMPPMessageContextItemCoreDataStorageObject.m create mode 100644 Extensions/MessageStorage/XMPPMessageCoreDataStorage.h create mode 100644 Extensions/MessageStorage/XMPPMessageCoreDataStorage.m create mode 100644 Extensions/MessageStorage/XMPPMessageCoreDataStorageObject+ContextHelpers.h create mode 100644 Extensions/MessageStorage/XMPPMessageCoreDataStorageObject+ContextHelpers.m create mode 100644 Extensions/MessageStorage/XMPPMessageCoreDataStorageObject+Protected.h create mode 100644 Extensions/MessageStorage/XMPPMessageCoreDataStorageObject.h create mode 100644 Extensions/MessageStorage/XMPPMessageCoreDataStorageObject.m create mode 100644 Xcode/Testing-Shared/XMPPMessageCoreDataStorageTests.m diff --git a/Core/XMPPFramework.h b/Core/XMPPFramework.h index 87f6ef49f8..ec4e80fba9 100644 --- a/Core/XMPPFramework.h +++ b/Core/XMPPFramework.h @@ -34,6 +34,7 @@ #import "XMPPTimer.h" #import "XMPPCoreDataStorage.h" #import "XMPPCoreDataStorageProtected.h" +#import "NSManagedObject+XMPPCoreDataStorage.h" #import "NSXMLElement+XEP_0203.h" #import "XMPPFileTransfer.h" #import "XMPPIncomingFileTransfer.h" @@ -163,7 +164,14 @@ #import "XMPPRoomLightCoreDataStorage.h" #import "XMPPRoomLightCoreDataStorageProtected.h" #import "XMPPRoomLightMessageCoreDataStorageObject.h" - +#import "XMPPMessageCoreDataStorage.h" +#import "XMPPMessageCoreDataStorageObject.h" +#import "XMPPMessageCoreDataStorageObject+Protected.h" +#import "XMPPMessageCoreDataStorageObject+ContextHelpers.h" +#import "XMPPMessageContextCoreDataStorageObject.h" +#import "XMPPMessageContextCoreDataStorageObject+Protected.h" +#import "XMPPMessageContextItemCoreDataStorageObject.h" +#import "XMPPMessageContextItemCoreDataStorageObject+Protected.h" FOUNDATION_EXPORT double XMPPFrameworkVersionNumber; FOUNDATION_EXPORT const unsigned char XMPPFrameworkVersionString[]; diff --git a/Extensions/CoreDataStorage/NSManagedObject+XMPPCoreDataStorage.h b/Extensions/CoreDataStorage/NSManagedObject+XMPPCoreDataStorage.h new file mode 100644 index 0000000000..f76cf79c3d --- /dev/null +++ b/Extensions/CoreDataStorage/NSManagedObject+XMPPCoreDataStorage.h @@ -0,0 +1,33 @@ +#import +#import "XMPPJID.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface NSManagedObject (XMPPCoreDataStorage) + +/// @brief Inserts a managed object with an entity whose name matches the class name. +/// @discussion An assertion will be triggered if no matching entity is found in the model. ++ (instancetype)xmpp_insertNewObjectInManagedObjectContext:(NSManagedObjectContext *)managedObjectContext; + +/// @brief Returns a fetch request for an entity whose name matches the class name. +/// @discussion An assertion will be triggered if no matching entity is found in the model. ++ (NSFetchRequest *)xmpp_fetchRequestInManagedObjectContext:(NSManagedObjectContext *)managedObjectContext; + +/// @brief Returns a predicate for filtering managed objects on JID component attributes. +/// @discussion The provided keypaths are relative to the fetched entity and the filtering logic follows @c [XMPPJID @c isEqualToJID:options:] implementation. ++ (NSPredicate *)xmpp_jidPredicateWithDomainKeyPath:(NSString *)domainKeyPath + resourceKeyPath:(NSString *)resourceKeyPath + userKeyPath:(NSString *)userKeyPath + value:(XMPPJID *)value + compareOptions:(XMPPJIDCompareOptions)compareOptions; + +@end + +@interface NSManagedObjectContext (XMPPCoreDataStorage) + +/// Executes the provided fetch request raising an assertion upon failure. +- (NSArray *)xmpp_executeForcedSuccessFetchRequest:(NSFetchRequest *)fetchRequest; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Extensions/CoreDataStorage/NSManagedObject+XMPPCoreDataStorage.m b/Extensions/CoreDataStorage/NSManagedObject+XMPPCoreDataStorage.m new file mode 100644 index 0000000000..697917b9d5 --- /dev/null +++ b/Extensions/CoreDataStorage/NSManagedObject+XMPPCoreDataStorage.m @@ -0,0 +1,65 @@ +#import "NSManagedObject+XMPPCoreDataStorage.h" + +@implementation NSManagedObject (XMPPCoreDataStorage) + ++ (instancetype)xmpp_insertNewObjectInManagedObjectContext:(NSManagedObjectContext *)managedObjectContext +{ + return [[self alloc] initWithEntity:[self xmpp_entityInManagedObjectContext:managedObjectContext] + insertIntoManagedObjectContext:managedObjectContext]; +} + ++ (NSFetchRequest *)xmpp_fetchRequestInManagedObjectContext:(NSManagedObjectContext *)managedObjectContext +{ + NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init]; + fetchRequest.entity = [self xmpp_entityInManagedObjectContext:managedObjectContext]; + return fetchRequest; +} + ++ (NSPredicate *)xmpp_jidPredicateWithDomainKeyPath:(NSString *)domainKeyPath resourceKeyPath:(NSString *)resourceKeyPath userKeyPath:(NSString *)userKeyPath value:(XMPPJID *)value compareOptions:(XMPPJIDCompareOptions)compareOptions +{ + NSMutableArray *subpredicates = [[NSMutableArray alloc] init]; + + if (compareOptions & XMPPJIDCompareDomain) { + [subpredicates addObject:[NSPredicate predicateWithFormat:@"%K = %@", domainKeyPath, value.domain]]; + } + + if (compareOptions & XMPPJIDCompareResource) { + [subpredicates addObject:[NSPredicate predicateWithFormat:@"%K = %@", resourceKeyPath, value.resource]]; + } + + if (compareOptions & XMPPJIDCompareUser) { + [subpredicates addObject:[NSPredicate predicateWithFormat:@"%K = %@", userKeyPath, value.user]]; + } + + return [NSCompoundPredicate andPredicateWithSubpredicates:subpredicates]; +} + ++ (NSEntityDescription *)xmpp_entityInManagedObjectContext:(NSManagedObjectContext *)managedObjectContext +{ + NSUInteger selfEntityIndex = [managedObjectContext.persistentStoreCoordinator.managedObjectModel.entities indexOfObjectPassingTest:^BOOL(NSEntityDescription * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { + BOOL matchesSelf = [obj.managedObjectClassName isEqualToString:NSStringFromClass(self)]; + if (matchesSelf) { + *stop = YES; + } + return matchesSelf; + }]; + NSAssert(selfEntityIndex != NSNotFound, @"Entity for %@ not found", self); + + return managedObjectContext.persistentStoreCoordinator.managedObjectModel.entities[selfEntityIndex]; +} + +@end + +@implementation NSManagedObjectContext (XMPPCoreDataStorage) + +- (NSArray *)xmpp_executeForcedSuccessFetchRequest:(NSFetchRequest *)fetchRequest +{ + NSError *error; + NSArray *fetchResult = [self executeFetchRequest:fetchRequest error:&error]; + if (!fetchResult) { + NSAssert(NO, @"Fetch request %@ failed with error %@", fetchRequest, error); + } + return fetchResult; +} + +@end diff --git a/Extensions/CoreDataStorage/XMPPCoreDataStorage.m b/Extensions/CoreDataStorage/XMPPCoreDataStorage.m index 39218e30f0..813331c956 100644 --- a/Extensions/CoreDataStorage/XMPPCoreDataStorage.m +++ b/Extensions/CoreDataStorage/XMPPCoreDataStorage.m @@ -560,7 +560,7 @@ - (NSPersistentStoreCoordinator *)persistentStoreCoordinator { return; } - + XMPPLogVerbose(@"%@: Creating persistentStoreCoordinator", [self class]); persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:mom]; diff --git a/Extensions/MessageStorage/XMPPMessage.xcdatamodeld/XMPPMessage.xcdatamodel/elements b/Extensions/MessageStorage/XMPPMessage.xcdatamodeld/XMPPMessage.xcdatamodel/elements new file mode 100644 index 0000000000000000000000000000000000000000..5d2171caf1d4a2bd3c25bbe13b18301d7f726adb GIT binary patch literal 134828 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 literal 0 HcmV?d00001 diff --git a/Extensions/MessageStorage/XMPPMessageContextCoreDataStorageObject+Protected.h b/Extensions/MessageStorage/XMPPMessageContextCoreDataStorageObject+Protected.h new file mode 100644 index 0000000000..7e5be17584 --- /dev/null +++ b/Extensions/MessageStorage/XMPPMessageContextCoreDataStorageObject+Protected.h @@ -0,0 +1,50 @@ +#import "XMPPMessageContextCoreDataStorageObject.h" + +NS_ASSUME_NONNULL_BEGIN + +@class XMPPMessageCoreDataStorageObject, XMPPMessageContextJIDItemCoreDataStorageObject, XMPPMessageContextMarkerItemCoreDataStorageObject, XMPPMessageContextStringItemCoreDataStorageObject, XMPPMessageContextTimestampItemCoreDataStorageObject; + +@interface XMPPMessageContextCoreDataStorageObject (Protected) + +/// The message the context object is assigned to. +@property (nonatomic, strong, nullable) XMPPMessageCoreDataStorageObject *message; + +/// The JID values aggregated by the context object. +@property (nonatomic, copy, nullable) NSSet *jidItems; + +/// The markers aggregated by the context object. +@property (nonatomic, copy, nullable) NSSet *markerItems; + +/// The string values aggregated by the context object. +@property (nonatomic, copy, nullable) NSSet *stringItems; + +/// The timestamp values aggregated by the context object. +@property (nonatomic, copy, nullable) NSSet *timestampItems; + +@end + +@interface XMPPMessageContextCoreDataStorageObject (CoreDataGeneratedRelationshipAccesssors) + +- (void)addJidItemsObject:(XMPPMessageContextJIDItemCoreDataStorageObject *)value; +- (void)removeJidItemsObject:(XMPPMessageContextJIDItemCoreDataStorageObject *)value; +- (void)addJidItems:(NSSet *)value; +- (void)removeJidItems:(NSSet *)value; + +- (void)addMarkerItemsObject:(XMPPMessageContextMarkerItemCoreDataStorageObject *)value; +- (void)removeMarkerItemsObject:(XMPPMessageContextMarkerItemCoreDataStorageObject *)value; +- (void)addMarkerItems:(NSSet *)value; +- (void)removeMarkerItems:(NSSet *)value; + +- (void)addStringItemsObject:(XMPPMessageContextStringItemCoreDataStorageObject *)value; +- (void)removeStringItemsObject:(XMPPMessageContextStringItemCoreDataStorageObject *)value; +- (void)addStringItems:(NSSet *)value; +- (void)removeStringItems:(NSSet *)value; + +- (void)addTimestampItemsObject:(XMPPMessageContextTimestampItemCoreDataStorageObject *)value; +- (void)removeTimestampItemsObject:(XMPPMessageContextTimestampItemCoreDataStorageObject *)value; +- (void)addTimestampItems:(NSSet *)value; +- (void)removeTimestampItems:(NSSet *)value; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Extensions/MessageStorage/XMPPMessageContextCoreDataStorageObject.h b/Extensions/MessageStorage/XMPPMessageContextCoreDataStorageObject.h new file mode 100644 index 0000000000..1ead01a7fa --- /dev/null +++ b/Extensions/MessageStorage/XMPPMessageContextCoreDataStorageObject.h @@ -0,0 +1,15 @@ +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + An auxiliary context storage object aggregating module-provided values assigned to a stored message. + + @see XMPPMessageCoreDataStorageObject + @see XMPPMessageContextItemCoreDataStorageObject + */ +@interface XMPPMessageContextCoreDataStorageObject : NSManagedObject + +@end + +NS_ASSUME_NONNULL_END diff --git a/Extensions/MessageStorage/XMPPMessageContextCoreDataStorageObject.m b/Extensions/MessageStorage/XMPPMessageContextCoreDataStorageObject.m new file mode 100644 index 0000000000..8c14ef9faa --- /dev/null +++ b/Extensions/MessageStorage/XMPPMessageContextCoreDataStorageObject.m @@ -0,0 +1,18 @@ +#import "XMPPMessageContextCoreDataStorageObject.h" +#import "XMPPMessageContextCoreDataStorageObject+Protected.h" + +@interface XMPPMessageContextCoreDataStorageObject () + +@property (nonatomic, strong, nullable) XMPPMessageCoreDataStorageObject *message; +@property (nonatomic, copy, nullable) NSSet *jidItems; +@property (nonatomic, copy, nullable) NSSet *markerItems; +@property (nonatomic, copy, nullable) NSSet *stringItems; +@property (nonatomic, copy, nullable) NSSet *timestampItems; + +@end + +@implementation XMPPMessageContextCoreDataStorageObject + +@dynamic message, jidItems, markerItems, stringItems, timestampItems; + +@end diff --git a/Extensions/MessageStorage/XMPPMessageContextItemCoreDataStorageObject+Protected.h b/Extensions/MessageStorage/XMPPMessageContextItemCoreDataStorageObject+Protected.h new file mode 100644 index 0000000000..0d887b24fe --- /dev/null +++ b/Extensions/MessageStorage/XMPPMessageContextItemCoreDataStorageObject+Protected.h @@ -0,0 +1,91 @@ +#import "XMPPMessageContextItemCoreDataStorageObject.h" +#import "XMPPJID.h" + +NS_ASSUME_NONNULL_BEGIN + +@class XMPPMessageContextCoreDataStorageObject; + +typedef NS_ENUM(int16_t, XMPPMessageDirection); + +/// A tag assigned to a JID auxiliary value. +typedef NSString * XMPPMessageContextJIDItemTag NS_EXTENSIBLE_STRING_ENUM; + +/// An tag assigned to an auxiliary marker. +typedef NSString * XMPPMessageContextMarkerItemTag NS_EXTENSIBLE_STRING_ENUM; + +/// A tag assigned to a string auxiliary value. +typedef NSString * XMPPMessageContextStringItemTag NS_EXTENSIBLE_STRING_ENUM; + +/// A tag assigned to a timestamp auxiliary value. +typedef NSString * XMPPMessageContextTimestampItemTag NS_EXTENSIBLE_STRING_ENUM; + +@interface XMPPMessageContextItemCoreDataStorageObject (Protected) + +/// The context element aggregating the value. +@property (nonatomic, strong, nullable) XMPPMessageContextCoreDataStorageObject *contextElement; + +@end + +/// A storage object representing a module-provided JID value assigned to a stored message. +@interface XMPPMessageContextJIDItemCoreDataStorageObject : XMPPMessageContextItemCoreDataStorageObject + +/// The tag assigned to the value. +@property (nonatomic, copy, nullable) XMPPMessageContextJIDItemTag tag; + +/// The stored JID value. +@property (nonatomic, strong, nullable) XMPPJID *value; + +/// Returns a predicate to fetch values with the specified tag. ++ (NSPredicate *)tagPredicateWithValue:(XMPPMessageContextJIDItemTag)value; + +/// Returns a predicate to fetch items with the specified value. ++ (NSPredicate *)jidPredicateWithValue:(XMPPJID *)value compareOptions:(XMPPJIDCompareOptions)compareOptions; + +@end + +/// A storage object representing a module-provided marker assigned to a stored message. +@interface XMPPMessageContextMarkerItemCoreDataStorageObject : XMPPMessageContextItemCoreDataStorageObject + +/// The tag assigned to the marker. +@property (nonatomic, copy, nullable) XMPPMessageContextMarkerItemTag tag; + +/// Returns a predicate to fetch markers with the specified tag. ++ (NSPredicate *)tagPredicateWithValue:(XMPPMessageContextMarkerItemTag)value; + +@end + +/// A storage object representing a module-provided string value assigned to a stored message. +@interface XMPPMessageContextStringItemCoreDataStorageObject : XMPPMessageContextItemCoreDataStorageObject + +/// The tag assigned to the value. +@property (nonatomic, copy, nullable) XMPPMessageContextStringItemTag tag; + +/// The stored string value. +@property (nonatomic, copy, nullable) NSString *value; + +/// Returns a predicate to fetch values with the specified tag. ++ (NSPredicate *)tagPredicateWithValue:(XMPPMessageContextStringItemTag)tag; + +/// Returns a predicate to fetch items with the specified value. ++ (NSPredicate *)stringPredicateWithValue:(NSString *)value; + +@end + +/// A storage object representing a module-provided timestamp value assigned to a stored message. +@interface XMPPMessageContextTimestampItemCoreDataStorageObject : XMPPMessageContextItemCoreDataStorageObject + +/// The tag assigned to the value. +@property (nonatomic, copy, nullable) XMPPMessageContextTimestampItemTag tag; + +/// The stored timestamp value. +@property (nonatomic, strong, nullable) NSDate *value; + +/// Returns a predicate to fetch values with the specified tag. ++ (NSPredicate *)tagPredicateWithValue:(XMPPMessageContextTimestampItemTag)value; + +/// Returns a predicate to fetch items with values in the specified range. ++ (NSPredicate *)timestampRangePredicateWithStartValue:(nullable NSDate *)startValue endValue:(nullable NSDate *)endValue; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Extensions/MessageStorage/XMPPMessageContextItemCoreDataStorageObject.h b/Extensions/MessageStorage/XMPPMessageContextItemCoreDataStorageObject.h new file mode 100644 index 0000000000..a8bdd3567d --- /dev/null +++ b/Extensions/MessageStorage/XMPPMessageContextItemCoreDataStorageObject.h @@ -0,0 +1,151 @@ +#import +#import "XMPPJID.h" + +NS_ASSUME_NONNULL_BEGIN + +@class XMPPMessageCoreDataStorageObject; + +typedef NS_ENUM(int16_t, XMPPMessageDirection); +typedef NS_ENUM(int16_t, XMPPMessageType); + +typedef NS_ENUM(NSInteger, XMPPMessageContentCompareOperator) { + /// Content is equal the search string. + XMPPMessageContentCompareOperatorEquals, + /// Content begins with the search string. + XMPPMessageContentCompareOperatorBeginsWith, + /// Content contains the search string. + XMPPMessageContentCompareOperatorContains, + /// Content ends with the search string. + XMPPMessageContentCompareOperatorEndsWith, + /// Content is equal to the search string and the search string can contain wildcard characters. + XMPPMessageContentCompareOperatorLike, + /// Content matches the the search string interpreted as a regular expression. + XMPPMessageContentCompareOperatorMatches +}; + +typedef NS_OPTIONS(NSInteger, XMPPMessageContentCompareOptions) { + /// Content comparison is case-insensitive. + XMPPMessageContentCompareCaseInsensitive = 1 << 0, + /// Content comparison is diacritic-insensitive. + XMPPMessageContentCompareDiacriticInsensitive = 1 << 1 +}; + +/** + A storage object representing a module-provided value assigned to a stored message. + + @see XMPPMessageCoreDataStorageObject + @see XMPPMessageContextCoreDataStorageObject + */ +@interface XMPPMessageContextItemCoreDataStorageObject : NSManagedObject + +@end + +@interface XMPPMessageContextItemCoreDataStorageObject (XMPPMessageCoreDataStorageFetch) + +/** + Returns a fetch request for timestamp context values with associated messages. + + A common application use case involves fetching temporally ordered messages. In terms of the message storage Core Data model, + this translates to fetching timestamp context values with specific predicates and then looking up the message objects they are attached to. + + The modules that assign custom timestamp context values will also provide appropriate predicates to be used with this method. + It is application's responsibility to avoid fetches with duplicate messages when composing predicates coming from multiple modules. + */ ++ (NSFetchRequest *)requestByTimestampsWithPredicate:(NSPredicate *)predicate + inAscendingOrder:(BOOL)isInAscendingOrder + fromManagedObjectContext:(NSManagedObjectContext *)managedObjectContext; + +/** + Returns a predicate to be provided to @c requestByTimestampsWithPredicate:inAscendingOrder:fromManagedObjectContext: + that limits the fetch results to only include the single most relevant stream context timestamp per message. + + @see requestByTimestampsWithPredicate:inAscendingOrder:fromManagedObjectContext: + */ ++ (NSPredicate *)streamTimestampKindPredicate; + +/** + Returns a predicate to be provided to @c requestByTimestampsWithPredicate:inAscendingOrder:fromManagedObjectContext: + that limits the fetch results to only include timestamp values from the given range. + + In order to request an open range, provide a nil value for the respective boundary. + + @see requestByTimestampsWithPredicate:inAscendingOrder:fromManagedObjectContext: + */ ++ (NSPredicate *)timestampRangePredicateWithStartValue:(nullable NSDate *)startValue endValue:(nullable NSDate *)endValue; + +/** + Returns a predicate to be provided to @c requestByTimestampsWithPredicate:inAscendingOrder:fromManagedObjectContext: + that limits the fetch results to only include timestamp values for messages with the given @c fromJID value. + + @see requestByTimestampsWithPredicate:inAscendingOrder:fromManagedObjectContext: + */ ++ (NSPredicate *)messageFromJIDPredicateWithValue:(XMPPJID *)value compareOptions:(XMPPJIDCompareOptions)compareOptions; + +/** + Returns a predicate to be provided to @c requestByTimestampsWithPredicate:inAscendingOrder:fromManagedObjectContext: + that limits the fetch results to only include timestamp values for messages with the given @c toJID value. + + @see requestByTimestampsWithPredicate:inAscendingOrder:fromManagedObjectContext: + */ ++ (NSPredicate *)messageToJIDPredicateWithValue:(XMPPJID *)value compareOptions:(XMPPJIDCompareOptions)compareOptions; + +/** + Returns a predicate to be provided to @c requestByTimestampsWithPredicate:inAscendingOrder:fromManagedObjectContext: + that limits the fetch results to only include timestamp values for messages exchanged with an entity with the given JID value. + + The relevant messages in this case are the outgoing ones with a matching @c toJID value and incoming ones with a matching @c fromJID value. + + @see requestByTimestampsWithPredicate:inAscendingOrder:fromManagedObjectContext: + */ ++ (NSPredicate *)messageRemotePartyJIDPredicateWithValue:(XMPPJID *)value compareOptions:(XMPPJIDCompareOptions)compareOptions; + +/** + Returns a predicate to be provided to @c requestByTimestampsWithPredicate:inAscendingOrder:fromManagedObjectContext: + that limits the fetch results to only include timestamp values for messages with specific body content. + + @see requestByTimestampsWithPredicate:inAscendingOrder:fromManagedObjectContext: + */ ++ (NSPredicate *)messageBodyPredicateWithValue:(NSString *)value + compareOperator:(XMPPMessageContentCompareOperator)compareOperator + options:(XMPPMessageContentCompareOptions)options; + +/** + Returns a predicate to be provided to @c requestByTimestampsWithPredicate:inAscendingOrder:fromManagedObjectContext: + that limits the fetch results to only include timestamp values for messages with specific subject content. + + @see requestByTimestampsWithPredicate:inAscendingOrder:fromManagedObjectContext: + */ ++ (NSPredicate *)messageSubjectPredicateWithValue:(NSString *)value + compareOperator:(XMPPMessageContentCompareOperator)compareOperator + options:(XMPPMessageContentCompareOptions)options; + +/** + Returns a predicate to be provided to @c requestByTimestampsWithPredicate:inAscendingOrder:fromManagedObjectContext: + that limits the fetch results to only include timestamp values for messages from the given thread. + + @see requestByTimestampsWithPredicate:inAscendingOrder:fromManagedObjectContext: + */ ++ (NSPredicate *)messageThreadPredicateWithValue:(NSString *)value; + +/** + Returns a predicate to be provided to @c requestByTimestampsWithPredicate:inAscendingOrder:fromManagedObjectContext: + that limits the fetch results to only include timestamp values for messages with the specified direction. + + @see requestByTimestampsWithPredicate:inAscendingOrder:fromManagedObjectContext: + */ ++ (NSPredicate *)messageDirectionPredicateWithValue:(XMPPMessageDirection)value; + +/** + Returns a predicate to be provided to @c requestByTimestampsWithPredicate:inAscendingOrder:fromManagedObjectContext: + that limits the fetch results to only include timestamp values for messages of the specified type. + + @see requestByTimestampsWithPredicate:inAscendingOrder:fromManagedObjectContext: + */ ++ (NSPredicate *)messageTypePredicateWithValue:(XMPPMessageType)value; + +/// Returns the message the context item is associated with. +- (XMPPMessageCoreDataStorageObject *)message; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Extensions/MessageStorage/XMPPMessageContextItemCoreDataStorageObject.m b/Extensions/MessageStorage/XMPPMessageContextItemCoreDataStorageObject.m new file mode 100644 index 0000000000..8d92e14b37 --- /dev/null +++ b/Extensions/MessageStorage/XMPPMessageContextItemCoreDataStorageObject.m @@ -0,0 +1,194 @@ +#import "XMPPMessageContextItemCoreDataStorageObject.h" +#import "XMPPMessageContextItemCoreDataStorageObject+Protected.h" +#import "XMPPJID.h" +#import "NSManagedObject+XMPPCoreDataStorage.h" + +@interface XMPPMessageContextItemCoreDataStorageObject () + +@property (nonatomic, strong, nullable) XMPPMessageContextCoreDataStorageObject *contextElement; + +@end + +@implementation XMPPMessageContextItemCoreDataStorageObject + +@dynamic contextElement; + ++ (NSPredicate *)tagPredicateWithValue:(NSString *)value +{ + return [NSPredicate predicateWithFormat:@"%K = %@", NSStringFromSelector(@selector(tag)), value]; +} + +@end + +@interface XMPPMessageContextJIDItemCoreDataStorageObject () + +@property (nonatomic, copy, nullable) NSString *valueDomain; +@property (nonatomic, copy, nullable) NSString *valueResource; +@property (nonatomic, copy, nullable) NSString *valueUser; + +@end + +@interface XMPPMessageContextJIDItemCoreDataStorageObject (CoreDataGeneratedPrimitiveAccessors) + +- (XMPPJID *)primitiveValue; +- (void)setPrimitiveValue:(XMPPJID *)value; +- (void)setPrimitiveValueDomain:(NSString *)value; +- (void)setPrimitiveValueResource:(NSString *)value; +- (void)setPrimitiveValueUser:(NSString *)value; + +@end + +@implementation XMPPMessageContextJIDItemCoreDataStorageObject + +@dynamic tag, valueDomain, valueResource, valueUser; + +#pragma mark - value transient property + +- (XMPPJID *)value +{ + [self willAccessValueForKey:NSStringFromSelector(@selector(value))]; + XMPPJID *value = [self primitiveValue]; + [self didAccessValueForKey:NSStringFromSelector(@selector(value))]; + + if (value) { + return value; + } + + XMPPJID *newValue = [XMPPJID jidWithUser:self.valueUser domain:self.valueDomain resource:self.valueResource]; + [self setPrimitiveValue:newValue]; + + return newValue; +} + +- (void)setValue:(XMPPJID *)value +{ + if ([self.value isEqualToJID:value]) { + return; + } + + [self willChangeValueForKey:NSStringFromSelector(@selector(value))]; + [self willChangeValueForKey:NSStringFromSelector(@selector(valueDomain))]; + [self willChangeValueForKey:NSStringFromSelector(@selector(valueResource))]; + [self willChangeValueForKey:NSStringFromSelector(@selector(valueUser))]; + [self setPrimitiveValue:value]; + [self setPrimitiveValueDomain:value.domain]; + [self setPrimitiveValueResource:value.resource]; + [self setPrimitiveValueUser:value.user]; + [self didChangeValueForKey:NSStringFromSelector(@selector(valueDomain))]; + [self didChangeValueForKey:NSStringFromSelector(@selector(valueResource))]; + [self didChangeValueForKey:NSStringFromSelector(@selector(valueUser))]; + [self didChangeValueForKey:NSStringFromSelector(@selector(value))]; +} + +- (void)setValueDomain:(NSString *)valueDomain +{ + if ([self.valueDomain isEqualToString:valueDomain]) { + return; + } + + [self willChangeValueForKey:NSStringFromSelector(@selector(valueDomain))]; + [self willChangeValueForKey:NSStringFromSelector(@selector(value))]; + [self setPrimitiveValueDomain:valueDomain]; + [self setPrimitiveValue:nil]; + [self didChangeValueForKey:NSStringFromSelector(@selector(value))]; + [self didChangeValueForKey:NSStringFromSelector(@selector(valueDomain))]; +} + +- (void)setValueResource:(NSString *)valueResource +{ + if ([self.valueResource isEqualToString:valueResource]) { + return; + } + + [self willChangeValueForKey:NSStringFromSelector(@selector(valueResource))]; + [self willChangeValueForKey:NSStringFromSelector(@selector(value))]; + [self setPrimitiveValueResource:valueResource]; + [self setPrimitiveValue:nil]; + [self didChangeValueForKey:NSStringFromSelector(@selector(value))]; + [self didChangeValueForKey:NSStringFromSelector(@selector(valueResource))]; +} + +- (void)setValueUser:(NSString *)valueUser +{ + if ([self.valueUser isEqualToString:valueUser]) { + return; + } + + [self willChangeValueForKey:NSStringFromSelector(@selector(valueUser))]; + [self willChangeValueForKey:NSStringFromSelector(@selector(value))]; + [self setPrimitiveValueUser:valueUser]; + [self setPrimitiveValue:nil]; + [self didChangeValueForKey:NSStringFromSelector(@selector(value))]; + [self didChangeValueForKey:NSStringFromSelector(@selector(valueUser))]; +} + +#pragma mark - Public + ++ (NSPredicate *)tagPredicateWithValue:(XMPPMessageContextJIDItemTag)value +{ + return [super tagPredicateWithValue:value]; +} + ++ (NSPredicate *)jidPredicateWithValue:(XMPPJID *)value compareOptions:(XMPPJIDCompareOptions)compareOptions +{ + return [self xmpp_jidPredicateWithDomainKeyPath:NSStringFromSelector(@selector(valueDomain)) + resourceKeyPath:NSStringFromSelector(@selector(valueResource)) + userKeyPath:NSStringFromSelector(@selector(valueUser)) + value:value + compareOptions:compareOptions]; +} + +@end + +@implementation XMPPMessageContextMarkerItemCoreDataStorageObject + +@dynamic tag; + ++ (NSPredicate *)tagPredicateWithValue:(XMPPMessageContextMarkerItemTag)value +{ + return [super tagPredicateWithValue:value]; +} + +@end + +@implementation XMPPMessageContextStringItemCoreDataStorageObject + +@dynamic tag, value; + ++ (NSPredicate *)tagPredicateWithValue:(XMPPMessageContextStringItemTag)value +{ + return [super tagPredicateWithValue:value]; +} + ++ (NSPredicate *)stringPredicateWithValue:(NSString *)value +{ + return [NSPredicate predicateWithFormat:@"%K = %@", NSStringFromSelector(@selector(value)), value]; +} + +@end + +@implementation XMPPMessageContextTimestampItemCoreDataStorageObject + +@dynamic tag, value; + ++ (NSPredicate *)tagPredicateWithValue:(XMPPMessageContextTimestampItemTag)value +{ + return [super tagPredicateWithValue:value]; +} + ++ (NSPredicate *)timestampRangePredicateWithStartValue:(NSDate *)startValue endValue:(NSDate *)endValue +{ + NSMutableArray *subpredicates = [[NSMutableArray alloc] init]; + + if (startValue) { + [subpredicates addObject:[NSPredicate predicateWithFormat:@"%K >= %@", NSStringFromSelector(@selector(value)), startValue]]; + } + + if (endValue) { + [subpredicates addObject:[NSPredicate predicateWithFormat:@"%K <= %@", NSStringFromSelector(@selector(value)), endValue]]; + } + + return subpredicates.count == 1 ? subpredicates.firstObject : [NSCompoundPredicate andPredicateWithSubpredicates:subpredicates]; +} + +@end diff --git a/Extensions/MessageStorage/XMPPMessageCoreDataStorage.h b/Extensions/MessageStorage/XMPPMessageCoreDataStorage.h new file mode 100644 index 0000000000..4f090507c2 --- /dev/null +++ b/Extensions/MessageStorage/XMPPMessageCoreDataStorage.h @@ -0,0 +1,15 @@ +#import "XMPPCoreDataStorage.h" + +/** + A client message storage implementation that supports per-module extensibility while maintaining a fixed underlying Core Data model. + + The design is based on assigning auxiliary context objects to each stored XMPP message. Those context objects aggregate arbitrary sets of tagged primitive values. + By defining their own context aggregations and value tags, modules can extend storage capabilities and expose them via a simple API using categories on the core classes. + + The application-facing API consists of the main interface and any categories provided by module authors. The protected interface provides module helper methods. + + @see XMPPMessageCoreDataStorageObject + */ +@interface XMPPMessageCoreDataStorage : XMPPCoreDataStorage + +@end diff --git a/Extensions/MessageStorage/XMPPMessageCoreDataStorage.m b/Extensions/MessageStorage/XMPPMessageCoreDataStorage.m new file mode 100644 index 0000000000..9d75cd791c --- /dev/null +++ b/Extensions/MessageStorage/XMPPMessageCoreDataStorage.m @@ -0,0 +1,5 @@ +#import "XMPPMessageCoreDataStorage.h" + +@implementation XMPPMessageCoreDataStorage + +@end diff --git a/Extensions/MessageStorage/XMPPMessageCoreDataStorageObject+ContextHelpers.h b/Extensions/MessageStorage/XMPPMessageCoreDataStorageObject+ContextHelpers.h new file mode 100644 index 0000000000..ed1ec5b69e --- /dev/null +++ b/Extensions/MessageStorage/XMPPMessageCoreDataStorageObject+ContextHelpers.h @@ -0,0 +1,78 @@ +#import "XMPPMessageCoreDataStorageObject.h" +#import "XMPPMessageContextCoreDataStorageObject+Protected.h" +#import "XMPPMessageContextItemCoreDataStorageObject+Protected.h" + +NS_ASSUME_NONNULL_BEGIN + +/// An API to be used by modules to manipulate auxiliary context objects assigned to a stored message. +@interface XMPPMessageCoreDataStorageObject (ContextHelpers) + +/// Inserts a new context element associated with the message. +- (XMPPMessageContextCoreDataStorageObject *)appendContextElement; + +/** + @brief Enumerates the message's context elements until the lookup block returns a non-nil value and returns that value. + @discussion This method expects the lookup block to only return a non-nil value for a single element and will trigger an assertion otherwise. + */ +- (nullable id)lookupInContextWithBlock:(id __nullable (^)(XMPPMessageContextCoreDataStorageObject *contextElement))lookupBlock; + +@end + +/// An API to be used by modules to manipulate auxiliary context object values assigned to a stored message. +@interface XMPPMessageContextCoreDataStorageObject (ContextHelpers) + +/// Inserts a new JID value associated with the context element. +- (XMPPMessageContextJIDItemCoreDataStorageObject *)appendJIDItemWithTag:(XMPPMessageContextJIDItemTag)tag value:(XMPPJID *)value; + +/// Inserts a new marker associated with the context element. +- (XMPPMessageContextMarkerItemCoreDataStorageObject *)appendMarkerItemWithTag:(XMPPMessageContextMarkerItemTag)tag; + +/// Inserts a new string value associated with the context element. +- (XMPPMessageContextStringItemCoreDataStorageObject *)appendStringItemWithTag:(XMPPMessageContextStringItemTag)tag value:(NSString *)value; + +/// Inserts a new timestamp value associated with the context element. +- (XMPPMessageContextTimestampItemCoreDataStorageObject *)appendTimestampItemWithTag:(XMPPMessageContextTimestampItemTag)tag value:(NSDate *)value; + +/// Removes all JID values with the given tag associated with the context element. +- (void)removeJIDItemsWithTag:(XMPPMessageContextJIDItemTag)tag; + +/// Removes all markers with the given tag associated with the context element. +- (void)removeMarkerItemsWithTag:(XMPPMessageContextMarkerItemTag)tag; + +/// Removes all string values with the given tag associated with the context element. +- (void)removeStringItemsWithTag:(XMPPMessageContextStringItemTag)tag; + +/// Removes all timestamp values with the given tag associated with the context element. +- (void)removeTimestampItemsWithTag:(XMPPMessageContextTimestampItemTag)tag; + +/// Returns all JID values with the given tag associated with the context element. +- (NSSet *)jidItemValuesForTag:(XMPPMessageContextJIDItemTag)tag; + +/// @brief Returns the unique JID value with the given tag associated with the context element. +/// @discussion Will trigger an assertion if there is more than one matching value. +- (nullable XMPPJID *)jidItemValueForTag:(XMPPMessageContextJIDItemTag)tag; + +/// Returns the number of markers with the given tag associated with the context element. +- (NSInteger)markerItemCountForTag:(XMPPMessageContextMarkerItemTag)tag; + +/// @brief Tests whether there is a marker with the given tag associated with the context element. +/// @discussion Will trigger an assertion if there is more than one matching marker. +- (BOOL)hasMarkerItemForTag:(XMPPMessageContextMarkerItemTag)tag; + +/// Returns all string values with the given tag associated with the context element. +- (NSSet *)stringItemValuesForTag:(XMPPMessageContextStringItemTag)tag; + +/// @brief Returns the unique string value with the given tag associated with the context element. +/// @discussion Will trigger an assertion if there is more than one matching value. +- (nullable NSString *)stringItemValueForTag:(XMPPMessageContextStringItemTag)tag; + +/// Returns all timestamp values with the given tag associated with the context element. +- (NSSet *)timestampItemValuesForTag:(XMPPMessageContextTimestampItemTag)tag; + +/// @brief Returns the unique timestamp value with the given tag associated with the context element. +/// @discussion Will trigger an assertion if there is more than one matching value. +- (nullable NSDate *)timestampItemValueForTag:(XMPPMessageContextTimestampItemTag)tag; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Extensions/MessageStorage/XMPPMessageCoreDataStorageObject+ContextHelpers.m b/Extensions/MessageStorage/XMPPMessageCoreDataStorageObject+ContextHelpers.m new file mode 100644 index 0000000000..c2c325ec54 --- /dev/null +++ b/Extensions/MessageStorage/XMPPMessageCoreDataStorageObject+ContextHelpers.m @@ -0,0 +1,221 @@ +#import "XMPPMessageCoreDataStorageObject+ContextHelpers.h" +#import "XMPPMessageCoreDataStorageObject+Protected.h" +#import "XMPPMessageContextCoreDataStorageObject+Protected.h" +#import "NSManagedObject+XMPPCoreDataStorage.h" + +@implementation XMPPMessageCoreDataStorageObject (ContextHelpers) + +- (XMPPMessageContextCoreDataStorageObject *)appendContextElement +{ + NSAssert(self.managedObjectContext, @"Attempted to append a context element to a message not associated with any managed object context"); + + XMPPMessageContextCoreDataStorageObject *insertedElement = [XMPPMessageContextCoreDataStorageObject xmpp_insertNewObjectInManagedObjectContext:self.managedObjectContext]; + insertedElement.message = self; + return insertedElement; +} + +- (id)lookupInContextWithBlock:(id (^)(XMPPMessageContextCoreDataStorageObject * _Nonnull))lookupBlock +{ + id lookupResult; + for (XMPPMessageContextCoreDataStorageObject *contextElement in self.contextElements) { + id elementResult = lookupBlock(contextElement); + if (!elementResult) { + continue; + } + NSAssert(!lookupResult, @"A unique lookup result is expected"); + lookupResult = elementResult; +#ifdef NS_BLOCK_ASSERTIONS + break; +#endif + } + return lookupResult; +} + +@end + +@implementation XMPPMessageContextCoreDataStorageObject (ContextHelpers) + +- (XMPPMessageContextJIDItemCoreDataStorageObject *)appendJIDItemWithTag:(XMPPMessageContextJIDItemTag)tag value:(XMPPJID *)value +{ + NSAssert(self.managedObjectContext, @"Attempted to append an item to a context element not associated with any managed object context"); + + XMPPMessageContextJIDItemCoreDataStorageObject *insertedItem = [XMPPMessageContextJIDItemCoreDataStorageObject xmpp_insertNewObjectInManagedObjectContext:self.managedObjectContext]; + insertedItem.tag = tag; + insertedItem.value = value; + insertedItem.contextElement = self; + return insertedItem; +} + +- (XMPPMessageContextMarkerItemCoreDataStorageObject *)appendMarkerItemWithTag:(XMPPMessageContextMarkerItemTag)tag +{ + NSAssert(self.managedObjectContext, @"Attempted to append an item to a context element not associated with any managed object context"); + + XMPPMessageContextMarkerItemCoreDataStorageObject *insertedItem = [XMPPMessageContextMarkerItemCoreDataStorageObject xmpp_insertNewObjectInManagedObjectContext:self.managedObjectContext]; + insertedItem.tag = tag; + insertedItem.contextElement = self; + return insertedItem; +} + +- (XMPPMessageContextStringItemCoreDataStorageObject *)appendStringItemWithTag:(XMPPMessageContextStringItemTag)tag value:(NSString *)value +{ + NSAssert(self.managedObjectContext, @"Attempted to append an item to a context element not associated with any managed object context"); + + XMPPMessageContextStringItemCoreDataStorageObject *insertedItem = [XMPPMessageContextStringItemCoreDataStorageObject xmpp_insertNewObjectInManagedObjectContext:self.managedObjectContext]; + insertedItem.tag = tag; + insertedItem.value = value; + insertedItem.contextElement = self; + return insertedItem; +} + +- (XMPPMessageContextTimestampItemCoreDataStorageObject *)appendTimestampItemWithTag:(XMPPMessageContextTimestampItemTag)tag value:(NSDate *)value +{ + NSAssert(self.managedObjectContext, @"Attempted to append an item to a context element not associated with any managed object context"); + + XMPPMessageContextTimestampItemCoreDataStorageObject *insertedItem = [XMPPMessageContextTimestampItemCoreDataStorageObject xmpp_insertNewObjectInManagedObjectContext:self.managedObjectContext]; + insertedItem.tag = tag; + insertedItem.value = value; + insertedItem.contextElement = self; + return insertedItem; +} + +- (void)removeJIDItemsWithTag:(XMPPMessageContextJIDItemTag)tag +{ + NSAssert(self.managedObjectContext, @"Attempted to remove an item from a context element not associated with any managed object context"); + + for (XMPPMessageContextJIDItemCoreDataStorageObject *jidItem in [self jidItemsForTag:tag expectingSingleElement:NO]) { + [self removeJidItemsObject:jidItem]; + [self.managedObjectContext deleteObject:jidItem]; + } +} + +- (void)removeMarkerItemsWithTag:(XMPPMessageContextMarkerItemTag)tag +{ + NSAssert(self.managedObjectContext, @"Attempted to remove an item from a context element not associated with any managed object context"); + + for (XMPPMessageContextMarkerItemCoreDataStorageObject *markerItem in [self markerItemsForTag:tag expectingSingleElement:NO]) { + [self removeMarkerItemsObject:markerItem]; + [self.managedObjectContext deleteObject:markerItem]; + } +} + +- (void)removeStringItemsWithTag:(XMPPMessageContextStringItemTag)tag +{ + NSAssert(self.managedObjectContext, @"Attempted to remove an item from a context element not associated with any managed object context"); + + for (XMPPMessageContextStringItemCoreDataStorageObject *stringItem in [self stringItemsForTag:tag expectingSingleElement:NO]) { + [self removeStringItemsObject:stringItem]; + [self.managedObjectContext deleteObject:stringItem]; + } +} + +- (void)removeTimestampItemsWithTag:(XMPPMessageContextTimestampItemTag)tag +{ + NSAssert(self.managedObjectContext, @"Attempted to remove an item from a context element not associated with any managed object context"); + + for (XMPPMessageContextTimestampItemCoreDataStorageObject *timestampItem in [self timestampItemsForTag:tag expectingSingleElement:NO]) { + [self removeTimestampItemsObject:timestampItem]; + [self.managedObjectContext deleteObject:timestampItem]; + } +} + +- (NSSet *)jidItemValuesForTag:(XMPPMessageContextJIDItemTag)tag +{ + return [[self jidItemsForTag:tag expectingSingleElement:NO] valueForKey:NSStringFromSelector(@selector(value))]; +} + +- (XMPPJID *)jidItemValueForTag:(XMPPMessageContextJIDItemTag)tag +{ + return [[self jidItemsForTag:tag expectingSingleElement:YES] anyObject].value; +} + +- (NSInteger)markerItemCountForTag:(XMPPMessageContextMarkerItemTag)tag +{ + return [self markerItemsForTag:tag expectingSingleElement:NO].count; +} + +- (BOOL)hasMarkerItemForTag:(XMPPMessageContextMarkerItemTag)tag +{ + return [[self markerItemsForTag:tag expectingSingleElement:YES] anyObject] != nil; +} + +- (NSSet *)stringItemValuesForTag:(XMPPMessageContextStringItemTag)tag +{ + return [[self stringItemsForTag:tag expectingSingleElement:NO] valueForKey:NSStringFromSelector(@selector(value))]; +} + +- (NSString *)stringItemValueForTag:(XMPPMessageContextStringItemTag)tag +{ + return [[self stringItemsForTag:tag expectingSingleElement:YES] anyObject].value; +} + +- (NSSet *)timestampItemValuesForTag:(XMPPMessageContextTimestampItemTag)tag +{ + return [[self timestampItemsForTag:tag expectingSingleElement:NO] valueForKey:NSStringFromSelector(@selector(value))]; +} + +- (NSDate *)timestampItemValueForTag:(XMPPMessageContextTimestampItemTag)tag +{ + return [[self timestampItemsForTag:tag expectingSingleElement:YES] anyObject].value; +} + +- (NSSet *)jidItemsForTag:(XMPPMessageContextJIDItemTag)tag expectingSingleElement:(BOOL)isSingleElementExpected +{ + NSSet *filteredSet = [self.jidItems objectsPassingTest:^BOOL(XMPPMessageContextJIDItemCoreDataStorageObject * _Nonnull obj, BOOL * _Nonnull stop) { + BOOL matchesTag = [obj.tag isEqualToString:tag]; +#ifdef NS_BLOCK_ASSERTIONS + if (matchesTag && isSingleElementExpected) { + *stop = YES; + } +#endif + return matchesTag; + }]; + NSAssert(!(isSingleElementExpected && filteredSet.count > 1) , @"Only one item expected"); + return filteredSet; +} + +- (NSSet *)markerItemsForTag:(XMPPMessageContextMarkerItemTag)tag expectingSingleElement:(BOOL)isSingleElementExpected +{ + NSSet *filteredSet = [self.markerItems objectsPassingTest:^BOOL(XMPPMessageContextMarkerItemCoreDataStorageObject * _Nonnull obj, BOOL * _Nonnull stop) { + BOOL matchesTag = [obj.tag isEqualToString:tag]; +#ifdef NS_BLOCK_ASSERTIONS + if (matchesTag && isSingleElementExpected) { + *stop = YES; + } +#endif + return matchesTag; + }]; + NSAssert(!(isSingleElementExpected && filteredSet.count > 1) , @"Only one item expected"); + return filteredSet; +} + +- (NSSet *)stringItemsForTag:(XMPPMessageContextStringItemTag)tag expectingSingleElement:(BOOL)isSingleElementExpected +{ + NSSet *filteredSet = [self.stringItems objectsPassingTest:^BOOL(XMPPMessageContextStringItemCoreDataStorageObject * _Nonnull obj, BOOL * _Nonnull stop) { + BOOL matchesTag = [obj.tag isEqualToString:tag]; +#ifdef NS_BLOCK_ASSERTIONS + if (matchesTag && isSingleElementExpected) { + *stop = YES; + } +#endif + return matchesTag; + }]; + NSAssert(!(isSingleElementExpected && filteredSet.count > 1) , @"Only one item expected"); + return filteredSet; +} + +- (NSSet *)timestampItemsForTag:(XMPPMessageContextTimestampItemTag)tag expectingSingleElement:(BOOL)isSingleElementExpected +{ + NSSet *filteredSet = [self.timestampItems objectsPassingTest:^BOOL(XMPPMessageContextTimestampItemCoreDataStorageObject * _Nonnull obj, BOOL * _Nonnull stop) { + BOOL matchesTag = [obj.tag isEqualToString:tag]; +#ifdef NS_BLOCK_ASSERTIONS + if (matchesTag && isSingleElementExpected) { + *stop = YES; + } +#endif + return matchesTag; + }]; + NSAssert(!(isSingleElementExpected && filteredSet.count > 1) , @"Only one item expected"); + return filteredSet; +} + +@end diff --git a/Extensions/MessageStorage/XMPPMessageCoreDataStorageObject+Protected.h b/Extensions/MessageStorage/XMPPMessageCoreDataStorageObject+Protected.h new file mode 100644 index 0000000000..3e404e1eb5 --- /dev/null +++ b/Extensions/MessageStorage/XMPPMessageCoreDataStorageObject+Protected.h @@ -0,0 +1,78 @@ +#import "XMPPMessageCoreDataStorageObject.h" + +NS_ASSUME_NONNULL_BEGIN + +@class XMPPMessageContextCoreDataStorageObject; + +/// An API to be used by modules to manipulate core message objects. +@interface XMPPMessageCoreDataStorageObject (Protected) + +/// The persistent attribute storing the domain component of @c fromJID property. +@property (nonatomic, copy, nullable) NSString *fromDomain; + +/// The persistent attribute storing the resource component of @c fromJID property. +@property (nonatomic, copy, nullable) NSString *fromResource; + +/// The persistent attribute storing the user component of @c fromJID property. +@property (nonatomic, copy, nullable) NSString *fromUser; + +/// The persistent attribute storing the domain component of @c toJID property. +@property (nonatomic, copy, nullable) NSString *toDomain; + +/// The persistent attribute storing the resource component of @c toJID property. +@property (nonatomic, copy, nullable) NSString *toResource; + +/// The persistent attribute storing the user component of @c toJID property. +@property (nonatomic, copy, nullable) NSString *toUser; + +/// The auxiliary context objects assigned to the message. +@property (nonatomic, copy, nullable) NSSet *contextElements; + +/// @brief Returns the message object from the given context that has a stream element event with the given ID recorded. +/// @discussion As the stream element event IDs are expected to be unique, this method will trigger an assertion if more than one matching object is found. ++ (nullable XMPPMessageCoreDataStorageObject *)findWithStreamEventID:(NSString *)streamEventID + inManagedObjectContext:(NSManagedObjectContext *)managedObjectContext; + +/// @brief Records stream element event properties for the incoming message. +/// @discussion This method will trigger an assertion unless invoked on an incoming message. It will also trigger an assertion when invoked more than once. +- (void)registerIncomingMessageStreamEventID:(NSString *)streamEventID + streamJID:(XMPPJID *)streamJID + streamEventTimestamp:(NSDate *)streamEventTimestamp; + +/// @brief Records the core RFC 3921/6121 properties of an incoming message from the given XML representation. +/// @discussion This method will trigger an assertion unless invoked on an incoming message. Subsequent invocations will overwrite previous values. +- (void)registerIncomingMessageCore:(XMPPMessage *)message; + +/** + Records stream element event properties for the sent message that has a pending outgoing event registration. + + This method will trigger an assertion unless invoked on an outgoing message. + It will also trigger an assertion if not matched with a prior @c registerOutgoingMessageStreamEventID: invocation. + */ +- (void)registerOutgoingMessageStreamJID:(XMPPJID *)streamJID streamEventTimestamp:(NSDate *)streamEventTimestamp; + +/** + Retires the current stream element event timestamp or marks the initial timestamp for retirement if no timestamp is currently registered. + + A single message object can be associated with multiple timestamps, e.g. there can be several transmission attempts for an outgoing message + or a message can have both stream timestamp and delayed delivery timestamp assigned. + + At the same time, a common application use case involves fetching temporally ordered messages. In terms of the message storage Core Data model, + this translates to fetching timestamp context values with specific tags and then looking up the message objects they are attached to. + + For this approach to work, there needs to be at most one timestamp per message that meets the fetch criteria; retiring stream timestamps allows to exclude duplicates. + */ +- (void)retireStreamTimestamp; + +@end + +@interface XMPPMessageCoreDataStorageObject (CoreDataGeneratedRelationshipAccesssors) + +- (void)addContextElementsObject:(XMPPMessageContextCoreDataStorageObject *)value; +- (void)removeContextElementsObject:(XMPPMessageContextCoreDataStorageObject *)value; +- (void)addContextElements:(NSSet *)value; +- (void)removeContextElements:(NSSet *)value; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Extensions/MessageStorage/XMPPMessageCoreDataStorageObject.h b/Extensions/MessageStorage/XMPPMessageCoreDataStorageObject.h new file mode 100644 index 0000000000..87f5c8a606 --- /dev/null +++ b/Extensions/MessageStorage/XMPPMessageCoreDataStorageObject.h @@ -0,0 +1,86 @@ +#import + +NS_ASSUME_NONNULL_BEGIN + +@class XMPPJID, XMPPMessage; + +typedef NS_ENUM(int16_t, XMPPMessageDirection) { + /// A value indicating that the message's origin is not defined. + XMPPMessageDirectionUnspecified, + /// A value indicating that the message has been received from the stream. + XMPPMessageDirectionIncoming, + /// A value indicating that the message is originating from the device. + XMPPMessageDirectionOutgoing +}; + +typedef NS_ENUM(int16_t, XMPPMessageType) { + /// A value indicating normal message type as per RFC 3921/6121 + XMPPMessageTypeNormal, + /// A value indicating chat message type as per RFC 3921/6121 + XMPPMessageTypeChat, + /// A value indicating error message type as per RFC 3921/6121 + XMPPMessageTypeError, + /// A value indicating groupchat message type as per RFC 3921/6121 + XMPPMessageTypeGroupchat, + /// A value indicating headline message type as per RFC 3921/6121 + XMPPMessageTypeHeadline +}; + +/** + An object storing the core XMPP message properties defined in RFC 3921/6121. + + @see XMPPMessageCoreDataStorage + @see XMPPMessageContextCoreDataStorageObject + @see XMPPMessageContextItemCoreDataStorageObject + */ +@interface XMPPMessageCoreDataStorageObject : NSManagedObject + +/// The value of "from" attribute (transient). +@property (nonatomic, strong, nullable) XMPPJID *fromJID; + +/// The value of "to" attribute (transient). +@property (nonatomic, strong, nullable) XMPPJID *toJID; + +/// The contents of "body" child element. +@property (nonatomic, copy, nullable) NSString *body; + +/// The value of "id" attribute. +@property (nonatomic, copy, nullable) NSString *stanzaID; + +/// The contents of "subject" child element. +@property (nonatomic, copy, nullable) NSString *subject; + +/// The contents of "thread" child element. +@property (nonatomic, copy, nullable) NSString *thread; + +/// The transmission direction from client's point of view. +@property (nonatomic, assign) XMPPMessageDirection direction; + +/// The value of "type" attribute. +@property (nonatomic, assign) XMPPMessageType type; + +/// @brief Returns the XML representation of the message including only the core RFC 3921/6121 properties. +/// @discussion Applications employing store-then-send approach to messaging can use this method to obtain the seed of an outgoing message stanza they later decorate with extension-derived values. +- (XMPPMessage *)coreMessage; + +/** + Records a unique outgoing XMPP stream element event ID for the message. + + After recording the ID, the application should use the @c sendElement:registeringEventWithID:andGetReceipt: method to send the message, providing the recorded value. + This way, modules will be able to track the message in their stream callbacks and update the storage accordingly. + + This method will trigger an assertion unless invoked on an outgoing message. It will also trigger an assertion if called more than once per actual transmission attempt. + */ +- (void)registerOutgoingMessageStreamEventID:(NSString *)outgoingMessageStreamEventID; + +/// @brief Returns the local stream JID for the most recent stream element event associated with the message. +/// @discussion Incoming messages always have a single stream element event associated with them. Outgoing messages can have 0 or more, one per each transmission attempt. +- (nullable XMPPJID *)streamJID; + +/// @brief Returns the timestamp for the most recent stream element event associated with the message. +/// @discussion Incoming messages always have a single stream element event associated with them. Outgoing messages can have 0 or more, one per each transmission attempt. +- (nullable NSDate *)streamTimestamp; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Extensions/MessageStorage/XMPPMessageCoreDataStorageObject.m b/Extensions/MessageStorage/XMPPMessageCoreDataStorageObject.m new file mode 100644 index 0000000000..6e24e9d4d6 --- /dev/null +++ b/Extensions/MessageStorage/XMPPMessageCoreDataStorageObject.m @@ -0,0 +1,527 @@ +#import "XMPPMessageCoreDataStorageObject.h" +#import "XMPPMessageCoreDataStorageObject+Protected.h" +#import "XMPPMessageCoreDataStorageObject+ContextHelpers.h" +#import "XMPPMessageContextItemCoreDataStorageObject.h" +#import "NSManagedObject+XMPPCoreDataStorage.h" +#import "XMPPJID.h" +#import "XMPPMessage.h" + +static XMPPMessageContextJIDItemTag const XMPPMessageContextStreamJIDTag = @"XMPPMessageContextStreamJID"; +static XMPPMessageContextMarkerItemTag const XMPPMessageContextPendingStreamContextAssignmentTag = @"XMPPMessageContextPendingStreamContextAssignment"; +static XMPPMessageContextMarkerItemTag const XMPPMessageContextLatestStreamTimestampRetirementTag = @"XMPPMessageContextLatestStreamTimestampRetirement"; +static XMPPMessageContextStringItemTag const XMPPMessageContextStreamEventIDTag = @"XMPPMessageContextStreamEventID"; +static XMPPMessageContextTimestampItemTag const XMPPMessageContextActiveStreamTimestampTag = @"XMPPMessageContextActiveStreamTimestamp"; +static XMPPMessageContextTimestampItemTag const XMPPMessageContextRetiredStreamTimestampTag = @"XMPPMessageContextRetiredStreamTimestamp"; + +@interface XMPPMessageCoreDataStorageObject () + +@property (nonatomic, copy, nullable) NSString *fromDomain; +@property (nonatomic, copy, nullable) NSString *fromResource; +@property (nonatomic, copy, nullable) NSString *fromUser; +@property (nonatomic, copy, nullable) NSString *toDomain; +@property (nonatomic, copy, nullable) NSString *toResource; +@property (nonatomic, copy, nullable) NSString *toUser; + +@property (nonatomic, copy, nullable) NSSet *contextElements; + +@end + +@interface XMPPMessageCoreDataStorageObject (CoreDataGeneratedPrimitiveAccessors) + +- (XMPPJID *)primitiveFromJID; +- (void)setPrimitiveFromJID:(XMPPJID *)value; +- (void)setPrimitiveFromDomain:(NSString *)value; +- (void)setPrimitiveFromResource:(NSString *)value; +- (void)setPrimitiveFromUser:(NSString *)value; + +- (XMPPJID *)primitiveToJID; +- (void)setPrimitiveToJID:(XMPPJID *)value; +- (void)setPrimitiveToDomain:(NSString *)value; +- (void)setPrimitiveToResource:(NSString *)value; +- (void)setPrimitiveToUser:(NSString *)value; + +@end + +@implementation XMPPMessageCoreDataStorageObject + +@dynamic fromDomain, fromResource, fromUser, toDomain, toResource, toUser, body, stanzaID, subject, thread, direction, type, contextElements; + +#pragma mark - fromJID transient property + +- (XMPPJID *)fromJID +{ + [self willAccessValueForKey:NSStringFromSelector(@selector(fromJID))]; + XMPPJID *fromJID = [self primitiveFromJID]; + [self didAccessValueForKey:NSStringFromSelector(@selector(fromJID))]; + + if (fromJID) { + return fromJID; + } + + XMPPJID *newFromJID = [XMPPJID jidWithUser:self.fromUser domain:self.fromDomain resource:self.fromResource]; + [self setPrimitiveFromJID:newFromJID]; + + return newFromJID; +} + +- (void)setFromJID:(XMPPJID *)fromJID +{ + if ([self.fromJID isEqualToJID:fromJID options:XMPPJIDCompareFull]) { + return; + } + + [self willChangeValueForKey:NSStringFromSelector(@selector(fromJID))]; + [self willChangeValueForKey:NSStringFromSelector(@selector(fromDomain))]; + [self willChangeValueForKey:NSStringFromSelector(@selector(fromResource))]; + [self willChangeValueForKey:NSStringFromSelector(@selector(fromUser))]; + [self setPrimitiveFromJID:fromJID]; + [self setPrimitiveFromDomain:fromJID.domain]; + [self setPrimitiveFromResource:fromJID.resource]; + [self setPrimitiveFromUser:fromJID.user]; + [self didChangeValueForKey:NSStringFromSelector(@selector(fromDomain))]; + [self didChangeValueForKey:NSStringFromSelector(@selector(fromResource))]; + [self didChangeValueForKey:NSStringFromSelector(@selector(fromUser))]; + [self didChangeValueForKey:NSStringFromSelector(@selector(fromJID))]; +} + +- (void)setFromDomain:(NSString *)fromDomain +{ + if ([self.fromDomain isEqualToString:fromDomain]) { + return; + } + + [self willChangeValueForKey:NSStringFromSelector(@selector(fromDomain))]; + [self willChangeValueForKey:NSStringFromSelector(@selector(fromJID))]; + [self setPrimitiveFromDomain:fromDomain]; + [self setPrimitiveFromJID:nil]; + [self didChangeValueForKey:NSStringFromSelector(@selector(fromJID))]; + [self didChangeValueForKey:NSStringFromSelector(@selector(fromDomain))]; +} + +- (void)setFromResource:(NSString *)fromResource +{ + if ([self.fromResource isEqualToString:fromResource]) { + return; + } + + [self willChangeValueForKey:NSStringFromSelector(@selector(fromResource))]; + [self willChangeValueForKey:NSStringFromSelector(@selector(fromJID))]; + [self setPrimitiveFromResource:fromResource]; + [self setPrimitiveFromJID:nil]; + [self didChangeValueForKey:NSStringFromSelector(@selector(fromJID))]; + [self didChangeValueForKey:NSStringFromSelector(@selector(fromResource))]; +} + +- (void)setFromUser:(NSString *)fromUser +{ + if ([self.fromUser isEqualToString:fromUser]) { + return; + } + + [self willChangeValueForKey:NSStringFromSelector(@selector(fromUser))]; + [self willChangeValueForKey:NSStringFromSelector(@selector(fromJID))]; + [self setPrimitiveFromUser:fromUser]; + [self setPrimitiveFromJID:nil]; + [self didChangeValueForKey:NSStringFromSelector(@selector(fromJID))]; + [self didChangeValueForKey:NSStringFromSelector(@selector(fromUser))]; +} + +#pragma mark - toJID transient property + +- (XMPPJID *)toJID +{ + [self willAccessValueForKey:NSStringFromSelector(@selector(toJID))]; + XMPPJID *toJID = [self primitiveToJID]; + [self didAccessValueForKey:NSStringFromSelector(@selector(toJID))]; + + if (toJID) { + return toJID; + } + + XMPPJID *newToJID = [XMPPJID jidWithUser:self.toUser domain:self.toDomain resource:self.toResource]; + [self setPrimitiveToJID:newToJID]; + + return newToJID; +} + +- (void)setToJID:(XMPPJID *)toJID +{ + if ([self.toJID isEqualToJID:toJID options:XMPPJIDCompareFull]) { + return; + } + + [self willChangeValueForKey:NSStringFromSelector(@selector(toJID))]; + [self willChangeValueForKey:NSStringFromSelector(@selector(toDomain))]; + [self willChangeValueForKey:NSStringFromSelector(@selector(toResource))]; + [self willChangeValueForKey:NSStringFromSelector(@selector(toUser))]; + [self setPrimitiveToJID:toJID]; + [self setPrimitiveToDomain:toJID.domain]; + [self setPrimitiveToResource:toJID.resource]; + [self setPrimitiveToUser:toJID.user]; + [self didChangeValueForKey:NSStringFromSelector(@selector(toDomain))]; + [self didChangeValueForKey:NSStringFromSelector(@selector(toResource))]; + [self didChangeValueForKey:NSStringFromSelector(@selector(toUser))]; + [self didChangeValueForKey:NSStringFromSelector(@selector(toJID))]; +} + +- (void)setToDomain:(NSString *)toDomain +{ + if ([self.toDomain isEqualToString:toDomain]) { + return; + } + + [self willChangeValueForKey:NSStringFromSelector(@selector(toDomain))]; + [self willChangeValueForKey:NSStringFromSelector(@selector(toJID))]; + [self setPrimitiveToDomain:toDomain]; + [self setPrimitiveToJID:nil]; + [self didChangeValueForKey:NSStringFromSelector(@selector(toJID))]; + [self didChangeValueForKey:NSStringFromSelector(@selector(toDomain))]; +} + +- (void)setToResource:(NSString *)toResource +{ + if ([self.toResource isEqualToString:toResource]) { + return; + } + + [self willChangeValueForKey:NSStringFromSelector(@selector(toResource))]; + [self willChangeValueForKey:NSStringFromSelector(@selector(toJID))]; + [self setPrimitiveToResource:toResource]; + [self setPrimitiveToJID:nil]; + [self didChangeValueForKey:NSStringFromSelector(@selector(toJID))]; + [self didChangeValueForKey:NSStringFromSelector(@selector(toResource))]; +} + +- (void)setToUser:(NSString *)toUser +{ + if ([self.toUser isEqualToString:toUser]) { + return; + } + + [self willChangeValueForKey:NSStringFromSelector(@selector(toUser))]; + [self willChangeValueForKey:NSStringFromSelector(@selector(toJID))]; + [self setPrimitiveToUser:toUser]; + [self setPrimitiveToJID:nil]; + [self didChangeValueForKey:NSStringFromSelector(@selector(toJID))]; + [self didChangeValueForKey:NSStringFromSelector(@selector(toUser))]; +} + +#pragma mark - Public + ++ (XMPPMessageCoreDataStorageObject *)findWithStreamEventID:(NSString *)streamEventID inManagedObjectContext:(NSManagedObjectContext *)managedObjectContext +{ + NSFetchRequest *fetchRequest = [XMPPMessageContextStringItemCoreDataStorageObject xmpp_fetchRequestInManagedObjectContext:managedObjectContext]; + NSArray *predicates = @[[XMPPMessageContextStringItemCoreDataStorageObject stringPredicateWithValue:streamEventID], + [XMPPMessageContextStringItemCoreDataStorageObject tagPredicateWithValue:XMPPMessageContextStreamEventIDTag]]; + fetchRequest.predicate = [NSCompoundPredicate andPredicateWithSubpredicates:predicates]; + + NSArray *fetchResult = [managedObjectContext xmpp_executeForcedSuccessFetchRequest:fetchRequest]; + NSAssert(fetchResult.count <= 1, @"Expected a single context item for any given stream event ID"); + + return fetchResult.firstObject.contextElement.message; +} + +- (XMPPMessage *)coreMessage +{ + NSString *typeString; + switch (self.type) { + case XMPPMessageTypeChat: + typeString = @"chat"; + break; + + case XMPPMessageTypeError: + typeString = @"error"; + break; + + case XMPPMessageTypeGroupchat: + typeString = @"groupchat"; + break; + + case XMPPMessageTypeHeadline: + typeString = @"headline"; + break; + + case XMPPMessageTypeNormal: + typeString = @"normal"; + break; + } + + XMPPMessage *message = [[XMPPMessage alloc] initWithType:typeString to:self.toJID elementID:self.stanzaID]; + + if (self.body) { + [message addBody:self.body]; + } + if (self.subject) { + [message addSubject:self.subject]; + } + if (self.thread) { + [message addThread:self.thread]; + } + + return message; +} + +- (void)registerIncomingMessageCore:(XMPPMessage *)message +{ + NSAssert(self.direction == XMPPMessageDirectionIncoming, @"Only applicable to incoming message objects"); + + self.fromJID = [message from]; + self.toJID = [message to]; + self.body = [message body]; + self.stanzaID = [message elementID]; + self.subject = [message subject]; + self.thread = [message thread]; + + if ([[message type] isEqualToString:@"chat"]) { + self.type = XMPPMessageTypeChat; + } else if ([[message type] isEqualToString:@"error"]) { + self.type = XMPPMessageTypeError; + } else if ([[message type] isEqualToString:@"groupchat"]) { + self.type = XMPPMessageTypeGroupchat; + } else if ([[message type] isEqualToString:@"headline"]) { + self.type = XMPPMessageTypeHeadline; + } else { + self.type = XMPPMessageTypeNormal; + } +} + +- (void)registerIncomingMessageStreamEventID:(NSString *)streamEventID streamJID:(XMPPJID *)streamJID streamEventTimestamp:(NSDate *)streamEventTimestamp +{ + NSAssert(self.direction == XMPPMessageDirectionIncoming, @"Only applicable to incoming message objects"); + NSAssert(![self lookupCurrentStreamContext], @"Another stream context element already exists"); + + XMPPMessageContextCoreDataStorageObject *streamContext = [self appendContextElement]; + [streamContext appendStringItemWithTag:XMPPMessageContextStreamEventIDTag value:streamEventID]; + [streamContext appendJIDItemWithTag:XMPPMessageContextStreamJIDTag value:streamJID]; + [streamContext appendTimestampItemWithTag:XMPPMessageContextActiveStreamTimestampTag value:streamEventTimestamp]; +} + +- (void)registerOutgoingMessageStreamEventID:(NSString *)outgoingMessageStreamEventID +{ + NSAssert(self.direction == XMPPMessageDirectionOutgoing, @"Only applicable to outgoing message objects"); + NSAssert(![self lookupPendingStreamContext], @"Pending stream context element already exists"); + + XMPPMessageContextCoreDataStorageObject *streamContext = [self appendContextElement]; + [streamContext appendStringItemWithTag:XMPPMessageContextStreamEventIDTag value:outgoingMessageStreamEventID]; + [streamContext appendMarkerItemWithTag:XMPPMessageContextPendingStreamContextAssignmentTag]; +} + +- (void)registerOutgoingMessageStreamJID:(XMPPJID *)streamJID streamEventTimestamp:(NSDate *)streamEventTimestamp +{ + NSAssert(self.direction == XMPPMessageDirectionOutgoing, @"Only applicable to outgoing message objects"); + + XMPPMessageContextCoreDataStorageObject *streamContext = [self lookupPendingStreamContext]; + NSAssert(streamContext, @"No pending stream context element found"); + + XMPPMessageContextTimestampItemTag timestampTag; + if ([self lookupActiveStreamContext]) { + [self retireStreamTimestamp]; + timestampTag = XMPPMessageContextActiveStreamTimestampTag; + } else if (![self lookupLatestRetiredStreamContext]) { + timestampTag = XMPPMessageContextActiveStreamTimestampTag; + } else { + timestampTag = XMPPMessageContextRetiredStreamTimestampTag; + } + + [streamContext removeMarkerItemsWithTag:XMPPMessageContextPendingStreamContextAssignmentTag]; + [streamContext appendJIDItemWithTag:XMPPMessageContextStreamJIDTag value:streamJID]; + [streamContext appendTimestampItemWithTag:timestampTag value:streamEventTimestamp]; +} + +- (XMPPJID *)streamJID +{ + return [[self lookupCurrentStreamContext] jidItemValueForTag:XMPPMessageContextStreamJIDTag]; +} + +- (NSDate *)streamTimestamp +{ + XMPPMessageContextCoreDataStorageObject *latestStreamContext = [self lookupCurrentStreamContext]; + return [latestStreamContext timestampItemValueForTag:XMPPMessageContextActiveStreamTimestampTag] ?: [latestStreamContext timestampItemValueForTag:XMPPMessageContextRetiredStreamTimestampTag]; +} + +- (void)retireStreamTimestamp +{ + XMPPMessageContextCoreDataStorageObject *previousRetiredStreamContext = [self lookupLatestRetiredStreamContext]; + XMPPMessageContextCoreDataStorageObject *activeStreamContext = [self lookupActiveStreamContext]; + + if (activeStreamContext) { + [previousRetiredStreamContext removeMarkerItemsWithTag:XMPPMessageContextLatestStreamTimestampRetirementTag]; + + NSDate *retiredStreamTimestamp = [activeStreamContext timestampItemValueForTag:XMPPMessageContextActiveStreamTimestampTag]; + [activeStreamContext removeTimestampItemsWithTag:XMPPMessageContextActiveStreamTimestampTag]; + [activeStreamContext appendTimestampItemWithTag:XMPPMessageContextRetiredStreamTimestampTag value:retiredStreamTimestamp]; + [activeStreamContext appendMarkerItemWithTag:XMPPMessageContextLatestStreamTimestampRetirementTag]; + } else if (!previousRetiredStreamContext) { + XMPPMessageContextCoreDataStorageObject *initialPendingStreamContext = [self lookupPendingStreamContext]; + [initialPendingStreamContext appendMarkerItemWithTag:XMPPMessageContextLatestStreamTimestampRetirementTag]; + } else { + NSAssert(NO, @"No stream context element found for retiring"); + } +} + +#pragma mark - Overridden + +- (void)awakeFromSnapshotEvents:(NSSnapshotEventType)flags +{ + [super awakeFromSnapshotEvents:flags]; + + [self setPrimitiveFromJID:nil]; + [self setPrimitiveToJID:nil]; +} + +#pragma mark - Private + +- (XMPPMessageContextCoreDataStorageObject *)lookupPendingStreamContext +{ + return [self lookupInContextWithBlock:^id _Nullable(XMPPMessageContextCoreDataStorageObject * _Nonnull contextElement) { + return [contextElement hasMarkerItemForTag:XMPPMessageContextPendingStreamContextAssignmentTag] ? contextElement : nil; + }]; +} + +- (XMPPMessageContextCoreDataStorageObject *)lookupCurrentStreamContext +{ + return [self lookupActiveStreamContext] ?: [self lookupLatestRetiredStreamContext]; +} + +- (XMPPMessageContextCoreDataStorageObject *)lookupActiveStreamContext +{ + return [self lookupInContextWithBlock:^id _Nullable(XMPPMessageContextCoreDataStorageObject * _Nonnull contextElement) { + return [contextElement timestampItemValueForTag:XMPPMessageContextActiveStreamTimestampTag] ? contextElement : nil; + }]; +} + +- (XMPPMessageContextCoreDataStorageObject *)lookupLatestRetiredStreamContext +{ + return [self lookupInContextWithBlock:^id _Nullable(XMPPMessageContextCoreDataStorageObject * _Nonnull contextElement) { + return [contextElement hasMarkerItemForTag:XMPPMessageContextLatestStreamTimestampRetirementTag] ? contextElement : nil; + }]; +} + +@end + +@implementation XMPPMessageContextItemCoreDataStorageObject (XMPPMessageCoreDataStorageFetch) + ++ (NSFetchRequest *)requestByTimestampsWithPredicate:(NSPredicate *)predicate inAscendingOrder:(BOOL)isInAscendingOrder fromManagedObjectContext:(NSManagedObjectContext *)managedObjectContext +{ + NSFetchRequest *fetchRequest = [XMPPMessageContextTimestampItemCoreDataStorageObject xmpp_fetchRequestInManagedObjectContext:managedObjectContext]; + fetchRequest.predicate = predicate; + fetchRequest.sortDescriptors = @[[NSSortDescriptor sortDescriptorWithKey:NSStringFromSelector(@selector(value)) ascending:isInAscendingOrder]]; + return fetchRequest; +} + ++ (NSPredicate *)streamTimestampKindPredicate +{ + return [XMPPMessageContextTimestampItemCoreDataStorageObject tagPredicateWithValue:XMPPMessageContextActiveStreamTimestampTag]; +} + ++ (NSPredicate *)timestampRangePredicateWithStartValue:(nullable NSDate *)startValue endValue:(nullable NSDate *)endValue +{ + return [XMPPMessageContextTimestampItemCoreDataStorageObject timestampRangePredicateWithStartValue:startValue endValue:endValue]; +} + ++ (NSPredicate *)messageFromJIDPredicateWithValue:(XMPPJID *)value compareOptions:(XMPPJIDCompareOptions)compareOptions +{ + return [self xmpp_jidPredicateWithDomainKeyPath:[[self messageKeyPath] stringByAppendingFormat:@".%@", NSStringFromSelector(@selector(fromDomain))] + resourceKeyPath:[[self messageKeyPath] stringByAppendingFormat:@".%@", NSStringFromSelector(@selector(fromResource))] + userKeyPath:[[self messageKeyPath] stringByAppendingFormat:@".%@", NSStringFromSelector(@selector(fromUser))] + value:value + compareOptions:compareOptions]; +} + ++ (NSPredicate *)messageToJIDPredicateWithValue:(XMPPJID *)value compareOptions:(XMPPJIDCompareOptions)compareOptions +{ + return [self xmpp_jidPredicateWithDomainKeyPath:[[self messageKeyPath] stringByAppendingFormat:@".%@", NSStringFromSelector(@selector(toDomain))] + resourceKeyPath:[[self messageKeyPath] stringByAppendingFormat:@".%@", NSStringFromSelector(@selector(toResource))] + userKeyPath:[[self messageKeyPath] stringByAppendingFormat:@".%@", NSStringFromSelector(@selector(toUser))] + value:value + compareOptions:compareOptions]; +} + ++ (NSPredicate *)messageRemotePartyJIDPredicateWithValue:(XMPPJID *)value compareOptions:(XMPPJIDCompareOptions)compareOptions +{ + NSArray *outgoingMessagePredicates = @[[self messageToJIDPredicateWithValue:value compareOptions:compareOptions], + [XMPPMessageContextItemCoreDataStorageObject messageDirectionPredicateWithValue:XMPPMessageDirectionOutgoing]]; + NSArray *incomingMessagePredicates = @[[self messageFromJIDPredicateWithValue:value compareOptions:compareOptions], + [XMPPMessageContextItemCoreDataStorageObject messageDirectionPredicateWithValue:XMPPMessageDirectionIncoming]]; + + return [NSCompoundPredicate orPredicateWithSubpredicates:@[[NSCompoundPredicate andPredicateWithSubpredicates:outgoingMessagePredicates], + [NSCompoundPredicate andPredicateWithSubpredicates:incomingMessagePredicates]]]; +} + ++ (NSPredicate *)messageBodyPredicateWithValue:(NSString *)value compareOperator:(XMPPMessageContentCompareOperator)compareOperator options:(XMPPMessageContentCompareOptions)options +{ + return [self messageContentPredicateWithKey:NSStringFromSelector(@selector(body)) value:value compareOperator:compareOperator options:options]; +} + ++ (NSPredicate *)messageSubjectPredicateWithValue:(NSString *)value compareOperator:(XMPPMessageContentCompareOperator)compareOperator options:(XMPPMessageContentCompareOptions)options +{ + return [self messageContentPredicateWithKey:NSStringFromSelector(@selector(subject)) value:value compareOperator:compareOperator options:options]; +} + ++ (NSPredicate *)messageThreadPredicateWithValue:(NSString *)value +{ + return [NSPredicate predicateWithFormat:@"%K.%K = %@", [self messageKeyPath], NSStringFromSelector(@selector(thread)), value]; +} + ++ (NSPredicate *)messageDirectionPredicateWithValue:(XMPPMessageDirection)value +{ + return [NSPredicate predicateWithFormat:@"%K.%K = %d", [self messageKeyPath], NSStringFromSelector(@selector(direction)), value]; +} + ++ (NSPredicate *)messageTypePredicateWithValue:(XMPPMessageType)value +{ + return [NSPredicate predicateWithFormat:@"%K.%K = %d", [self messageKeyPath], NSStringFromSelector(@selector(type)), value]; +} + ++ (NSPredicate *)messageContentPredicateWithKey:(NSString *)contentKey value:(NSString *)value compareOperator:(XMPPMessageContentCompareOperator)compareOperator options:(XMPPMessageContentCompareOptions)options +{ + NSMutableString *predicateFormat = [[NSMutableString alloc] initWithFormat:@"%@.%@ ", [self messageKeyPath], contentKey]; + + switch (compareOperator) { + case XMPPMessageContentCompareOperatorEquals: + [predicateFormat appendString:@"= "]; + break; + case XMPPMessageContentCompareOperatorBeginsWith: + [predicateFormat appendString:@"BEGINSWITH "]; + break; + case XMPPMessageContentCompareOperatorContains: + [predicateFormat appendString:@"CONTAINS "]; + break; + case XMPPMessageContentCompareOperatorEndsWith: + [predicateFormat appendString:@"ENDSWITH "]; + break; + case XMPPMessageContentCompareOperatorLike: + [predicateFormat appendString:@"LIKE "]; + break; + case XMPPMessageContentCompareOperatorMatches: + [predicateFormat appendString:@"MATCHES "]; + break; + } + + NSMutableString *optionString = [[NSMutableString alloc] init]; + if (options & XMPPMessageContentCompareCaseInsensitive) { + [optionString appendString:@"c"]; + } + if (options & XMPPMessageContentCompareDiacriticInsensitive) { + [optionString appendString:@"d"]; + } + if (optionString.length > 0) { + [predicateFormat appendFormat:@"[%@] ", optionString]; + } + + [predicateFormat appendString:@"%@"]; + + return [NSPredicate predicateWithFormat:predicateFormat, value]; +} + ++ (NSString *)messageKeyPath +{ + return [NSString stringWithFormat:@"%@.%@", NSStringFromSelector(@selector(contextElement)), NSStringFromSelector(@selector(message))]; +} + +- (XMPPMessageCoreDataStorageObject *)message +{ + return self.contextElement.message; +} + +@end diff --git a/XMPPFramework.xcodeproj/project.pbxproj b/XMPPFramework.xcodeproj/project.pbxproj index a48131ff9f..c61da53e1a 100644 --- a/XMPPFramework.xcodeproj/project.pbxproj +++ b/XMPPFramework.xcodeproj/project.pbxproj @@ -938,6 +938,42 @@ D9E6A05A1F92DEE100D8BFCB /* XMPPStanzaIdModule.m in Sources */ = {isa = PBXBuildFile; fileRef = D9E6A0561F92DEE100D8BFCB /* XMPPStanzaIdModule.m */; }; D9E6A05B1F92DEE100D8BFCB /* XMPPStanzaIdModule.m in Sources */ = {isa = PBXBuildFile; fileRef = D9E6A0561F92DEE100D8BFCB /* XMPPStanzaIdModule.m */; }; D9E6A05C1F92DEE100D8BFCB /* XMPPStanzaIdModule.m in Sources */ = {isa = PBXBuildFile; fileRef = D9E6A0561F92DEE100D8BFCB /* XMPPStanzaIdModule.m */; }; + DD1784121F3C9FA800D662A6 /* XMPPMessage.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = DD1784061F3C9FA800D662A6 /* XMPPMessage.xcdatamodeld */; }; + DD1784131F3C9FA800D662A6 /* XMPPMessage.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = DD1784061F3C9FA800D662A6 /* XMPPMessage.xcdatamodeld */; }; + DD1784141F3C9FA800D662A6 /* XMPPMessage.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = DD1784061F3C9FA800D662A6 /* XMPPMessage.xcdatamodeld */; }; + DD1784151F3C9FA800D662A6 /* XMPPMessageCoreDataStorageObject.h in Headers */ = {isa = PBXBuildFile; fileRef = DD1784081F3C9FA800D662A6 /* XMPPMessageCoreDataStorageObject.h */; settings = {ATTRIBUTES = (Public, ); }; }; + DD1784161F3C9FA800D662A6 /* XMPPMessageCoreDataStorageObject.h in Headers */ = {isa = PBXBuildFile; fileRef = DD1784081F3C9FA800D662A6 /* XMPPMessageCoreDataStorageObject.h */; settings = {ATTRIBUTES = (Public, ); }; }; + DD1784171F3C9FA800D662A6 /* XMPPMessageCoreDataStorageObject.h in Headers */ = {isa = PBXBuildFile; fileRef = DD1784081F3C9FA800D662A6 /* XMPPMessageCoreDataStorageObject.h */; settings = {ATTRIBUTES = (Public, ); }; }; + DD1784181F3C9FA800D662A6 /* XMPPMessageCoreDataStorageObject.m in Sources */ = {isa = PBXBuildFile; fileRef = DD1784091F3C9FA800D662A6 /* XMPPMessageCoreDataStorageObject.m */; }; + DD1784191F3C9FA800D662A6 /* XMPPMessageCoreDataStorageObject.m in Sources */ = {isa = PBXBuildFile; fileRef = DD1784091F3C9FA800D662A6 /* XMPPMessageCoreDataStorageObject.m */; }; + DD17841A1F3C9FA800D662A6 /* XMPPMessageCoreDataStorageObject.m in Sources */ = {isa = PBXBuildFile; fileRef = DD1784091F3C9FA800D662A6 /* XMPPMessageCoreDataStorageObject.m */; }; + DD17841B1F3C9FA800D662A6 /* XMPPMessageContextCoreDataStorageObject.h in Headers */ = {isa = PBXBuildFile; fileRef = DD17840A1F3C9FA800D662A6 /* XMPPMessageContextCoreDataStorageObject.h */; settings = {ATTRIBUTES = (Public, ); }; }; + DD17841C1F3C9FA800D662A6 /* XMPPMessageContextCoreDataStorageObject.h in Headers */ = {isa = PBXBuildFile; fileRef = DD17840A1F3C9FA800D662A6 /* XMPPMessageContextCoreDataStorageObject.h */; settings = {ATTRIBUTES = (Public, ); }; }; + DD17841D1F3C9FA800D662A6 /* XMPPMessageContextCoreDataStorageObject.h in Headers */ = {isa = PBXBuildFile; fileRef = DD17840A1F3C9FA800D662A6 /* XMPPMessageContextCoreDataStorageObject.h */; settings = {ATTRIBUTES = (Public, ); }; }; + DD17841E1F3C9FA800D662A6 /* XMPPMessageContextCoreDataStorageObject.m in Sources */ = {isa = PBXBuildFile; fileRef = DD17840B1F3C9FA800D662A6 /* XMPPMessageContextCoreDataStorageObject.m */; }; + DD17841F1F3C9FA800D662A6 /* XMPPMessageContextCoreDataStorageObject.m in Sources */ = {isa = PBXBuildFile; fileRef = DD17840B1F3C9FA800D662A6 /* XMPPMessageContextCoreDataStorageObject.m */; }; + DD1784201F3C9FA800D662A6 /* XMPPMessageContextCoreDataStorageObject.m in Sources */ = {isa = PBXBuildFile; fileRef = DD17840B1F3C9FA800D662A6 /* XMPPMessageContextCoreDataStorageObject.m */; }; + DD1784211F3C9FA800D662A6 /* XMPPMessageCoreDataStorage.h in Headers */ = {isa = PBXBuildFile; fileRef = DD17840C1F3C9FA800D662A6 /* XMPPMessageCoreDataStorage.h */; settings = {ATTRIBUTES = (Public, ); }; }; + DD1784221F3C9FA800D662A6 /* XMPPMessageCoreDataStorage.h in Headers */ = {isa = PBXBuildFile; fileRef = DD17840C1F3C9FA800D662A6 /* XMPPMessageCoreDataStorage.h */; settings = {ATTRIBUTES = (Public, ); }; }; + DD1784231F3C9FA800D662A6 /* XMPPMessageCoreDataStorage.h in Headers */ = {isa = PBXBuildFile; fileRef = DD17840C1F3C9FA800D662A6 /* XMPPMessageCoreDataStorage.h */; settings = {ATTRIBUTES = (Public, ); }; }; + DD1784241F3C9FA800D662A6 /* XMPPMessageCoreDataStorage.m in Sources */ = {isa = PBXBuildFile; fileRef = DD17840D1F3C9FA800D662A6 /* XMPPMessageCoreDataStorage.m */; }; + DD1784251F3C9FA800D662A6 /* XMPPMessageCoreDataStorage.m in Sources */ = {isa = PBXBuildFile; fileRef = DD17840D1F3C9FA800D662A6 /* XMPPMessageCoreDataStorage.m */; }; + DD1784261F3C9FA800D662A6 /* XMPPMessageCoreDataStorage.m in Sources */ = {isa = PBXBuildFile; fileRef = DD17840D1F3C9FA800D662A6 /* XMPPMessageCoreDataStorage.m */; }; + DD19E4011F8CA02000CED8EF /* XMPPMessageContextItemCoreDataStorageObject+Protected.h in Headers */ = {isa = PBXBuildFile; fileRef = DD19E4001F8C9FCB00CED8EF /* XMPPMessageContextItemCoreDataStorageObject+Protected.h */; settings = {ATTRIBUTES = (Public, ); }; }; + DD19E4021F8CA02000CED8EF /* XMPPMessageContextItemCoreDataStorageObject+Protected.h in Headers */ = {isa = PBXBuildFile; fileRef = DD19E4001F8C9FCB00CED8EF /* XMPPMessageContextItemCoreDataStorageObject+Protected.h */; settings = {ATTRIBUTES = (Public, ); }; }; + DD19E4031F8CA02100CED8EF /* XMPPMessageContextItemCoreDataStorageObject+Protected.h in Headers */ = {isa = PBXBuildFile; fileRef = DD19E4001F8C9FCB00CED8EF /* XMPPMessageContextItemCoreDataStorageObject+Protected.h */; settings = {ATTRIBUTES = (Public, ); }; }; + DD19E4041F8CA06D00CED8EF /* XMPPMessageContextCoreDataStorageObject+Protected.h in Headers */ = {isa = PBXBuildFile; fileRef = DD19E3FF1F8C9F0F00CED8EF /* XMPPMessageContextCoreDataStorageObject+Protected.h */; settings = {ATTRIBUTES = (Public, ); }; }; + DD19E4051F8CA06E00CED8EF /* XMPPMessageContextCoreDataStorageObject+Protected.h in Headers */ = {isa = PBXBuildFile; fileRef = DD19E3FF1F8C9F0F00CED8EF /* XMPPMessageContextCoreDataStorageObject+Protected.h */; settings = {ATTRIBUTES = (Public, ); }; }; + DD19E4061F8CA06F00CED8EF /* XMPPMessageContextCoreDataStorageObject+Protected.h in Headers */ = {isa = PBXBuildFile; fileRef = DD19E3FF1F8C9F0F00CED8EF /* XMPPMessageContextCoreDataStorageObject+Protected.h */; settings = {ATTRIBUTES = (Public, ); }; }; + DD19E40D1F8CA3E200CED8EF /* XMPPMessageCoreDataStorageObject+ContextHelpers.h in Headers */ = {isa = PBXBuildFile; fileRef = DD19E40B1F8CA3E200CED8EF /* XMPPMessageCoreDataStorageObject+ContextHelpers.h */; settings = {ATTRIBUTES = (Public, ); }; }; + DD19E40E1F8CA3E200CED8EF /* XMPPMessageCoreDataStorageObject+ContextHelpers.h in Headers */ = {isa = PBXBuildFile; fileRef = DD19E40B1F8CA3E200CED8EF /* XMPPMessageCoreDataStorageObject+ContextHelpers.h */; settings = {ATTRIBUTES = (Public, ); }; }; + DD19E40F1F8CA3E200CED8EF /* XMPPMessageCoreDataStorageObject+ContextHelpers.h in Headers */ = {isa = PBXBuildFile; fileRef = DD19E40B1F8CA3E200CED8EF /* XMPPMessageCoreDataStorageObject+ContextHelpers.h */; settings = {ATTRIBUTES = (Public, ); }; }; + DD19E4101F8CA3E200CED8EF /* XMPPMessageCoreDataStorageObject+ContextHelpers.m in Sources */ = {isa = PBXBuildFile; fileRef = DD19E40C1F8CA3E200CED8EF /* XMPPMessageCoreDataStorageObject+ContextHelpers.m */; }; + DD19E4111F8CA3E200CED8EF /* XMPPMessageCoreDataStorageObject+ContextHelpers.m in Sources */ = {isa = PBXBuildFile; fileRef = DD19E40C1F8CA3E200CED8EF /* XMPPMessageCoreDataStorageObject+ContextHelpers.m */; }; + DD19E4121F8CA3E200CED8EF /* XMPPMessageCoreDataStorageObject+ContextHelpers.m in Sources */ = {isa = PBXBuildFile; fileRef = DD19E40C1F8CA3E200CED8EF /* XMPPMessageCoreDataStorageObject+ContextHelpers.m */; }; + DD1E12301F5EE6100012A506 /* XMPPMessageCoreDataStorageObject+Protected.h in Headers */ = {isa = PBXBuildFile; fileRef = DD1E122F1F5EE4DA0012A506 /* XMPPMessageCoreDataStorageObject+Protected.h */; settings = {ATTRIBUTES = (Public, ); }; }; + DD1E12311F5EE6110012A506 /* XMPPMessageCoreDataStorageObject+Protected.h in Headers */ = {isa = PBXBuildFile; fileRef = DD1E122F1F5EE4DA0012A506 /* XMPPMessageCoreDataStorageObject+Protected.h */; settings = {ATTRIBUTES = (Public, ); }; }; + DD1E12321F5EE6120012A506 /* XMPPMessageCoreDataStorageObject+Protected.h in Headers */ = {isa = PBXBuildFile; fileRef = DD1E122F1F5EE4DA0012A506 /* XMPPMessageCoreDataStorageObject+Protected.h */; settings = {ATTRIBUTES = (Public, ); }; }; DD1E73331ED885FD009B529B /* XMPPRoomLightCoreDataStorage+XEP_0313.h in Headers */ = {isa = PBXBuildFile; fileRef = DD1E73311ED885FD009B529B /* XMPPRoomLightCoreDataStorage+XEP_0313.h */; settings = {ATTRIBUTES = (Public, ); }; }; DD1E73341ED885FD009B529B /* XMPPRoomLightCoreDataStorage+XEP_0313.h in Headers */ = {isa = PBXBuildFile; fileRef = DD1E73311ED885FD009B529B /* XMPPRoomLightCoreDataStorage+XEP_0313.h */; settings = {ATTRIBUTES = (Public, ); }; }; DD1E73351ED885FD009B529B /* XMPPRoomLightCoreDataStorage+XEP_0313.h in Headers */ = {isa = PBXBuildFile; fileRef = DD1E73311ED885FD009B529B /* XMPPRoomLightCoreDataStorage+XEP_0313.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -947,6 +983,18 @@ DD1E733A1ED88622009B529B /* XMPPRoomLightCoreDataStorageProtected.h in Headers */ = {isa = PBXBuildFile; fileRef = DD1E73391ED88622009B529B /* XMPPRoomLightCoreDataStorageProtected.h */; settings = {ATTRIBUTES = (Public, ); }; }; DD1E733B1ED88622009B529B /* XMPPRoomLightCoreDataStorageProtected.h in Headers */ = {isa = PBXBuildFile; fileRef = DD1E73391ED88622009B529B /* XMPPRoomLightCoreDataStorageProtected.h */; settings = {ATTRIBUTES = (Public, ); }; }; DD1E733C1ED88622009B529B /* XMPPRoomLightCoreDataStorageProtected.h in Headers */ = {isa = PBXBuildFile; fileRef = DD1E73391ED88622009B529B /* XMPPRoomLightCoreDataStorageProtected.h */; settings = {ATTRIBUTES = (Public, ); }; }; + DD1E80671F56AC4D003C21A0 /* XMPPMessageContextItemCoreDataStorageObject.h in Headers */ = {isa = PBXBuildFile; fileRef = DD1E80651F56AC4D003C21A0 /* XMPPMessageContextItemCoreDataStorageObject.h */; settings = {ATTRIBUTES = (Public, ); }; }; + DD1E80681F56AC4D003C21A0 /* XMPPMessageContextItemCoreDataStorageObject.h in Headers */ = {isa = PBXBuildFile; fileRef = DD1E80651F56AC4D003C21A0 /* XMPPMessageContextItemCoreDataStorageObject.h */; settings = {ATTRIBUTES = (Public, ); }; }; + DD1E80691F56AC4D003C21A0 /* XMPPMessageContextItemCoreDataStorageObject.h in Headers */ = {isa = PBXBuildFile; fileRef = DD1E80651F56AC4D003C21A0 /* XMPPMessageContextItemCoreDataStorageObject.h */; settings = {ATTRIBUTES = (Public, ); }; }; + DD1E806A1F56AC4D003C21A0 /* XMPPMessageContextItemCoreDataStorageObject.m in Sources */ = {isa = PBXBuildFile; fileRef = DD1E80661F56AC4D003C21A0 /* XMPPMessageContextItemCoreDataStorageObject.m */; }; + DD1E806B1F56AC4D003C21A0 /* XMPPMessageContextItemCoreDataStorageObject.m in Sources */ = {isa = PBXBuildFile; fileRef = DD1E80661F56AC4D003C21A0 /* XMPPMessageContextItemCoreDataStorageObject.m */; }; + DD1E806C1F56AC4D003C21A0 /* XMPPMessageContextItemCoreDataStorageObject.m in Sources */ = {isa = PBXBuildFile; fileRef = DD1E80661F56AC4D003C21A0 /* XMPPMessageContextItemCoreDataStorageObject.m */; }; + DDFFF40A1F3DE10700B99353 /* NSManagedObject+XMPPCoreDataStorage.h in Headers */ = {isa = PBXBuildFile; fileRef = DDFFF4081F3DE10700B99353 /* NSManagedObject+XMPPCoreDataStorage.h */; settings = {ATTRIBUTES = (Public, ); }; }; + DDFFF40B1F3DE10700B99353 /* NSManagedObject+XMPPCoreDataStorage.h in Headers */ = {isa = PBXBuildFile; fileRef = DDFFF4081F3DE10700B99353 /* NSManagedObject+XMPPCoreDataStorage.h */; settings = {ATTRIBUTES = (Public, ); }; }; + DDFFF40C1F3DE10700B99353 /* NSManagedObject+XMPPCoreDataStorage.h in Headers */ = {isa = PBXBuildFile; fileRef = DDFFF4081F3DE10700B99353 /* NSManagedObject+XMPPCoreDataStorage.h */; settings = {ATTRIBUTES = (Public, ); }; }; + DDFFF40D1F3DE10700B99353 /* NSManagedObject+XMPPCoreDataStorage.m in Sources */ = {isa = PBXBuildFile; fileRef = DDFFF4091F3DE10700B99353 /* NSManagedObject+XMPPCoreDataStorage.m */; }; + DDFFF40E1F3DE10700B99353 /* NSManagedObject+XMPPCoreDataStorage.m in Sources */ = {isa = PBXBuildFile; fileRef = DDFFF4091F3DE10700B99353 /* NSManagedObject+XMPPCoreDataStorage.m */; }; + DDFFF40F1F3DE10700B99353 /* NSManagedObject+XMPPCoreDataStorage.m in Sources */ = {isa = PBXBuildFile; fileRef = DDFFF4091F3DE10700B99353 /* NSManagedObject+XMPPCoreDataStorage.m */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -1543,9 +1591,25 @@ D9DCD6BF1E625B4D0010D1C7 /* AppKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AppKit.framework; path = Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.12.sdk/System/Library/Frameworks/AppKit.framework; sourceTree = DEVELOPER_DIR; }; D9E6A0551F92DEE100D8BFCB /* XMPPStanzaIdModule.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = XMPPStanzaIdModule.h; sourceTree = ""; }; D9E6A0561F92DEE100D8BFCB /* XMPPStanzaIdModule.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = XMPPStanzaIdModule.m; sourceTree = ""; }; + DD1784071F3C9FA800D662A6 /* XMPPMessage.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = XMPPMessage.xcdatamodel; sourceTree = ""; }; + DD1784081F3C9FA800D662A6 /* XMPPMessageCoreDataStorageObject.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = XMPPMessageCoreDataStorageObject.h; sourceTree = ""; }; + DD1784091F3C9FA800D662A6 /* XMPPMessageCoreDataStorageObject.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = XMPPMessageCoreDataStorageObject.m; sourceTree = ""; }; + DD17840A1F3C9FA800D662A6 /* XMPPMessageContextCoreDataStorageObject.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = XMPPMessageContextCoreDataStorageObject.h; sourceTree = ""; }; + DD17840B1F3C9FA800D662A6 /* XMPPMessageContextCoreDataStorageObject.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = XMPPMessageContextCoreDataStorageObject.m; sourceTree = ""; }; + DD17840C1F3C9FA800D662A6 /* XMPPMessageCoreDataStorage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = XMPPMessageCoreDataStorage.h; sourceTree = ""; }; + DD17840D1F3C9FA800D662A6 /* XMPPMessageCoreDataStorage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = XMPPMessageCoreDataStorage.m; sourceTree = ""; }; + DD19E3FF1F8C9F0F00CED8EF /* XMPPMessageContextCoreDataStorageObject+Protected.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "XMPPMessageContextCoreDataStorageObject+Protected.h"; sourceTree = ""; }; + DD19E4001F8C9FCB00CED8EF /* XMPPMessageContextItemCoreDataStorageObject+Protected.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "XMPPMessageContextItemCoreDataStorageObject+Protected.h"; sourceTree = ""; }; + DD19E40B1F8CA3E200CED8EF /* XMPPMessageCoreDataStorageObject+ContextHelpers.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "XMPPMessageCoreDataStorageObject+ContextHelpers.h"; sourceTree = ""; }; + DD19E40C1F8CA3E200CED8EF /* XMPPMessageCoreDataStorageObject+ContextHelpers.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "XMPPMessageCoreDataStorageObject+ContextHelpers.m"; sourceTree = ""; }; + DD1E122F1F5EE4DA0012A506 /* XMPPMessageCoreDataStorageObject+Protected.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "XMPPMessageCoreDataStorageObject+Protected.h"; sourceTree = ""; }; DD1E73311ED885FD009B529B /* XMPPRoomLightCoreDataStorage+XEP_0313.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "XMPPRoomLightCoreDataStorage+XEP_0313.h"; sourceTree = ""; }; DD1E73321ED885FD009B529B /* XMPPRoomLightCoreDataStorage+XEP_0313.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "XMPPRoomLightCoreDataStorage+XEP_0313.m"; sourceTree = ""; }; DD1E73391ED88622009B529B /* XMPPRoomLightCoreDataStorageProtected.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = XMPPRoomLightCoreDataStorageProtected.h; sourceTree = ""; }; + DD1E80651F56AC4D003C21A0 /* XMPPMessageContextItemCoreDataStorageObject.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = XMPPMessageContextItemCoreDataStorageObject.h; sourceTree = ""; }; + DD1E80661F56AC4D003C21A0 /* XMPPMessageContextItemCoreDataStorageObject.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = XMPPMessageContextItemCoreDataStorageObject.m; sourceTree = ""; }; + DDFFF4081F3DE10700B99353 /* NSManagedObject+XMPPCoreDataStorage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSManagedObject+XMPPCoreDataStorage.h"; sourceTree = ""; }; + DDFFF4091F3DE10700B99353 /* NSManagedObject+XMPPCoreDataStorage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSManagedObject+XMPPCoreDataStorage.m"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -1804,6 +1868,7 @@ D9DCD1431E6250930010D1C7 /* Reconnect */, D9DCD1461E6250930010D1C7 /* Roster */, D9DCD15E1E6250930010D1C7 /* SystemInputActivityMonitor */, + DD1784051F3C9FA800D662A6 /* MessageStorage */, D9DCD1611E6250930010D1C7 /* XEP-0009 */, D9DCD1681E6250930010D1C7 /* XEP-0012 */, D9DCD16D1E6250930010D1C7 /* XEP-0016 */, @@ -1863,6 +1928,8 @@ D9DCD1221E6250920010D1C7 /* XMPPCoreDataStorage.h */, D9DCD1231E6250920010D1C7 /* XMPPCoreDataStorage.m */, D9DCD1241E6250920010D1C7 /* XMPPCoreDataStorageProtected.h */, + DDFFF4081F3DE10700B99353 /* NSManagedObject+XMPPCoreDataStorage.h */, + DDFFF4091F3DE10700B99353 /* NSManagedObject+XMPPCoreDataStorage.m */, ); path = CoreDataStorage; sourceTree = ""; @@ -2581,6 +2648,27 @@ name = Frameworks; sourceTree = ""; }; + DD1784051F3C9FA800D662A6 /* MessageStorage */ = { + isa = PBXGroup; + children = ( + DD1784061F3C9FA800D662A6 /* XMPPMessage.xcdatamodeld */, + DD17840C1F3C9FA800D662A6 /* XMPPMessageCoreDataStorage.h */, + DD17840D1F3C9FA800D662A6 /* XMPPMessageCoreDataStorage.m */, + DD1784081F3C9FA800D662A6 /* XMPPMessageCoreDataStorageObject.h */, + DD1E122F1F5EE4DA0012A506 /* XMPPMessageCoreDataStorageObject+Protected.h */, + DD1784091F3C9FA800D662A6 /* XMPPMessageCoreDataStorageObject.m */, + DD19E40B1F8CA3E200CED8EF /* XMPPMessageCoreDataStorageObject+ContextHelpers.h */, + DD19E40C1F8CA3E200CED8EF /* XMPPMessageCoreDataStorageObject+ContextHelpers.m */, + DD17840A1F3C9FA800D662A6 /* XMPPMessageContextCoreDataStorageObject.h */, + DD19E3FF1F8C9F0F00CED8EF /* XMPPMessageContextCoreDataStorageObject+Protected.h */, + DD17840B1F3C9FA800D662A6 /* XMPPMessageContextCoreDataStorageObject.m */, + DD1E80651F56AC4D003C21A0 /* XMPPMessageContextItemCoreDataStorageObject.h */, + DD19E4001F8C9FCB00CED8EF /* XMPPMessageContextItemCoreDataStorageObject+Protected.h */, + DD1E80661F56AC4D003C21A0 /* XMPPMessageContextItemCoreDataStorageObject.m */, + ); + path = MessageStorage; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXHeadersBuildPhase section */ @@ -2632,6 +2720,8 @@ D9DCD2681E6250930010D1C7 /* XMPPProcessOne.h in Headers */, D9DCD2E51E6250930010D1C7 /* XMPPCapsCoreDataStorageObject.h in Headers */, D9DCD27D1E6250930010D1C7 /* XMPPRoster.h in Headers */, + DDFFF40A1F3DE10700B99353 /* NSManagedObject+XMPPCoreDataStorage.h in Headers */, + DD1E80671F56AC4D003C21A0 /* XMPPMessageContextItemCoreDataStorageObject.h in Headers */, D9DCD25E1E6250930010D1C7 /* OMEMOModule.h in Headers */, D9DCD2581E6250930010D1C7 /* NSXMLElement+OMEMO.h in Headers */, D9DCD31C1E6250930010D1C7 /* NSXMLElement+XEP_0297.h in Headers */, @@ -2640,11 +2730,13 @@ D9DCD24B1E6250930010D1C7 /* XMPPBandwidthMonitor.h in Headers */, D9DCD2901E6250930010D1C7 /* XMPPRoomCoreDataStorage.h in Headers */, D9DCD3081E6250930010D1C7 /* XMPPAutoPing.h in Headers */, + DD1784151F3C9FA800D662A6 /* XMPPMessageCoreDataStorageObject.h in Headers */, D9DCD2F61E6250930010D1C7 /* XMPPvCardAvatarModule.h in Headers */, D9DCD28B1E6250930010D1C7 /* XMPPLastActivity.h in Headers */, D9DCD2601E6250930010D1C7 /* OMEMOPreKey.h in Headers */, D9DCD25A1E6250930010D1C7 /* OMEMOBundle.h in Headers */, D9DCD2A61E6250930010D1C7 /* XMPPMUC.h in Headers */, + DD19E4041F8CA06D00CED8EF /* XMPPMessageContextCoreDataStorageObject+Protected.h in Headers */, D9DCD27F1E6250930010D1C7 /* XMPPRosterPrivate.h in Headers */, D9DCD2661E6250930010D1C7 /* XMPPMessage+OMEMO.h in Headers */, D9DCD2851E6250930010D1C7 /* XMPPIQ+JabberRPCResonse.h in Headers */, @@ -2691,12 +2783,15 @@ D9DCD3021E6250930010D1C7 /* XMPPStreamManagementMemoryStorage.h in Headers */, D9DCD2CC1E6250930010D1C7 /* XMPPPubSub.h in Headers */, D9DCD25C1E6250930010D1C7 /* OMEMOKeyData.h in Headers */, + DD1784211F3C9FA800D662A6 /* XMPPMessageCoreDataStorage.h in Headers */, D9DCD2941E6250930010D1C7 /* XMPPRoomOccupantCoreDataStorageObject.h in Headers */, D9DCD29C1E6250930010D1C7 /* XMPPRoomOccupantHybridMemoryStorageObject.h in Headers */, + DD1E12301F5EE6100012A506 /* XMPPMessageCoreDataStorageObject+Protected.h in Headers */, D9DCD26E1E6250930010D1C7 /* XMPPResourceCoreDataStorageObject.h in Headers */, D9DCD2831E6250930010D1C7 /* XMPPIQ+JabberRPC.h in Headers */, D9DCD3201E6250930010D1C7 /* XMPPMessageArchiveManagement.h in Headers */, D9DCD2D61E6250930010D1C7 /* NSDate+XMPPDateTimeProfiles.h in Headers */, + DD19E40D1F8CA3E200CED8EF /* XMPPMessageCoreDataStorageObject+ContextHelpers.h in Headers */, D9DCD2BA1E6250930010D1C7 /* XMPPvCardTempAdrTypes.h in Headers */, D9DCD2C81E6250930010D1C7 /* XMPPResultSet.h in Headers */, D9DCD2B41E6250930010D1C7 /* XMPPvCardTempCoreDataStorageObject.h in Headers */, @@ -2730,6 +2825,8 @@ 0D44BB211E5370ED000930E0 /* XMPPStream.h in Headers */, 0D44BB1F1E5370ED000930E0 /* XMPPPresence.h in Headers */, 0D44BB191E5370ED000930E0 /* XMPPMessage.h in Headers */, + DD17841B1F3C9FA800D662A6 /* XMPPMessageContextCoreDataStorageObject.h in Headers */, + DD19E4011F8CA02000CED8EF /* XMPPMessageContextItemCoreDataStorageObject+Protected.h in Headers */, 0D44BB161E5370ED000930E0 /* XMPPJID.h in Headers */, 0D44BB4C1E537105000930E0 /* XMPPDeprecatedPlainAuthentication.h in Headers */, 0D44BB141E5370ED000930E0 /* XMPPIQ.h in Headers */, @@ -2797,6 +2894,8 @@ D9DCD4BE1E6256D90010D1C7 /* XMPPProcessOne.h in Headers */, D9DCD4BF1E6256D90010D1C7 /* XMPPCapsCoreDataStorageObject.h in Headers */, D9DCD4C01E6256D90010D1C7 /* XMPPRoster.h in Headers */, + DDFFF40B1F3DE10700B99353 /* NSManagedObject+XMPPCoreDataStorage.h in Headers */, + DD1E80681F56AC4D003C21A0 /* XMPPMessageContextItemCoreDataStorageObject.h in Headers */, D9DCD4C11E6256D90010D1C7 /* OMEMOModule.h in Headers */, D9DCD4C21E6256D90010D1C7 /* NSXMLElement+OMEMO.h in Headers */, D9DCD4C31E6256D90010D1C7 /* NSXMLElement+XEP_0297.h in Headers */, @@ -2805,11 +2904,13 @@ D9DCD4C61E6256D90010D1C7 /* XMPPBandwidthMonitor.h in Headers */, D9DCD4C71E6256D90010D1C7 /* XMPPRoomCoreDataStorage.h in Headers */, D9DCD4C81E6256D90010D1C7 /* XMPPAutoPing.h in Headers */, + DD1784161F3C9FA800D662A6 /* XMPPMessageCoreDataStorageObject.h in Headers */, D9DCD4C91E6256D90010D1C7 /* XMPPvCardAvatarModule.h in Headers */, D9DCD4CA1E6256D90010D1C7 /* XMPPLastActivity.h in Headers */, D9DCD4CB1E6256D90010D1C7 /* OMEMOPreKey.h in Headers */, D9DCD4CC1E6256D90010D1C7 /* OMEMOBundle.h in Headers */, D9DCD4CD1E6256D90010D1C7 /* XMPPMUC.h in Headers */, + DD19E4051F8CA06E00CED8EF /* XMPPMessageContextCoreDataStorageObject+Protected.h in Headers */, D9DCD4CE1E6256D90010D1C7 /* XMPPRosterPrivate.h in Headers */, D9DCD4CF1E6256D90010D1C7 /* XMPPMessage+OMEMO.h in Headers */, D9DCD4D01E6256D90010D1C7 /* XMPPIQ+JabberRPCResonse.h in Headers */, @@ -2856,12 +2957,15 @@ D9DCD4F61E6256D90010D1C7 /* XMPPStreamManagementMemoryStorage.h in Headers */, D9DCD4F71E6256D90010D1C7 /* XMPPPubSub.h in Headers */, D9DCD4F81E6256D90010D1C7 /* OMEMOKeyData.h in Headers */, + DD1784221F3C9FA800D662A6 /* XMPPMessageCoreDataStorage.h in Headers */, D9DCD4F91E6256D90010D1C7 /* XMPPRoomOccupantCoreDataStorageObject.h in Headers */, D9DCD4FA1E6256D90010D1C7 /* XMPPRoomOccupantHybridMemoryStorageObject.h in Headers */, + DD1E12311F5EE6110012A506 /* XMPPMessageCoreDataStorageObject+Protected.h in Headers */, D9DCD4FB1E6256D90010D1C7 /* XMPPResourceCoreDataStorageObject.h in Headers */, D9DCD4FC1E6256D90010D1C7 /* XMPPIQ+JabberRPC.h in Headers */, D9DCD4FD1E6256D90010D1C7 /* XMPPMessageArchiveManagement.h in Headers */, D9DCD4FE1E6256D90010D1C7 /* NSDate+XMPPDateTimeProfiles.h in Headers */, + DD19E40E1F8CA3E200CED8EF /* XMPPMessageCoreDataStorageObject+ContextHelpers.h in Headers */, D9DCD4FF1E6256D90010D1C7 /* XMPPvCardTempAdrTypes.h in Headers */, D9DCD5001E6256D90010D1C7 /* XMPPResultSet.h in Headers */, D9DCD5011E6256D90010D1C7 /* XMPPvCardTempCoreDataStorageObject.h in Headers */, @@ -2895,6 +2999,8 @@ D9DCD51C1E6256D90010D1C7 /* XMPPStream.h in Headers */, D9DCD51D1E6256D90010D1C7 /* XMPPPresence.h in Headers */, D9DCD51E1E6256D90010D1C7 /* XMPPMessage.h in Headers */, + DD17841C1F3C9FA800D662A6 /* XMPPMessageContextCoreDataStorageObject.h in Headers */, + DD19E4021F8CA02000CED8EF /* XMPPMessageContextItemCoreDataStorageObject+Protected.h in Headers */, D9DCD51F1E6256D90010D1C7 /* XMPPJID.h in Headers */, D9DCD5201E6256D90010D1C7 /* XMPPDeprecatedPlainAuthentication.h in Headers */, D9DCD5211E6256D90010D1C7 /* XMPPIQ.h in Headers */, @@ -2962,6 +3068,8 @@ D9DCD6211E6258CF0010D1C7 /* XMPPProcessOne.h in Headers */, D9DCD6221E6258CF0010D1C7 /* XMPPCapsCoreDataStorageObject.h in Headers */, D9DCD6231E6258CF0010D1C7 /* XMPPRoster.h in Headers */, + DDFFF40C1F3DE10700B99353 /* NSManagedObject+XMPPCoreDataStorage.h in Headers */, + DD1E80691F56AC4D003C21A0 /* XMPPMessageContextItemCoreDataStorageObject.h in Headers */, D9DCD6241E6258CF0010D1C7 /* OMEMOModule.h in Headers */, D9DCD6251E6258CF0010D1C7 /* NSXMLElement+OMEMO.h in Headers */, D9DCD6261E6258CF0010D1C7 /* NSXMLElement+XEP_0297.h in Headers */, @@ -2970,11 +3078,13 @@ D9DCD6291E6258CF0010D1C7 /* XMPPBandwidthMonitor.h in Headers */, D9DCD62A1E6258CF0010D1C7 /* XMPPRoomCoreDataStorage.h in Headers */, D9DCD62B1E6258CF0010D1C7 /* XMPPAutoPing.h in Headers */, + DD1784171F3C9FA800D662A6 /* XMPPMessageCoreDataStorageObject.h in Headers */, D9DCD62C1E6258CF0010D1C7 /* XMPPvCardAvatarModule.h in Headers */, D9DCD62D1E6258CF0010D1C7 /* XMPPLastActivity.h in Headers */, D9DCD62E1E6258CF0010D1C7 /* OMEMOPreKey.h in Headers */, D9DCD62F1E6258CF0010D1C7 /* OMEMOBundle.h in Headers */, D9DCD6301E6258CF0010D1C7 /* XMPPMUC.h in Headers */, + DD19E4061F8CA06F00CED8EF /* XMPPMessageContextCoreDataStorageObject+Protected.h in Headers */, D9DCD6311E6258CF0010D1C7 /* XMPPRosterPrivate.h in Headers */, D9DCD6321E6258CF0010D1C7 /* XMPPMessage+OMEMO.h in Headers */, D9DCD6331E6258CF0010D1C7 /* XMPPIQ+JabberRPCResonse.h in Headers */, @@ -3021,12 +3131,15 @@ D9DCD6591E6258CF0010D1C7 /* XMPPStreamManagementMemoryStorage.h in Headers */, D9DCD65A1E6258CF0010D1C7 /* XMPPPubSub.h in Headers */, D9DCD65B1E6258CF0010D1C7 /* OMEMOKeyData.h in Headers */, + DD1784231F3C9FA800D662A6 /* XMPPMessageCoreDataStorage.h in Headers */, D9DCD65C1E6258CF0010D1C7 /* XMPPRoomOccupantCoreDataStorageObject.h in Headers */, D9DCD65D1E6258CF0010D1C7 /* XMPPRoomOccupantHybridMemoryStorageObject.h in Headers */, + DD1E12321F5EE6120012A506 /* XMPPMessageCoreDataStorageObject+Protected.h in Headers */, D9DCD65E1E6258CF0010D1C7 /* XMPPResourceCoreDataStorageObject.h in Headers */, D9DCD65F1E6258CF0010D1C7 /* XMPPIQ+JabberRPC.h in Headers */, D9DCD6601E6258CF0010D1C7 /* XMPPMessageArchiveManagement.h in Headers */, D9DCD6611E6258CF0010D1C7 /* NSDate+XMPPDateTimeProfiles.h in Headers */, + DD19E40F1F8CA3E200CED8EF /* XMPPMessageCoreDataStorageObject+ContextHelpers.h in Headers */, D9DCD6621E6258CF0010D1C7 /* XMPPvCardTempAdrTypes.h in Headers */, D9DCD6631E6258CF0010D1C7 /* XMPPResultSet.h in Headers */, D9DCD6641E6258CF0010D1C7 /* XMPPvCardTempCoreDataStorageObject.h in Headers */, @@ -3060,6 +3173,8 @@ D9DCD67F1E6258CF0010D1C7 /* XMPPStream.h in Headers */, D9DCD6801E6258CF0010D1C7 /* XMPPPresence.h in Headers */, D9DCD6811E6258CF0010D1C7 /* XMPPMessage.h in Headers */, + DD17841D1F3C9FA800D662A6 /* XMPPMessageContextCoreDataStorageObject.h in Headers */, + DD19E4031F8CA02100CED8EF /* XMPPMessageContextItemCoreDataStorageObject+Protected.h in Headers */, D9DCD6821E6258CF0010D1C7 /* XMPPJID.h in Headers */, D9DCD6831E6258CF0010D1C7 /* XMPPDeprecatedPlainAuthentication.h in Headers */, D9DCD6841E6258CF0010D1C7 /* XMPPIQ.h in Headers */, @@ -3437,6 +3552,7 @@ D9DCD3131E6250930010D1C7 /* XEP_0223.m in Sources */, D9DCD2651E6250930010D1C7 /* XMPPIQ+OMEMO.m in Sources */, D9DCD3151E6250930010D1C7 /* XMPPAttentionModule.m in Sources */, + DD1E806A1F56AC4D003C21A0 /* XMPPMessageContextItemCoreDataStorageObject.m in Sources */, D9DCD2551E6250930010D1C7 /* XMPPOutgoingFileTransfer.m in Sources */, D9DCD2951E6250930010D1C7 /* XMPPRoomOccupantCoreDataStorageObject.m in Sources */, D9DCD2931E6250930010D1C7 /* XMPPRoomMessageCoreDataStorageObject.m in Sources */, @@ -3477,6 +3593,7 @@ D9DCD24C1E6250930010D1C7 /* XMPPBandwidthMonitor.m in Sources */, D9DCD3111E6250930010D1C7 /* NSXMLElement+XEP_0203.m in Sources */, 0D44BB551E537105000930E0 /* XMPPXOAuth2Google.m in Sources */, + DD1784241F3C9FA800D662A6 /* XMPPMessageCoreDataStorage.m in Sources */, D9DCD2EB1E6250930010D1C7 /* XMPPMessageArchiving.xcdatamodeld in Sources */, D9DCD2A51E6250930010D1C7 /* XMPPMessage+XEP0045.m in Sources */, D9DCD2DF1E6250930010D1C7 /* XMPPTransports.m in Sources */, @@ -3536,6 +3653,7 @@ D9DCD2A91E6250930010D1C7 /* XMPPRoom.m in Sources */, D9DCD25B1E6250930010D1C7 /* OMEMOBundle.m in Sources */, D9DCD2E61E6250930010D1C7 /* XMPPCapsCoreDataStorageObject.m in Sources */, + DD1784181F3C9FA800D662A6 /* XMPPMessageCoreDataStorageObject.m in Sources */, D9DCD2D51E6250930010D1C7 /* XMPPRegistration.m in Sources */, D9DCD27E1E6250930010D1C7 /* XMPPRoster.m in Sources */, D9DCD3051E6250930010D1C7 /* XMPPStreamManagementStanzas.m in Sources */, @@ -3559,12 +3677,16 @@ D9DCD2DD1E6250930010D1C7 /* XMPPSoftwareVersion.m in Sources */, D9DCD28A1E6250930010D1C7 /* XMPPIQ+LastActivity.m in Sources */, D9DCD2B31E6250930010D1C7 /* XMPPvCardCoreDataStorageObject.m in Sources */, + DDFFF40D1F3DE10700B99353 /* NSManagedObject+XMPPCoreDataStorage.m in Sources */, D9DCD2B51E6250930010D1C7 /* XMPPvCardTempCoreDataStorageObject.m in Sources */, D9DCD3231E6250930010D1C7 /* XMPPMessage+XEP_0333.m in Sources */, D9DCD2A11E6250930010D1C7 /* XMPPRoomMessageMemoryStorageObject.m in Sources */, D9DCD2721E6250930010D1C7 /* XMPPRosterCoreDataStorage.m in Sources */, 0D44BB681E537110000930E0 /* DDList.m in Sources */, + DD17841E1F3C9FA800D662A6 /* XMPPMessageContextCoreDataStorageObject.m in Sources */, 0D44BB701E537110000930E0 /* XMPPSRVResolver.m in Sources */, + DD19E4101F8CA3E200CED8EF /* XMPPMessageCoreDataStorageObject+ContextHelpers.m in Sources */, + DD1784121F3C9FA800D662A6 /* XMPPMessage.xcdatamodeld in Sources */, D9DCD3191E6250930010D1C7 /* XMPPMessage+XEP_0280.m in Sources */, 0D44BB6E1E537110000930E0 /* XMPPIDTracker.m in Sources */, D9DCD2DB1E6250930010D1C7 /* XMPPMessage+XEP_0085.m in Sources */, @@ -3593,6 +3715,7 @@ D9DCD40D1E6256D90010D1C7 /* XEP_0223.m in Sources */, D9DCD40E1E6256D90010D1C7 /* XMPPIQ+OMEMO.m in Sources */, D9DCD40F1E6256D90010D1C7 /* XMPPAttentionModule.m in Sources */, + DD1E806B1F56AC4D003C21A0 /* XMPPMessageContextItemCoreDataStorageObject.m in Sources */, D9DCD4101E6256D90010D1C7 /* XMPPOutgoingFileTransfer.m in Sources */, D9DCD4111E6256D90010D1C7 /* XMPPRoomOccupantCoreDataStorageObject.m in Sources */, D9DCD4121E6256D90010D1C7 /* XMPPRoomMessageCoreDataStorageObject.m in Sources */, @@ -3633,6 +3756,7 @@ D9DCD4321E6256D90010D1C7 /* XMPPBandwidthMonitor.m in Sources */, D9DCD4331E6256D90010D1C7 /* NSXMLElement+XEP_0203.m in Sources */, D9DCD4341E6256D90010D1C7 /* XMPPXOAuth2Google.m in Sources */, + DD1784251F3C9FA800D662A6 /* XMPPMessageCoreDataStorage.m in Sources */, D9DCD4351E6256D90010D1C7 /* XMPPMessageArchiving.xcdatamodeld in Sources */, D9DCD4361E6256D90010D1C7 /* XMPPMessage+XEP0045.m in Sources */, D9DCD4371E6256D90010D1C7 /* XMPPTransports.m in Sources */, @@ -3692,6 +3816,7 @@ D9DCD46C1E6256D90010D1C7 /* XMPPRoom.m in Sources */, D9DCD46D1E6256D90010D1C7 /* OMEMOBundle.m in Sources */, D9DCD46E1E6256D90010D1C7 /* XMPPCapsCoreDataStorageObject.m in Sources */, + DD1784191F3C9FA800D662A6 /* XMPPMessageCoreDataStorageObject.m in Sources */, D9DCD46F1E6256D90010D1C7 /* XMPPRegistration.m in Sources */, D9DCD4701E6256D90010D1C7 /* XMPPRoster.m in Sources */, D9DCD4711E6256D90010D1C7 /* XMPPStreamManagementStanzas.m in Sources */, @@ -3715,12 +3840,16 @@ D9DCD4821E6256D90010D1C7 /* XMPPSoftwareVersion.m in Sources */, D9DCD4831E6256D90010D1C7 /* XMPPIQ+LastActivity.m in Sources */, D9DCD4841E6256D90010D1C7 /* XMPPvCardCoreDataStorageObject.m in Sources */, + DDFFF40E1F3DE10700B99353 /* NSManagedObject+XMPPCoreDataStorage.m in Sources */, D9DCD4851E6256D90010D1C7 /* XMPPvCardTempCoreDataStorageObject.m in Sources */, D9DCD4861E6256D90010D1C7 /* XMPPMessage+XEP_0333.m in Sources */, D9DCD4871E6256D90010D1C7 /* XMPPRoomMessageMemoryStorageObject.m in Sources */, D9DCD4881E6256D90010D1C7 /* XMPPRosterCoreDataStorage.m in Sources */, D9DCD4891E6256D90010D1C7 /* DDList.m in Sources */, + DD17841F1F3C9FA800D662A6 /* XMPPMessageContextCoreDataStorageObject.m in Sources */, D9DCD48A1E6256D90010D1C7 /* XMPPSRVResolver.m in Sources */, + DD19E4111F8CA3E200CED8EF /* XMPPMessageCoreDataStorageObject+ContextHelpers.m in Sources */, + DD1784131F3C9FA800D662A6 /* XMPPMessage.xcdatamodeld in Sources */, D9DCD48B1E6256D90010D1C7 /* XMPPMessage+XEP_0280.m in Sources */, D9DCD48C1E6256D90010D1C7 /* XMPPIDTracker.m in Sources */, D9DCD48D1E6256D90010D1C7 /* XMPPMessage+XEP_0085.m in Sources */, @@ -3749,6 +3878,7 @@ D9DCD5701E6258CF0010D1C7 /* XEP_0223.m in Sources */, D9DCD5711E6258CF0010D1C7 /* XMPPIQ+OMEMO.m in Sources */, D9DCD5721E6258CF0010D1C7 /* XMPPAttentionModule.m in Sources */, + DD1E806C1F56AC4D003C21A0 /* XMPPMessageContextItemCoreDataStorageObject.m in Sources */, D9DCD5731E6258CF0010D1C7 /* XMPPOutgoingFileTransfer.m in Sources */, D9DCD5741E6258CF0010D1C7 /* XMPPRoomOccupantCoreDataStorageObject.m in Sources */, D9DCD5751E6258CF0010D1C7 /* XMPPRoomMessageCoreDataStorageObject.m in Sources */, @@ -3789,6 +3919,7 @@ D9DCD5951E6258CF0010D1C7 /* XMPPBandwidthMonitor.m in Sources */, D9DCD5961E6258CF0010D1C7 /* NSXMLElement+XEP_0203.m in Sources */, D9DCD5971E6258CF0010D1C7 /* XMPPXOAuth2Google.m in Sources */, + DD1784261F3C9FA800D662A6 /* XMPPMessageCoreDataStorage.m in Sources */, D9DCD5981E6258CF0010D1C7 /* XMPPMessageArchiving.xcdatamodeld in Sources */, D9DCD5991E6258CF0010D1C7 /* XMPPMessage+XEP0045.m in Sources */, D9DCD59A1E6258CF0010D1C7 /* XMPPTransports.m in Sources */, @@ -3848,6 +3979,7 @@ D9DCD5CF1E6258CF0010D1C7 /* XMPPRoom.m in Sources */, D9DCD5D01E6258CF0010D1C7 /* OMEMOBundle.m in Sources */, D9DCD5D11E6258CF0010D1C7 /* XMPPCapsCoreDataStorageObject.m in Sources */, + DD17841A1F3C9FA800D662A6 /* XMPPMessageCoreDataStorageObject.m in Sources */, D9DCD5D21E6258CF0010D1C7 /* XMPPRegistration.m in Sources */, D9DCD5D31E6258CF0010D1C7 /* XMPPRoster.m in Sources */, D9DCD5D41E6258CF0010D1C7 /* XMPPStreamManagementStanzas.m in Sources */, @@ -3871,12 +4003,16 @@ D9DCD5E51E6258CF0010D1C7 /* XMPPSoftwareVersion.m in Sources */, D9DCD5E61E6258CF0010D1C7 /* XMPPIQ+LastActivity.m in Sources */, D9DCD5E71E6258CF0010D1C7 /* XMPPvCardCoreDataStorageObject.m in Sources */, + DDFFF40F1F3DE10700B99353 /* NSManagedObject+XMPPCoreDataStorage.m in Sources */, D9DCD5E81E6258CF0010D1C7 /* XMPPvCardTempCoreDataStorageObject.m in Sources */, D9DCD5E91E6258CF0010D1C7 /* XMPPMessage+XEP_0333.m in Sources */, D9DCD5EA1E6258CF0010D1C7 /* XMPPRoomMessageMemoryStorageObject.m in Sources */, D9DCD5EB1E6258CF0010D1C7 /* XMPPRosterCoreDataStorage.m in Sources */, D9DCD5EC1E6258CF0010D1C7 /* DDList.m in Sources */, + DD1784201F3C9FA800D662A6 /* XMPPMessageContextCoreDataStorageObject.m in Sources */, D9DCD5ED1E6258CF0010D1C7 /* XMPPSRVResolver.m in Sources */, + DD19E4121F8CA3E200CED8EF /* XMPPMessageCoreDataStorageObject+ContextHelpers.m in Sources */, + DD1784141F3C9FA800D662A6 /* XMPPMessage.xcdatamodeld in Sources */, D9DCD5EE1E6258CF0010D1C7 /* XMPPMessage+XEP_0280.m in Sources */, D9DCD5EF1E6258CF0010D1C7 /* XMPPIDTracker.m in Sources */, D9DCD5F01E6258CF0010D1C7 /* XMPPMessage+XEP_0085.m in Sources */, @@ -4288,6 +4424,16 @@ sourceTree = ""; versionGroupType = wrapper.xcdatamodel; }; + DD1784061F3C9FA800D662A6 /* XMPPMessage.xcdatamodeld */ = { + isa = XCVersionGroup; + children = ( + DD1784071F3C9FA800D662A6 /* XMPPMessage.xcdatamodel */, + ); + currentVersion = DD1784071F3C9FA800D662A6 /* XMPPMessage.xcdatamodel */; + path = XMPPMessage.xcdatamodeld; + sourceTree = ""; + versionGroupType = wrapper.xcdatamodel; + }; /* End XCVersionGroup section */ }; rootObject = 0D44BAE11E537066000930E0 /* Project object */; diff --git a/Xcode/Testing-Carthage/XMPPFrameworkTests.xcodeproj/project.pbxproj b/Xcode/Testing-Carthage/XMPPFrameworkTests.xcodeproj/project.pbxproj index c7b0708fcb..22209bf74f 100644 --- a/Xcode/Testing-Carthage/XMPPFrameworkTests.xcodeproj/project.pbxproj +++ b/Xcode/Testing-Carthage/XMPPFrameworkTests.xcodeproj/project.pbxproj @@ -64,6 +64,9 @@ D9DCD70E1E625C560010D1C7 /* OMEMOTestStorage.m in Sources */ = {isa = PBXBuildFile; fileRef = D99C5E0C1D99C48100FB068A /* OMEMOTestStorage.m */; }; D9DCD7191E625CAE0010D1C7 /* XMPPFramework.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D9DCD6B01E625A9B0010D1C7 /* XMPPFramework.framework */; }; D9E35E701D90B894002E7CF7 /* OMEMOElementTests.m in Sources */ = {isa = PBXBuildFile; fileRef = D9E35E6F1D90B894002E7CF7 /* OMEMOElementTests.m */; }; + DD4003DC1F7527B10078D144 /* XMPPMessageCoreDataStorageTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DD4003DB1F7527B10078D144 /* XMPPMessageCoreDataStorageTests.m */; }; + DD4003DD1F7527D70078D144 /* XMPPMessageCoreDataStorageTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DD4003DB1F7527B10078D144 /* XMPPMessageCoreDataStorageTests.m */; }; + DD4003DE1F7527D70078D144 /* XMPPMessageCoreDataStorageTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DD4003DB1F7527B10078D144 /* XMPPMessageCoreDataStorageTests.m */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -137,6 +140,7 @@ D9DCD3EC1E6255E10010D1C7 /* XMPPFrameworkTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = XMPPFrameworkTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; D9DCD7151E625C560010D1C7 /* XMPPFrameworkTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = XMPPFrameworkTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; D9E35E6F1D90B894002E7CF7 /* OMEMOElementTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = OMEMOElementTests.m; path = "../Testing-Shared/OMEMOElementTests.m"; sourceTree = SOURCE_ROOT; }; + DD4003DB1F7527B10078D144 /* XMPPMessageCoreDataStorageTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = XMPPMessageCoreDataStorageTests.m; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -207,6 +211,7 @@ D973A0791D2F18040096F3ED /* XMPPStorageHintTests.m */, D973A07A1D2F18040096F3ED /* XMPPURITests.m */, D973A07B1D2F18040096F3ED /* XMPPvCardTests.m */, + DD4003DB1F7527B10078D144 /* XMPPMessageCoreDataStorageTests.m */, 63F50D971C60208200CA0201 /* Info.plist */, ); name = XMPPFrameworkTests; @@ -386,6 +391,7 @@ buildActionMask = 2147483647; files = ( D973A07C1D2F18040096F3ED /* CapabilitiesHashingTest.m in Sources */, + DD4003DC1F7527B10078D144 /* XMPPMessageCoreDataStorageTests.m in Sources */, D973A0811D2F18040096F3ED /* XMPPMUCLightTests.m in Sources */, D973A07D1D2F18040096F3ED /* EncodeDecodeTest.m in Sources */, D973A0831D2F18040096F3ED /* XMPPRoomLightCoreDataStorageTests.m in Sources */, @@ -411,6 +417,7 @@ buildActionMask = 2147483647; files = ( D9DCD3D51E6255E10010D1C7 /* CapabilitiesHashingTest.m in Sources */, + DD4003DD1F7527D70078D144 /* XMPPMessageCoreDataStorageTests.m in Sources */, D9DCD3D61E6255E10010D1C7 /* XMPPMUCLightTests.m in Sources */, D9DCD3D71E6255E10010D1C7 /* EncodeDecodeTest.m in Sources */, D9DCD3D81E6255E10010D1C7 /* XMPPRoomLightCoreDataStorageTests.m in Sources */, @@ -436,6 +443,7 @@ buildActionMask = 2147483647; files = ( D9DCD6FE1E625C560010D1C7 /* CapabilitiesHashingTest.m in Sources */, + DD4003DE1F7527D70078D144 /* XMPPMessageCoreDataStorageTests.m in Sources */, D9DCD6FF1E625C560010D1C7 /* XMPPMUCLightTests.m in Sources */, D9DCD7001E625C560010D1C7 /* EncodeDecodeTest.m in Sources */, D9DCD7011E625C560010D1C7 /* XMPPRoomLightCoreDataStorageTests.m in Sources */, diff --git a/Xcode/Testing-Shared/XMPPMessageCoreDataStorageTests.m b/Xcode/Testing-Shared/XMPPMessageCoreDataStorageTests.m new file mode 100644 index 0000000000..e49e73f28b --- /dev/null +++ b/Xcode/Testing-Shared/XMPPMessageCoreDataStorageTests.m @@ -0,0 +1,586 @@ +// +// XMPPMessageCoreDataStorageTests.m +// XMPPFrameworkTests +// +// Created by Piotr Wegrzynek on 10/08/2017. +// +// + +#import +@import XMPPFramework; + +@interface XMPPMessageCoreDataStorageTests : XCTestCase + +@property (nonatomic, strong) XMPPMessageCoreDataStorage *storage; + +@end + +@implementation XMPPMessageCoreDataStorageTests + +- (void)setUp +{ + [super setUp]; + + self.storage = [[XMPPMessageCoreDataStorage alloc] initWithDatabaseFilename:NSStringFromSelector(self.invocation.selector) + storeOptions:nil]; + self.storage.autoRemovePreviousDatabaseFile = YES; +} + +- (void)testMessageTransientPropertyDirectUpdates +{ + XMPPMessageCoreDataStorageObject *message = + [XMPPMessageCoreDataStorageObject xmpp_insertNewObjectInManagedObjectContext:self.storage.mainThreadManagedObjectContext]; + message.fromJID = [XMPPJID jidWithString:@"user1@domain1/resource1"]; + message.toJID = [XMPPJID jidWithString:@"user2@domain2/resource2"]; + + [self.storage.mainThreadManagedObjectContext save:NULL]; + [self.storage.mainThreadManagedObjectContext refreshObject:message mergeChanges:NO]; + + XCTAssertEqualObjects(message.fromJID, [XMPPJID jidWithString:@"user1@domain1/resource1"]); + XCTAssertEqualObjects(message.toJID, [XMPPJID jidWithString:@"user2@domain2/resource2"]); +} + +- (void)testMessageTransientPropertyMergeUpdates +{ + XMPPMessageCoreDataStorageObject *message = + [XMPPMessageCoreDataStorageObject xmpp_insertNewObjectInManagedObjectContext:self.storage.mainThreadManagedObjectContext]; + message.fromJID = [XMPPJID jidWithString:@"user1@domain1/resource1"]; + message.toJID = [XMPPJID jidWithString:@"user2@domain2/resource2"]; + + [self.storage.mainThreadManagedObjectContext save:NULL]; + + [self expectationForMainThreadStorageManagedObjectsChangeNotificationWithUserInfoKey:NSRefreshedObjectsKey count:1 handler: + ^BOOL(__kindof NSManagedObject *object) { + return [object isKindOfClass:[XMPPMessageCoreDataStorageObject class]]; + }]; + + [self.storage scheduleBlock:^{ + XMPPMessageCoreDataStorageObject *storageMessage = [self.storage.managedObjectContext objectWithID:message.objectID]; + storageMessage.fromJID = [XMPPJID jidWithString:@"user1a@domain1a/resource1a"]; + storageMessage.toJID = [XMPPJID jidWithString:@"user2a@domain2a/resource2a"]; + [self.storage save]; + }]; + + [self waitForExpectationsWithTimeout:5 handler:^(NSError * _Nullable error) { + XCTAssert([message.fromJID isEqualToJID:[XMPPJID jidWithString:@"user1a@domain1a/resource1a"]]); + XCTAssert([message.toJID isEqualToJID:[XMPPJID jidWithString:@"user2a@domain2a/resource2a"]]); + }]; +} + +- (void)testMessageTransientPropertyKeyValueObserving +{ + XMPPMessageCoreDataStorageObject *message = + [XMPPMessageCoreDataStorageObject xmpp_insertNewObjectInManagedObjectContext:self.storage.mainThreadManagedObjectContext]; + + [self keyValueObservingExpectationForObject:message + keyPath:NSStringFromSelector(@selector(fromJID)) + expectedValue:[XMPPJID jidWithString:@"user1@domain1/resource1"]]; + [self keyValueObservingExpectationForObject:message + keyPath:NSStringFromSelector(@selector(toJID)) + expectedValue:[XMPPJID jidWithString:@"user2@domain2/resource2"]]; + + message.fromJID = [XMPPJID jidWithString:@"user1@domain1/resource1"]; + message.toJID = [XMPPJID jidWithString:@"user2@domain2/resource2"]; + + [self waitForExpectationsWithTimeout:0 handler:nil]; +} + +- (void)testIncomingMessageRegistration +{ + NSDictionary *messageTypes = @{@"chat": @(XMPPMessageTypeChat), + @"error": @(XMPPMessageTypeError), + @"groupchat": @(XMPPMessageTypeGroupchat), + @"headline": @(XMPPMessageTypeHeadline), + @"normal": @(XMPPMessageTypeNormal)}; + + for (NSString *typeString in messageTypes) { + NSMutableString *messageString = [NSMutableString string]; + [messageString appendFormat: @"", typeString]; + [messageString appendString: @" body"]; + [messageString appendString: @" subject"]; + [messageString appendString: @" thread"]; + [messageString appendString: @""]; + + XMPPMessageCoreDataStorageObject *message = + [XMPPMessageCoreDataStorageObject xmpp_insertNewObjectInManagedObjectContext:self.storage.mainThreadManagedObjectContext]; + message.direction = XMPPMessageDirectionIncoming; + [message registerIncomingMessageStreamEventID:[NSString stringWithFormat:@"eventID_%@", typeString] + streamJID:[XMPPJID jidWithString:@"user2@domain2/resource2"] + streamEventTimestamp:[NSDate dateWithTimeIntervalSinceReferenceDate:0]]; + [message registerIncomingMessageCore:[[XMPPMessage alloc] initWithXMLString:messageString error:NULL]]; + + XCTAssertEqualObjects(message.fromJID, [XMPPJID jidWithString:@"user1@domain1/resource1"]); + XCTAssertEqualObjects(message.toJID, [XMPPJID jidWithString:@"user2@domain2/resource2"]); + XCTAssertEqualObjects(message.body, @"body"); + XCTAssertEqualObjects(message.stanzaID, @"messageID"); + XCTAssertEqualObjects(message.subject, @"subject"); + XCTAssertEqualObjects(message.thread, @"thread"); + XCTAssertEqual(message.type, messageTypes[typeString].intValue); + XCTAssertEqualObjects([message streamJID], [XMPPJID jidWithString:@"user2@domain2/resource2"]); + XCTAssertEqualObjects([message streamTimestamp], [NSDate dateWithTimeIntervalSinceReferenceDate:0]); + } +} + +- (void)testOutgoingMessageRegistration +{ + XMPPMessageCoreDataStorageObject *message = + [XMPPMessageCoreDataStorageObject xmpp_insertNewObjectInManagedObjectContext:self.storage.mainThreadManagedObjectContext]; + message.direction = XMPPMessageDirectionOutgoing; + [message registerOutgoingMessageStreamEventID:@"outgoingMessageEventID"]; + + XMPPMessageCoreDataStorageObject *foundMessage = [XMPPMessageCoreDataStorageObject findWithStreamEventID:@"outgoingMessageEventID" + inManagedObjectContext:self.storage.mainThreadManagedObjectContext]; + + XCTAssertEqualObjects(message, foundMessage); +} + +- (void)testSentMessageRegistration +{ + XMPPMessageCoreDataStorageObject *message = + [XMPPMessageCoreDataStorageObject xmpp_insertNewObjectInManagedObjectContext:self.storage.mainThreadManagedObjectContext]; + message.direction = XMPPMessageDirectionOutgoing; + [message registerOutgoingMessageStreamEventID:@"outgoingMessageEventID"]; + [message registerOutgoingMessageStreamJID:[XMPPJID jidWithString:@"user@domain/resource"] + streamEventTimestamp:[NSDate dateWithTimeIntervalSinceReferenceDate:0]]; + + XCTAssertEqualObjects([message streamJID], [XMPPJID jidWithString:@"user@domain/resource"]); + XCTAssertEqualObjects([message streamTimestamp], [NSDate dateWithTimeIntervalSinceReferenceDate:0]); +} + +- (void)testRepeatedSentMessageRegistration +{ + XMPPMessageCoreDataStorageObject *message = + [XMPPMessageCoreDataStorageObject xmpp_insertNewObjectInManagedObjectContext:self.storage.mainThreadManagedObjectContext]; + message.direction = XMPPMessageDirectionOutgoing; + [message registerOutgoingMessageStreamEventID:@"initialEventID"]; + [message registerOutgoingMessageStreamJID:[XMPPJID jidWithString:@"user1@domain1/resource1"] + streamEventTimestamp:[NSDate dateWithTimeIntervalSinceReferenceDate:0]]; + [message registerOutgoingMessageStreamEventID:@"subsequentEventID"]; + [message registerOutgoingMessageStreamJID:[XMPPJID jidWithString:@"user2@domain2/resource2"] + streamEventTimestamp:[NSDate dateWithTimeIntervalSinceReferenceDate:1]]; + + XCTAssertEqualObjects([message streamJID], [XMPPJID jidWithString:@"user2@domain2/resource2"]); + XCTAssertEqualObjects([message streamTimestamp], [NSDate dateWithTimeIntervalSinceReferenceDate:1]); +} + +- (void)testRetiredSentMessageRegistration +{ + XMPPMessageCoreDataStorageObject *message = + [XMPPMessageCoreDataStorageObject xmpp_insertNewObjectInManagedObjectContext:self.storage.mainThreadManagedObjectContext]; + message.direction = XMPPMessageDirectionOutgoing; + [message registerOutgoingMessageStreamEventID:@"eventID"]; + [message retireStreamTimestamp]; + [message registerOutgoingMessageStreamJID:[XMPPJID jidWithString:@"user@domain/resource"] + streamEventTimestamp:[NSDate dateWithTimeIntervalSinceReferenceDate:0]]; + + XCTAssertEqualObjects([message streamJID], [XMPPJID jidWithString:@"user@domain/resource"]); + XCTAssertEqualObjects([message streamTimestamp], [NSDate dateWithTimeIntervalSinceReferenceDate:0]); +} + +- (void)testBasicStreamTimestampMessageContextFetch +{ + XMPPMessageCoreDataStorageObject *firstMessage = + [XMPPMessageCoreDataStorageObject xmpp_insertNewObjectInManagedObjectContext:self.storage.mainThreadManagedObjectContext]; + firstMessage.direction = XMPPMessageDirectionIncoming; + [firstMessage registerIncomingMessageStreamEventID:@"firstMessageEventID" + streamJID:[XMPPJID jidWithString:@"user@domain/resource"] + streamEventTimestamp:[NSDate dateWithTimeIntervalSinceReferenceDate:0]]; + + XMPPMessageCoreDataStorageObject *secondMessage = + [XMPPMessageCoreDataStorageObject xmpp_insertNewObjectInManagedObjectContext:self.storage.mainThreadManagedObjectContext]; + secondMessage.direction = XMPPMessageDirectionIncoming; + [secondMessage registerIncomingMessageStreamEventID:@"secondMessageEventID" + streamJID:[XMPPJID jidWithString:@"user@domain/resource"] + streamEventTimestamp:[NSDate dateWithTimeIntervalSinceReferenceDate:1]]; + + NSFetchRequest *fetchRequest = + [XMPPMessageContextItemCoreDataStorageObject requestByTimestampsWithPredicate:[XMPPMessageContextItemCoreDataStorageObject streamTimestampKindPredicate] + inAscendingOrder:YES + fromManagedObjectContext:self.storage.mainThreadManagedObjectContext]; + NSArray *result = [self.storage.mainThreadManagedObjectContext executeFetchRequest:fetchRequest error:NULL]; + + XCTAssertEqual(result.count, 2); + XCTAssertEqualObjects(result[0].message, firstMessage); + XCTAssertEqualObjects(result[1].message, secondMessage); +} + +- (void)testRetiredStreamTimestampMessageContextFetch +{ + XMPPMessageCoreDataStorageObject *message = + [XMPPMessageCoreDataStorageObject xmpp_insertNewObjectInManagedObjectContext:self.storage.mainThreadManagedObjectContext]; + message.direction = XMPPMessageDirectionOutgoing; + [message registerOutgoingMessageStreamEventID:@"retiredMessageEventID"]; + [message registerOutgoingMessageStreamJID:[XMPPJID jidWithString:@"user@domain/resource"] + streamEventTimestamp:[NSDate dateWithTimeIntervalSinceReferenceDate:0]]; + [message registerOutgoingMessageStreamEventID:@"retiringMessageEventID"]; + [message registerOutgoingMessageStreamJID:[XMPPJID jidWithString:@"user@domain/resource"] + streamEventTimestamp:[NSDate dateWithTimeIntervalSinceReferenceDate:1]]; + + NSFetchRequest *fetchRequest = + [XMPPMessageContextItemCoreDataStorageObject requestByTimestampsWithPredicate:[XMPPMessageContextItemCoreDataStorageObject streamTimestampKindPredicate] + inAscendingOrder:YES + fromManagedObjectContext:self.storage.mainThreadManagedObjectContext]; + NSArray *result = [self.storage.mainThreadManagedObjectContext executeFetchRequest:fetchRequest error:NULL]; + + XCTAssertEqual(result.count, 1); + XCTAssertEqualObjects([result[0].message streamTimestamp], [NSDate dateWithTimeIntervalSinceReferenceDate:1]); +} + +- (void)testRelevantMessageJIDContextFetch +{ + XMPPMessageCoreDataStorageObject *incomingMessage = + [XMPPMessageCoreDataStorageObject xmpp_insertNewObjectInManagedObjectContext:self.storage.mainThreadManagedObjectContext]; + incomingMessage.direction = XMPPMessageDirectionIncoming; + [incomingMessage registerIncomingMessageStreamEventID:@"incomingMessageEventID" + streamJID:[XMPPJID jidWithString:@"user1@domain1/resource1"] + streamEventTimestamp:[NSDate dateWithTimeIntervalSinceReferenceDate:0]]; + [incomingMessage registerIncomingMessageCore:[[XMPPMessage alloc] initWithXMLString:@"" error:NULL]]; + + XMPPMessageCoreDataStorageObject *outgoingMessage = + [XMPPMessageCoreDataStorageObject xmpp_insertNewObjectInManagedObjectContext:self.storage.mainThreadManagedObjectContext]; + outgoingMessage.direction = XMPPMessageDirectionOutgoing; + outgoingMessage.toJID = [XMPPJID jidWithString:@"user2@domain2/resource2"]; + [outgoingMessage registerOutgoingMessageStreamEventID:@"outgoingMessageEventID"]; + [outgoingMessage registerOutgoingMessageStreamJID:[XMPPJID jidWithString:@"user1@domain1/resource1"] + streamEventTimestamp:[NSDate dateWithTimeIntervalSinceReferenceDate:1]]; + + NSPredicate *fromJIDPredicate = + [XMPPMessageContextItemCoreDataStorageObject messageFromJIDPredicateWithValue:[XMPPJID jidWithString:@"user2@domain2/resource2"] + compareOptions:XMPPJIDCompareFull]; + NSFetchRequest *fromJIDFetchRequest = + [XMPPMessageContextItemCoreDataStorageObject requestByTimestampsWithPredicate:fromJIDPredicate + inAscendingOrder:YES + fromManagedObjectContext:self.storage.mainThreadManagedObjectContext]; + NSArray *fromJIDResult = + [self.storage.mainThreadManagedObjectContext executeFetchRequest:fromJIDFetchRequest error:NULL]; + + NSPredicate *toJIDPredicate = [XMPPMessageContextItemCoreDataStorageObject messageToJIDPredicateWithValue:[XMPPJID jidWithString:@"user2@domain2/resource2"] + compareOptions:XMPPJIDCompareFull]; + NSFetchRequest *toJIDFetchRequest = + [XMPPMessageContextItemCoreDataStorageObject requestByTimestampsWithPredicate:toJIDPredicate + inAscendingOrder:YES + fromManagedObjectContext:self.storage.mainThreadManagedObjectContext]; + NSArray *toJIDResult = + [self.storage.mainThreadManagedObjectContext executeFetchRequest:toJIDFetchRequest error:NULL]; + + NSPredicate *remotePartyJIDPredicate = + [XMPPMessageContextItemCoreDataStorageObject messageRemotePartyJIDPredicateWithValue:[XMPPJID jidWithString:@"user2@domain2/resource2"] + compareOptions:XMPPJIDCompareFull]; + NSFetchRequest *remotePartyJIDFetchRequest = + [XMPPMessageContextItemCoreDataStorageObject requestByTimestampsWithPredicate:remotePartyJIDPredicate + inAscendingOrder:YES + fromManagedObjectContext:self.storage.mainThreadManagedObjectContext]; + NSArray *remotePartyJIDResult = + [self.storage.mainThreadManagedObjectContext executeFetchRequest:remotePartyJIDFetchRequest error:NULL]; + + XCTAssertEqual(fromJIDResult.count, 1); + XCTAssertEqualObjects(fromJIDResult[0].message, incomingMessage); + + XCTAssertEqual(toJIDResult.count, 1); + XCTAssertEqualObjects(toJIDResult[0].message, outgoingMessage); + + XCTAssertEqual(remotePartyJIDResult.count, 2); + XCTAssertEqualObjects(remotePartyJIDResult[0].message, incomingMessage); + XCTAssertEqualObjects(remotePartyJIDResult[1].message, outgoingMessage); +} + +- (void)testTimestampRangeContextFetch +{ + XMPPMessageCoreDataStorageObject *message = + [XMPPMessageCoreDataStorageObject xmpp_insertNewObjectInManagedObjectContext:self.storage.mainThreadManagedObjectContext]; + message.direction = XMPPMessageDirectionIncoming; + [message registerIncomingMessageStreamEventID:@"eventID" + streamJID:[XMPPJID jidWithString:@"user@domain/resource"] + streamEventTimestamp:[NSDate dateWithTimeIntervalSinceReferenceDate:0]]; + + NSPredicate *startEndPredicate = + [XMPPMessageContextItemCoreDataStorageObject timestampRangePredicateWithStartValue:[NSDate dateWithTimeIntervalSinceReferenceDate:-1] + endValue:[NSDate dateWithTimeIntervalSinceReferenceDate:1]]; + NSFetchRequest *startEndFetchRequest = + [XMPPMessageContextItemCoreDataStorageObject requestByTimestampsWithPredicate:startEndPredicate + inAscendingOrder:YES + fromManagedObjectContext:self.storage.mainThreadManagedObjectContext]; + NSArray *startEndResult = + [self.storage.mainThreadManagedObjectContext executeFetchRequest:startEndFetchRequest error:NULL]; + + NSPredicate *startEndEdgeCasePredicate = + [XMPPMessageContextItemCoreDataStorageObject timestampRangePredicateWithStartValue:[NSDate dateWithTimeIntervalSinceReferenceDate:0] + endValue:[NSDate dateWithTimeIntervalSinceReferenceDate:0]]; + NSFetchRequest *startEndEdgeCaseFetchRequest = + [XMPPMessageContextItemCoreDataStorageObject requestByTimestampsWithPredicate:startEndEdgeCasePredicate + inAscendingOrder:YES + fromManagedObjectContext:self.storage.mainThreadManagedObjectContext]; + NSArray *startEndEdgeCaseResult = + [self.storage.mainThreadManagedObjectContext executeFetchRequest:startEndEdgeCaseFetchRequest error:NULL]; + + NSPredicate *startPredicate = + [XMPPMessageContextItemCoreDataStorageObject timestampRangePredicateWithStartValue:[NSDate dateWithTimeIntervalSinceReferenceDate:-1] + endValue:nil]; + NSFetchRequest *startFetchRequest = + [XMPPMessageContextItemCoreDataStorageObject requestByTimestampsWithPredicate:startPredicate + inAscendingOrder:YES + fromManagedObjectContext:self.storage.mainThreadManagedObjectContext]; + NSArray *startResult = + [self.storage.mainThreadManagedObjectContext executeFetchRequest:startFetchRequest error:NULL]; + + NSPredicate *startEdgeCasePredicate = + [XMPPMessageContextItemCoreDataStorageObject timestampRangePredicateWithStartValue:[NSDate dateWithTimeIntervalSinceReferenceDate:0] + endValue:nil]; + NSFetchRequest *startEdgeCaseFetchRequest = + [XMPPMessageContextItemCoreDataStorageObject requestByTimestampsWithPredicate:startEdgeCasePredicate + inAscendingOrder:YES + fromManagedObjectContext:self.storage.mainThreadManagedObjectContext]; + NSArray *startEdgeCaseResult = + [self.storage.mainThreadManagedObjectContext executeFetchRequest:startEdgeCaseFetchRequest error:NULL]; + + NSPredicate *endPredicate = + [XMPPMessageContextItemCoreDataStorageObject timestampRangePredicateWithStartValue:nil + endValue:[NSDate dateWithTimeIntervalSinceReferenceDate:1]]; + NSFetchRequest *endFetchRequest = + [XMPPMessageContextItemCoreDataStorageObject requestByTimestampsWithPredicate:endPredicate + inAscendingOrder:YES + fromManagedObjectContext:self.storage.mainThreadManagedObjectContext]; + NSArray *endResult = + [self.storage.mainThreadManagedObjectContext executeFetchRequest:endFetchRequest error:NULL]; + + NSPredicate *endEdgeCasePredicate = + [XMPPMessageContextItemCoreDataStorageObject timestampRangePredicateWithStartValue:nil + endValue:[NSDate dateWithTimeIntervalSinceReferenceDate:0]]; + NSFetchRequest *endEdgeCaseFetchRequest = + [XMPPMessageContextItemCoreDataStorageObject requestByTimestampsWithPredicate:endEdgeCasePredicate + inAscendingOrder:YES + fromManagedObjectContext:self.storage.mainThreadManagedObjectContext]; + NSArray *endEdgeCaseResult = + [self.storage.mainThreadManagedObjectContext executeFetchRequest:endEdgeCaseFetchRequest error:NULL]; + + NSPredicate *missPredicate = + [XMPPMessageContextItemCoreDataStorageObject timestampRangePredicateWithStartValue:[NSDate dateWithTimeIntervalSinceReferenceDate:1] + endValue:[NSDate dateWithTimeIntervalSinceReferenceDate:2]]; + NSFetchRequest *missFetchRequest = + [XMPPMessageContextItemCoreDataStorageObject requestByTimestampsWithPredicate:missPredicate + inAscendingOrder:YES + fromManagedObjectContext:self.storage.mainThreadManagedObjectContext]; + NSArray *missResult = + [self.storage.mainThreadManagedObjectContext executeFetchRequest:missFetchRequest error:NULL]; + + XCTAssertEqual(startEndResult.count, 1); + XCTAssertEqualObjects(startEndResult[0].message, message); + XCTAssertEqual(startEndEdgeCaseResult.count, 1); + XCTAssertEqualObjects(startEndEdgeCaseResult[0].message, message); + + XCTAssertEqual(startResult.count, 1); + XCTAssertEqualObjects(startResult[0].message, message); + XCTAssertEqual(startEdgeCaseResult.count, 1); + XCTAssertEqualObjects(startEdgeCaseResult[0].message, message); + + XCTAssertEqual(endResult.count, 1); + XCTAssertEqualObjects(endResult[0].message, message); + XCTAssertEqual(endEdgeCaseResult.count, 1); + XCTAssertEqualObjects(endEdgeCaseResult[0].message, message); + + XCTAssertEqual(missResult.count, 0); +} + +- (void)testMessageSubjectContextFetch +{ + XMPPMessageCoreDataStorageObject *matchingMessage = + [XMPPMessageCoreDataStorageObject xmpp_insertNewObjectInManagedObjectContext:self.storage.mainThreadManagedObjectContext]; + matchingMessage.direction = XMPPMessageDirectionIncoming; + [matchingMessage registerIncomingMessageStreamEventID:@"matchingMessageEventID" + streamJID:[XMPPJID jidWithString:@"user@domain/resource"] + streamEventTimestamp:[NSDate dateWithTimeIntervalSinceReferenceDate:0]]; + matchingMessage.subject = @"I implore you!"; + + XMPPMessageCoreDataStorageObject *otherMessage = + [XMPPMessageCoreDataStorageObject xmpp_insertNewObjectInManagedObjectContext:self.storage.mainThreadManagedObjectContext]; + otherMessage.direction = XMPPMessageDirectionIncoming; + [otherMessage registerIncomingMessageStreamEventID:@"otherMessageEventID" + streamJID:[XMPPJID jidWithString:@"user@domain/resource"] + streamEventTimestamp:[NSDate dateWithTimeIntervalSinceReferenceDate:1]]; + + XMPPMessageContentCompareOptions options = XMPPMessageContentCompareCaseInsensitive|XMPPMessageContentCompareDiacriticInsensitive; + + NSPredicate *equalityPredicate = [XMPPMessageContextItemCoreDataStorageObject messageSubjectPredicateWithValue:@"I implore you!" + compareOperator:XMPPMessageContentCompareOperatorEquals + options:options]; + NSPredicate *prefixPredicate = [XMPPMessageContextItemCoreDataStorageObject messageSubjectPredicateWithValue:@"i implore" + compareOperator:XMPPMessageContentCompareOperatorBeginsWith + options:options]; + NSPredicate *containmentPredicate = + [XMPPMessageContextItemCoreDataStorageObject messageSubjectPredicateWithValue:@"implore" + compareOperator:XMPPMessageContentCompareOperatorContains + options:options]; + NSPredicate *suffixPredicate = [XMPPMessageContextItemCoreDataStorageObject messageSubjectPredicateWithValue:@"you!" + compareOperator:XMPPMessageContentCompareOperatorEndsWith + options:options]; + NSPredicate *likePredicate = [XMPPMessageContextItemCoreDataStorageObject messageSubjectPredicateWithValue:@"I implore *!" + compareOperator:XMPPMessageContentCompareOperatorLike + options:options]; + NSPredicate *matchPredicate = [XMPPMessageContextItemCoreDataStorageObject messageSubjectPredicateWithValue:@"I implore .*!" + compareOperator:XMPPMessageContentCompareOperatorMatches + options:options]; + + for (NSPredicate *predicate in @[equalityPredicate, prefixPredicate, containmentPredicate, suffixPredicate, likePredicate, matchPredicate]) { + NSFetchRequest *fetchRequest = + [XMPPMessageContextItemCoreDataStorageObject requestByTimestampsWithPredicate:predicate + inAscendingOrder:YES + fromManagedObjectContext:self.storage.mainThreadManagedObjectContext]; + NSArray *result = [self.storage.mainThreadManagedObjectContext executeFetchRequest:fetchRequest + error:NULL]; + + XCTAssertEqual(result.count, 1); + XCTAssertEqualObjects(result[0].message, matchingMessage); + } +} + +- (void)testMessageBodyContextFetch +{ + XMPPMessageCoreDataStorageObject *matchingMessage = + [XMPPMessageCoreDataStorageObject xmpp_insertNewObjectInManagedObjectContext:self.storage.mainThreadManagedObjectContext]; + matchingMessage.direction = XMPPMessageDirectionIncoming; + [matchingMessage registerIncomingMessageStreamEventID:@"matchingMessageEventID" + streamJID:[XMPPJID jidWithString:@"user@domain/resource"] + streamEventTimestamp:[NSDate dateWithTimeIntervalSinceReferenceDate:0]]; + matchingMessage.body = @"Wherefore art thou, Romeo?"; + + XMPPMessageCoreDataStorageObject *otherMessage = + [XMPPMessageCoreDataStorageObject xmpp_insertNewObjectInManagedObjectContext:self.storage.mainThreadManagedObjectContext]; + otherMessage.direction = XMPPMessageDirectionIncoming; + [otherMessage registerIncomingMessageStreamEventID:@"otherMessageEventID" + streamJID:[XMPPJID jidWithString:@"user@domain/resource"] + streamEventTimestamp:[NSDate dateWithTimeIntervalSinceReferenceDate:1]]; + + XMPPMessageContentCompareOptions options = XMPPMessageContentCompareCaseInsensitive|XMPPMessageContentCompareDiacriticInsensitive; + + NSPredicate *equalityPredicate = [XMPPMessageContextItemCoreDataStorageObject messageBodyPredicateWithValue:@"Wherefore art thou, Romeo?" + compareOperator:XMPPMessageContentCompareOperatorEquals + options:options]; + NSPredicate *prefixPredicate = [XMPPMessageContextItemCoreDataStorageObject messageBodyPredicateWithValue:@"wherefore" + compareOperator:XMPPMessageContentCompareOperatorBeginsWith + options:options]; + NSPredicate *containmentPredicate = [XMPPMessageContextItemCoreDataStorageObject messageBodyPredicateWithValue:@"art thou" + compareOperator:XMPPMessageContentCompareOperatorContains + options:options]; + NSPredicate *suffixPredicate = [XMPPMessageContextItemCoreDataStorageObject messageBodyPredicateWithValue:@"romeo?" + compareOperator:XMPPMessageContentCompareOperatorEndsWith + options:options]; + NSPredicate *likePredicate = [XMPPMessageContextItemCoreDataStorageObject messageBodyPredicateWithValue:@"Wherefore art thou, *" + compareOperator:XMPPMessageContentCompareOperatorLike + options:options]; + NSPredicate *matchPredicate = [XMPPMessageContextItemCoreDataStorageObject messageBodyPredicateWithValue:@"Wherefore art thou, .*" + compareOperator:XMPPMessageContentCompareOperatorMatches + options:options]; + + for (NSPredicate *predicate in @[equalityPredicate, prefixPredicate, containmentPredicate, suffixPredicate, likePredicate, matchPredicate]) { + NSFetchRequest *fetchRequest = + [XMPPMessageContextItemCoreDataStorageObject requestByTimestampsWithPredicate:predicate + inAscendingOrder:YES + fromManagedObjectContext:self.storage.mainThreadManagedObjectContext]; + NSArray *result = [self.storage.mainThreadManagedObjectContext executeFetchRequest:fetchRequest + error:NULL]; + + XCTAssertEqual(result.count, 1); + XCTAssertEqualObjects(result[0].message, matchingMessage); + } +} + +- (void)testMessageThreadContextFetch +{ + XMPPMessageCoreDataStorageObject *matchingMessage = + [XMPPMessageCoreDataStorageObject xmpp_insertNewObjectInManagedObjectContext:self.storage.mainThreadManagedObjectContext]; + matchingMessage.direction = XMPPMessageDirectionIncoming; + [matchingMessage registerIncomingMessageStreamEventID:@"matchingMessageEventID" + streamJID:[XMPPJID jidWithString:@"user@domain/resource"] + streamEventTimestamp:[NSDate dateWithTimeIntervalSinceReferenceDate:0]]; + matchingMessage.thread = @"e0ffe42b28561960c6b12b944a092794b9683a38"; + + XMPPMessageCoreDataStorageObject *otherMessage = + [XMPPMessageCoreDataStorageObject xmpp_insertNewObjectInManagedObjectContext:self.storage.mainThreadManagedObjectContext]; + otherMessage.direction = XMPPMessageDirectionIncoming; + [otherMessage registerIncomingMessageStreamEventID:@"otherMessageEventID" + streamJID:[XMPPJID jidWithString:@"user@domain/resource"] + streamEventTimestamp:[NSDate dateWithTimeIntervalSinceReferenceDate:1]]; + + NSPredicate *predicate = [XMPPMessageContextItemCoreDataStorageObject messageThreadPredicateWithValue:@"e0ffe42b28561960c6b12b944a092794b9683a38"]; + NSFetchRequest *fetchRequest = + [XMPPMessageContextItemCoreDataStorageObject requestByTimestampsWithPredicate:predicate + inAscendingOrder:YES + fromManagedObjectContext:self.storage.mainThreadManagedObjectContext]; + NSArray *result = [self.storage.mainThreadManagedObjectContext executeFetchRequest:fetchRequest error:NULL]; + + XCTAssertEqual(result.count, 1); + XCTAssertEqualObjects(result[0].message, matchingMessage); +} + +- (void)testMessageTypeContextFetch +{ + NSArray *messageTypes = @[@(XMPPMessageTypeChat), + @(XMPPMessageTypeError), + @(XMPPMessageTypeGroupchat), + @(XMPPMessageTypeHeadline), + @(XMPPMessageTypeNormal)]; + for (NSNumber *typeNumber in messageTypes) { + XMPPMessageCoreDataStorageObject *message = + [XMPPMessageCoreDataStorageObject xmpp_insertNewObjectInManagedObjectContext:self.storage.mainThreadManagedObjectContext]; + message.direction = XMPPMessageDirectionIncoming; + [message registerIncomingMessageStreamEventID:[NSString stringWithFormat:@"message%@EventID", typeNumber] + streamJID:[XMPPJID jidWithString:@"user@domain/resource"] + streamEventTimestamp:[NSDate dateWithTimeIntervalSinceReferenceDate:0]]; + message.stanzaID = [NSString stringWithFormat:@"message%@ID", typeNumber]; + message.type = typeNumber.integerValue; + } + + for (NSNumber *typeNumber in messageTypes) { + NSPredicate *predicate = [XMPPMessageContextItemCoreDataStorageObject messageTypePredicateWithValue:typeNumber.integerValue]; + NSFetchRequest *fetchRequest = + [XMPPMessageContextItemCoreDataStorageObject requestByTimestampsWithPredicate:predicate + inAscendingOrder:YES + fromManagedObjectContext:self.storage.mainThreadManagedObjectContext]; + NSArray *result = [self.storage.mainThreadManagedObjectContext executeFetchRequest:fetchRequest error:NULL]; + + XCTAssertEqual(result.count, 1); + XCTAssertEqualObjects(result[0].message.stanzaID, ([NSString stringWithFormat:@"message%@ID", typeNumber])); + } +} + +- (void)testCoreMessageCreation +{ + XMPPMessageCoreDataStorageObject *message = + [XMPPMessageCoreDataStorageObject xmpp_insertNewObjectInManagedObjectContext:self.storage.mainThreadManagedObjectContext]; + message.toJID = [XMPPJID jidWithString:@"user2@domain2/resource2"]; + message.body = @"body"; + message.stanzaID = @"messageID"; + message.subject = @"subject"; + message.thread = @"thread"; + + NSDictionary *messageTypes = @{@"chat": @(XMPPMessageTypeChat), + @"error": @(XMPPMessageTypeError), + @"groupchat": @(XMPPMessageTypeGroupchat), + @"headline": @(XMPPMessageTypeHeadline), + @"normal": @(XMPPMessageTypeNormal)}; + + for (NSString *typeString in messageTypes){ + message.type = messageTypes[typeString].intValue; + + XMPPMessage *xmppMessage = [message coreMessage]; + + XCTAssertEqualObjects([xmppMessage to], [XMPPJID jidWithString:@"user2@domain2/resource2"]); + XCTAssertEqualObjects([xmppMessage body], @"body"); + XCTAssertEqualObjects([xmppMessage elementID], @"messageID"); + XCTAssertEqualObjects([xmppMessage subject], @"subject"); + XCTAssertEqualObjects([xmppMessage thread], @"thread"); + XCTAssertEqualObjects([xmppMessage type], typeString); + } +} + +- (XCTestExpectation *)expectationForMainThreadStorageManagedObjectsChangeNotificationWithUserInfoKey:(NSString *)userInfoKey count:(NSInteger)expectedObjectCount handler:(BOOL (^)(__kindof NSManagedObject *object))handler +{ + return [self expectationForNotification:NSManagedObjectContextObjectsDidChangeNotification object:self.storage.mainThreadManagedObjectContext handler: + ^BOOL(NSNotification * _Nonnull notification) { + return [notification.userInfo[userInfoKey] objectsPassingTest:^BOOL(id _Nonnull obj, BOOL * _Nonnull stop) { + return handler ? handler(obj) : YES; + }].count == expectedObjectCount; + }]; +} + +@end diff --git a/Xcode/Testing-iOS/XMPPFrameworkTests.xcodeproj/project.pbxproj b/Xcode/Testing-iOS/XMPPFrameworkTests.xcodeproj/project.pbxproj index 393a79e644..c34b137a77 100644 --- a/Xcode/Testing-iOS/XMPPFrameworkTests.xcodeproj/project.pbxproj +++ b/Xcode/Testing-iOS/XMPPFrameworkTests.xcodeproj/project.pbxproj @@ -25,6 +25,7 @@ D99C5E0E1D99C48100FB068A /* OMEMOTestStorage.m in Sources */ = {isa = PBXBuildFile; fileRef = D99C5E0C1D99C48100FB068A /* OMEMOTestStorage.m */; }; D9E35E701D90B894002E7CF7 /* OMEMOElementTests.m in Sources */ = {isa = PBXBuildFile; fileRef = D9E35E6F1D90B894002E7CF7 /* OMEMOElementTests.m */; }; DD1E732C1ED86B7D009B529B /* XMPPPubSubTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DD1E732B1ED86B7D009B529B /* XMPPPubSubTests.m */; }; + DD3559711F3CA50C000D25BA /* XMPPMessageCoreDataStorageTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DD3559701F3CA50C000D25BA /* XMPPMessageCoreDataStorageTests.m */; }; FDD2AB232C05507F2045FFFC /* Pods_XMPPFrameworkTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CD0B17267211A912DE2098E /* Pods_XMPPFrameworkTests.framework */; }; /* End PBXBuildFile section */ @@ -55,6 +56,7 @@ D99C5E0C1D99C48100FB068A /* OMEMOTestStorage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = OMEMOTestStorage.m; path = "../../Testing-Shared/OMEMOTestStorage.m"; sourceTree = ""; }; D9E35E6F1D90B894002E7CF7 /* OMEMOElementTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = OMEMOElementTests.m; path = "../../Testing-Shared/OMEMOElementTests.m"; sourceTree = ""; }; DD1E732B1ED86B7D009B529B /* XMPPPubSubTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = XMPPPubSubTests.m; path = "../../Testing-Shared/XMPPPubSubTests.m"; sourceTree = ""; }; + DD3559701F3CA50C000D25BA /* XMPPMessageCoreDataStorageTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = XMPPMessageCoreDataStorageTests.m; path = "../../Testing-Shared/XMPPMessageCoreDataStorageTests.m"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -108,6 +110,7 @@ D973A07A1D2F18040096F3ED /* XMPPURITests.m */, D973A07B1D2F18040096F3ED /* XMPPvCardTests.m */, DD1E732B1ED86B7D009B529B /* XMPPPubSubTests.m */, + DD3559701F3CA50C000D25BA /* XMPPMessageCoreDataStorageTests.m */, 63F50D971C60208200CA0201 /* Info.plist */, D973A06E1D2F18030096F3ED /* XMPPFrameworkTests-Bridging-Header.h */, ); @@ -263,6 +266,7 @@ D973A07E1D2F18040096F3ED /* XMPPHTTPFileUploadTests.m in Sources */, D973A0821D2F18040096F3ED /* XMPPPushTests.swift in Sources */, D9E35E701D90B894002E7CF7 /* OMEMOElementTests.m in Sources */, + DD3559711F3CA50C000D25BA /* XMPPMessageCoreDataStorageTests.m in Sources */, D973A0851D2F18040096F3ED /* XMPPStorageHintTests.m in Sources */, D973A0891D2F18310096F3ED /* XMPPSwift.swift in Sources */, DD1E732C1ED86B7D009B529B /* XMPPPubSubTests.m in Sources */, diff --git a/Xcode/Testing-macOS/XMPPFrameworkTests.xcodeproj/project.pbxproj b/Xcode/Testing-macOS/XMPPFrameworkTests.xcodeproj/project.pbxproj index 3c8030996f..391d4571d4 100644 --- a/Xcode/Testing-macOS/XMPPFrameworkTests.xcodeproj/project.pbxproj +++ b/Xcode/Testing-macOS/XMPPFrameworkTests.xcodeproj/project.pbxproj @@ -25,6 +25,7 @@ D99C5E091D95EBA100FB068A /* OMEMOTestStorage.m in Sources */ = {isa = PBXBuildFile; fileRef = D99C5E081D95EBA100FB068A /* OMEMOTestStorage.m */; }; D9E35E6E1D90B2C5002E7CF7 /* OMEMOElementTests.m in Sources */ = {isa = PBXBuildFile; fileRef = D9E35E6D1D90B2C5002E7CF7 /* OMEMOElementTests.m */; }; D9F20D011D836080002A8D6F /* OMEMOModuleTests.m in Sources */ = {isa = PBXBuildFile; fileRef = D9F20D001D836080002A8D6F /* OMEMOModuleTests.m */; }; + DD24E0031F70F31300FA813C /* XMPPMessageCoreDataStorageTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DD24E0021F70F31300FA813C /* XMPPMessageCoreDataStorageTests.m */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -52,6 +53,7 @@ D99C5E081D95EBA100FB068A /* OMEMOTestStorage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = OMEMOTestStorage.m; path = "../../Testing-Shared/OMEMOTestStorage.m"; sourceTree = ""; }; D9E35E6D1D90B2C5002E7CF7 /* OMEMOElementTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = OMEMOElementTests.m; path = "../../Testing-Shared/OMEMOElementTests.m"; sourceTree = ""; }; D9F20D001D836080002A8D6F /* OMEMOModuleTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = OMEMOModuleTests.m; path = "../../Testing-Shared/OMEMOModuleTests.m"; sourceTree = ""; }; + DD24E0021F70F31300FA813C /* XMPPMessageCoreDataStorageTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = XMPPMessageCoreDataStorageTests.m; path = "../../Testing-Shared/XMPPMessageCoreDataStorageTests.m"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -108,6 +110,7 @@ D973A0A11D2F1EF60096F3ED /* XMPPSwift.swift */, D973A0A21D2F1EF60096F3ED /* XMPPURITests.m */, D973A0A31D2F1EF60096F3ED /* XMPPvCardTests.m */, + DD24E0021F70F31300FA813C /* XMPPMessageCoreDataStorageTests.m */, D973A0921D2F1EB10096F3ED /* Info.plist */, ); path = XMPPFrameworkTests; @@ -246,6 +249,7 @@ buildActionMask = 2147483647; files = ( D973A0A41D2F1EF60096F3ED /* CapabilitiesHashingTest.m in Sources */, + DD24E0031F70F31300FA813C /* XMPPMessageCoreDataStorageTests.m in Sources */, D9F20D011D836080002A8D6F /* OMEMOModuleTests.m in Sources */, D973A0A91D2F1EF60096F3ED /* XMPPMUCLightTests.m in Sources */, D973A0A51D2F1EF60096F3ED /* EncodeDecodeTest.m in Sources */,