Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Merge pull request #224 from darrylhthomas/simplenote-tags

Issue #104 Simplenote tags. (Initial merge, pending database update logic)
  • Loading branch information...
commit 3a45ca9ccf2cee226b1f98668dbcd5769289c364 2 parents e240917 + 3c7aff9
@scrod scrod authored
View
2  JSON/NSString+BSJSONAdditions.m
@@ -61,7 +61,7 @@ - (NSString *)jsonStringValue
break;
*/
default:
- [jsonString appendFormat:@"%c", nextChar];
+ [jsonString appendFormat:@"%C", nextChar];
break;
}
}
View
3  NSString_NV.m
@@ -148,6 +148,7 @@ + (NSString*)relativeDateStringWithAbsoluteTime:(CFAbsoluteTime)absTime {
return dateString;
}
+// TODO: possibly obsolete? SN api2 formats dates as doubles from start of unix epoch
CFDateFormatterRef simplenoteDateFormatter(int lowPrecision) {
//CFStringRef dateStr = CFSTR("2010-01-02 23:23:31.876229");
static CFDateFormatterRef dateFormatter = NULL;
@@ -167,11 +168,13 @@ CFDateFormatterRef simplenoteDateFormatter(int lowPrecision) {
return lowPrecision ? lowPrecisionDateFormatter : dateFormatter;
}
+// TODO: possibly obsolete? SN api2 formats dates as doubles from start of unix epoch
+ (NSString*)simplenoteDateWithAbsoluteTime:(CFAbsoluteTime)absTime {
CFStringRef str = CFDateFormatterCreateStringWithAbsoluteTime(NULL, simplenoteDateFormatter(0), absTime);
return [(id)str autorelease];
}
+// TODO: possibly obsolete? SN api2 formats dates as doubles from start of unix epoch
- (CFAbsoluteTime)absoluteTimeFromSimplenoteDate {
CFAbsoluteTime absTime = 0;
View
1  Notation.xcodeproj/project.pbxproj
@@ -1098,6 +1098,7 @@
isa = PBXProject;
buildConfigurationList = 212B842709780B5000F3597F /* Build configuration list for PBXProject "Notation" */;
compatibilityVersion = "Xcode 3.1";
+ developmentRegion = English;
hasScannedForEncodings = 1;
knownRegions = (
English,
View
4 NotationSyncServiceManager.m
@@ -164,6 +164,10 @@ - (void)makeNotesMatchList:(NSArray*)MDEntries fromSyncSession:(id <SyncServiceS
[locallyChangedNotes addObject:note];
} else if (changeDiff == NSOrderedAscending) {
[remotelyChangedNotes addObject:note];
+ } else {
+ //if the note is considered unchanged, still give the sync service an
+ //opportunity to update metadata/tags with info returned by the list
+ [syncSession applyMetadataUpdatesToNote:note localEntry:thisServiceInfo remoteEntry:remoteInfo];
}
} else if (changeDiff != NSOrderedDescending) {
//nah ah ah, a delete should not stick if local mod time is newer! otherwise local changes will be lost
View
145 SimplenoteEntryCollector.m
@@ -26,6 +26,7 @@
#import "SyncResponseFetcher.h"
#import "SimplenoteSession.h"
#import "NSString_NV.h"
+#import "NSDictionary+BSJSONAdditions.h"
#import "SynchronizedNoteProtocol.h"
#import "NoteObject.h"
#import "DeletedNoteObject.h"
@@ -117,9 +118,9 @@ - (SyncResponseFetcher*)fetcherForEntry:(id)entry {
originalNote = entry;
entry = [[entry syncServicesMD] objectForKey:SimplenoteServiceName];
}
- NSURL *noteURL = [SimplenoteSession servletURLWithPath:@"/api/note" parameters:
+ NSURL *noteURL = [SimplenoteSession servletURLWithPath: [NSString stringWithFormat:@"/api2/data/%@", [entry objectForKey: @"key"]] parameters:
[NSDictionary dictionaryWithObjectsAndKeys: email, @"email",
- authToken, @"auth", [entry objectForKey:@"key"], @"key", nil]];
+ authToken, @"auth", nil]];
SyncResponseFetcher *fetcher = [[SyncResponseFetcher alloc] initWithURL:noteURL POSTData:nil delegate:self];
//remember the note for later? why not.
if (originalNote) [fetcher setRepresentedObject:originalNote];
@@ -141,18 +142,41 @@ - (NSDictionary*)preparedDictionaryWithFetcher:(SyncResponseFetcher*)fetcher rec
//logic abstracted for subclassing
NSString *bodyString = [[[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding] autorelease];
- NSDictionary *headers = [fetcher headers];
- NSMutableDictionary *entry = [NSMutableDictionary dictionaryWithCapacity:5];
- [entry setObject:[headers objectForKey:@"Note-Key"] forKey:@"key"];
- [entry setObject:[NSNumber numberWithInt:[[headers objectForKey:@"Note-Deleted"] intValue]] forKey:@"deleted"];
- [entry setObject:[NSNumber numberWithDouble:[[headers objectForKey:@"Note-Createdate"] absoluteTimeFromSimplenoteDate]] forKey:@"create"];
- [entry setObject:[NSNumber numberWithDouble:[[headers objectForKey:@"Note-Modifydate"] absoluteTimeFromSimplenoteDate]] forKey:@"modify"];
+ NSDictionary *rawObject = nil;
+ @try {
+ rawObject = [NSDictionary dictionaryWithJSONString:bodyString];
+ }
+ @catch (NSException *e) {
+ NSLog(@"Exception while parsing Simplenote JSON note object: %@", [e reason]);
+ }
+ @finally {
+ if (!rawObject)
+ return nil;
+ }
+
+ NSMutableDictionary *entry = [NSMutableDictionary dictionaryWithCapacity:12];
+ [entry setObject:[rawObject objectForKey:@"key"] forKey:@"key"];
+ [entry setObject:[NSNumber numberWithInt:[[rawObject objectForKey:@"deleted"] intValue]] forKey:@"deleted"];
+ // Normalize dates from unix epoch timestamps to mac os x epoch timestamps
+ [entry setObject:[NSNumber numberWithDouble:[[NSDate dateWithTimeIntervalSince1970:[[rawObject objectForKey:@"createdate"] doubleValue]] timeIntervalSinceReferenceDate]] forKey:@"create"];
+ [entry setObject:[NSNumber numberWithDouble:[[NSDate dateWithTimeIntervalSince1970:[[rawObject objectForKey:@"modifydate"] doubleValue]] timeIntervalSinceReferenceDate]] forKey:@"modify"];
+ [entry setObject:[NSNumber numberWithInt:[[rawObject objectForKey:@"syncnum"] intValue]] forKey:@"syncnum"];
+ [entry setObject:[NSNumber numberWithInt:[[rawObject objectForKey:@"version"] intValue]] forKey:@"version"];
+ [entry setObject:[NSNumber numberWithInt:[[rawObject objectForKey:@"minversion"] intValue]] forKey:@"minversion"];
+ if ([rawObject objectForKey:@"sharekey"]) {
+ [entry setObject:[rawObject objectForKey:@"sharekey"] forKey:@"sharekey"];
+ }
+ if ([rawObject objectForKey:@"publishkey"]) {
+ [entry setObject:[rawObject objectForKey:@"publishkey"] forKey:@"publishkey"];
+ }
+ [entry setObject:[rawObject objectForKey:@"systemtags"] forKey:@"systemtags"];
+ [entry setObject:[rawObject objectForKey:@"tags"] forKey:@"tags"];
if ([[fetcher representedObject] conformsToProtocol:@protocol(SynchronizedNote)]) [entry setObject:[fetcher representedObject] forKey:@"NoteObject"];
- [entry setObject:bodyString forKey:@"content"];
-
+ [entry setObject:[rawObject objectForKey:@"content"] forKey:@"content"];
+
//NSLog(@"fetched entry %@" , entry);
-
+
return entry;
}
@@ -166,7 +190,17 @@ - (void)syncResponseFetcher:(SyncResponseFetcher*)fetcher receivedData:(NSData*)
[NSNumber numberWithInt:[fetcher statusCode]], @"StatusCode", nil]];
}
} else {
- [entriesCollected addObject:[self preparedDictionaryWithFetcher:fetcher receivedData:data]];
+ NSDictionary *preparedDictionary = [self preparedDictionaryWithFetcher:fetcher receivedData:data];
+ if (!preparedDictionary) {
+ // Parsing JSON failed. Is this the right way to handle the error?
+ id obj = [fetcher representedObject];
+ if (obj) {
+ [entriesInError addObject: [NSDictionary dictionaryWithObjectsAndKeys: obj, @"NoteObject",
+ [NSNumber numberWithInt:[fetcher statusCode]], @"StatusCode", nil]];
+ }
+ } else {
+ [entriesCollected addObject: preparedDictionary];
+ }
}
if (entryFinishedCount >= [entriesToCollect count] || stopped) {
@@ -224,18 +258,32 @@ - (SyncResponseFetcher*)_fetcherForNote:(NoteObject*)aNote creator:(BOOL)doesCre
CFAbsoluteTime modNum = doesCreate ? modifiedDateOfNote(aNote) : [[info objectForKey:@"modify"] doubleValue];
//always set the mod date, set created date if we are creating, set the key if we are updating
- NSMutableDictionary *params = [NSMutableDictionary dictionaryWithObjectsAndKeys: email, @"email", authToken, @"auth", nil];
- if (modNum > 0.0) [params setObject:[NSString simplenoteDateWithAbsoluteTime:modNum] forKey:@"modify"];
- if (doesCreate) [params setObject:[NSString simplenoteDateWithAbsoluteTime:createdDateOfNote(aNote)] forKey:@"create"];
- if (!doesCreate) [params setObject:[info objectForKey:@"key"] forKey:@"key"]; //raises its own exception if key is nil
+ NSDictionary *params = [NSDictionary dictionaryWithObjectsAndKeys: email, @"email", authToken, @"auth", nil];
NSMutableString *noteBody = [[[aNote combinedContentWithContextSeparator: /* explicitly assume default separator if creating */
doesCreate ? nil : [info objectForKey:SimplenoteSeparatorKey]] mutableCopy] autorelease];
//simpletext iPhone app loses any tab characters
[noteBody replaceTabsWithSpacesOfWidth:[[GlobalPrefs defaultPrefs] numberOfSpacesInTab]];
- NSURL *noteURL = [SimplenoteSession servletURLWithPath:@"/api/note" parameters:params];
- SyncResponseFetcher *fetcher = [[SyncResponseFetcher alloc] initWithURL:noteURL bodyStringAsUTF8B64:noteBody delegate:self];
+ NSMutableDictionary *rawObject = [NSMutableDictionary dictionaryWithCapacity: 12];
+ if (modNum > 0.0) [rawObject setObject:[NSNumber numberWithDouble:[[NSDate dateWithTimeIntervalSinceReferenceDate:modNum] timeIntervalSince1970]] forKey:@"modifydate"];
+ if (doesCreate) [rawObject setObject:[NSNumber numberWithDouble:[[NSDate dateWithTimeIntervalSinceReferenceDate:createdDateOfNote(aNote)] timeIntervalSince1970]] forKey:@"createdate"];
+
+ NSArray *tags = [aNote orderedLabelTitles];
+ // Don't send an empty tagset if this note has never been synced via sn-api2
+ if ([tags count] || ([info objectForKey:@"syncnum"] != nil)) {
+ [rawObject setObject:tags forKey:@"tags"];
+ }
+
+ [rawObject setObject:noteBody forKey:@"content"];
+
+ NSURL *noteURL = nil;
+ if (doesCreate) {
+ noteURL = [SimplenoteSession servletURLWithPath:@"/api2/data" parameters:params];
+ } else {
+ noteURL = [SimplenoteSession servletURLWithPath:[NSString stringWithFormat:@"/api2/data/%@", [info objectForKey:@"key"]] parameters:params];
+ }
+ SyncResponseFetcher *fetcher = [[SyncResponseFetcher alloc] initWithURL:noteURL POSTData:[[rawObject jsonStringValue] dataUsingEncoding:NSUTF8StringEncoding] contentType:@"application/json" delegate:self];
[fetcher setRepresentedObject:aNote];
return [fetcher autorelease];
}
@@ -262,10 +310,13 @@ - (SyncResponseFetcher*)fetcherForDeletingNote:(DeletedNoteObject*)aDeletedNote
}
NSAssert([info objectForKey:@"key"], @"fetcherForDeletingNote: got deleted note and couldn't find a key anywhere!");
- NSURL *noteURL = [SimplenoteSession servletURLWithPath:@"/api/delete" parameters:
+ //in keeping with nv's behavior with sn api1, deleting only marks a note as deleted.
+ //may want to implement actual purging (using HTTP DELETE) in the future
+ NSURL *noteURL = [SimplenoteSession servletURLWithPath:[NSString stringWithFormat:@"/api2/data/%@", [info objectForKey:@"key"]] parameters:
[NSDictionary dictionaryWithObjectsAndKeys: email, @"email",
- authToken, @"auth", [info objectForKey:@"key"], @"key", nil]];
- SyncResponseFetcher *fetcher = [[SyncResponseFetcher alloc] initWithURL:noteURL POSTData:nil delegate:self];
+ authToken, @"auth", nil]];
+ NSData *postData = [[[NSDictionary dictionaryWithObject:[NSNumber numberWithInt:1] forKey:@"deleted"] jsonStringValue] dataUsingEncoding:NSUTF8StringEncoding];
+ SyncResponseFetcher *fetcher = [[SyncResponseFetcher alloc] initWithURL:noteURL POSTData:postData contentType:@"application/json" delegate:self];
[fetcher setRepresentedObject:aDeletedNote];
return [fetcher autorelease];
@@ -306,9 +357,31 @@ - (void)stop {
#endif
- (NSDictionary*)preparedDictionaryWithFetcher:(SyncResponseFetcher*)fetcher receivedData:(NSData*)data {
- NSString *keyString = [[[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding] autorelease];
+
+ NSString *bodyString = [[[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding] autorelease];
+
+ NSDictionary *rawObject = nil;
+ @try {
+ rawObject = [NSDictionary dictionaryWithJSONString:bodyString];
+ }
+ @catch (NSException *e) {
+ NSLog(@"Exception while parsing Simplenote JSON note object: %@", [e reason]);
+ }
+ @finally {
+ if (!rawObject)
+ return nil;
+ }
+
+ NSString *keyString = [rawObject objectForKey:@"key"];
NSMutableDictionary *result = [NSMutableDictionary dictionaryWithCapacity:5];
+ NSMutableDictionary *syncMD = [NSMutableDictionary dictionaryWithCapacity:5];
+ [syncMD setObject:[rawObject objectForKey:@"key"] forKey:@"key"];
+ [syncMD setObject:[NSNumber numberWithDouble:[[NSDate dateWithTimeIntervalSince1970:[[rawObject objectForKey:@"createdate"] doubleValue]] timeIntervalSinceReferenceDate]] forKey:@"create"];
+ [syncMD setObject:[NSNumber numberWithDouble:[[NSDate dateWithTimeIntervalSince1970:[[rawObject objectForKey:@"modifydate"] doubleValue]] timeIntervalSinceReferenceDate]] forKey:@"modify"];
+ [syncMD setObject:[NSNumber numberWithInt:[[rawObject objectForKey:@"syncnum"] intValue]] forKey:@"syncnum"];
+ [syncMD setObject:[NSNumber numberWithBool:NO] forKey:@"dirty"];
+
if ([fetcher representedObject]) {
id <SynchronizedNote> aNote = [fetcher representedObject];
[result setObject:aNote forKey:@"NoteObject"];
@@ -323,10 +396,8 @@ - (NSDictionary*)preparedDictionaryWithFetcher:(SyncResponseFetcher*)fetcher rec
NSAssert([aNote isKindOfClass:[NoteObject class]], @"received a non-noteobject from a fetcherForCreatingNote: operation!");
//don't need to store a separator for newly-created notes; when nil it is presumed the default separator
- [aNote setSyncObjectAndKeyMD:[NSDictionary dictionaryWithObjectsAndKeys:
- [NSNumber numberWithDouble:modifiedDateOfNote(aNote)], @"modify",
- [NSNumber numberWithDouble:createdDateOfNote(aNote)], @"create",
- keyString, @"key", nil] forService:SimplenoteServiceName];
+ [aNote setSyncObjectAndKeyMD:syncMD forService:SimplenoteServiceName];
+
[(NoteObject*)aNote makeNoteDirtyUpdateTime:NO updateFile:NO];
} else if (@selector(fetcherForDeletingNote:) == fetcherOpSEL) {
//this note has been successfully deleted, and can now have its Simplenote syncServiceMD entry removed
@@ -334,8 +405,28 @@ - (NSDictionary*)preparedDictionaryWithFetcher:(SyncResponseFetcher*)fetcher rec
NSAssert([aNote isKindOfClass:[DeletedNoteObject class]], @"received a non-deletednoteobject from a fetcherForDeletingNote: operation");
[aNote removeAllSyncMDForService:SimplenoteServiceName];
} else if (@selector(fetcherForUpdatingNote:) == fetcherOpSEL) {
+ // SN api2 can return a content key in an update response containing
+ // the merged changes from other clients....
+ if ([rawObject objectForKey:@"content"]) {
+ NSUInteger bodyLoc = 0;
+ NSString *separator = nil;
+ NSString *combinedContent = [rawObject objectForKey:@"content"];
+ NSString *newTitle = [combinedContent syntheticTitleAndSeparatorWithContext:&separator bodyLoc:&bodyLoc oldTitle:titleOfNote(aNote) maxTitleLen:60];
+
+ [(NoteObject *)aNote updateWithSyncBody:[combinedContent substringFromIndex:bodyLoc] andTitle:newTitle];
+ }
- //[aNote removeKey:@"dirty" forService:SimplenoteServiceName];
+ // Tags may have been changed by another client...
+ NSSet *localTags = [NSSet setWithArray:[(NoteObject *)aNote orderedLabelTitles]];
+ NSSet *remoteTags = [NSSet setWithArray:[rawObject objectForKey:@"tags"]];
+ if (![localTags isEqualToSet:remoteTags]) {
+ NSLog(@"Updating tags with remote values.");
+ NSString *newLabelString = [[remoteTags allObjects] componentsJoinedByString:@" "];
+ [(NoteObject *)aNote setLabelString:newLabelString];
+ }
+
+ [aNote setSyncObjectAndKeyMD:syncMD forService: SimplenoteServiceName];
+ //NSLog(@"note update:\n %@", [aNote syncServicesMD]);
} else {
NSLog(@"%s called with unknown opSEL: %s", _cmd, fetcherOpSEL);
}
View
6 SimplenoteSession.h
@@ -60,6 +60,10 @@ extern NSString *SimplenoteSeparatorKey;
NSMutableSet *collectorsInProgress;
+ //used to span multiple partial index fetches (when mark is present in response)
+ NSMutableArray *indexEntryBuffer;
+ NSString *indexMark;
+
id delegate;
}
@@ -75,8 +79,10 @@ extern NSString *SimplenoteSeparatorKey;
- (BOOL)reachabilityFailed;
- (NSComparisonResult)localEntry:(NSDictionary*)localEntry compareToRemoteEntry:(NSDictionary*)remoteEntry;
+-(void)applyMetadataUpdatesToNote:(id <SynchronizedNote>)aNote localEntry:(NSDictionary *)localEntry remoteEntry: (NSDictionary *)remoteEntry;
- (BOOL)remoteEntryWasMarkedDeleted:(NSDictionary*)remoteEntry;
- (BOOL)entryHasLocalChanges:(NSDictionary*)entry;
+- (BOOL)tagsShouldBeMergedForEntry:(NSDictionary*)entry;
+ (void)registerLocalModificationForNote:(id <SynchronizedNote>)aNote;
View
190 SimplenoteSession.m
@@ -28,7 +28,7 @@
#import "GlobalPrefs.h"
#import "NotationPrefs.h"
#import "NSString_NV.h"
-#import "NSArray+BSJSONAdditions.h"
+#import "NSDictionary+BSJSONAdditions.h"
#import "AttributedPlainText.h"
#import "InvocationRecorder.h"
#import "SynchronizedNoteProtocol.h"
@@ -41,6 +41,7 @@
NSString *SimplenoteServiceName = @"SN";
NSString *SimplenoteSeparatorKey = @"SepStr";
+#define kSimplenoteSessionIndexBatchSize 100
@implementation SimplenoteSession
@@ -60,7 +61,7 @@ + (NSString*)nameOfKeyElement {
+ (NSURL*)servletURLWithPath:(NSString*)path parameters:(NSDictionary*)params {
NSAssert(path != nil, @"path is required");
- //path example: "/api/note"
+ //path example: "/api2/index"
NSString *queryStr = params ? [NSString stringWithFormat:@"?%@", [params URLEncodedString]] : @"";
return [NSURL URLWithString:[NSString stringWithFormat:@"https://simple-note.appspot.com%@%@", path, queryStr]];
@@ -128,40 +129,97 @@ - (BOOL)reachabilityFailed {
- (NSComparisonResult)localEntry:(NSDictionary*)localEntry compareToRemoteEntry:(NSDictionary*)remoteEntry {
//simplenote-specific logic to determine whether to upload localEntry as a newer version of remoteEntry
- NSNumber *modifiedLocalNumber = [localEntry objectForKey:@"modify"];
- NSNumber *modifiedRemoteNumber = [remoteEntry objectForKey:@"modify"];
-
- if ([modifiedLocalNumber isKindOfClass:[NSNumber class]] && [modifiedRemoteNumber isKindOfClass:[NSNumber class]]) {
- CFAbsoluteTime localAbsTime = floor([modifiedLocalNumber doubleValue]);
- CFAbsoluteTime remoteAbsTime = floor([modifiedRemoteNumber doubleValue]);
+ //all dirty local notes are to be sent to the server
+ if ([self entryHasLocalChanges: localEntry]) {
+ return NSOrderedDescending;
+ }
+ Class numberClass = [NSNumber class];
+ //local notes lacking syncnum MD are either completely new or were synced with api1
+ if (![localEntry objectForKey:@"syncnum"]) {
+ NSNumber *modifiedLocalNumber = [localEntry objectForKey:@"modify"];
+ NSNumber *modifiedRemoteNumber = [remoteEntry objectForKey:@"modify"];
+
+ if ([modifiedLocalNumber isKindOfClass:numberClass] && [modifiedRemoteNumber isKindOfClass:numberClass]) {
+ CFAbsoluteTime localAbsTime = floor([modifiedLocalNumber doubleValue]);
+ CFAbsoluteTime remoteAbsTime = floor([modifiedRemoteNumber doubleValue]);
+
+ if (localAbsTime > remoteAbsTime) {
+ return NSOrderedDescending;
+ } else if (localAbsTime < remoteAbsTime) {
+ return NSOrderedAscending;
+ }
+ return NSOrderedSame;
+ }
+ } else {
+ NSNumber *syncnumLocalNumber = [localEntry objectForKey:@"syncnum"];
+ NSNumber *syncnumRemoteNumber = [remoteEntry objectForKey:@"syncnum"];
- if (localAbsTime > remoteAbsTime) {
- return NSOrderedDescending;
- } else if (localAbsTime < remoteAbsTime) {
- return NSOrderedAscending;
+ if ([syncnumLocalNumber isKindOfClass:numberClass] && [syncnumRemoteNumber isKindOfClass:numberClass]) {
+ int localSyncnum = [syncnumLocalNumber intValue];
+ int remoteSyncnum = [syncnumRemoteNumber intValue];
+ if (localSyncnum < remoteSyncnum) {
+ return NSOrderedAscending;
+ }
+ return NSOrderedSame; // NSOrderedDescending is not possible with syncnum versioning
}
- return NSOrderedSame;
}
//no comparison posible is the same as no comparison necessary for this method;
//the locally-added or remotely-added cases should not need to look at modification dates
- NSLog(@"%@ or %@ are lacking a date-modified property!", localEntry, remoteEntry);
+ //TODO: Should we default to NSOrderedDescending (to force server-side sync) when in doubt??
+ NSLog(@"%@ or %@ are lacking syncnum property and date-modified property!", localEntry, remoteEntry);
return NSOrderedSame;
}
+-(void)applyMetadataUpdatesToNote:(id <SynchronizedNote>)aNote localEntry:(NSDictionary *)localEntry remoteEntry: (NSDictionary *)remoteEntry {
+ //tags may have updated even if content wasn't, or we may never have synced tags
+ NSSet *localTagset = [NSSet setWithArray:[(NoteObject *)aNote orderedLabelTitles]];
+ NSSet *remoteTagset = [NSSet setWithArray:[remoteEntry objectForKey:@"tags"]];
+ if (![localTagset isEqualToSet:remoteTagset]) {
+ NSLog(@"Tagsets differ. Updating.");
+ NSString *newLabelString = nil;
+ if ([self tagsShouldBeMergedForEntry:localEntry]) {
+ NSMutableSet *mergedTags = [NSMutableSet setWithSet:localTagset];
+ [mergedTags unionSet:remoteTagset];
+ if ([mergedTags count]) {
+ newLabelString = [[mergedTags allObjects] componentsJoinedByString:@" "];
+ }
+ } else {
+ if ([remoteTagset count]) {
+ newLabelString = [[remoteTagset allObjects] componentsJoinedByString:@" "];
+ }
+ }
+ [(NoteObject *)aNote setLabelString:newLabelString];
+ }
+
+ //set the metadata from the server if this is the first time syncing with api2
+ if (![localEntry objectForKey:@"syncnum"]) {
+ NSDictionary *updatedMetadata = [NSDictionary dictionaryWithObjectsAndKeys:[remoteEntry objectForKey:@"syncnum"], @"syncnum", [remoteEntry objectForKey:@"version"], @"version", [remoteEntry objectForKey:@"modify"], @"modify", nil];
+
+ [aNote setSyncObjectAndKeyMD: updatedMetadata forService: SimplenoteServiceName];
+ }
+}
+
- (BOOL)remoteEntryWasMarkedDeleted:(NSDictionary*)remoteEntry {
return [[remoteEntry objectForKey:@"deleted"] intValue] == 1;
}
- (BOOL)entryHasLocalChanges:(NSDictionary*)entry {
- return [[entry objectForKey:@"dirty"] intValue] == 1;
+ return [[entry objectForKey:@"dirty"] boolValue];
+}
+- (BOOL)tagsShouldBeMergedForEntry:(NSDictionary*)entry {
+ // If the local note doesn't have a syncnum, then it has not been synced with sn-api2
+ return ([entry objectForKey:@"syncnum"] == nil);
}
+ (void)registerLocalModificationForNote:(id <SynchronizedNote>)aNote {
//if this note has been synced with this service at least once, mirror the mod date
+ //mod date should no longer be necessary with SN api2, but doesn't hurt. what's really important is marking the note dirty.
NSDictionary *aDict = [[aNote syncServicesMD] objectForKey:SimplenoteServiceName];
if (aDict) {
NSAssert([aNote isKindOfClass:[NoteObject class]], @"can't modify a non-note!");
- [aNote setSyncObjectAndKeyMD:[NSDictionary dictionaryWithObject:
- [NSNumber numberWithDouble:modifiedDateOfNote((NoteObject*)aNote)] forKey:@"modify"]
+ [aNote setSyncObjectAndKeyMD:[NSDictionary dictionaryWithObjectsAndKeys:
+ [NSNumber numberWithDouble:modifiedDateOfNote((NoteObject*)aNote)], @"modify",
+ [NSNumber numberWithBool:YES], @"dirty",
+ nil]
forService:SimplenoteServiceName];
} //if note has no metadata for this service, mod times don't matter because it will be added, anyway
@@ -239,8 +297,15 @@ - (SyncResponseFetcher*)loginFetcher {
- (SyncResponseFetcher*)listFetcher {
if (!listFetcher) {
NSAssert(authToken != nil, @"no authtoken found");
- NSURL *listURL = [SimplenoteSession servletURLWithPath:@"/api/index" parameters:
- [NSDictionary dictionaryWithObjectsAndKeys: emailAddress, @"email", authToken, @"auth", nil]];
+ NSMutableDictionary *params = [NSMutableDictionary dictionaryWithCapacity: 4];
+ [params setObject:[NSString stringWithFormat:@"%u", kSimplenoteSessionIndexBatchSize] forKey:@"length"];
+ [params setObject:emailAddress forKey:@"email"];
+ [params setObject:authToken forKey:@"auth"];
+ if (indexMark) {
+ [params setObject:indexMark forKey:@"mark"];
+ }
+ NSURL *listURL = [SimplenoteSession servletURLWithPath:@"/api2/index" parameters:
+ params];
listFetcher = [[SyncResponseFetcher alloc] initWithURL:listURL POSTData:nil delegate:self];
}
return listFetcher;
@@ -317,6 +382,10 @@ - (void)stop {
[[[collectorsInProgress copy] autorelease] makeObjectsPerformSelector:@selector(stop)];
[loginFetcher cancel];
[listFetcher cancel];
+ [indexEntryBuffer release];
+ indexEntryBuffer = nil;
+ [indexMark release];
+ indexMark = nil;
}
//these two methods and probably more are general enough to be abstracted into NotationSyncServiceManager
@@ -563,12 +632,20 @@ - (void)changedEntryCollectorDidFinish:(SimplenoteEntryCollector *)collector {
NSString *newTitle = [combinedContent syntheticTitleAndSeparatorWithContext:&separator bodyLoc:&bodyLoc oldTitle:titleOfNote(aNote) maxTitleLen:60];
[aNote updateWithSyncBody:[combinedContent substringFromIndex:bodyLoc] andTitle:newTitle];
+ NSMutableSet *labelTitles = [NSMutableSet setWithArray:[info objectForKey:@"tags"]];
+ if ([self tagsShouldBeMergedForEntry:[[aNote syncServicesMD] objectForKey:SimplenoteServiceName]]) {
+ [labelTitles addObjectsFromArray:[aNote orderedLabelTitles]];
+ }
+ if ([labelTitles count]) {
+ [aNote setLabelString:[[labelTitles allObjects] componentsJoinedByString:@" "]];
+ } else {
+ [aNote setLabelString:nil];
+ }
NSNumber *modNum = [info objectForKey:@"modify"];
//NSLog(@"updating mod time for note %@ to %@", aNote, modNum);
[aNote setDateModified:[modNum doubleValue]];
- [aNote setSyncObjectAndKeyMD:[NSDictionary dictionaryWithObjectsAndKeys:modNum, @"modify", separator, SimplenoteSeparatorKey, nil]
- forService:SimplenoteServiceName];
+ [aNote setSyncObjectAndKeyMD:[NSDictionary dictionaryWithObjectsAndKeys:[info objectForKey:@"syncnum"], @"syncnum", modNum, @"modify", separator, SimplenoteSeparatorKey, [NSNumber numberWithBool: NO], @"dirty", nil] forService:SimplenoteServiceName];
[changedNotes addObject:aNote];
}
[self stopSuppressingPushingForNotes:[NSArray arrayWithObject:aNote]];
@@ -704,15 +781,14 @@ - (NSArray*)_notesWithEntries:(NSArray*)entries {
[attributedBody addLinkAttributesForRange:NSMakeRange(0, [attributedBody length])];
[attributedBody addStrikethroughNearDoneTagsForRange:NSMakeRange(0, [attributedBody length])];
- NoteObject *note = [[NoteObject alloc] initWithNoteBody:attributedBody title:title delegate:delegate format:SingleDatabaseFormat labels:nil];
+ NSString *labelString = [[info objectForKey:@"tags"] count] ? [[info objectForKey:@"tags"] componentsJoinedByString:@" "] : nil;
+ NoteObject *note = [[NoteObject alloc] initWithNoteBody:attributedBody title:title delegate:delegate format:SingleDatabaseFormat labels:labelString];
if (note) {
NSNumber *modNum = [info objectForKey:@"modify"];
[note setDateAdded:[[info objectForKey:@"create"] doubleValue]];
[note setDateModified:[modNum doubleValue]];
- //also set mod time, key, and sepWCtx for this note's syncServicesMD
- [note setSyncObjectAndKeyMD:[NSDictionary dictionaryWithObjectsAndKeys:modNum, @"modify",
- [info objectForKey:@"key"], @"key", separator, SimplenoteSeparatorKey, nil]
- forService:SimplenoteServiceName];
+ //also set syncnum, version, mod time, key, and sepWCtx for this note's syncServicesMD
+ [note setSyncObjectAndKeyMD:[NSDictionary dictionaryWithObjectsAndKeys:[info objectForKey:@"syncnum"], @"syncnum", [info objectForKey:@"version"], @"version", modNum, @"modify", [info objectForKey:@"key"], @"key", separator, SimplenoteSeparatorKey, nil] forService:SimplenoteServiceName];
[newNotes addObject:note];
[note release];
@@ -938,11 +1014,14 @@ - (void)syncResponseFetcher:(SyncResponseFetcher*)fetcher receivedData:(NSData*)
[self _stoppedWithErrorString:NSLocalizedString(@"No authorization token", @"Simplenote-specific error")];
}
} else if (fetcher == listFetcher) {
-
lastIndexAuthFailed = NO;
+ NSDictionary *responseDictionary = nil;
NSArray *rawEntries = nil;
@try {
- rawEntries = [NSArray arrayWithJSONString:bodyString];
+ responseDictionary = [NSDictionary dictionaryWithJSONString:bodyString];
+ if (responseDictionary) {
+ rawEntries = [responseDictionary objectForKey:@"data"];
+ }
} @catch (NSException *e) {
NSLog(@"Exception while parsing Simplenote JSON index: %@", [e reason]);
} @finally {
@@ -951,31 +1030,70 @@ - (void)syncResponseFetcher:(SyncResponseFetcher*)fetcher receivedData:(NSData*)
return;
}
}
- //convert dates and "deleted" indicator into NSNumbers
+ //convert syncnum, dates and "deleted" indicator into NSNumbers
NSMutableArray *entries = [NSMutableArray arrayWithCapacity:[rawEntries count]];
NSUInteger i = 0;
for (i=0; i<[rawEntries count]; i++) {
NSDictionary *rawEntry = [rawEntries objectAtIndex:i];
NSString *noteKey = [rawEntry objectForKey:@"key"];
- NSString *modifiedDateString = [rawEntry objectForKey:@"modify"];
+ NSNumber *syncnum = [NSNumber numberWithInt:[[rawEntry objectForKey:@"syncnum"] intValue]];
+ NSNumber *modified = [NSNumber numberWithDouble:[[NSDate dateWithTimeIntervalSince1970:[[rawEntry objectForKey:@"modifydate"] doubleValue]] timeIntervalSinceReferenceDate]];
+ NSNumber *minversion = [NSNumber numberWithInt:[[rawEntry objectForKey:@"minversion"] intValue]];
+ NSNumber *version = [NSNumber numberWithInt:[[rawEntry objectForKey:@"version"] intValue]];
+ NSArray *tags = [rawEntry objectForKey:@"tags"];
+ NSArray *systemtags = [rawEntry objectForKey:@"systemtags"];
- if ([noteKey length] && [modifiedDateString length]) {
- //convenient intermediate format
+ if ([noteKey length] && [syncnum intValue] && [modified doubleValue]) {
+ //convenient intermediate format, including all metadata
+ //in the index, so we don't need to fetch the individual note if
+ //content hasn't changed
[entries addObject:[NSDictionary dictionaryWithObjectsAndKeys:
noteKey, @"key",
[NSNumber numberWithInt:[[rawEntry objectForKey:@"deleted"] intValue]], @"deleted",
- [NSNumber numberWithDouble:[modifiedDateString absoluteTimeFromSimplenoteDate]], @"modify", nil]];
+ modified, @"modify",
+ syncnum, @"syncnum",
+ minversion, @"minversion",
+ version, @"version",
+ systemtags, @"systemtags",
+ tags, @"tags",
+ nil]];
}
}
- [self _updateSyncTime];
[lastErrorString autorelease];
lastErrorString = nil;
reachabilityFailed = NO;
- [delegate syncSession:self receivedFullNoteList:entries];
+ if (!indexEntryBuffer) {
+ indexEntryBuffer = [[NSMutableArray alloc] initWithArray: entries];
+ } else {
+ [indexEntryBuffer addObjectsFromArray: entries];
+ }
+
+ //sn api2 will only return up to [length(max=100)] entries per call to /api2/index.
+ //if more entries remain, the response includes a 'mark' key to use in the next
+ //request. When this happens, we want to kick off another fetcher and act as though
+ //it were simply a continuation of the current one (meaning we don't want
+ //other tasks to wake up until we've processed the full note list).
+ //Ultimately, we should probably re-architect this so that a partitioned
+ //index can be processed in a less-hacky manner.
+ //we can no longer rely on the URL for listFetcher remaining constant,
+ //so we don't reuse it. (we could consider extending SyncResponseFetcher to support
+ //dynamic URLS instead)
+ [listFetcher autorelease];
+ listFetcher = nil;
+ [indexMark release];
+ indexMark = [[responseDictionary objectForKey:@"mark"] copy];
+ if (indexMark) {
+ [[self listFetcher] start];
+ } else {
+ [self _updateSyncTime];
+ [delegate syncSession:self receivedFullNoteList:indexEntryBuffer];
+ [indexEntryBuffer autorelease];
+ indexEntryBuffer = nil;
+ }
} else {
NSLog(@"unknown fetcher returned: %@, body: %@", fetcher, bodyString);
@@ -1001,6 +1119,8 @@ - (void)dealloc {
[emailAddress release];
[password release];
[authToken release];
+ [indexMark release];
+ [indexEntryBuffer release];
[listFetcher release];
[loginFetcher release];
[collectorsInProgress release];
View
2  SyncResponseFetcher.h
@@ -30,6 +30,7 @@
NSMutableData *receivedData;
NSData *dataToSend;
+ NSString *dataToSendContentType;
NSURLConnection *urlConnection;
NSURL *requestURL;
NSDictionary *headers;
@@ -45,6 +46,7 @@
- (id)initWithURL:(NSURL*)aURL bodyStringAsUTF8B64:(NSString*)stringToEncode delegate:(id)aDelegate;
- (id)initWithURL:(NSURL*)aURL POSTData:(NSData*)POSTData delegate:(id)aDelegate;
+- (id)initWithURL:(NSURL*)aURL POSTData:(NSData*)POSTData contentType:(NSString*)contentType delegate:(id)aDelegate;
- (void)setRepresentedObject:(id)anObject;
- (id)representedObject;
- (NSInvocation*)successInvocation;
View
11 SyncResponseFetcher.m
@@ -49,12 +49,16 @@ - (id)initWithURL:(NSURL*)aURL bodyStringAsUTF8B64:(NSString*)stringToEncode del
}
- (id)initWithURL:(NSURL*)aURL POSTData:(NSData*)POSTData delegate:(id)aDelegate {
-
+ return [self initWithURL:aURL POSTData:POSTData contentType:nil delegate:aDelegate];
+}
+
+- (id)initWithURL:(NSURL*)aURL POSTData:(NSData*)POSTData contentType:(NSString*)contentType delegate:(id)aDelegate {
if ([self init]) {
receivedData = [[NSMutableData alloc] init];
requestURL = [aURL retain];
delegate = aDelegate;
dataToSend = [POSTData retain];
+ dataToSendContentType = [contentType copy];
}
return self;
}
@@ -93,6 +97,10 @@ - (BOOL)start {
//if POSTData is nil, do a plain GET request
if (dataToSend) {
+ if (dataToSendContentType) {
+ [request addValue:dataToSendContentType forHTTPHeaderField:@"Content-Type"];
+ }
+
[request setHTTPBody:dataToSend];
[request setHTTPMethod:@"POST"];
}
@@ -136,6 +144,7 @@ - (void)cancel {
- (void)dealloc {
[dataToSend release];
+ [dataToSendContentType release];
[requestURL release];
[receivedData release];
[urlConnection release];
View
1  SyncServiceSessionProtocol.h
@@ -37,6 +37,7 @@
- (id)initWithNotationPrefs:(NotationPrefs*)prefs;
- (NSComparisonResult)localEntry:(NSDictionary*)localEntry compareToRemoteEntry:(NSDictionary*)remoteEntry;
+- (void)applyMetadataUpdatesToNote:(id <SynchronizedNote>)aNote localEntry:(NSDictionary *)localEntry remoteEntry: (NSDictionary *)remoteEntry;
- (BOOL)remoteEntryWasMarkedDeleted:(NSDictionary*)remoteEntry;
+ (void)registerLocalModificationForNote:(id <SynchronizedNote>)aNote;
Please sign in to comment.
Something went wrong with that request. Please try again.