Skip to content

Instantly share code, notes, and snippets.

@scionwest
Created December 7, 2014 20:48
Show Gist options
  • Save scionwest/589da62bb2614cd96b44 to your computer and use it in GitHub Desktop.
Save scionwest/589da62bb2614cd96b44 to your computer and use it in GitHub Desktop.
//
// LBTagSync.m
// Life Blog
//
// Created by Sully on 3/1/14.
// Copyright (c) 2014 AllocateThis! Studios. All rights reserved.
//
#import "LBTagSync.h"
#import "LBTagRepository.h"
#import "EvernoteSDK.h"
#import "NSDate+EDAMAdditions.h"
#import "Tag.h"
@interface LBTagSync ()
@property (strong, nonatomic) NSManagedObjectContext *managedContext;
// Sync properties
@property (nonatomic) NSInteger lastUpdateCount;
@property (nonatomic) NSDate *lastSyncTime;
@property (nonatomic) int currentChunkUSN;
@property (strong, nonatomic) NSArray *selectedTagsForSync;
@end
@implementation LBTagSync
#pragma mark - Property lazy instantiations.
@synthesize lastUpdateCount = _lastUpdateCount;
@synthesize lastSyncTime = _lastSyncTime;
- (void)setLastUpdateCount:(NSInteger)lastUpdateCount {
[[NSUserDefaults standardUserDefaults] setInteger:lastUpdateCount forKey:@"lastUpdateCount"];
[[NSUserDefaults standardUserDefaults] synchronize];
}
- (NSInteger)lastUpdateCount {
return [[NSUserDefaults standardUserDefaults] integerForKey:@"lastUpdateCount"];
}
- (void)setLastSyncTime:(NSDate *)lastSyncTime {
if (lastSyncTime) {
[[NSUserDefaults standardUserDefaults] setObject:lastSyncTime forKey:@"lastSyncTime"];
} else {
[[NSUserDefaults standardUserDefaults] removeObjectForKey:@"lastSyncTime"];
}
[[NSUserDefaults standardUserDefaults] synchronize];
}
- (NSDate *)lastSyncTime {
return (NSDate *)[[NSUserDefaults standardUserDefaults] objectForKey:@"lastSyncTime"];
}
#pragma mark - Initializers
- (id)initWithManagedContext:(NSManagedObjectContext *)context {
self = [super init];
if (self) {
self.managedContext = context;
self.selectedTagsForSync = [[NSArray alloc] init];
}
return self;
}
#pragma mark - Synchronization
- (void)syncSelectedTags:(NSArray *)tags WithCompletion:(void (^)(NSArray *))completion failure:(void (^)(NSError *))failure {
// Begin syncing tags.
// Verify that the user is authenticated. If not, we return the error.
if (![[EvernoteSession sharedSession] isAuthenticated]) {
NSLog(@"Failed to sync. User is not authenticated.");
NSDictionary *authenticationErrorInfo = @{ @"Failure" : @"Could not begin tag synchronization.",
@"Reason" : @"The user is not currently authenticated with the Evernote server." };
NSError *authenticationError = [NSError errorWithDomain:@"com.AllocateThis.LifeBlog.LBTagRepository.syncWithCompletion" code:1 userInfo:authenticationErrorInfo];
failure(authenticationError);
} else {
// Convert our last sync time stamp to an Evernote time stamp.
EDAMTimestamp lastSyncTime = [self.lastSyncTime enedamTimestamp];
// Cache the tag list that the user has selected for syncing.
self.selectedTagsForSync = tags;
NSLog(@"Saving any non-committed changes to Core Data.");
NSError *error = nil;
[self.managedContext save:&error];
// If saving failed, we perform our callback, set syncing to false and abort.
if (error) {
failure(error);
} else {
NSLog(@"Synchronization of Tags beginning.");
// Get the current sync state from the server.
[[EvernoteNoteStore noteStore] getSyncStateWithSuccess:^(EDAMSyncState *syncState) {
// Check if our last time stamp is earlier than the required time to perform a full sync.
// If we are good, we check if our update count is equal to the server's. If not, we update.
if ((syncState.fullSyncBefore > lastSyncTime) || (syncState.updateCount > self.lastUpdateCount)) {
NSLog(@"Parsing sync state.");
// Fetch and cache all of the tags that have been modified or created since last sync.
[self fetchUpdatedTagsFromServer:syncState withChunkUSN:(int)self.lastUpdateCount withCompletion:^(NSArray *serverTags, NSArray *expungedTags) {
// Process all of the downloaded tags and push any new or changed tags back to the server.
[self applyServerTags:serverTags andExpungedTags:expungedTags fromServerWithCompletion:^(NSArray *serverTags) {
// We save the time stamp and last update count prior to pushing changes.
// In the event that the push fails, we don't need to worry about pulling down the same tags again.
// PushUpdatedTags: will update these values after each new/modified tag is pushed.
self.lastSyncTime = [NSDate date];
self.lastUpdateCount = syncState.updateCount;
[self pushUpdatedTags:serverTags toServerWithCompletion:^(NSArray *syncedTags) {
NSLog(@"Synchronization completed. %lu tags in sync.", (unsigned long)[syncedTags count]);
completion(syncedTags);
} withFailure:^(NSError *error) {
failure(error);
}];
// processing tag (the actual sync process) failed.
} withFailure:^(NSError *error) {
NSLog(@"Failed to process tags during synchronization. Synchronization failed.");
failure(error);
}];
// fetching server tags failed.
} failure:^(NSError *error) {
NSLog(@"Failed to properly fetch updated tags from the server. Sync canceled.");
failure(error);
}];
} else {
[[EvernoteNoteStore noteStore] listTagsWithSuccess:^(NSArray *tags) {
[self pushUpdatedTags:tags toServerWithCompletion:^(NSArray *syncedTags) {
NSLog(@"Synchronization completed. %lu tags in sync.", (unsigned long)[syncedTags count]);
completion(syncedTags);
} withFailure:^(NSError *error) {
NSLog(@"Failed to push updated tags to the server. Sync process aborted.");
failure(error);
}];
} failure:^(NSError *error) {
NSLog(@"Failed to obtain the current set of server tags. Sync process aborted.");
failure(error);
}];
// Push all of our local changes to the server.
}
} failure:^(NSError *error) {
NSLog(@"Failed to obtain the current sync state. Sync process aborted.");
failure(error);
}];
}
}
}
- (void)fetchUpdatedTagsFromServer:(EDAMSyncState *)syncState withChunkUSN:(int)chunkUSN
withCompletion:(void(^)(NSArray *serverTags, NSArray *expungedTags))completion
failure:(void(^)(NSError *error))failure {
// If we have never updated, perform full sync.
BOOL fullSync = NO;
if (!self.lastUpdateCount)
fullSync = YES;
NSLog(@"Fetching new, modified or expunged tags from the server.");
[[EvernoteNoteStore noteStore] getSyncChunkAfterUSN:chunkUSN maxEntries:200 fullSyncOnly:fullSync success:^(EDAMSyncChunk *syncChunk) {
// Mutable array to cache our tags in.
__block NSMutableArray *modifiedTagsCache = [[NSMutableArray alloc] init];
__block NSMutableArray *expungedTagsCache = [[NSMutableArray alloc] init];
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"name BEGINSWITH %@", @"zlifeblog_deleted"];
NSArray *deletedTags = [syncChunk.tags filteredArrayUsingPredicate:predicate];
[syncChunk.tags removeObjectsInArray:deletedTags];
// Cache changed server tags
for (EDAMTag *tag in syncChunk.tags) {
// Only sync tags that are specified in our sync list.
if (!self.selectedTagsForSync || [self.selectedTagsForSync containsObject:tag.guid]) {// Can use instead of Guid due to tags being unique.
[modifiedTagsCache addObject:tag];
}
NSLog(@"Caching server tag %@", tag.name);
}
// Cached expunged server tags.
NSLog(@"Caching %ld expunged Tag guid's.", (unsigned long)[syncChunk.expungedTags count]);
for (NSString *expungedTag in syncChunk.expungedTags) {
[expungedTagsCache addObject:expungedTag];
}
// We have finished caching all of the new, modified or expunged tags.
// Now we need to see if we have processed all of the server changes or if there are more to send us.
if (syncChunk.chunkHighUSN < syncChunk.updateCount) {
// There are more updates, so let's process the next batch from the server.
[self fetchUpdatedTagsFromServer:syncState withChunkUSN:syncChunk.chunkHighUSN withCompletion:^(NSArray *cachedTags, NSArray *expungedTags) {
// Insert the results from our nested invocation into our current tags collection.
[modifiedTagsCache addObjectsFromArray:cachedTags];
[expungedTagsCache addObjectsFromArray:expungedTags];
// Callback with the joined array.
completion([modifiedTagsCache copy], [expungedTagsCache copy]);
} failure:^(NSError *error) {
NSLog(@"Failed to properly fetch updated tags from the server. Sync canceled.");
failure(error);
}];
} else {
// All good to go, everything is cached and there are no more on the server.
completion([modifiedTagsCache copy], [expungedTagsCache copy]);
}
} failure:^(NSError *error) {
NSLog(@"Caching server tags failed.");
failure(error);
}];
}
- (void)applyServerTags:(NSArray *)serverTags andExpungedTags:(NSArray *)expungedTags fromServerWithCompletion:(void(^)(NSArray *syncedTags))completion withFailure:(void(^)(NSError *error))failure {
NSLog(@"Fetching tags completed.");
[self saveNewTags:serverTags];
[self updateExistingTags:serverTags];
[self renameExistingTags:serverTags];
[self expungeTags:expungedTags];
// Save our local changes.
NSError *saveError = nil;
//if ([self.managedContext.updatedObjects count]) {
[self.managedContext save:&saveError];
//}
if (saveError) {
NSLog(@"Failed to save newly cached tags.");
failure(saveError);
} else {
LBTagRepository *repository = [[LBTagRepository alloc] initWithManagedContext:self.managedContext];
completion([repository fetchAll]);
}
}
- (void)pushUpdatedTags:(NSArray *)serverTags toServerWithCompletion:(void(^)(NSArray *syncedTags))completion withFailure:(void(^)(NSError *error))failure {
[self pushNewTags:serverTags withCompletion:^{
//[self pushUpdatedTags:serverTags toServerWithCompletion:^(NSArray *syncedTags) {
[self pushLocallyExpungedTags:serverTags withCompletion:^{
LBTagRepository *repository = [[LBTagRepository alloc] initWithManagedContext:self.managedContext];
NSError *saveError = nil;
if ([self.managedContext.updatedObjects count]) {
[self.managedContext save:&saveError];
}
if (saveError) {
NSLog(@"Failed to save newly cached tags.");
failure(saveError);
} else {
completion([repository fetchAll]);
}
} withFailure:^(NSError *error) {
failure(error); // expunge tags.
}];
//} withFailure:^(NSError *error) {
// failure(error); // updatedTags.
//}];
} withFailure:^(NSError *error) {
failure(error); // newTags.
}];
}
#pragma mark - Sync Server tags to Local Tags
- (void)saveNewTags:(NSArray *)serverTags {
LBTagRepository *repository = [[LBTagRepository alloc] initWithManagedContext:self.managedContext];
int numberOfNewTags = 0;
for (EDAMTag *serverTag in serverTags) {
// Make sure it is not a tag we already deleted.
NSRange rangeOfName = [serverTag.name rangeOfString:@"zlifeblog_deleted_"];
if (rangeOfName.location != NSNotFound) {
continue; // Move to next item.
}
NSPredicate *filter = [NSPredicate predicateWithFormat:@"name like %@ AND guid like %@", serverTag.name, serverTag.guid];
NSArray *array = [repository fetchTagsWithPredicate:filter];
// If we do not have a tag matching the name and guid of this server tag, we know it is a new tag.
if (![array firstObject]) {
[repository createNewTagFromEvernoteTag:serverTag andAutomaticallySave:YES];
numberOfNewTags++;
NSLog(@"%@ tag created in Core Data.", serverTag.name);
}
}
}
- (void)updateExistingTags:(NSArray *)serverTags {
LBTagRepository *repository = [[LBTagRepository alloc] initWithManagedContext:self.managedContext];
int numberOfUpdates = 0;
for (EDAMTag *serverTag in serverTags) {
NSPredicate *filter = [NSPredicate predicateWithFormat:@"name like %@ AND guid like %@", serverTag.name, serverTag.guid];
Tag *localTag = [repository fetchTagWithPredicate:filter];
// If we have a local tag, we need to determine if it was edited on the serve side.
if (localTag) {
// We only update our existing tags. If the local tag is newer, we ignore it.
// The server updates will happen in another method, later on during the sync process.
if ([localTag.updateSequenceNum intValue] < serverTag.updateSequenceNum) {
[repository convertEvernoteTag:serverTag toManagedTag:localTag convertingOnlyChangedProperties:YES];
numberOfUpdates++;
NSLog(@"%@ tag updated in Core Data.", serverTag.name);
}
}
}
}
- (void)renameExistingTags:(NSArray *)serverTags {
LBTagRepository *repository = [[LBTagRepository alloc] initWithManagedContext:self.managedContext];
for (EDAMTag *serverTag in serverTags) {
NSPredicate *filter = [NSPredicate predicateWithFormat:@"guid like %@ AND name != %@", serverTag.guid, serverTag.name];
Tag *localTag = [repository fetchTagWithPredicate:filter];
// If we have a local tag, the tag was renamed on the server side.
if (localTag) {
NSLog(@"%@ renamed to %@ in Core Data.", localTag.name, serverTag.name);
[repository convertManagedTag:localTag toEvernoteTag:serverTag convertingOnlyChangedProperties:YES];
}
}
}
- (void)expungeTags:(NSArray *)expungedTags {
LBTagRepository *repository = [[LBTagRepository alloc] initWithManagedContext:self.managedContext];
for (NSString *expungedTag in expungedTags) {
NSPredicate *filter = [NSPredicate predicateWithFormat:@"guid like %@", expungedTag];
Tag *localTag = [repository fetchTagWithPredicate:filter];
if (localTag) {
NSLog(@"%@ deleted in Core Data.", localTag.name);
[repository deleteTag:localTag];
}
}
}
#pragma mark - Sync local tags to the Server
- (void)pushNewTags:(NSArray *)serverTags withCompletion:(void(^)())completion withFailure:(void(^)(NSError *error))failure {
LBTagRepository *repository = [[LBTagRepository alloc] initWithManagedContext:self.managedContext];
NSArray *localTags = [repository fetchAll];
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
queue.maxConcurrentOperationCount = 4;
NSOperation *completionOperation = [NSBlockOperation blockOperationWithBlock:^{
completion();
}];
for (Tag *localTag in localTags) {
NSPredicate *filter = [NSPredicate predicateWithFormat:@"name like %@ AND guid like %@", localTag.name, localTag.guid];
NSArray *array = [serverTags filteredArrayUsingPredicate:filter];
// If we do not have a tag matching the name and guid of this server tag, we know it is a new tag.
if (![array firstObject]) {
EDAMTag *serverTag = [[EDAMTag alloc] initWithGuid:nil name:localTag.name parentGuid:nil updateSequenceNum:0];
NSLog(@"Pushing new %@ tag to the server.", localTag.name);
NSOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
[[EvernoteNoteStore noteStore] createTag:serverTag success:^(EDAMTag *tag) {
NSLog(@"Upload of %@ tag (%@) completed.", serverTag.name, [localTag description]);
// We force a complete update to all changed properties on the local tag to match the server's.
[repository convertEvernoteTag:tag toManagedTag:localTag convertingOnlyChangedProperties:YES];
self.lastSyncTime = [NSDate date];
self.lastUpdateCount = tag.updateSequenceNum;
NSLog(@"Server Update Count: %d", serverTag.updateSequenceNum);
NSLog(@"Client Update Cound: %@", [localTag description]);
dispatch_semaphore_signal(semaphore);
} failure:^(NSError *error) {
NSLog(@"Failed to upload %@ tag.", serverTag.name);
dispatch_semaphore_signal(semaphore);
[queue cancelAllOperations];
failure(error);
}];
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
}];
[completionOperation addDependency:operation];
[queue addOperation:operation];
}
}
[queue addOperation:completionOperation];
}
- (void)pushUpdatesToTags:(NSArray *)serverTags withCompletion:(void(^)())completion withFailure:(void(^)(NSError *error))failure {
LBTagRepository *repository = [[LBTagRepository alloc] initWithManagedContext:self.managedContext];
NSArray *localTags = [repository fetchAll];
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
queue.maxConcurrentOperationCount = 4;
NSOperation *completionOperation = [NSBlockOperation blockOperationWithBlock:^{
completion();
}];
for (Tag *localTag in localTags) {
NSPredicate *filter = [NSPredicate predicateWithFormat:@"name like %@ AND guid like %@", localTag.name, localTag.guid];
NSArray *array = [serverTags filteredArrayUsingPredicate:filter];
// If we found a matching server tag, we check if the server's version is outdated. If so, we updated it.
if ([array firstObject]) {
EDAMTag *serverTag = [repository convertManagedTag:localTag toEvernoteTag:[array firstObject] convertingOnlyChangedProperties:YES];
NSOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
[[EvernoteNoteStore noteStore] updateTag:serverTag success:^(int32_t usn) {
NSLog(@"Uploaded an updated version of %@ tag.", serverTag.name);
[repository convertEvernoteTag:serverTag toManagedTag:localTag convertingOnlyChangedProperties:YES];
self.lastSyncTime = [NSDate date];
self.lastUpdateCount = usn;
dispatch_semaphore_signal(semaphore);
} failure:^(NSError *error) {
}];
[[EvernoteNoteStore noteStore] createTag:serverTag success:^(EDAMTag *tag) {
NSLog(@"Upload of %@ tag completed.", serverTag.name);
// We force a complete update to all changed properties on the local tag to match the server's.
[repository convertEvernoteTag:tag toManagedTag:localTag convertingOnlyChangedProperties:YES];
dispatch_semaphore_signal(semaphore);
} failure:^(NSError *error) {
NSLog(@"Failed to upload %@ tag.", serverTag.name);
dispatch_semaphore_signal(semaphore);
[queue cancelAllOperations];
failure(error);
}];
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
}];
[completionOperation addDependency:operation];
[queue addOperation:operation];
}
}
[queue addOperation:completionOperation];
}
- (void)pushLocallyExpungedTags:(NSArray *)serverTags withCompletion:(void(^)())completion withFailure:(void(^)(NSError *error))failure {
LBTagRepository *repository = [[LBTagRepository alloc] initWithManagedContext:self.managedContext];
NSPredicate *expungedFilter = [NSPredicate predicateWithFormat:@"expunged == YES"];
NSArray *localTags = [[repository fetchAllIncludingExpunged:YES] filteredArrayUsingPredicate:expungedFilter];
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
queue.maxConcurrentOperationCount = 4;
__block NSMutableArray *queuedLocalTagsToDelete = [[NSMutableArray alloc] init];
__block bool hasError = NO;
NSOperation *completionOperation = [NSBlockOperation blockOperationWithBlock:^{
for (Tag *localTag in queuedLocalTagsToDelete) {
NSLog(@"Deleting local %@ tag", localTag.name);
[self.managedContext deleteObject:localTag];
}
// Save our deletions.
NSError *error = nil;
[self.managedContext save:&error];
if (error || hasError) {
failure(error);
} else {
completion();
}
}];
for (EDAMTag *st in serverTags) {
NSLog(@"Server Tag - %@", st.name);
}
for (Tag *localTag in localTags) {
// We abort the rest if we already have a itteration that failed.
if (hasError) {
break;
}
NSLog(@"%@", [localTag description]);
NSPredicate *serverFilter = [NSPredicate predicateWithFormat:@"guid == %@", localTag.guid];
if ([[serverTags filteredArrayUsingPredicate:serverFilter] firstObject]) {
EDAMTag *serverTag = [[serverTags filteredArrayUsingPredicate:serverFilter] firstObject];
int number = 0;
NSPredicate *renamedTagPredicate = nil;
do {
NSString *newName = [NSString stringWithFormat:@"zlifeblog_deleted_%@_%d", serverTag.name, number];
serverTag.name = newName;
renamedTagPredicate = [NSPredicate predicateWithFormat:@"name != %@", serverTag.name];
} while (![[serverTags filteredArrayUsingPredicate:renamedTagPredicate] firstObject]);
NSLog(@"Pushing deletion of %@ tag to the server.", localTag.name);
NSOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
[[EvernoteNoteStore noteStore] updateTag:serverTag success:^(int32_t usn) {
self.lastSyncTime = [NSDate date];
self.lastUpdateCount = usn;
NSLog(@"%@ tag flagged as deleted on server.", serverTag.name);
// Queue this local tag for deletion. We do not keep local copies of expunged tags.
[queuedLocalTagsToDelete addObject:localTag];
dispatch_semaphore_signal(semaphore);
} failure:^(NSError *error) {
NSLog(@"Failed to flag %@ tag as deleted on the server.", serverTag.name);
hasError = YES;
dispatch_semaphore_signal(semaphore);
return;
}];
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
}];
[completionOperation addDependency:operation];
[queue addOperation:operation];
}
}
[queue addOperation:completionOperation];
}
@end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment