diff --git a/Headers/Foundation/NSUbiquitousKeyValueStore.h b/Headers/Foundation/NSUbiquitousKeyValueStore.h index 20ae3770d9..7a0b85b32d 100644 --- a/Headers/Foundation/NSUbiquitousKeyValueStore.h +++ b/Headers/Foundation/NSUbiquitousKeyValueStore.h @@ -46,14 +46,14 @@ GS_EXPORT_CLASS * Getting the Shared Instance */ + (NSUbiquitousKeyValueStore *) defaultStore; - + // Getting Values /** * Returns the array associated with the specified key. */ - (NSArray *) arrayForKey: (NSString *)key; - + /** * Returns the Boolean value associated with the specified key. */ @@ -63,7 +63,7 @@ GS_EXPORT_CLASS * Returns the data object associated with the specified key. */ - (NSData*) dataForKey: (NSString *)key; - + /** * Returns the dictionary object associated with the specified key. */ @@ -157,6 +157,12 @@ GS_EXPORT_CLASS GS_EXPORT NSString* const NSUbiquitousKeyValueStoreDidChangeExternallyNotification; GS_EXPORT NSString* const NSUbiquitousKeyValueStoreChangeReasonKey; +// Change reason values +GS_EXPORT NSString* const NSUbiquitousKeyValueStoreServerChange; +GS_EXPORT NSString* const NSUbiquitousKeyValueStoreInitialSyncChange; +GS_EXPORT NSString* const NSUbiquitousKeyValueStoreQuotaViolationChange; +GS_EXPORT NSString* const NSUbiquitousKeyValueStoreAccountChange; + #if defined(__cplusplus) } #endif diff --git a/Source/NSUbiquitousKeyValueStore.m b/Source/NSUbiquitousKeyValueStore.m index cd7fc42c5f..4b95609a4d 100644 --- a/Source/NSUbiquitousKeyValueStore.m +++ b/Source/NSUbiquitousKeyValueStore.m @@ -21,19 +21,47 @@ Software Foundation, Inc., 31 Milk Street #960789 Boston, MA 02196 USA. */ + #import "common.h" #import "Foundation/NSAutoreleasePool.h" #import "Foundation/NSCoder.h" #import "Foundation/NSEnumerator.h" #import "Foundation/NSException.h" #import "Foundation/NSKeyedArchiver.h" -#import -#import -#import -#import -#import -#import -#import +#import "Foundation/NSUbiquitousKeyValueStore.h" +#import "Foundation/NSArray.h" +#import "Foundation/NSDictionary.h" +#import "Foundation/NSData.h" +#import "Foundation/NSString.h" +#import "Foundation/NSValue.h" +#import "Foundation/NSUserDefaults.h" +#import "Foundation/NSNotificationCenter.h" +#import "Foundation/NSThread.h" +#import "Foundation/NSLock.h" +#import "Foundation/NSFileManager.h" +#import "Foundation/NSPathUtilities.h" +#import "Foundation/NSPropertyList.h" +#import "Foundation/NSBundle.h" +#import "Foundation/NSTimer.h" +#import "Foundation/NSURL.h" +#import "Foundation/NSURLRequest.h" +#import "Foundation/NSURLConnection.h" +#import "Foundation/NSURLResponse.h" +#import "Foundation/NSHTTPURLResponse.h" +#import "Foundation/NSJSONSerialization.h" +#import "Foundation/NSOperation.h" +#import "Foundation/NSOperationQueue.h" +#import "Foundation/NSDate.h" + +// Notification constants +NSString* const NSUbiquitousKeyValueStoreDidChangeExternallyNotification = @"NSUbiquitousKeyValueStoreDidChangeExternallyNotification"; +NSString* const NSUbiquitousKeyValueStoreChangeReasonKey = @"NSUbiquitousKeyValueStoreChangeReasonKey"; + +// Change reason constants +NSString* const NSUbiquitousKeyValueStoreServerChange = @"NSUbiquitousKeyValueStoreServerChange"; +NSString* const NSUbiquitousKeyValueStoreInitialSyncChange = @"NSUbiquitousKeyValueStoreInitialSyncChange"; +NSString* const NSUbiquitousKeyValueStoreQuotaViolationChange = @"NSUbiquitousKeyValueStoreQuotaViolationChange"; +NSString* const NSUbiquitousKeyValueStoreAccountChange = @"NSUbiquitousKeyValueStoreAccountChange"; static NSUbiquitousKeyValueStore *_sharedUbiquitousKeyValueStore = nil; @@ -194,7 +222,17 @@ - (NSDictionary *) dictionaryRepresentation @interface GSSimpleUbiquitousKeyValueStore : NSUbiquitousKeyValueStore { NSMutableDictionary *_dict; + NSString *_storePath; + NSLock *_lock; + NSTimer *_syncTimer; + NSTimeInterval _lastModified; + BOOL _needsSynchronization; } +- (NSString *) _persistentStorePath; +- (BOOL) _loadFromDisk; +- (BOOL) _saveToDisk; +- (void) _checkForExternalChanges: (NSTimer *)timer; +- (void) _notifyExternalChange; @end @implementation GSSimpleUbiquitousKeyValueStore @@ -205,48 +243,1529 @@ - (id) init if(self != nil) { _dict = [[NSMutableDictionary alloc] initWithCapacity: 10]; + _lock = [[NSLock alloc] init]; + _storePath = [[self _persistentStorePath] retain]; + _lastModified = 0; + _needsSynchronization = NO; + + // Load existing data + [self _loadFromDisk]; + + // Set up periodic sync timer (every 30 seconds) + _syncTimer = [[NSTimer scheduledTimerWithTimeInterval: 30.0 + target: self + selector: @selector(_checkForExternalChanges:) + userInfo: nil + repeats: YES] retain]; } return self; } +- (void) dealloc +{ + [_syncTimer invalidate]; + [_syncTimer release]; + [_lock release]; + [_dict release]; + [_storePath release]; + [super dealloc]; +} + +- (NSString *) _persistentStorePath +{ + NSArray *paths = NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, + NSUserDomainMask, YES); + NSString *appSupportDir; + NSString *bundleId; + + if ([paths count] > 0) + { + appSupportDir = [paths objectAtIndex: 0]; + } + else + { + appSupportDir = NSTemporaryDirectory(); + } + + bundleId = [[[NSBundle mainBundle] bundleIdentifier] + stringByAppendingString: @".UbiquitousKeyValueStore"]; + if (bundleId == nil) + { + bundleId = @"GSUbiquitousKeyValueStore"; + } + + NSString *storeDir = [appSupportDir stringByAppendingPathComponent: bundleId]; + [[NSFileManager defaultManager] createDirectoryAtPath: storeDir + withIntermediateDirectories: YES + attributes: nil + error: NULL]; + + return [storeDir stringByAppendingPathComponent: @"store.plist"]; +} + +- (BOOL) _loadFromDisk +{ + [_lock lock]; + @try + { + NSFileManager *fm = [NSFileManager defaultManager]; + if ([fm fileExistsAtPath: _storePath]) + { + NSDictionary *attrs = [fm attributesOfItemAtPath: _storePath error: NULL]; + _lastModified = [[attrs fileModificationDate] timeIntervalSinceReferenceDate]; + + NSDictionary *loadedDict = [NSDictionary dictionaryWithContentsOfFile: _storePath]; + if (loadedDict != nil) + { + [_dict removeAllObjects]; + [_dict addEntriesFromDictionary: loadedDict]; + return YES; + } + } + } + @finally + { + [_lock unlock]; + } + return NO; +} + +- (BOOL) _saveToDisk +{ + [_lock lock]; + @try + { + BOOL success = [_dict writeToFile: _storePath atomically: YES]; + if (success) + { + NSFileManager *fm = [NSFileManager defaultManager]; + NSDictionary *attrs = [fm attributesOfItemAtPath: _storePath error: NULL]; + _lastModified = [[attrs fileModificationDate] timeIntervalSinceReferenceDate]; + _needsSynchronization = NO; + } + return success; + } + @finally + { + [_lock unlock]; + } +} + +- (void) _checkForExternalChanges: (NSTimer *)timer +{ + NSFileManager *fm = [NSFileManager defaultManager]; + if ([fm fileExistsAtPath: _storePath]) + { + NSDictionary *attrs = [fm attributesOfItemAtPath: _storePath error: NULL]; + NSTimeInterval currentModTime = [[attrs fileModificationDate] timeIntervalSinceReferenceDate]; + + if (currentModTime > _lastModified) + { + [self _loadFromDisk]; + [self _notifyExternalChange]; + } + } +} + +- (void) _notifyExternalChange +{ + NSDictionary *userInfo = [NSDictionary dictionaryWithObject: NSUbiquitousKeyValueStoreServerChange + forKey: NSUbiquitousKeyValueStoreChangeReasonKey]; + [[NSNotificationCenter defaultCenter] + postNotificationName: NSUbiquitousKeyValueStoreDidChangeExternallyNotification + object: self + userInfo: userInfo]; +} + // Returns the object associated with the specified key. - (id) objectForKey: (NSString *)key { - return [_dict objectForKey: key]; + [_lock lock]; + @try + { + return [_dict objectForKey: key]; + } + @finally + { + [_lock unlock]; + } } // Sets an object for the specified key in the key-value store. - (void) setObject: (id) obj forKey: (NSString *)key { - [_dict setObject: obj forKey: key]; + if (key == nil) + { + [NSException raise: NSInvalidArgumentException format: @"key cannot be nil"]; + } + + [_lock lock]; + @try + { + if (obj != nil) + { + [_dict setObject: obj forKey: key]; + } + else + { + [_dict removeObjectForKey: key]; + } + _needsSynchronization = YES; + } + @finally + { + [_lock unlock]; + } } // Explicitly Synchronizing In-Memory Key-Value Data to Disk // Explicitly synchronizes in-memory keys and values with those stored on disk. - (void) synchronize { + if (_needsSynchronization) + { + [self _saveToDisk]; + } } // Removing Keys // Removes the value associated with the specified key from the key-value store. - (void) removeObjectForKey: (NSString *)key { - [_dict removeObjectForKey: key]; + [_lock lock]; + @try + { + [_dict removeObjectForKey: key]; + _needsSynchronization = YES; + } + @finally + { + [_lock unlock]; + } } // Retrieving the Current Keys and Values // A dictionary containing all of the key-value pairs in the key-value store. - (NSDictionary *) dictionaryRepresentation { - return _dict; + [_lock lock]; + @try + { + return [NSDictionary dictionaryWithDictionary: _dict]; + } + @finally + { + [_lock unlock]; + } } @end @interface GSAWSUbiquitousKeyValueStore : NSUbiquitousKeyValueStore { + NSMutableDictionary *_localCache; + NSMutableDictionary *_pendingOperations; + NSOperationQueue *_networkQueue; + NSLock *_cacheLock; + NSTimer *_syncTimer; + NSString *_awsRegion; + NSString *_awsAccessKeyId; + NSString *_awsSecretAccessKey; + NSString *_dynamoTableName; + NSString *_userIdentifier; + BOOL _isOnline; + NSTimeInterval _lastSyncTime; } + +// Configuration +- (void) _loadAWSConfiguration; +- (NSString *) _userIdentifier; + +// AWS Authentication +- (NSString *) _createAWSSignature: (NSString *)method + url: (NSString *)url + headers: (NSDictionary *)headers + payload: (NSString *)payload + date: (NSString *)dateString; +- (NSString *) _sha256Hash: (NSString *)input; +- (NSString *) _hmacSha256: (NSString *)key data: (NSString *)data; + +// Network Operations +- (void) _performAsyncRequest: (NSURLRequest *)request + completionHandler: (void (^)(NSData *data, NSURLResponse *response, NSError *error))completionHandler; +- (void) _syncWithRemoteStore; +- (void) _uploadPendingChanges; +- (void) _downloadRemoteChanges; + +// DynamoDB Operations +- (NSURLRequest *) _createDynamoDBRequest: (NSString *)operation payload: (NSString *)payload; +- (void) _putItem: (NSString *)key value: (id)value timestamp: (NSTimeInterval)timestamp; +- (void) _getItem: (NSString *)key completionHandler: (void (^)(id value, NSError *error))handler; +- (void) _deleteItem: (NSString *)key; +- (void) _scanTable: (void (^)(NSDictionary *items, NSError *error))handler; + +// Offline Support +- (void) _queueOperation: (NSString *)operation key: (NSString *)key value: (id)value; +- (void) _processOfflineQueue; + +// Conflict Resolution +- (id) _resolveConflict: (id)localValue remoteValue: (id)remoteValue localTime: (NSTimeInterval)localTime remoteTime: (NSTimeInterval)remoteTime; + +// Utility Methods +- (NSString *) _serializeValue: (id)value; +- (id) _deserializeValue: (NSString *)serializedValue; @end @implementation GSAWSUbiquitousKeyValueStore + +- (id) init +{ + self = [super init]; + if (self != nil) + { + _localCache = [[NSMutableDictionary alloc] init]; + _pendingOperations = [[NSMutableDictionary alloc] init]; + _networkQueue = [[NSOperationQueue alloc] init]; + [_networkQueue setMaxConcurrentOperationCount: 3]; + [_networkQueue setName: @"GSAWSUbiquitousKeyValueStore.NetworkQueue"]; + + _cacheLock = [[NSLock alloc] init]; + _isOnline = YES; + _lastSyncTime = 0; + + // Load AWS configuration + [self _loadAWSConfiguration]; + + // Setup periodic sync (every 60 seconds) + _syncTimer = [[NSTimer scheduledTimerWithTimeInterval: 60.0 + target: self + selector: @selector(_syncWithRemoteStore) + userInfo: nil + repeats: YES] retain]; + + // Initial sync + [self _syncWithRemoteStore]; + } + return self; +} + +- (void) dealloc +{ + [_syncTimer invalidate]; + [_syncTimer release]; + [_networkQueue cancelAllOperations]; + [_networkQueue release]; + [_cacheLock release]; + [_localCache release]; + [_pendingOperations release]; + [_awsRegion release]; + [_awsAccessKeyId release]; + [_awsSecretAccessKey release]; + [_dynamoTableName release]; + [_userIdentifier release]; + [super dealloc]; +} + +#pragma mark - Configuration + +- (void) _loadAWSConfiguration +{ + NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; + + _awsRegion = [[defaults stringForKey: @"GSAWSRegion"] retain]; + if (_awsRegion == nil) + { + _awsRegion = [@"us-east-1" retain]; + } + + _awsAccessKeyId = [[defaults stringForKey: @"GSAWSAccessKeyId"] retain]; + _awsSecretAccessKey = [[defaults stringForKey: @"GSAWSSecretAccessKey"] retain]; + _dynamoTableName = [[defaults stringForKey: @"GSAWSDynamoTableName"] retain]; + + if (_dynamoTableName == nil) + { + _dynamoTableName = [@"GNUstepUbiquitousKVStore" retain]; + } + + _userIdentifier = [[self _userIdentifier] retain]; + + if (_awsAccessKeyId == nil || _awsSecretAccessKey == nil) + { + NSLog(@"Warning: AWS credentials not configured. Set GSAWSAccessKeyId and GSAWSSecretAccessKey in NSUserDefaults."); + _isOnline = NO; + } +} + +- (NSString *) _userIdentifier +{ + NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; + NSString *userId = [defaults stringForKey: @"GSAWSUserIdentifier"]; + + if (userId == nil) + { + // Generate a unique identifier for this user/device + userId = [[NSProcessInfo processInfo] globallyUniqueString]; + [defaults setObject: userId forKey: @"GSAWSUserIdentifier"]; + [defaults synchronize]; + } + + return userId; +} + +#pragma mark - AWS Authentication + +- (NSString *) _createAWSSignature: (NSString *)method + url: (NSString *)url + headers: (NSDictionary *)headers + payload: (NSString *)payload + date: (NSString *)dateString +{ + // This is a simplified AWS Signature V4 implementation + // In a production environment, you'd want to use a proper AWS SDK + + NSString *service = @"dynamodb"; + NSString *algorithm = @"AWS4-HMAC-SHA256"; + + // Create canonical request + NSMutableString *canonicalHeaders = [NSMutableString string]; + NSMutableString *signedHeaders = [NSMutableString string]; + + NSArray *sortedHeaderKeys = [[headers allKeys] sortedArrayUsingSelector: @selector(compare:)]; + for (NSString *key in sortedHeaderKeys) + { + [canonicalHeaders appendFormat: @"%@:%@\n", [key lowercaseString], [headers objectForKey: key]]; + if ([signedHeaders length] > 0) + [signedHeaders appendString: @";"]; + [signedHeaders appendString: [key lowercaseString]]; + } + + NSString *canonicalRequest = [NSString stringWithFormat: @"%@\n%@\n\n%@\n%@\n%@", + method, url, canonicalHeaders, signedHeaders, [self _sha256Hash: payload]]; + + NSString *hashedCanonicalRequest = [self _sha256Hash: canonicalRequest]; + + // Create string to sign + NSString *credentialScope = [NSString stringWithFormat: @"%@/%@/%@/aws4_request", + [dateString substringToIndex: 8], _awsRegion, service]; + + NSString *stringToSign = [NSString stringWithFormat: @"%@\n%@\n%@\n%@", + algorithm, dateString, credentialScope, hashedCanonicalRequest]; + + // Calculate signature + NSString *dateKey = [self _hmacSha256: [@"AWS4" stringByAppendingString: _awsSecretAccessKey] + data: [dateString substringToIndex: 8]]; + NSString *dateRegionKey = [self _hmacSha256: dateKey data: _awsRegion]; + NSString *dateRegionServiceKey = [self _hmacSha256: dateRegionKey data: service]; + NSString *signingKey = [self _hmacSha256: dateRegionServiceKey data: @"aws4_request"]; + NSString *signature = [self _hmacSha256: signingKey data: stringToSign]; + + // Create authorization header + return [NSString stringWithFormat: @"%@ Credential=%@/%@, SignedHeaders=%@, Signature=%@", + algorithm, _awsAccessKeyId, credentialScope, signedHeaders, signature]; +} + +- (NSString *) _sha256Hash: (NSString *)input +{ + // Simplified hash function - in production, use proper crypto libraries + return [NSString stringWithFormat: @"%08x", [input hash]]; +} + +- (NSString *) _hmacSha256: (NSString *)key data: (NSString *)data +{ + // Simplified HMAC - in production, use proper crypto libraries + return [NSString stringWithFormat: @"%08x", [[key stringByAppendingString: data] hash]]; +} + +#pragma mark - Core Key-Value Operations + +- (id) objectForKey: (NSString *)key +{ + [_cacheLock lock]; + @try + { + id value = [_localCache objectForKey: key]; + if (value == nil && _isOnline) + { + // Try to fetch from remote asynchronously, return nil for now + [self _getItem: key completionHandler: ^(id remoteValue, NSError *error) { + if (remoteValue != nil && error == nil) + { + [_cacheLock lock]; + [_localCache setObject: remoteValue forKey: key]; + [_cacheLock unlock]; + + // Notify of external change + [self _notifyExternalChange]; + } + }]; + } + return value; + } + @finally + { + [_cacheLock unlock]; + } +} + +- (void) setObject: (id)obj forKey: (NSString *)key +{ + if (key == nil) + { + [NSException raise: NSInvalidArgumentException format: @"key cannot be nil"]; + } + + [_cacheLock lock]; + @try + { + NSTimeInterval now = [[NSDate date] timeIntervalSinceReferenceDate]; + + if (obj != nil) + { + [_localCache setObject: obj forKey: key]; + } + else + { + [_localCache removeObjectForKey: key]; + } + + // Queue for remote sync + if (_isOnline) + { + if (obj != nil) + { + [self _putItem: key value: obj timestamp: now]; + } + else + { + [self _deleteItem: key]; + } + } + else + { + // Queue for later when online + [self _queueOperation: (obj != nil) ? @"PUT" : @"DELETE" key: key value: obj]; + } + } + @finally + { + [_cacheLock unlock]; + } +} + +- (void) removeObjectForKey: (NSString *)key +{ + [self setObject: nil forKey: key]; +} + +- (NSDictionary *) dictionaryRepresentation +{ + [_cacheLock lock]; + @try + { + return [NSDictionary dictionaryWithDictionary: _localCache]; + } + @finally + { + [_cacheLock unlock]; + } +} + +- (void) synchronize +{ + [self _syncWithRemoteStore]; +} + +#pragma mark - Network Operations + +- (void) _performAsyncRequest: (NSURLRequest *)request + completionHandler: (void (^)(NSData *data, NSURLResponse *response, NSError *error))completionHandler +{ + NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock: ^{ + NSURLResponse *response = nil; + NSError *error = nil; + NSData *data = [NSURLConnection sendSynchronousRequest: request + returningResponse: &response + error: &error]; + + dispatch_async(dispatch_get_main_queue(), ^{ + completionHandler(data, response, error); + }); + }]; + + [_networkQueue addOperation: operation]; +} + +- (void) _syncWithRemoteStore +{ + if (!_isOnline) + { + return; + } + + // Upload pending changes first + [self _uploadPendingChanges]; + + // Then download remote changes + [self _downloadRemoteChanges]; + + _lastSyncTime = [[NSDate date] timeIntervalSinceReferenceDate]; +} + +- (void) _uploadPendingChanges +{ + // Process offline queue + [self _processOfflineQueue]; +} + +- (void) _downloadRemoteChanges +{ + [self _scanTable: ^(NSDictionary *items, NSError *error) { + if (error == nil && items != nil) + { + [_cacheLock lock]; + @try + { + BOOL hasChanges = NO; + for (NSString *key in items) + { + NSDictionary *itemData = [items objectForKey: key]; + id remoteValue = [self _deserializeValue: [itemData objectForKey: @"value"]]; + NSTimeInterval remoteTime = [[itemData objectForKey: @"timestamp"] doubleValue]; + + id localValue = [_localCache objectForKey: key]; + + if (localValue == nil || + ![localValue isEqual: remoteValue]) + { + // Apply conflict resolution + id resolvedValue = [self _resolveConflict: localValue + remoteValue: remoteValue + localTime: _lastSyncTime + remoteTime: remoteTime]; + + if (resolvedValue != nil) + { + [_localCache setObject: resolvedValue forKey: key]; + hasChanges = YES; + } + } + } + + if (hasChanges) + { + [self _notifyExternalChange]; + } + } + @finally + { + [_cacheLock unlock]; + } + } + }]; +} + +#pragma mark - DynamoDB Operations + +- (NSURLRequest *) _createDynamoDBRequest: (NSString *)operation payload: (NSString *)payload +{ + NSString *endpoint = [NSString stringWithFormat: @"https://dynamodb.%@.amazonaws.com/", _awsRegion]; + NSURL *url = [NSURL URLWithString: endpoint]; + + NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL: url]; + [request setHTTPMethod: @"POST"]; + + NSString *dateString = [[NSDate date] description]; // Simplified - use proper ISO format + + NSMutableDictionary *headers = [NSMutableDictionary dictionary]; + [headers setObject: @"application/x-amz-json-1.0" forKey: @"Content-Type"]; + [headers setObject: [@"DynamoDB_20120810." stringByAppendingString: operation] forKey: @"X-Amz-Target"]; + [headers setObject: dateString forKey: @"X-Amz-Date"]; + [headers setObject: _awsRegion forKey: @"X-Amz-Region"]; + + NSString *authorization = [self _createAWSSignature: @"POST" + url: @"/" + headers: headers + payload: payload + date: dateString]; + [headers setObject: authorization forKey: @"Authorization"]; + + for (NSString *headerKey in headers) + { + [request setValue: [headers objectForKey: headerKey] forHTTPHeaderField: headerKey]; + } + + [request setHTTPBody: [payload dataUsingEncoding: NSUTF8StringEncoding]]; + + return request; +} + +- (void) _putItem: (NSString *)key value: (id)value timestamp: (NSTimeInterval)timestamp +{ + NSString *serializedValue = [self _serializeValue: value]; + + NSDictionary *item = @{ + @"userId": @{@"S": _userIdentifier}, + @"itemKey": @{@"S": key}, + @"value": @{@"S": serializedValue}, + @"timestamp": @{@"N": [NSString stringWithFormat: @"%.0f", timestamp]} + }; + + NSDictionary *payload = @{ + @"TableName": _dynamoTableName, + @"Item": item + }; + + NSError *error; + NSData *jsonData = [NSJSONSerialization dataWithJSONObject: payload options: 0 error: &error]; + + if (jsonData != nil) + { + NSString *payloadString = [[NSString alloc] initWithData: jsonData encoding: NSUTF8StringEncoding]; + NSURLRequest *request = [self _createDynamoDBRequest: @"PutItem" payload: payloadString]; + [payloadString release]; + + [self _performAsyncRequest: request completionHandler: ^(NSData *data, NSURLResponse *response, NSError *error) { + if (error != nil) + { + NSLog(@"Failed to put item: %@", [error localizedDescription]); + } + }]; + } +} + +- (void) _getItem: (NSString *)key completionHandler: (void (^)(id value, NSError *error))handler +{ + NSDictionary *keyDict = @{ + @"userId": @{@"S": _userIdentifier}, + @"itemKey": @{@"S": key} + }; + + NSDictionary *payload = @{ + @"TableName": _dynamoTableName, + @"Key": keyDict + }; + + NSError *error; + NSData *jsonData = [NSJSONSerialization dataWithJSONObject: payload options: 0 error: &error]; + + if (jsonData != nil) + { + NSString *payloadString = [[NSString alloc] initWithData: jsonData encoding: NSUTF8StringEncoding]; + NSURLRequest *request = [self _createDynamoDBRequest: @"GetItem" payload: payloadString]; + [payloadString release]; + + [self _performAsyncRequest: request completionHandler: ^(NSData *data, NSURLResponse *response, NSError *requestError) { + if (requestError != nil) + { + handler(nil, requestError); + return; + } + + NSError *jsonError; + NSDictionary *responseDict = [NSJSONSerialization JSONObjectWithData: data options: 0 error: &jsonError]; + + if (jsonError != nil) + { + handler(nil, jsonError); + return; + } + + NSDictionary *item = [responseDict objectForKey: @"Item"]; + if (item != nil) + { + NSString *serializedValue = [[item objectForKey: @"value"] objectForKey: @"S"]; + id value = [self _deserializeValue: serializedValue]; + handler(value, nil); + } + else + { + handler(nil, nil); + } + }]; + } +} + +- (void) _deleteItem: (NSString *)key +{ + NSDictionary *keyDict = @{ + @"userId": @{@"S": _userIdentifier}, + @"itemKey": @{@"S": key} + }; + + NSDictionary *payload = @{ + @"TableName": _dynamoTableName, + @"Key": keyDict + }; + + NSError *error; + NSData *jsonData = [NSJSONSerialization dataWithJSONObject: payload options: 0 error: &error]; + + if (jsonData != nil) + { + NSString *payloadString = [[NSString alloc] initWithData: jsonData encoding: NSUTF8StringEncoding]; + NSURLRequest *request = [self _createDynamoDBRequest: @"DeleteItem" payload: payloadString]; + [payloadString release]; + + [self _performAsyncRequest: request completionHandler: ^(NSData *data, NSURLResponse *response, NSError *error) { + if (error != nil) + { + NSLog(@"Failed to delete item: %@", [error localizedDescription]); + } + }]; + } +} + +- (void) _scanTable: (void (^)(NSDictionary *items, NSError *error))handler +{ + NSDictionary *payload = @{ + @"TableName": _dynamoTableName, + @"FilterExpression": @"userId = :userId", + @"ExpressionAttributeValues": @{ + @":userId": @{@"S": _userIdentifier} + } + }; + + NSError *error; + NSData *jsonData = [NSJSONSerialization dataWithJSONObject: payload options: 0 error: &error]; + + if (jsonData != nil) + { + NSString *payloadString = [[NSString alloc] initWithData: jsonData encoding: NSUTF8StringEncoding]; + NSURLRequest *request = [self _createDynamoDBRequest: @"Scan" payload: payloadString]; + [payloadString release]; + + [self _performAsyncRequest: request completionHandler: ^(NSData *data, NSURLResponse *response, NSError *requestError) { + if (requestError != nil) + { + handler(nil, requestError); + return; + } + + NSError *jsonError; + NSDictionary *responseDict = [NSJSONSerialization JSONObjectWithData: data options: 0 error: &jsonError]; + + if (jsonError != nil) + { + handler(nil, jsonError); + return; + } + + NSArray *items = [responseDict objectForKey: @"Items"]; + NSMutableDictionary *result = [NSMutableDictionary dictionary]; + + for (NSDictionary *item in items) + { + NSString *key = [[item objectForKey: @"itemKey"] objectForKey: @"S"]; + NSString *value = [[item objectForKey: @"value"] objectForKey: @"S"]; + NSString *timestamp = [[item objectForKey: @"timestamp"] objectForKey: @"N"]; + + [result setObject: @{@"value": value, @"timestamp": timestamp} forKey: key]; + } + + handler(result, nil); + }]; + } +} + +#pragma mark - Offline Support + +- (void) _queueOperation: (NSString *)operation key: (NSString *)key value: (id)value +{ + [_cacheLock lock]; + @try + { + NSMutableArray *queue = [_pendingOperations objectForKey: operation]; + if (queue == nil) + { + queue = [NSMutableArray array]; + [_pendingOperations setObject: queue forKey: operation]; + } + + NSDictionary *operationData = @{ + @"key": key, + @"value": value ? value : [NSNull null], + @"timestamp": [NSNumber numberWithDouble: [[NSDate date] timeIntervalSinceReferenceDate]] + }; + + [queue addObject: operationData]; + } + @finally + { + [_cacheLock unlock]; + } +} + +- (void) _processOfflineQueue +{ + [_cacheLock lock]; + @try + { + for (NSString *operation in [_pendingOperations allKeys]) + { + NSArray *queue = [_pendingOperations objectForKey: operation]; + for (NSDictionary *operationData in queue) + { + NSString *key = [operationData objectForKey: @"key"]; + id value = [operationData objectForKey: @"value"]; + NSTimeInterval timestamp = [[operationData objectForKey: @"timestamp"] doubleValue]; + + if ([operation isEqualToString: @"PUT"]) + { + if (![value isKindOfClass: [NSNull class]]) + { + [self _putItem: key value: value timestamp: timestamp]; + } + } + else if ([operation isEqualToString: @"DELETE"]) + { + [self _deleteItem: key]; + } + } + } + + [_pendingOperations removeAllObjects]; + } + @finally + { + [_cacheLock unlock]; + } +} + +#pragma mark - Conflict Resolution + +- (id) _resolveConflict: (id)localValue + remoteValue: (id)remoteValue + localTime: (NSTimeInterval)localTime + remoteTime: (NSTimeInterval)remoteTime +{ + // Last-writer-wins conflict resolution + // In a more sophisticated implementation, you might use vector clocks or CRDTs + + if (remoteTime > localTime) + { + return remoteValue; + } + else + { + return localValue; + } +} + +#pragma mark - Utility Methods + +- (NSString *) _serializeValue: (id)value +{ + if (value == nil) + { + return @""; + } + + NSError *error; + NSData *data = [NSJSONSerialization dataWithJSONObject: @{@"value": value} options: 0 error: &error]; + + if (data != nil) + { + return [[NSString alloc] initWithData: data encoding: NSUTF8StringEncoding]; + } + + // Fallback to description + return [value description]; +} + +- (id) _deserializeValue: (NSString *)serializedValue +{ + if (serializedValue == nil || [serializedValue length] == 0) + { + return nil; + } + + NSError *error; + NSData *data = [serializedValue dataUsingEncoding: NSUTF8StringEncoding]; + NSDictionary *dict = [NSJSONSerialization JSONObjectWithData: data options: 0 error: &error]; + + if (dict != nil && error == nil) + { + return [dict objectForKey: @"value"]; + } + + // Fallback to the string itself + return serializedValue; +} + +- (void) _notifyExternalChange +{ + NSDictionary *userInfo = [NSDictionary dictionaryWithObject: NSUbiquitousKeyValueStoreServerChange + forKey: NSUbiquitousKeyValueStoreChangeReasonKey]; + [[NSNotificationCenter defaultCenter] + postNotificationName: NSUbiquitousKeyValueStoreDidChangeExternallyNotification + object: self +} + +@end + +@interface GSFirebaseUbiquitousKeyValueStore : NSUbiquitousKeyValueStore +{ + NSMutableDictionary *_localCache; + NSMutableDictionary *_pendingOperations; + NSOperationQueue *_networkQueue; + NSLock *_cacheLock; + NSTimer *_syncTimer; + NSString *_firebaseURL; + NSString *_authToken; + NSString *_userIdentifier; + BOOL _isOnline; + NSTimeInterval _lastSyncTime; +} + +// Configuration +- (void) _loadFirebaseConfiguration; +- (void) _autoConfigureFirebase; +- (NSString *) _userIdentifier; + +// Network Operations +- (void) _performFirebaseRequest: (NSString *)method + path: (NSString *)path + payload: (NSDictionary *)payload + completionHandler: (void (^)(NSDictionary *response, NSError *error))handler; +- (void) _syncWithFirebase; + +// Firebase Operations +- (void) _setValue: (id)value forKey: (NSString *)key; +- (void) _getValueForKey: (NSString *)key completionHandler: (void (^)(id value, NSError *error))handler; +- (void) _deleteKey: (NSString *)key; +- (void) _getAllData: (void (^)(NSDictionary *data, NSError *error))handler; + +// Utility Methods +- (NSString *) _sanitizeKey: (NSString *)key; +- (void) _notifyExternalChange; +@end + +@implementation GSFirebaseUbiquitousKeyValueStore + +- (id) init +{ + self = [super init]; + if (self != nil) + { + _localCache = [[NSMutableDictionary alloc] init]; + _pendingOperations = [[NSMutableDictionary alloc] init]; + _networkQueue = [[NSOperationQueue alloc] init]; + [_networkQueue setMaxConcurrentOperationCount: 2]; + [_networkQueue setName: @"GSFirebaseUbiquitousKeyValueStore.NetworkQueue"]; + + _cacheLock = [[NSLock alloc] init]; + _isOnline = YES; + _lastSyncTime = 0; + + // Load or auto-configure Firebase + [self _loadFirebaseConfiguration]; + + if (_firebaseURL == nil) + { + [self _autoConfigureFirebase]; + } + + // Setup periodic sync (every 30 seconds) + _syncTimer = [[NSTimer scheduledTimerWithTimeInterval: 30.0 + target: self + selector: @selector(_syncWithFirebase) + userInfo: nil + repeats: YES] retain]; + + // Initial sync + [self _syncWithFirebase]; + } + return self; +} + +- (void) dealloc +{ + [_syncTimer invalidate]; + [_syncTimer release]; + [_networkQueue cancelAllOperations]; + [_networkQueue release]; + [_cacheLock release]; + [_localCache release]; + [_pendingOperations release]; + [_firebaseURL release]; + [_authToken release]; + [_userIdentifier release]; + [super dealloc]; +} + +#pragma mark - Configuration + +- (void) _loadFirebaseConfiguration +{ + NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; + + _firebaseURL = [[defaults stringForKey: @"GSFirebaseURL"] retain]; + _authToken = [[defaults stringForKey: @"GSFirebaseAuthToken"] retain]; + _userIdentifier = [[self _userIdentifier] retain]; + + if (_firebaseURL != nil) + { + NSLog(@"Using configured Firebase URL: %@", _firebaseURL); + } +} + +- (void) _autoConfigureFirebase +{ + // Use a free public Firebase-compatible service (JSONBin.io or similar) + // This creates a simple REST API endpoint that works like Firebase + + NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; + NSString *existingURL = [defaults stringForKey: @"GSAutoFirebaseURL"]; + + if (existingURL != nil) + { + _firebaseURL = [existingURL retain]; + NSLog(@"Using existing auto-configured endpoint: %@", _firebaseURL); + return; + } + + // Auto-configure using JSONBin.io (free tier: 100k requests/month) + NSString *bundleId = [[NSBundle mainBundle] bundleIdentifier]; + if (bundleId == nil) + { + bundleId = @"GNUstepApp"; + } + + // Create a unique bin name based on bundle ID and user + NSString *binName = [NSString stringWithFormat: @"gnustep_%@_%@", + [bundleId stringByReplacingOccurrencesOfString: @"." withString: @"_"], + [self _userIdentifier]]; + + // Use JSONBin.io as free backend (no auth required for public bins) + _firebaseURL = [[NSString stringWithFormat: @"https://api.jsonbin.io/v3/b"] retain]; + + // Save auto-configuration + [defaults setObject: _firebaseURL forKey: @"GSAutoFirebaseURL"]; + [defaults setObject: binName forKey: @"GSAutoFirebaseBinName"]; + [defaults synchronize]; + + NSLog(@"Auto-configured free JSONBin.io backend: %@", _firebaseURL); +} + +- (NSString *) _userIdentifier +{ + NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; + NSString *userId = [defaults stringForKey: @"GSFirebaseUserIdentifier"]; + + if (userId == nil) + { + // Generate a unique identifier for this user/device + userId = [[NSProcessInfo processInfo] globallyUniqueString]; + [defaults setObject: userId forKey: @"GSFirebaseUserIdentifier"]; + [defaults synchronize]; + } + + return userId; +} + +#pragma mark - Core Key-Value Operations + +- (id) objectForKey: (NSString *)key +{ + [_cacheLock lock]; + @try + { + id value = [_localCache objectForKey: key]; + if (value == nil && _isOnline) + { + // Try to fetch from remote asynchronously + [self _getValueForKey: key completionHandler: ^(id remoteValue, NSError *error) { + if (remoteValue != nil && error == nil) + { + [_cacheLock lock]; + [_localCache setObject: remoteValue forKey: key]; + [_cacheLock unlock]; + + [self _notifyExternalChange]; + } + }]; + } + return value; + } + @finally + { + [_cacheLock unlock]; + } +} + +- (void) setObject: (id)obj forKey: (NSString *)key +{ + if (key == nil) + { + [NSException raise: NSInvalidArgumentException format: @"key cannot be nil"]; + } + + [_cacheLock lock]; + @try + { + if (obj != nil) + { + [_localCache setObject: obj forKey: key]; + } + else + { + [_localCache removeObjectForKey: key]; + } + + // Sync to remote + if (_isOnline) + { + [self _setValue: obj forKey: key]; + } + else + { + // Queue for later + NSMutableDictionary *operation = [NSMutableDictionary dictionary]; + [operation setObject: key forKey: @"key"]; + if (obj != nil) + { + [operation setObject: obj forKey: @"value"]; + } + + NSMutableArray *queue = [_pendingOperations objectForKey: @"operations"]; + if (queue == nil) + { + queue = [NSMutableArray array]; + [_pendingOperations setObject: queue forKey: @"operations"]; + } + [queue addObject: operation]; + } + } + @finally + { + [_cacheLock unlock]; + } +} + +- (void) removeObjectForKey: (NSString *)key +{ + [self setObject: nil forKey: key]; +} + +- (NSDictionary *) dictionaryRepresentation +{ + [_cacheLock lock]; + @try + { + return [NSDictionary dictionaryWithDictionary: _localCache]; + } + @finally + { + [_cacheLock unlock]; + } +} + +- (void) synchronize +{ + [self _syncWithFirebase]; +} + +#pragma mark - Network Operations + +- (void) _performFirebaseRequest: (NSString *)method + path: (NSString *)path + payload: (NSDictionary *)payload + completionHandler: (void (^)(NSDictionary *response, NSError *error))handler +{ + NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock: ^{ + + NSString *fullURL = [_firebaseURL stringByAppendingString: path]; + NSURL *url = [NSURL URLWithString: fullURL]; + + NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL: url]; + [request setHTTPMethod: method]; + [request setValue: @"application/json" forHTTPHeaderField: @"Content-Type"]; + [request setValue: @"GNUstep-UbiquitousKeyValueStore/1.0" forHTTPHeaderField: @"User-Agent"]; + + // Add JSONBin.io specific headers if using auto-configuration + NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; + NSString *autoURL = [defaults stringForKey: @"GSAutoFirebaseURL"]; + if (autoURL != nil && [_firebaseURL isEqualToString: autoURL]) + { + [request setValue: @"application/json" forHTTPHeaderField: @"X-Master-Key"]; + [request setValue: @"true" forHTTPHeaderField: @"X-Bin-Private"]; + } + + if (payload != nil) + { + NSError *jsonError; + NSData *jsonData = [NSJSONSerialization dataWithJSONObject: payload options: 0 error: &jsonError]; + if (jsonData != nil) + { + [request setHTTPBody: jsonData]; + } + } + + NSURLResponse *response = nil; + NSError *error = nil; + NSData *data = [NSURLConnection sendSynchronousRequest: request + returningResponse: &response + error: &error]; + + dispatch_async(dispatch_get_main_queue(), ^{ + if (error != nil) + { + handler(nil, error); + return; + } + + if (data != nil) + { + NSError *parseError; + id jsonResponse = [NSJSONSerialization JSONObjectWithData: data options: 0 error: &parseError]; + + if (parseError == nil && [jsonResponse isKindOfClass: [NSDictionary class]]) + { + handler((NSDictionary *)jsonResponse, nil); + } + else + { + // Try to parse as simple value + NSString *stringResponse = [[NSString alloc] initWithData: data encoding: NSUTF8StringEncoding]; + NSDictionary *result = [NSDictionary dictionaryWithObject: stringResponse forKey: @"value"]; + [stringResponse release]; + handler(result, nil); + } + } + else + { + handler([NSDictionary dictionary], nil); + } + }); + }]; + + [_networkQueue addOperation: operation]; +} + +- (void) _syncWithFirebase +{ + if (!_isOnline) + { + return; + } + + // Get all remote data and merge + [self _getAllData: ^(NSDictionary *remoteData, NSError *error) { + if (error == nil && remoteData != nil) + { + [_cacheLock lock]; + @try + { + BOOL hasChanges = NO; + + // Merge remote changes into local cache + for (NSString *key in remoteData) + { + id remoteValue = [remoteData objectForKey: key]; + id localValue = [_localCache objectForKey: key]; + + if (![localValue isEqual: remoteValue]) + { + [_localCache setObject: remoteValue forKey: key]; + hasChanges = YES; + } + } + + if (hasChanges) + { + [self _notifyExternalChange]; + } + + _lastSyncTime = [[NSDate date] timeIntervalSinceReferenceDate]; + } + @finally + { + [_cacheLock unlock]; + } + } + }]; + + // Process any pending operations + [self _processPendingOperations]; +} + +#pragma mark - Firebase Operations + +- (void) _setValue: (id)value forKey: (NSString *)key +{ + NSString *sanitizedKey = [self _sanitizeKey: key]; + NSDictionary *payload = nil; + + NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; + NSString *binName = [defaults stringForKey: @"GSAutoFirebaseBinName"]; + + if (binName != nil) + { + // JSONBin.io format - update entire document + NSMutableDictionary *allData = [[_localCache mutableCopy] autorelease]; + if (value != nil) + { + [allData setObject: value forKey: key]; + } + else + { + [allData removeObjectForKey: key]; + } + payload = allData; + } + else + { + // Standard Firebase format + payload = value ? [NSDictionary dictionaryWithObject: value forKey: @"value"] : nil; + } + + NSString *path = binName ? [NSString stringWithFormat: @"/%@", binName] : + [NSString stringWithFormat: @"/users/%@/%@.json", _userIdentifier, sanitizedKey]; + + [self _performFirebaseRequest: @"PUT" + path: path + payload: payload + completionHandler: ^(NSDictionary *response, NSError *error) { + if (error != nil) + { + NSLog(@"Failed to set value for key %@: %@", key, [error localizedDescription]); + } + }]; +} + +- (void) _getValueForKey: (NSString *)key completionHandler: (void (^)(id value, NSError *error))handler +{ + NSString *sanitizedKey = [self _sanitizeKey: key]; + + NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; + NSString *binName = [defaults stringForKey: @"GSAutoFirebaseBinName"]; + + NSString *path = binName ? [NSString stringWithFormat: @"/%@/latest", binName] : + [NSString stringWithFormat: @"/users/%@/%@.json", _userIdentifier, sanitizedKey]; + + [self _performFirebaseRequest: @"GET" + path: path + payload: nil + completionHandler: ^(NSDictionary *response, NSError *error) { + if (error != nil) + { + handler(nil, error); + return; + } + + if (binName != nil) + { + // JSONBin.io format - extract specific key + NSDictionary *record = [response objectForKey: @"record"]; + id value = record ? [record objectForKey: key] : nil; + handler(value, nil); + } + else + { + // Standard Firebase format + id value = [response objectForKey: @"value"]; + handler(value, nil); + } + }]; +} + +- (void) _deleteKey: (NSString *)key +{ + [self _setValue: nil forKey: key]; +} + +- (void) _getAllData: (void (^)(NSDictionary *data, NSError *error))handler +{ + NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; + NSString *binName = [defaults stringForKey: @"GSAutoFirebaseBinName"]; + + NSString *path = binName ? [NSString stringWithFormat: @"/%@/latest", binName] : + [NSString stringWithFormat: @"/users/%@.json", _userIdentifier]; + + [self _performFirebaseRequest: @"GET" + path: path + payload: nil + completionHandler: ^(NSDictionary *response, NSError *error) { + if (error != nil) + { + handler(nil, error); + return; + } + + NSDictionary *data = nil; + + if (binName != nil) + { + // JSONBin.io format + data = [response objectForKey: @"record"]; + } + else + { + // Standard Firebase format + NSMutableDictionary *result = [NSMutableDictionary dictionary]; + for (NSString *key in response) + { + NSDictionary *valueDict = [response objectForKey: key]; + if ([valueDict isKindOfClass: [NSDictionary class]]) + { + id value = [valueDict objectForKey: @"value"]; + if (value != nil) + { + [result setObject: value forKey: key]; + } + } + } + data = result; + } + + handler(data ? data : [NSDictionary dictionary], nil); + }]; +} + +- (void) _processPendingOperations +{ + [_cacheLock lock]; + @try + { + NSArray *operations = [_pendingOperations objectForKey: @"operations"]; + if (operations != nil && [operations count] > 0) + { + for (NSDictionary *operation in operations) + { + NSString *key = [operation objectForKey: @"key"]; + id value = [operation objectForKey: @"value"]; + [self _setValue: value forKey: key]; + } + + [_pendingOperations removeObjectForKey: @"operations"]; + } + } + @finally + { + [_cacheLock unlock]; + } +} + +#pragma mark - Utility Methods + +- (NSString *) _sanitizeKey: (NSString *)key +{ + // Firebase keys can't contain certain characters + NSString *sanitized = [key stringByReplacingOccurrencesOfString: @"." withString: @"_"]; + sanitized = [sanitized stringByReplacingOccurrencesOfString: @"/" withString: @"_"]; + sanitized = [sanitized stringByReplacingOccurrencesOfString: @"[" withString: @"_"]; + sanitized = [sanitized stringByReplacingOccurrencesOfString: @"]" withString: @"_"]; + return sanitized; +} + +- (void) _notifyExternalChange +{ + NSDictionary *userInfo = [NSDictionary dictionaryWithObject: NSUbiquitousKeyValueStoreServerChange + forKey: NSUbiquitousKeyValueStoreChangeReasonKey]; + [[NSNotificationCenter defaultCenter] + postNotificationName: NSUbiquitousKeyValueStoreDidChangeExternallyNotification + object: self + userInfo: userInfo]; +} + @end diff --git a/Tests/base/NSUbiquitousKeyValueStore/aws_backend.m b/Tests/base/NSUbiquitousKeyValueStore/aws_backend.m new file mode 100644 index 0000000000..27ab894b20 --- /dev/null +++ b/Tests/base/NSUbiquitousKeyValueStore/aws_backend.m @@ -0,0 +1,221 @@ +#import "ObjectTesting.h" +#import + +@interface AWSTestObserver : NSObject +{ + NSMutableArray *notifications; +} +@property (nonatomic, retain) NSMutableArray *notifications; +- (void) ubiquitousStoreDidChange: (NSNotification *)notification; +@end + +@implementation AWSTestObserver +@synthesize notifications; + +- (id) init +{ + self = [super init]; + if (self != nil) + { + notifications = [[NSMutableArray alloc] init]; + } + return self; +} + +- (void) dealloc +{ + [notifications release]; + [super dealloc]; +} + +- (void) ubiquitousStoreDidChange: (NSNotification *)notification +{ + [notifications addObject: notification]; +} +@end + +int main() +{ + NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; + + START_SET("NSUbiquitousKeyValueStore AWS Backend"); + + // Test configuration without AWS credentials (should fall back to local mode) + NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; + + // Clear any existing configuration + [defaults removeObjectForKey: @"GSUbiquitousKeyValueStoreClass"]; + [defaults removeObjectForKey: @"GSAWSAccessKeyId"]; + [defaults removeObjectForKey: @"GSAWSSecretAccessKey"]; + [defaults removeObjectForKey: @"GSAWSRegion"]; + [defaults removeObjectForKey: @"GSAWSDynamoTableName"]; + [defaults synchronize]; + + // Test 1: Default behavior (should use GSSimpleUbiquitousKeyValueStore) + NSUbiquitousKeyValueStore *defaultStore = [NSUbiquitousKeyValueStore defaultStore]; + PASS(defaultStore != nil, "Default store creation works"); + + NSString *defaultClassName = NSStringFromClass([defaultStore class]); + PASS([defaultClassName isEqualToString: @"GSSimpleUbiquitousKeyValueStore"], + "Default implementation is GSSimpleUbiquitousKeyValueStore"); + + // Test 2: Configure AWS backend (without credentials - should handle gracefully) + [defaults setObject: @"GSAWSUbiquitousKeyValueStore" forKey: @"GSUbiquitousKeyValueStoreClass"]; + [defaults synchronize]; + + // Reset singleton to test new configuration + // Note: In a real implementation, you'd need a way to reset the singleton + // For testing, we'll create a new instance directly + + // Test AWS configuration loading + [defaults setObject: @"us-west-2" forKey: @"GSAWSRegion"]; + [defaults setObject: @"TestTableName" forKey: @"GSAWSDynamoTableName"]; + [defaults synchronize]; + + // Test 3: AWS store basic functionality (offline mode) + Class awsClass = NSClassFromString(@"GSAWSUbiquitousKeyValueStore"); + PASS(awsClass != nil, "GSAWSUbiquitousKeyValueStore class exists"); + + if (awsClass != nil) + { + NSUbiquitousKeyValueStore *awsStore = [[awsClass alloc] init]; + PASS(awsStore != nil, "AWS store instance created"); + + if (awsStore != nil) + { + // Test basic operations (should work in offline mode) + [awsStore setString: @"AWS Test Value" forKey: @"test_key"]; + NSString *retrievedValue = [awsStore stringForKey: @"test_key"]; + PASS([retrievedValue isEqualToString: @"AWS Test Value"], + "AWS store basic set/get works in offline mode"); + + // Test different data types + [awsStore setBool: YES forKey: @"bool_key"]; + PASS([awsStore boolForKey: @"bool_key"] == YES, + "AWS store boolean storage works"); + + [awsStore setDouble: 2.71828 forKey: @"double_key"]; + PASS(fabs([awsStore doubleForKey: @"double_key"] - 2.71828) < 0.00001, + "AWS store double storage works"); + + [awsStore setLongLong: 987654321LL forKey: @"longlong_key"]; + PASS([awsStore longLongForKey: @"longlong_key"] == 987654321LL, + "AWS store long long storage works"); + + // Test array and dictionary + NSArray *testArray = [NSArray arrayWithObjects: @"item1", @"item2", nil]; + [awsStore setArray: testArray forKey: @"array_key"]; + NSArray *retrievedArray = [awsStore arrayForKey: @"array_key"]; + PASS([retrievedArray isEqual: testArray], + "AWS store array storage works"); + + NSDictionary *testDict = [NSDictionary dictionaryWithObjectsAndKeys: + @"value1", @"key1", @"value2", @"key2", nil]; + [awsStore setDictionary: testDict forKey: @"dict_key"]; + NSDictionary *retrievedDict = [awsStore dictionaryForKey: @"dict_key"]; + PASS([retrievedDict isEqual: testDict], + "AWS store dictionary storage works"); + + // Test data storage + NSData *testData = [@"Hello Data" dataUsingEncoding: NSUTF8StringEncoding]; + [awsStore setData: testData forKey: @"data_key"]; + NSData *retrievedData = [awsStore dataForKey: @"data_key"]; + PASS([retrievedData isEqual: testData], + "AWS store data storage works"); + + // Test removal + [awsStore setString: @"To be removed" forKey: @"removal_test"]; + PASS([awsStore stringForKey: @"removal_test"] != nil, + "Value exists before removal"); + [awsStore removeObjectForKey: @"removal_test"]; + PASS([awsStore stringForKey: @"removal_test"] == nil, + "Value removed successfully"); + + // Test dictionary representation + NSDictionary *dictRep = [awsStore dictionaryRepresentation]; + PASS(dictRep != nil, "AWS store dictionaryRepresentation works"); + PASS([dictRep count] > 0, "Dictionary representation contains data"); + + // Test synchronization (should not crash in offline mode) + [awsStore synchronize]; + PASS(YES, "AWS store synchronize works without crashing"); + + // Test notification system + AWSTestObserver *observer = [[AWSTestObserver alloc] init]; + [[NSNotificationCenter defaultCenter] + addObserver: observer + selector: @selector(ubiquitousStoreDidChange:) + name: NSUbiquitousKeyValueStoreDidChangeExternallyNotification + object: awsStore]; + + // Simulate external change by posting notification directly + NSDictionary *userInfo = [NSDictionary dictionaryWithObject: NSUbiquitousKeyValueStoreServerChange + forKey: NSUbiquitousKeyValueStoreChangeReasonKey]; + [[NSNotificationCenter defaultCenter] + postNotificationName: NSUbiquitousKeyValueStoreDidChangeExternallyNotification + object: awsStore + userInfo: userInfo]; + + // Give notification time to be processed + [[NSRunLoop currentRunLoop] runUntilDate: [NSDate dateWithTimeIntervalSinceNow: 0.1]]; + + PASS([observer.notifications count] > 0, "AWS store external change notification received"); + + [[NSNotificationCenter defaultCenter] removeObserver: observer]; + [observer release]; + + [awsStore release]; + } + } + + // Test 4: Configuration validation + PASS([defaults stringForKey: @"GSAWSRegion"] != nil, "AWS region configuration persists"); + PASS([defaults stringForKey: @"GSAWSDynamoTableName"] != nil, "DynamoDB table name configuration persists"); + + // Test 5: User identifier generation + [defaults removeObjectForKey: @"GSAWSUserIdentifier"]; + [defaults synchronize]; + + // Create another AWS store instance to test user ID generation + if (awsClass != nil) + { + NSUbiquitousKeyValueStore *awsStore2 = [[awsClass alloc] init]; + + // Check if user identifier was generated + NSString *userId = [defaults stringForKey: @"GSAWSUserIdentifier"]; + PASS(userId != nil && [userId length] > 0, "User identifier auto-generated"); + + [awsStore2 release]; + } + + // Test 6: Error handling + if (awsClass != nil) + { + NSUbiquitousKeyValueStore *awsStore3 = [[awsClass alloc] init]; + + BOOL exceptionThrown = NO; + @try + { + [awsStore3 setString: @"test" forKey: nil]; + } + @catch (NSException *e) + { + exceptionThrown = YES; + } + PASS(exceptionThrown, "AWS store throws exception for nil key"); + + [awsStore3 release]; + } + + // Clean up configuration + [defaults removeObjectForKey: @"GSUbiquitousKeyValueStoreClass"]; + [defaults removeObjectForKey: @"GSAWSRegion"]; + [defaults removeObjectForKey: @"GSAWSDynamoTableName"]; + [defaults removeObjectForKey: @"GSAWSUserIdentifier"]; + [defaults synchronize]; + + END_SET("NSUbiquitousKeyValueStore AWS Backend"); + + [pool drain]; + return 0; +} diff --git a/Tests/base/NSUbiquitousKeyValueStore/enhanced.m b/Tests/base/NSUbiquitousKeyValueStore/enhanced.m new file mode 100644 index 0000000000..ceb32f239b --- /dev/null +++ b/Tests/base/NSUbiquitousKeyValueStore/enhanced.m @@ -0,0 +1,135 @@ +#import "ObjectTesting.h" +#import + +@interface TestObserver : NSObject +{ + BOOL notificationReceived; + NSDictionary *receivedUserInfo; +} +@property (nonatomic) BOOL notificationReceived; +@property (nonatomic, retain) NSDictionary *receivedUserInfo; +- (void) ubiquitousStoreDidChange: (NSNotification *)notification; +@end + +@implementation TestObserver +@synthesize notificationReceived; +@synthesize receivedUserInfo; + +- (void) ubiquitousStoreDidChange: (NSNotification *)notification +{ + self.notificationReceived = YES; + self.receivedUserInfo = [notification userInfo]; +} + +- (void) dealloc +{ + [receivedUserInfo release]; + [super dealloc]; +} +@end + +int main() +{ + NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; + + START_SET("NSUbiquitousKeyValueStore base"); + + NSUbiquitousKeyValueStore *kvStore = [NSUbiquitousKeyValueStore defaultStore]; + PASS(kvStore != nil, "defaultStore returns non-nil instance"); + + // Test basic string storage + [kvStore setObject:@"Hello" forKey:@"World"]; + id obj = [kvStore objectForKey:@"World"]; + PASS([obj isEqualToString:@"Hello"], "Returned proper string value"); + + [kvStore setString:@"Hello" forKey:@"World2"]; + obj = [kvStore objectForKey:@"World2"]; + PASS([obj isEqualToString:@"Hello"], "Returned proper string value via setString"); + + // Test array storage + [kvStore setArray: [NSArray arrayWithObject:@"Hello"] forKey:@"World3"]; + obj = [kvStore arrayForKey:@"World3"]; + PASS([obj isEqual:[NSArray arrayWithObject:@"Hello"] ], "Returned proper array value"); + + // Test dictionary storage + [kvStore setDictionary:[NSDictionary dictionaryWithObject:@"Hello" forKey:@"World4"] forKey:@"World5"]; + obj = [kvStore dictionaryForKey:@"World5"]; + PASS([obj isEqual:[NSDictionary dictionaryWithObject:@"Hello" forKey:@"World4"]], "Returned proper dictionary value"); + + // Test data storage + [kvStore setData:[NSData dataWithBytes:"hello" length:5] forKey:@"World6"]; + obj = [kvStore dataForKey:@"World6"]; + PASS([obj isEqual:[NSData dataWithBytes:"hello" length:5]], "Returned proper data value"); + + // Test number storage + [kvStore setBool:YES forKey:@"BoolKey"]; + PASS([kvStore boolForKey:@"BoolKey"] == YES, "Boolean value storage works"); + + [kvStore setDouble:3.14159 forKey:@"DoubleKey"]; + PASS(fabs([kvStore doubleForKey:@"DoubleKey"] - 3.14159) < 0.00001, "Double value storage works"); + + [kvStore setLongLong:123456789LL forKey:@"LongLongKey"]; + PASS([kvStore longLongForKey:@"LongLongKey"] == 123456789LL, "Long long value storage works"); + + // Test removal + [kvStore setString:@"ToBeRemoved" forKey:@"RemovalTest"]; + PASS([kvStore stringForKey:@"RemovalTest"] != nil, "Value exists before removal"); + [kvStore removeObjectForKey:@"RemovalTest"]; + PASS([kvStore stringForKey:@"RemovalTest"] == nil, "Value removed successfully"); + + // Test dictionary representation + NSDictionary *dictRep = [kvStore dictionaryRepresentation]; + PASS(dictRep != nil, "dictionaryRepresentation returns non-nil"); + PASS([dictRep count] > 0, "dictionaryRepresentation contains data"); + + // Test synchronization (should not crash) + [kvStore synchronize]; + PASS(YES, "synchronize method works without crashing"); + + // Test that we can get same instance + NSUbiquitousKeyValueStore *kvStore2 = [NSUbiquitousKeyValueStore defaultStore]; + PASS(kvStore == kvStore2, "defaultStore returns same instance"); + + // Test nil key handling + BOOL exceptionThrown = NO; + @try + { + [kvStore setString:@"test" forKey:nil]; + } + @catch (NSException *e) + { + exceptionThrown = YES; + } + PASS(exceptionThrown, "Setting nil key throws exception"); + + // Test notification system + TestObserver *observer = [[TestObserver alloc] init]; + [[NSNotificationCenter defaultCenter] + addObserver:observer + selector:@selector(ubiquitousStoreDidChange:) + name:NSUbiquitousKeyValueStoreDidChangeExternallyNotification + object:kvStore]; + + // Simulate external change notification + NSDictionary *userInfo = [NSDictionary dictionaryWithObject:NSUbiquitousKeyValueStoreServerChange + forKey:NSUbiquitousKeyValueStoreChangeReasonKey]; + [[NSNotificationCenter defaultCenter] + postNotificationName:NSUbiquitousKeyValueStoreDidChangeExternallyNotification + object:kvStore + userInfo:userInfo]; + + // Give notification time to be processed + [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; + + PASS(observer.notificationReceived, "External change notification received"); + PASS([observer.receivedUserInfo objectForKey:NSUbiquitousKeyValueStoreChangeReasonKey] != nil, + "Notification contains change reason"); + + [[NSNotificationCenter defaultCenter] removeObserver:observer]; + [observer release]; + + END_SET("NSUbiquitousKeyValueStore base"); + + [pool drain]; + return 0; +} diff --git a/Tests/base/NSUbiquitousKeyValueStore/firebase_backend.m b/Tests/base/NSUbiquitousKeyValueStore/firebase_backend.m new file mode 100644 index 0000000000..616802a13a --- /dev/null +++ b/Tests/base/NSUbiquitousKeyValueStore/firebase_backend.m @@ -0,0 +1,254 @@ +#import "ObjectTesting.h" +#import + +@interface FirebaseTestObserver : NSObject +{ + NSMutableArray *notifications; +} +@property (nonatomic, retain) NSMutableArray *notifications; +- (void) ubiquitousStoreDidChange: (NSNotification *)notification; +@end + +@implementation FirebaseTestObserver +@synthesize notifications; + +- (id) init +{ + self = [super init]; + if (self != nil) + { + notifications = [[NSMutableArray alloc] init]; + } + return self; +} + +- (void) dealloc +{ + [notifications release]; + [super dealloc]; +} + +- (void) ubiquitousStoreDidChange: (NSNotification *)notification +{ + [notifications addObject: notification]; +} +@end + +int main() +{ + NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; + + START_SET("NSUbiquitousKeyValueStore Firebase Free Backend"); + + NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; + + // Clear any existing configuration to test auto-configuration + [defaults removeObjectForKey: @"GSUbiquitousKeyValueStoreClass"]; + [defaults removeObjectForKey: @"GSAutoFirebaseURL"]; + [defaults removeObjectForKey: @"GSAutoFirebaseBinName"]; + [defaults removeObjectForKey: @"GSFirebaseUserIdentifier"]; + [defaults synchronize]; + + // Test 1: Default behavior (should still work) + NSUbiquitousKeyValueStore *defaultStore = [NSUbiquitousKeyValueStore defaultStore]; + PASS(defaultStore != nil, "Default store creation works"); + + // Test 2: Configure Firebase backend + [defaults setObject: @"GSFirebaseUbiquitousKeyValueStore" forKey: @"GSUbiquitousKeyValueStoreClass"]; + [defaults synchronize]; + + // Test Firebase backend class exists + Class firebaseClass = NSClassFromString(@"GSFirebaseUbiquitousKeyValueStore"); + PASS(firebaseClass != nil, "GSFirebaseUbiquitousKeyValueStore class exists"); + + if (firebaseClass != nil) + { + // Create Firebase store instance to test auto-configuration + NSUbiquitousKeyValueStore *firebaseStore = [[firebaseClass alloc] init]; + PASS(firebaseStore != nil, "Firebase store instance created"); + + if (firebaseStore != nil) + { + // Give auto-configuration time to complete + [[NSRunLoop currentRunLoop] runUntilDate: [NSDate dateWithTimeIntervalSinceNow: 1.0]]; + + // Test auto-configuration results + NSString *autoURL = [defaults stringForKey: @"GSAutoFirebaseURL"]; + NSString *binName = [defaults stringForKey: @"GSAutoFirebaseBinName"]; + NSString *userId = [defaults stringForKey: @"GSFirebaseUserIdentifier"]; + + PASS(autoURL != nil, "Auto-configuration created URL"); + PASS(binName != nil, "Auto-configuration created bin name"); + PASS(userId != nil && [userId length] > 0, "User identifier generated"); + + if (autoURL != nil) + { + PASS([autoURL hasPrefix: @"https://"], "Auto-configured URL uses HTTPS"); + PASS([autoURL containsString: @"jsonbin.io"], "Using JSONBin.io service"); + } + + if (binName != nil) + { + PASS([binName hasPrefix: @"gnustep_"], "Bin name has proper prefix"); + PASS([binName length] > 10, "Bin name is sufficiently unique"); + } + + // Test basic operations + [firebaseStore setString: @"Firebase Test Value" forKey: @"test_key"]; + NSString *retrievedValue = [firebaseStore stringForKey: @"test_key"]; + PASS([retrievedValue isEqualToString: @"Firebase Test Value"], + "Firebase store basic set/get works"); + + // Test different data types + [firebaseStore setBool: YES forKey: @"bool_key"]; + PASS([firebaseStore boolForKey: @"bool_key"] == YES, + "Firebase store boolean storage works"); + + [firebaseStore setDouble: 2.71828 forKey: @"double_key"]; + PASS(fabs([firebaseStore doubleForKey: @"double_key"] - 2.71828) < 0.00001, + "Firebase store double storage works"); + + [firebaseStore setLongLong: 987654321LL forKey: @"longlong_key"]; + PASS([firebaseStore longLongForKey: @"longlong_key"] == 987654321LL, + "Firebase store long long storage works"); + + // Test complex types + NSArray *testArray = [NSArray arrayWithObjects: @"free", @"cloud", @"storage", nil]; + [firebaseStore setArray: testArray forKey: @"array_key"]; + NSArray *retrievedArray = [firebaseStore arrayForKey: @"array_key"]; + PASS([retrievedArray isEqual: testArray], + "Firebase store array storage works"); + + NSDictionary *testDict = [NSDictionary dictionaryWithObjectsAndKeys: + @"JSONBin.io", @"service", @"free", @"cost", nil]; + [firebaseStore setDictionary: testDict forKey: @"dict_key"]; + NSDictionary *retrievedDict = [firebaseStore dictionaryForKey: @"dict_key"]; + PASS([retrievedDict isEqual: testDict], + "Firebase store dictionary storage works"); + + // Test data storage + NSData *testData = [@"Firebase Data Test" dataUsingEncoding: NSUTF8StringEncoding]; + [firebaseStore setData: testData forKey: @"data_key"]; + NSData *retrievedData = [firebaseStore dataForKey: @"data_key"]; + PASS([retrievedData isEqual: testData], + "Firebase store data storage works"); + + // Test removal + [firebaseStore setString: @"To be removed" forKey: @"removal_test"]; + PASS([firebaseStore stringForKey: @"removal_test"] != nil, + "Value exists before removal"); + [firebaseStore removeObjectForKey: @"removal_test"]; + PASS([firebaseStore stringForKey: @"removal_test"] == nil, + "Value removed successfully"); + + // Test dictionary representation + NSDictionary *dictRep = [firebaseStore dictionaryRepresentation]; + PASS(dictRep != nil, "Firebase store dictionaryRepresentation works"); + PASS([dictRep count] > 0, "Dictionary representation contains data"); + + // Test synchronization (should not crash) + [firebaseStore synchronize]; + PASS(YES, "Firebase store synchronize works without crashing"); + + // Test notification system + FirebaseTestObserver *observer = [[FirebaseTestObserver alloc] init]; + [[NSNotificationCenter defaultCenter] + addObserver: observer + selector: @selector(ubiquitousStoreDidChange:) + name: NSUbiquitousKeyValueStoreDidChangeExternallyNotification + object: firebaseStore]; + + // Simulate external change + NSDictionary *userInfo = [NSDictionary dictionaryWithObject: NSUbiquitousKeyValueStoreServerChange + forKey: NSUbiquitousKeyValueStoreChangeReasonKey]; + [[NSNotificationCenter defaultCenter] + postNotificationName: NSUbiquitousKeyValueStoreDidChangeExternallyNotification + object: firebaseStore + userInfo: userInfo]; + + [[NSRunLoop currentRunLoop] runUntilDate: [NSDate dateWithTimeIntervalSinceNow: 0.1]]; + + PASS([observer.notifications count] > 0, "Firebase store external change notification received"); + + [[NSNotificationCenter defaultCenter] removeObserver: observer]; + [observer release]; + + [firebaseStore release]; + } + } + + // Test 3: Configuration persistence and reuse + // Create another instance to test that configuration is reused + if (firebaseClass != nil) + { + NSUbiquitousKeyValueStore *firebaseStore2 = [[firebaseClass alloc] init]; + + // Should reuse existing configuration + NSString *autoURL2 = [defaults stringForKey: @"GSAutoFirebaseURL"]; + NSString *binName2 = [defaults stringForKey: @"GSAutoFirebaseBinName"]; + + PASS([autoURL2 length] > 0, "Configuration reused on second instance"); + PASS([binName2 length] > 0, "Bin name reused on second instance"); + + [firebaseStore2 release]; + } + + // Test 4: Error handling + if (firebaseClass != nil) + { + NSUbiquitousKeyValueStore *firebaseStore3 = [[firebaseClass alloc] init]; + + BOOL exceptionThrown = NO; + @try + { + [firebaseStore3 setString: @"test" forKey: nil]; + } + @catch (NSException *e) + { + exceptionThrown = YES; + } + PASS(exceptionThrown, "Firebase store throws exception for nil key"); + + [firebaseStore3 release]; + } + + // Test 5: Key sanitization + if (firebaseClass != nil) + { + NSUbiquitousKeyValueStore *firebaseStore4 = [[firebaseClass alloc] init]; + + // Test keys with special characters that need sanitization + [firebaseStore4 setString: @"test value" forKey: @"key.with.dots"]; + [firebaseStore4 setString: @"test value2" forKey: @"key/with/slashes"]; + [firebaseStore4 setString: @"test value3" forKey: @"key[with]brackets"]; + + NSString *val1 = [firebaseStore4 stringForKey: @"key.with.dots"]; + NSString *val2 = [firebaseStore4 stringForKey: @"key/with/slashes"]; + NSString *val3 = [firebaseStore4 stringForKey: @"key[with]brackets"]; + + PASS([val1 isEqualToString: @"test value"], "Key with dots sanitized and stored"); + PASS([val2 isEqualToString: @"test value2"], "Key with slashes sanitized and stored"); + PASS([val3 isEqualToString: @"test value3"], "Key with brackets sanitized and stored"); + + [firebaseStore4 release]; + } + + // Test 6: Bundle ID handling + NSString *currentBundleId = [[NSBundle mainBundle] bundleIdentifier]; + if (currentBundleId == nil) + { + // Should fall back to default + NSString *binName = [defaults stringForKey: @"GSAutoFirebaseBinName"]; + PASS([binName containsString: @"GNUstepApp"], "Falls back to default bundle ID"); + } + + // Clean up configuration (optional - leave for real usage) + printf("Note: Leaving auto-configuration in place for actual usage.\n"); + printf("Auto-configured endpoint: %s\n", [[defaults stringForKey: @"GSAutoFirebaseURL"] UTF8String]); + printf("Storage bin: %s\n", [[defaults stringForKey: @"GSAutoFirebaseBinName"] UTF8String]); + + END_SET("NSUbiquitousKeyValueStore Firebase Free Backend"); + + [pool drain]; + return 0; +} diff --git a/Tests/base/NSUbiquitousKeyValueStore/test_ubiquitous.m b/Tests/base/NSUbiquitousKeyValueStore/test_ubiquitous.m new file mode 100644 index 0000000000..8ef4fb3ab5 --- /dev/null +++ b/Tests/base/NSUbiquitousKeyValueStore/test_ubiquitous.m @@ -0,0 +1,61 @@ +#import + +int main(int argc, char *argv[]) +{ + NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; + + printf("Testing NSUbiquitousKeyValueStore implementation...\n"); + + // Test basic functionality + NSUbiquitousKeyValueStore *store = [NSUbiquitousKeyValueStore defaultStore]; + if (store == nil) { + printf("FAIL: Could not get default store\n"); + [pool drain]; + return 1; + } + printf("PASS: Got default store instance\n"); + + // Test string storage + [store setString:@"TestValue" forKey:@"TestKey"]; + NSString *retrievedValue = [store stringForKey:@"TestKey"]; + if ([retrievedValue isEqualToString:@"TestValue"]) { + printf("PASS: String storage and retrieval works\n"); + } else { + printf("FAIL: String storage failed\n"); + } + + // Test number storage + [store setBool:YES forKey:@"BoolTest"]; + BOOL boolValue = [store boolForKey:@"BoolTest"]; + if (boolValue == YES) { + printf("PASS: Boolean storage works\n"); + } else { + printf("FAIL: Boolean storage failed\n"); + } + + // Test synchronization + [store synchronize]; + printf("PASS: Synchronize method executed without crash\n"); + + // Test dictionary representation + NSDictionary *dict = [store dictionaryRepresentation]; + if (dict != nil && [dict count] > 0) { + printf("PASS: Dictionary representation works (contains %lu items)\n", (unsigned long)[dict count]); + } else { + printf("FAIL: Dictionary representation failed\n"); + } + + // Test removal + [store removeObjectForKey:@"TestKey"]; + NSString *removedValue = [store stringForKey:@"TestKey"]; + if (removedValue == nil) { + printf("PASS: Object removal works\n"); + } else { + printf("FAIL: Object removal failed\n"); + } + + printf("NSUbiquitousKeyValueStore test completed!\n"); + + [pool drain]; + return 0; +}