From c94db02a3360ffe6b397e15d2e41fc6a0502b511 Mon Sep 17 00:00:00 2001 From: Wolfgang Sourdeau Date: Thu, 19 Nov 2009 17:08:47 +0000 Subject: [PATCH] Monotone-Parent: 9f8af75f69269845084162a8844a51cf065ae8fc Monotone-Revision: 54e08d70ed3b12e1a8bb0a68c590698753bb3e02 Monotone-Author: wsourdeau@inverse.ca Monotone-Date: 2009-11-19T17:08:47 Monotone-Branch: ca.inverse.sogo --- ChangeLog | 23 ++ SoObjects/SOGo/SOGoCache.h | 16 +- SoObjects/SOGo/SOGoCache.m | 232 +++++++++------- SoObjects/SOGo/SOGoUser.m | 88 ++---- SoObjects/SOGo/SOGoUserDefaults.h | 2 +- SoObjects/SOGo/SOGoUserDefaults.m | 440 +++++++++++++++++------------- SoObjects/SOGo/SOGoUserManager.m | 56 ++-- 7 files changed, 464 insertions(+), 393 deletions(-) diff --git a/ChangeLog b/ChangeLog index 56bcbe9a5..3b08d13eb 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,5 +1,28 @@ 2009-11-19 Wolfgang Sourdeau + * SoObjects/SOGo/SOGoUser.m (-userDefaults, -userSettings): the + cache handling has now been put in the SOGoUserDefaults class. + + * SoObjects/SOGo/SOGoUserDefaults.m (-primaryFetchProfile): split + method to separate the cache data and the db data. We know invoke + the "jsonRepresentation" method, which is the real data accessor + now. + (-primaryStoreProfile): user profile data is now serialized as a + JSON string in order to avoid constant conversions between the + plist and the json formats, from and to the cache. + (-jsonRepresentation): we now handle the cache data from here, as + it made more sense and reduces the code size in SOGoUser.m. + (-fetchProfile): now a void method. + + * SoObjects/SOGo/SOGoCache.m (+initialize): the memcached host can + now be configured using the "SOGoMemCachedHost" user default. The + method now contains initialization code for global vars that used + to be in -init instead. + (-userSettingsForLogin:,-userDefaultsForLogin:,-userAttributesForLogin:): + refactoring: those accessors now only return an NSString + associated with their respective key. There is no longer any + dictionary de-/serialization occuring in SOGoCache. + * SoObjects/SOGo/SOGoParentFolder.m (-subFolders): we raise the exceptions we get only if the current request is a PROPFIND. This avoids a useless restart of SOGo when the database is down during diff --git a/SoObjects/SOGo/SOGoCache.h b/SoObjects/SOGo/SOGoCache.h index 84cb876fb..2c6177642 100644 --- a/SoObjects/SOGo/SOGoCache.h +++ b/SoObjects/SOGo/SOGoCache.h @@ -61,13 +61,17 @@ withName: (NSString *) userName; - (id) userNamed: (NSString *) name; -- (NSMutableDictionary *) userAttributesForLogin: (NSString *) theLogin; -- (NSDictionary *) userDefaultsForLogin: (NSString *) theLogin; -- (NSDictionary *) userSettingsForLogin: (NSString *) theLogin; +- (void) setUserAttributes: (NSString *) attributes + forLogin: (NSString *) login; +- (NSString *) userAttributesForLogin: (NSString *) theLogin; -- (void) cacheValues: (NSDictionary *) theAttributes - ofType: (NSString *) theType - forLogin: (NSString *) theLogin; +- (void) setUserDefaults: (NSString *) attributes + forLogin: (NSString *) login; +- (NSString *) userDefaultsForLogin: (NSString *) theLogin; + +- (void) setUserSettings: (NSString *) attributes + forLogin: (NSString *) login; +- (NSString *) userSettingsForLogin: (NSString *) theLogin; @end diff --git a/SoObjects/SOGo/SOGoCache.m b/SoObjects/SOGo/SOGoCache.m index 2bcd699ff..acd932dd2 100644 --- a/SoObjects/SOGo/SOGoCache.m +++ b/SoObjects/SOGo/SOGoCache.m @@ -31,8 +31,8 @@ */ #import +#import #import -#import #import #import #import @@ -47,14 +47,11 @@ #import "SOGoCache.h" -#import "NSDictionary+BSJSONAdditions.h" - // We define the default value for cleaning up cached // users' preferences. This value should be relatively // high to avoid useless database calls. -static NSTimeInterval cleanupInterval = 300; - -static NSString *memcachedServerName = @"localhost"; +static NSTimeInterval cleanupInterval = 0; +static NSString *memcachedServerName; #if defined(THREADSAFE) static NSLock *lock; @@ -62,12 +59,32 @@ static NSLock *lock; @implementation SOGoCache -#if defined(THREADSAFE) + (void) initialize { + NSString *cleanupSetting; + NSUserDefaults *ud; + + ud = [NSUserDefaults standardUserDefaults]; + // We fire our timer that will cleanup cache entries + cleanupSetting = [ud objectForKey: @"SOGoCacheCleanupInterval"]; + if (cleanupSetting && [cleanupSetting doubleValue] > 0.0) + cleanupInterval = [cleanupSetting doubleValue]; + if (cleanupInterval == 0.0) + cleanupInterval = 300; + + [self logWithFormat: @"Cache cleanup interval set every %f seconds for memcached", + cleanupInterval]; + + ASSIGN (memcachedServerName, [ud stringForKey: @"SOGoMemCachedHost"]); + if (!memcachedServerName) + memcachedServerName = @"localhost"; + [self logWithFormat: @"Using host '%@' as memcached server", + memcachedServerName]; + +#if defined(THREADSAFE) lock = [NSLock new]; -} #endif +} + (NSTimeInterval) cleanupInterval { @@ -82,7 +99,7 @@ static NSLock *lock; [lock lock]; #endif if (!sharedCache) - sharedCache = [[self alloc] init]; + sharedCache = [self new]; #if defined(THREADSAFE) [lock unlock]; #endif @@ -110,34 +127,28 @@ static NSLock *lock; { if ((self = [super init])) { - NSString *cleanupSetting; memcached_return error; - cache = [[NSMutableDictionary alloc] init]; - users = [[NSMutableDictionary alloc] init]; + cache = [NSMutableDictionary new]; + users = [NSMutableDictionary new]; - // localCache is used to avoid going all the time to the memcached server during - // each request. We'll cache the value we got from memcached for the duration - // of the current request - which is good enough for pretty much all caces. We - // surely don't want to get new defaults/settings during the _same_ requests, it - // could produce relatively strange behaviors - localCache = [[NSMutableDictionary alloc] init]; - - // We fire our timer that will cleanup cache entries - cleanupSetting = [[NSUserDefaults standardUserDefaults] - objectForKey: @"SOGoCacheCleanupInterval"]; - - if (cleanupSetting && [cleanupSetting doubleValue] > 0.0) - cleanupInterval = [cleanupSetting doubleValue]; - - [self logWithFormat: @"Cache cleanup interval set every %f seconds for memcached", - cleanupInterval]; + // localCache is used to avoid going all the time to the memcached + // server during each request. We'll cache the value we got from + // memcached for the duration of the current request - which is good + // enough for pretty much all cases. We surely don't want to get new + // defaults/settings during the _same_ requests, it could produce + // relatively strange behaviors + localCache = [NSMutableDictionary new]; handle = memcached_create(NULL); - if (handle) { - servers = memcached_server_list_append(NULL, [memcachedServerName UTF8String], 11211, &error); +#warning We could also make the port number configurable and even make use \ + of NGNetUtilities for that. + servers + = memcached_server_list_append(NULL, + [memcachedServerName UTF8String], + 11211, &error); error = memcached_server_push(handle, servers); } } @@ -242,99 +253,110 @@ static NSLock *lock; // For non-blocking cache method, see memcached_behavior_set and MEMCACHED_BEHAVIOR_NO_BLOCK // memcached is thread-safe so no need to lock here. // -- (void) cacheValues: (NSDictionary *) theAttributes - ofType: (NSString *) theType - forLogin: (NSString *) theLogin +- (void) _cacheValues: (NSString *) theAttributes + ofType: (NSString *) theType + forLogin: (NSString *) theLogin { memcached_return error; - const char *key, *value; - unsigned int len, vlen; + NSString *keyName; + NSData *key, *value; - if (!handle) - return; - - key = [[NSString stringWithFormat: @"%@+%@", theLogin, theType] UTF8String]; - len = strlen(key); - - value = [[theAttributes jsonStringValue] UTF8String]; - vlen = strlen(value); - - error = memcached_set(handle, key, len, value, vlen, cleanupInterval, 0); - - if (error != MEMCACHED_SUCCESS) - [self logWithFormat: @"memcached error: unable to cache values with subtype %@ for user %@", theType, theLogin]; - //else - //[self logWithFormat: @"memcached: cached values (%s) with subtype %@ for user %@", value, theType, theLogin]; -} - -- (NSDictionary *) _valuesOfType: (NSString *) theType - forLogin: (NSString *) theLogin -{ - NSDictionary *d; - NSString *k; - - const char *key; - unsigned int len; - - if (!handle) - return nil; - - k = [NSString stringWithFormat: @"%@+%@", theLogin, theType]; - key = [k UTF8String]; - len = strlen(key); - - d = [localCache objectForKey: k]; - - if (!d) + keyName = [NSString stringWithFormat: @"%@+%@", theLogin, theType]; + if (handle) { - memcached_return rc; - unsigned int flags; - size_t vlen; - char *s; + key = [keyName dataUsingEncoding: NSUTF8StringEncoding]; + value = [theAttributes dataUsingEncoding: NSUTF8StringEncoding]; + error = memcached_set(handle, + [key bytes], [key length], + [value bytes], [value length], + cleanupInterval, 0); - s = memcached_get(handle, key, len, &vlen, &flags, &rc); - - if (rc == MEMCACHED_SUCCESS && s) - { - NSString *v; - - v = [NSString stringWithUTF8String: s]; - d = [NSDictionary dictionaryWithJSONString: v]; - - // Cache the value in our localCache - if (d) - [localCache setObject: d forKey: k]; - else - [self errorWithFormat: @"Unable to convert (%@) to a JSON string for type: %@ and login: %@", v, theType, theLogin]; - - free(s); - } + if (error != MEMCACHED_SUCCESS) + [self logWithFormat: @"memcached error: unable to cache values with subtype '%@' for user '%@'", theType, theLogin]; + //else + //[self logWithFormat: @"memcached: cached values (%s) with subtype %@ + //for user %@", value, theType, theLogin]; } - - return d; + else + [self errorWithFormat: @"attempting to cache value for key '%@' while" + " no handle exists", keyName]; } - -- (NSMutableDictionary *) userAttributesForLogin: (NSString *) theLogin +- (NSString *) _valuesOfType: (NSString *) theType + forLogin: (NSString *) theLogin { - id o; + NSString *valueString, *keyName; + NSData *key; + char *value; + size_t vlen; + memcached_return rc; + unsigned int flags; - o = [self _valuesOfType: @"attributes" forLogin: theLogin]; + valueString = nil; - if (o) - return [NSMutableDictionary dictionaryWithDictionary: o]; + if (handle) + { + keyName = [NSString stringWithFormat: @"%@+%@", theLogin, theType]; + valueString = [localCache objectForKey: keyName]; + if (!valueString) + { + key = [keyName dataUsingEncoding: NSUTF8StringEncoding]; + value = memcached_get (handle, [key bytes], [key length], + &vlen, &flags, &rc); + if (rc == MEMCACHED_SUCCESS && value) + { + valueString + = [[NSString alloc] initWithBytesNoCopy: value + length: vlen + encoding: NSUTF8StringEncoding + freeWhenDone: YES]; + [valueString autorelease]; + // Cache the value in our localCache + [localCache setObject: valueString forKey: keyName]; + } + } + } + else + [self errorWithFormat: @"attempting to retrieved cached value for key" + @" '%@' while no handle exists", keyName]; - return nil; + return valueString; } -- (NSDictionary *) userDefaultsForLogin: (NSString *) theLogin +- (void) setUserAttributes: (NSString *) theAttributes + forLogin: (NSString *) login { - return [self _valuesOfType: @"defaults" forLogin: theLogin]; + [self _cacheValues: theAttributes ofType: @"attributes" + forLogin: login]; } -- (NSDictionary *) userSettingsForLogin: (NSString *) theLogin +- (NSString *) userAttributesForLogin: (NSString *) theLogin { - return [self _valuesOfType: @"settings" forLogin: theLogin]; + return [self _valuesOfType: @"attributes" forLogin: theLogin]; +} + +- (void) setUserDefaults: (NSString *) theAttributes + forLogin: (NSString *) login +{ + [self _cacheValues: theAttributes ofType: @"defaults" + forLogin: login]; +} + +- (NSString *) userDefaultsForLogin: (NSString *) theLogin +{ + return [self _valuesOfType: @"defaults" forLogin: theLogin]; +} + +- (void) setUserSettings: (NSString *) theAttributes + forLogin: (NSString *) login +{ + [self _cacheValues: theAttributes ofType: @"settings" + forLogin: login]; +} + +- (NSString *) userSettingsForLogin: (NSString *) theLogin +{ + return [self _valuesOfType: @"settings" forLogin: theLogin]; } @end diff --git a/SoObjects/SOGo/SOGoUser.m b/SoObjects/SOGo/SOGoUser.m index ffa8a1285..000755e65 100644 --- a/SoObjects/SOGo/SOGoUser.m +++ b/SoObjects/SOGo/SOGoUser.m @@ -37,7 +37,6 @@ #import #import -#import "SOGoUserManager.h" #import "NSArray+Utilities.h" #import "SOGoCache.h" #import "SOGoDateFormatter.h" @@ -45,6 +44,7 @@ #import "SOGoPermissions.h" #import "SOGoUserDefaults.h" #import "SOGoUserFolder.h" +#import "SOGoUserManager.h" #import "../../Main/SOGo.h" @@ -487,50 +487,35 @@ _timeValue (NSString *key) - (NSUserDefaults *) userDefaults { - NSDictionary *values; - - if (!_defaults) + if (!_defaults) { _defaults = [self primaryUserDefaults]; if (_defaults) { - values = [[SOGoCache sharedCache] userDefaultsForLogin: login]; + [_defaults fetchProfile]; + if ([_defaults values]) + { + BOOL b; + b = NO; - if (values) - { - [_defaults setValues: values]; + if (![[_defaults stringForKey: @"MessageCheck"] length]) + { + [_defaults setObject: defaultMessageCheck forKey: @"MessageCheck"]; + b = YES; + } + if (![[_defaults stringForKey: @"TimeZone"] length]) + { + [_defaults setObject: [serverTimeZone name] forKey: @"TimeZone"]; + b = YES; + } + + if (b) + [_defaults synchronize]; + + + // See explanation in -language + [self invalidateLanguage]; } - else - { - [_defaults fetchProfile]; - values = [_defaults values]; - - if (values) - { - BOOL b; - - b = NO; - - if (![[_defaults stringForKey: @"MessageCheck"] length]) - { - [_defaults setObject: defaultMessageCheck forKey: @"MessageCheck"]; - b = YES; - } - if (![[_defaults stringForKey: @"TimeZone"] length]) - { - [_defaults setObject: [serverTimeZone name] forKey: @"TimeZone"]; - b = YES; - } - - if (b) - [_defaults synchronize]; - - [[SOGoCache sharedCache] cacheValues: [_defaults values] ofType: @"defaults" forLogin: login]; - } - } - - // See explanation in -language - [self invalidateLanguage]; } } //else @@ -541,29 +526,12 @@ _timeValue (NSString *key) - (NSUserDefaults *) userSettings { - NSDictionary *values; - if (!_settings) { _settings = [self primaryUserSettings]; if (_settings) { - values = [[SOGoCache sharedCache] userSettingsForLogin: login]; - - if (values) - { - [_settings setValues: values]; - } - else - { - [_settings fetchProfile]; - values = [_settings values]; - - if (values) - { - [[SOGoCache sharedCache] cacheValues: values ofType: @"settings" forLogin: login]; - } - } + [_settings fetchProfile]; // See explanation in -language [self invalidateLanguage]; @@ -585,9 +553,9 @@ _timeValue (NSString *key) if (![language length]) { language = [[self userDefaults] stringForKey: @"Language"]; - // This is a workaround until we handle the connection errors to the db in a - // better way. It enables us to avoid retrieving the userDefaults too - // many times when the DB is down, causing a huge delay. + // This is a workaround until we handle the connection errors to the db + // in a better way. It enables us to avoid retrieving the userDefaults + // too many times when the DB is down, causing a huge delay. if (![language length]) language = [SOGoUser language]; diff --git a/SoObjects/SOGo/SOGoUserDefaults.h b/SoObjects/SOGo/SOGoUserDefaults.h index 99285c078..b2e025a1f 100644 --- a/SoObjects/SOGo/SOGoUserDefaults.h +++ b/SoObjects/SOGo/SOGoUserDefaults.h @@ -82,7 +82,7 @@ - (NSString *) jsonRepresentation; -- (BOOL) fetchProfile; +- (void) fetchProfile; /* saving changes */ diff --git a/SoObjects/SOGo/SOGoUserDefaults.m b/SoObjects/SOGo/SOGoUserDefaults.m index 1bab5ffb3..909e8f371 100644 --- a/SoObjects/SOGo/SOGoUserDefaults.m +++ b/SoObjects/SOGo/SOGoUserDefaults.m @@ -35,6 +35,7 @@ #import #import "NSObject+Utilities.h" +#import "NSString+Utilities.h" #import "NSDictionary+BSJSONAdditions.h" #import "SOGoUserDefaults.h" @@ -53,11 +54,11 @@ static NSString *uidColumnName = @"c_uid"; if (theURL && [theUID length] > 0 && [theFieldName length] > 0) { - fieldName = [theFieldName copy]; - url = [theURL copy]; - uid = [theUID copy]; - defFlags.ready = YES; - defFlags.isNew = YES; + ASSIGN (fieldName, theFieldName); + ASSIGN (url, theURL); + ASSIGN (uid, theUID); + defFlags.ready = NO; + defFlags.isNew = NO; } else { @@ -98,142 +99,193 @@ static NSString *uidColumnName = @"c_uid"; /* operation */ -- (BOOL) primaryFetchProfile +- (NSString *) _fetchJSONProfileFromDB { GCSChannelManager *cm; EOAdaptorChannel *channel; - NSDictionary *row; + NSDictionary *row, *plist; NSException *ex; NSString *sql, *value, *error; NSArray *attrs; - BOOL rc; NSData *plistData; - rc = NO; - + value = nil; + cm = [GCSChannelManager defaultChannelManager]; channel = [cm acquireOpenChannelForURL: [self tableURL]]; if (channel) { /* generate SQL */ defFlags.ready = YES; - sql = [NSString stringWithFormat: (@"SELECT %@" - @" FROM %@" - @" WHERE %@ = '%@'"), - fieldName, [[self tableURL] gcsTableName], - uidColumnName, [self uid]]; - - values = [[NSMutableDictionary alloc] init]; - + sql = [NSString stringWithFormat: @"SELECT %@ FROM %@ WHERE %@ = '%@'", + fieldName, [[self tableURL] gcsTableName], + uidColumnName, [self uid]]; /* run SQL */ ex = [channel evaluateExpressionX: sql]; if (ex) - [self errorWithFormat:@"could not run SQL '%@': %@", sql, ex]; + [self errorWithFormat:@"could not run SQL '%@': %@", sql, ex]; else - { - /* fetch schema */ - attrs = [channel describeResults: NO /* don't beautify */]; - - /* fetch values */ - row = [channel fetchAttributes: attrs withZone: NULL]; - defFlags.isNew = !row; - [channel cancelFetch]; - - /* remember values */ - value = [row objectForKey: fieldName]; - if ([value isNotNull]) - { - id v; - - value = [value stringByReplacingString: @"''" - withString: @"'"]; - value = [value stringByReplacingString: @"\\\\" - withString: @"\\"]; - plistData = [value dataUsingEncoding: NSUTF8StringEncoding]; - v = [NSPropertyListSerialization propertyListFromData: plistData - mutabilityOption: NSPropertyListMutableContainers - format: NULL - errorDescription: &error]; - if ([v isKindOfClass: [NSMutableDictionary class]]) - [values addEntriesFromDictionary: v]; - } - - defFlags.modified = NO; - rc = YES; - } + { + /* fetch schema */ + attrs = [channel describeResults: NO /* don't beautify */]; + + /* fetch values */ + row = [channel fetchAttributes: attrs withZone: NULL]; + [channel cancelFetch]; - [cm releaseChannel:channel]; + value = [row objectForKey: fieldName]; + if ([value isNotNull]) + { + defFlags.isNew = NO; +#warning The result is supposed to be unescaped, why re-unescaping it here ? + value = [value stringByReplacingString: @"''" withString: @"'"]; + value = [value stringByReplacingString: @"\\\\" withString: @"\\"]; + if (![value isJSONString]) + { + plistData = [value dataUsingEncoding: NSUTF8StringEncoding]; + plist = [NSPropertyListSerialization propertyListFromData: plistData + mutabilityOption: NSPropertyListMutableContainers + format: NULL + errorDescription: &error]; + if (plist) + { + [self logWithFormat: @"database value for '%@'" + @" (uid: '%@') is a plist", fieldName, uid]; + value = [plist jsonStringValue]; + } + else + { + [self errorWithFormat: @"failed to parse property list value" + @" (error: %@): %@", error, value]; + value = nil; + } + } + } + else + { + defFlags.isNew = YES; + value = nil; /* we discard any NSNull instance */ + } + } + + [cm releaseChannel: channel]; } else { defFlags.ready = NO; [self errorWithFormat:@"failed to acquire channel for URL: %@", - [self tableURL]]; + [self tableURL]]; + } + + return value; +} + +- (NSString *) jsonRepresentation +{ + SOGoCache *cache; + NSString *jsonValue; + + cache = [SOGoCache sharedCache]; + if ([fieldName isEqualToString: @"c_defaults"]) + jsonValue = [cache userDefaultsForLogin: uid]; + else + jsonValue = [cache userSettingsForLogin: uid]; + if ([jsonValue length]) + { + defFlags.ready = YES; + defFlags.isNew = NO; + } + else + { + jsonValue = [self _fetchJSONProfileFromDB]; + if ([jsonValue length]) + { + defFlags.isNew = NO; + if ([fieldName isEqualToString: @"c_defaults"]) + [cache setUserDefaults: jsonValue forLogin: uid]; + else + [cache setUserSettings: jsonValue forLogin: uid]; + } + else + { + defFlags.isNew = YES; + jsonValue = @"{}"; + } + } + + return jsonValue; +} + +- (void) primaryFetchProfile +{ + NSString *jsonValue; + + defFlags.modified = NO; + [values release]; + jsonValue = [self jsonRepresentation]; + values = [NSDictionary dictionaryWithJSONString: jsonValue]; + if (values) + [values retain]; + else + [self errorWithFormat: @"failure parsing json string: '%@'", jsonValue]; +} + +- (BOOL) _isReadyOrRetry +{ + BOOL rc; + + if (defFlags.ready) + rc = YES; + else + { + [self primaryFetchProfile]; + rc = defFlags.ready; } return rc; } -- (NSString *) _serializedDefaults +- (NSString *) _sqlJsonRepresentation: (NSString *) jsonRepresentation { - NSMutableString *serializedDefaults; - NSData *serializedDefaultsData; - NSString *error; + NSMutableString *sql; - error = nil; - serializedDefaultsData - = [NSPropertyListSerialization dataFromPropertyList: values - format: NSPropertyListOpenStepFormat - errorDescription: &error]; - if (error) - { - [self errorWithFormat: @"serializing the defaults: %@", error]; - serializedDefaults = nil; - [error release]; - } - else - { - serializedDefaults - = [[NSMutableString alloc] initWithData: serializedDefaultsData - encoding: NSUTF8StringEncoding]; - [serializedDefaults autorelease]; - [serializedDefaults replaceString: @"\\" withString: @"\\\\"]; - [serializedDefaults replaceString: @"'" withString: @"''"]; - } + sql = [jsonRepresentation mutableCopy]; + [sql autorelease]; + [sql replaceString: @"\\\\" withString: @"\\"]; + [sql replaceString: @"'" withString: @"''"]; - return serializedDefaults; + return sql; } -- (NSString *) generateSQLForInsert +- (NSString *) generateSQLForInsert: (NSString *) jsonRepresentation { - NSString *sql, *serializedDefaults; + NSString *sql; - serializedDefaults = [self _serializedDefaults]; - if (serializedDefaults) + if ([jsonRepresentation length]) sql = [NSString stringWithFormat: (@"INSERT INTO %@" - @" (%@, %@)" - @" VALUES ('%@', '%@')"), - [[self tableURL] gcsTableName], uidColumnName, fieldName, - [self uid], serializedDefaults]; + @" (%@, %@)" + @" VALUES ('%@', '%@')"), + [[self tableURL] gcsTableName], uidColumnName, fieldName, + [self uid], + [self _sqlJsonRepresentation: jsonRepresentation]]; else sql = nil; return sql; } -- (NSString *) generateSQLForUpdate +- (NSString *) generateSQLForUpdate: (NSString *) jsonRepresentation { - NSString *sql, *serializedDefaults; + NSString *sql; - serializedDefaults = [self _serializedDefaults]; - if (serializedDefaults) + if ([jsonRepresentation length]) sql = [NSString stringWithFormat: (@"UPDATE %@" - @" SET %@ = '%@'" - @" WHERE %@ = '%@'"), - [[self tableURL] gcsTableName], - fieldName, - serializedDefaults, - uidColumnName, [self uid]]; + @" SET %@ = '%@'" + @" WHERE %@ = '%@'"), + [[self tableURL] gcsTableName], + fieldName, + [self _sqlJsonRepresentation: jsonRepresentation], + uidColumnName, [self uid]]; else sql = nil; @@ -245,136 +297,135 @@ static NSString *uidColumnName = @"c_uid"; GCSChannelManager *cm; EOAdaptorChannel *channel; NSException *ex; - NSString *sql; + NSString *sql, *jsonRepresentation; + SOGoCache *cache; BOOL rc; rc = NO; - - cm = [GCSChannelManager defaultChannelManager]; - sql = ((defFlags.isNew) - ? [self generateSQLForInsert] - : [self generateSQLForUpdate]); - if (sql) + + jsonRepresentation = [values jsonStringValue]; + if (jsonRepresentation) { + sql = ((defFlags.isNew) + ? [self generateSQLForInsert: jsonRepresentation] + : [self generateSQLForUpdate: jsonRepresentation]); + cm = [GCSChannelManager defaultChannelManager]; channel = [cm acquireOpenChannelForURL: [self tableURL]]; if (channel) - { - defFlags.ready = YES; - [[channel adaptorContext] beginTransaction]; - ex = [channel evaluateExpressionX:sql]; - if (ex) - { - [self errorWithFormat: @"could not run SQL '%@': %@", sql, ex]; - [[channel adaptorContext] rollbackTransaction]; - } - else - { - if ([[channel adaptorContext] commitTransaction]) - { - rc = YES; - } - - defFlags.modified = NO; - defFlags.isNew = NO; - } - - [cm releaseChannel: channel]; - } + { + if ([[channel adaptorContext] beginTransaction]) + { + defFlags.ready = YES; + ex = [channel evaluateExpressionX:sql]; + if (ex) + { + [self errorWithFormat: @"could not run SQL '%@': %@", sql, ex]; + [[channel adaptorContext] rollbackTransaction]; + } + else + { + if ([[channel adaptorContext] commitTransaction]) + { + cache = [SOGoCache sharedCache]; + if ([fieldName isEqualToString: @"c_defaults"]) + [cache setUserDefaults: jsonRepresentation + forLogin: uid]; + else + [cache setUserSettings: jsonRepresentation + forLogin: uid]; + } + defFlags.modified = NO; + defFlags.isNew = NO; + } + [cm releaseChannel: channel]; + } + else + { + defFlags.ready = NO; + [cm releaseChannel: channel immediately: YES]; + } + } else - { - defFlags.ready = NO; - [self errorWithFormat: @"failed to acquire channel for URL: %@", - [self tableURL]]; - } + { + defFlags.ready = NO; + [self errorWithFormat: @"failed to acquire channel for URL: %@", + [self tableURL]]; + } } else - [self errorWithFormat: @"failed to generate SQL for storing defaults"]; - - if (rc) - [[SOGoCache sharedCache] cacheValues: values - ofType: ([fieldName isEqualToString: @"c_defaults"] ? @"defaults" : @"settings") - forLogin: uid]; + [self errorWithFormat: @"Unable to convert (%@) to a JSON string for" + @" type: %@ and login: %@", values, fieldName, uid]; return rc; } -- (BOOL) fetchProfile +- (void) fetchProfile { - return (values || [self primaryFetchProfile]); -} - -- (NSString *) jsonRepresentation -{ - NSString *jsonRep; - - [self fetchProfile]; - if (values) - jsonRep = [values jsonStringValue]; - else - jsonRep = @"{}"; - - return jsonRep; + if (!values) + [self primaryFetchProfile]; } /* value access */ - (void) setValues: (NSDictionary *) theValues { - [values release]; - - values = [[NSMutableDictionary alloc] init]; - [values addEntriesFromDictionary: theValues]; - defFlags.modified = NO; - defFlags.isNew = NO; + if ([self _isReadyOrRetry]) + { + [values release]; + values = [[NSMutableDictionary alloc] init]; + [values addEntriesFromDictionary: theValues]; + defFlags.modified = YES; + } } - (NSDictionary *) values { - return values; + NSDictionary *returnValues; + + if ([self _isReadyOrRetry]) + returnValues = values; + else + returnValues = nil; + + return returnValues; } - (void) setObject: (id) value forKey: (NSString *) key { id old; - - if (!defFlags.ready || ![self fetchProfile]) - return; - /* check whether the value is actually modified */ - if (!defFlags.modified) - { - old = [values objectForKey: key]; - if (old == value || [old isEqual: value]) /* value didn't change */ - return; - - /* we need to this because our typed accessors convert to strings */ - // TODO: especially problematic with bools - if ([value isKindOfClass: [NSString class]]) { - if (![old isKindOfClass: [NSString class]]) - if ([[old description] isEqualToString: value]) - return; - } + if ([self _isReadyOrRetry]) + { + /* check whether the value is actually modified */ + if (!defFlags.modified) + { + old = [values objectForKey: key]; + if (old == value || [old isEqual: value]) /* value didn't change */ + return; + +#warning Note that this work-around only works for first-level objects. + /* we need to this because our typed accessors convert to strings */ + // TODO: especially problematic with bools + if ([value isKindOfClass: [NSString class]]) { + if (![old isKindOfClass: [NSString class]]) + if ([[old description] isEqualToString: value]) + return; + } + } + + /* set in hash and mark as modified */ + if (value) + [values setObject: value forKey: key]; + else + [values removeObjectForKey: key]; + + defFlags.modified = YES; } - - /* set in hash and mark as modified */ - if (value) - [values setObject: value forKey: key]; - else - [values removeObjectForKey: key]; - - defFlags.modified = YES; } - (id) objectForKey: (NSString *) key { - id value; - - if (!defFlags.ready || ![self fetchProfile]) - value = nil; - else - value = [values objectForKey: key]; - - return value; + return [[self values] objectForKey: key]; } - (void) removeObjectForKey: (NSString *) key @@ -388,9 +439,10 @@ static NSString *uidColumnName = @"c_uid"; { // if (!defFlags.modified) /* was not modified */ // return YES; - + /* ensure fetched data (more or less guaranteed by modified!=0) */ - if (![self fetchProfile]) + [self fetchProfile]; + if (!values) return NO; /* store */ @@ -401,7 +453,9 @@ static NSString *uidColumnName = @"c_uid"; } /* refetch */ - return [self primaryFetchProfile]; + [self primaryFetchProfile]; + + return YES; } /* typed accessors */ diff --git a/SoObjects/SOGo/SOGoUserManager.m b/SoObjects/SOGo/SOGoUserManager.m index 0640083d0..2c63369ca 100644 --- a/SoObjects/SOGo/SOGoUserManager.m +++ b/SoObjects/SOGo/SOGoUserManager.m @@ -29,14 +29,15 @@ #import #import -#include "NSArray+Utilities.h" -#include "SOGoSource.h" -#include "SOGoUserManager.h" -#include "SOGoCache.h" -#include "SOGoSource.h" +#import "NSDictionary+BSJSONAdditions.h" +#import "NSArray+Utilities.h" +#import "SOGoSource.h" +#import "SOGoUserManager.h" +#import "SOGoCache.h" +#import "SOGoSource.h" -#include "LDAPSource.h" -#include "SQLSource.h" +#import "LDAPSource.h" +#import "SQLSource.h" static NSString *defaultMailDomain = nil; static NSString *LDAPContactInfoAttribute = nil; @@ -344,14 +345,15 @@ static NSLock *lock = nil; andPassword: (NSString *) password { NSMutableDictionary *currentUser; - NSString *dictPassword; + NSString *dictPassword, *jsonUser; BOOL checkOK; #if defined(THREADSAFE) [lock lock]; #endif - currentUser = [[SOGoCache sharedCache] userAttributesForLogin: login]; + jsonUser = [[SOGoCache sharedCache] userAttributesForLogin: login]; + currentUser = [NSMutableDictionary dictionaryWithJSONString: jsonUser]; dictPassword = [currentUser objectForKey: @"password"]; if (currentUser && dictPassword) checkOK = ([dictPassword isEqualToString: password]); @@ -363,12 +365,15 @@ static NSLock *lock = nil; currentUser = [NSMutableDictionary dictionary]; } - // It's important to cache the password here as we might have cached the user's entry - // in -contactInfosForUserWithUIDorEmail: and if we don't set the password and recache the - // entry, the password would never be cached for the user unless its entry expires from - // memcached's internal cache. + // It's important to cache the password here as we might have cached the + // user's entry in -contactInfosForUserWithUIDorEmail: and if we don't + // set the password and recache the entry, the password would never be + // cached for the user unless its entry expires from memcached's + // internal cache. [currentUser setObject: password forKey: @"password"]; - [[SOGoCache sharedCache] cacheValues: currentUser ofType: @"attributes" forLogin: login]; + [[SOGoCache sharedCache] + setUserAttributes: [currentUser jsonStringValue] + forLogin: login]; } else checkOK = NO; @@ -469,29 +474,23 @@ static NSLock *lock = nil; NSEnumerator *emails; NSString *key; -#if defined(THREADSAFE) - [lock lock]; -#endif key = [newUser objectForKey: @"c_uid"]; if (key) - { - [[SOGoCache sharedCache] cacheValues: newUser ofType: @"attributes" forLogin: key]; - } + [[SOGoCache sharedCache] + setUserAttributes: [newUser jsonStringValue] + forLogin: key]; emails = [[newUser objectForKey: @"emails"] objectEnumerator]; while ((key = [emails nextObject])) - { - [[SOGoCache sharedCache] cacheValues: newUser ofType: @"attributes" forLogin: key]; - } -#if defined(THREADSAFE) - [lock unlock]; -#endif + [[SOGoCache sharedCache] + setUserAttributes: [newUser jsonStringValue] + forLogin: key]; } - (NSDictionary *) contactInfosForUserWithUIDorEmail: (NSString *) uid { NSMutableDictionary *currentUser, *contactInfos; - NSString *aUID; + NSString *aUID, *jsonUser; BOOL newUser; if ([uid length] > 0) @@ -499,7 +498,8 @@ static NSLock *lock = nil; // Remove the "@" prefix used to identified groups in the ACL tables. aUID = [uid hasPrefix: @"@"] ? [uid substringFromIndex: 1] : uid; contactInfos = [NSMutableDictionary dictionary]; - currentUser = [[SOGoCache sharedCache] userAttributesForLogin: aUID]; + jsonUser = [[SOGoCache sharedCache] userAttributesForLogin: aUID]; + currentUser = [NSDictionary dictionaryWithJSONString: jsonUser]; #if defined(THREADSAFE) [lock lock]; #endif