* Adium is the legal property of its developers, whose names are listed in the copyright file included * with this source distribution. * This program is free software; you can redistribute it and/or modify it under the terms of the GNU * General Public License as published by the Free Software Foundation; either version 2 of the License, * or (at your option) any later version. * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General * Public License for more details. * You should have received a copy of the GNU General Public License along with this program; if not, * write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. #import "AIPreferenceContainer.h" #import "AIPreferenceController.h" #import <Adium/AIListObject.h> #import <Adium/AILoginControllerProtocol.h> #import <AIUtilities/AIDictionaryAdditions.h> #import <AIUtilities/AIStringAdditions.h> #import <Adium/AIAccount.h> @interface AIPreferenceContainer () - ( id ) initForGroup : ( NSString * ) inGroup object : ( AIListObject * ) inObject ; @property ( readonly , nonatomic ) NSMutableDictionary * prefs ; + ( void ) loadGlobalPrefsForObject: ( AIListObject * ) object ; //Lazily sets up our pref dict if needed - ( void ) setPrefValue: ( id ) val forKey: ( id ) key ; #define SAVE_OBJECT_PREFS_DELAY 10.0 #define PREFERENCE_CONTAINER_DEBUG static NSMutableDictionary * objectPrefs = nil ; static NSTimer * timer_savingOfObjectCache = nil ; static NSMutableDictionary * accountPrefs = nil ; static NSTimer * timer_savingOfAccountCache = nil ; * @brief Preference Container * A single AIPreferenceContainer instance provides read/write access preferences to a specific preference group, either * for the global preferences or for a specific object. * All contacts share a single plist on-disk, loaded into a single mutable dictionary in-memory, objectPrefs. * All accounts share a single plist on-disk, loaded into a single mutable dictionary in-memory, accountPrefs. * These global dictionaries provide per-object preference dictionaries, keyed by the object's internalObjectID. * Individual instances of AIPreferenceContainer make use of this shared store. Saving of changes is batched for all changes made during a * SAVE_OBJECT_PREFS_DELAY interval across all instances of AIPreferenceContainer for a given global dictionary. Because creating * the data representation of a large dictionary and writing it out can be time-consuming (certainly less than a second, but still long * enough to cause a perceptible delay for a user actively typing or interacting with Adium), saving is performed on a thread. @implementation AIPreferenceContainer static NSString * globalPrefsNameForObject ( AIListObject * object ) { if ( ! object ) return NULL ; return [ object isKindOfClass : [ AIAccount class ]] ? @"AccountPrefs" : @"ByObjectPrefs" ; #define globalPrefsName globalPrefsNameForObject(object) static NSMutableDictionary ** globalPrefsPtrForObject ( AIListObject * object ) { if ( ! object ) return NULL ; return [ object isKindOfClass : [ AIAccount class ]] ? & accountPrefs : & objectPrefs ; #define myGlobalPrefs globalPrefsPtrForObject(object) static NSTimer ** globalPrefsSaveTimerForObject ( AIListObject * object ) { if ( ! object ) return NULL ; return [ object isKindOfClass : [ AIAccount class ]] ? & timer_savingOfAccountCache : & timer_savingOfObjectCache ; #define myTimerForSavingGlobalPrefs globalPrefsSaveTimerForObject(object) + ( AIPreferenceContainer * ) preferenceContainerForGroup: ( NSString * ) inGroup object: ( AIListObject * ) inObject create: ( BOOL ) create NSMutableDictionary ** prefs = globalPrefsPtrForObject ( inObject ); [ self loadGlobalPrefsForObject : inObject ]; prefs = globalPrefsPtrForObject ( inObject ); NSString * globalPrefsKey = [ inObject . internalObjectID safeFilenameString ]; found = ( nil != [ * prefs objectForKey : globalPrefsKey ]); if ( inObject && ! found && ! create ) return nil ; return [[[ self alloc ] initForGroup : inGroup object : inObject ] autorelease ]; + ( void ) preferenceControllerWillClose //If a save of the object prefs is pending, perform it immediately since we are quitting if ( timer_savingOfObjectCache ) { [ objectPrefs writeToPath : [ adium . loginController userDirectory ] withName : @"ByObjectPrefs" ]; /* There's no guarantee that 'will close' is called in the same run loop as the actual program termination. * We've done our final save, though; don't let the timer fire again. [ timer_savingOfObjectCache invalidate ]; [ timer_savingOfObjectCache release ]; timer_savingOfObjectCache = nil ; //If a save of the account prefs is pending, perform it immediately since we are quitting if ( timer_savingOfAccountCache ) { [ accountPrefs writeToPath : [ adium . loginController userDirectory ] withName : @"AccountPrefs" ]; /* There's no guarantee that 'will close' is called in the same run loop as the actual program termination. * We've done our final save, though; don't let the timer fire again. [ timer_savingOfAccountCache invalidate ]; [ timer_savingOfAccountCache release ]; timer_savingOfObjectCache = nil ; - ( id ) initForGroup: ( NSString * ) inGroup object: ( AIListObject * ) inObject if (( self = [ super init ])) { group = [ inGroup retain ]; object = [ inObject retain ]; + ( BOOL ) automaticallyNotifiesObserversForKey: ( NSString * ) theKey @synthesize defaults , group ; * @brief Register defaults * These defaults will be added to any existing defaults; if there is overlap between keys, the new key-value pair will be used. - ( void ) registerDefaults: ( NSDictionary * ) inDefaults if ( ! defaults ) defaults = [[ NSMutableDictionary alloc ] init ]; [ defaults addEntriesFromDictionary : inDefaults ]; + ( void ) loadGlobalPrefsForObject: ( AIListObject * ) object NSAssert ( * myGlobalPrefs == nil , @"Attempting to load global prefs when they're already loaded" ); NSString * objectPrefsPath = [[ adium . loginController . userDirectory stringByAppendingPathComponent : globalPrefsName ] stringByAppendingPathExtension : @"plist" ]; NSString * errorString = nil ; NSData * data = [ NSData dataWithContentsOfFile : objectPrefsPath NSLog ( @"Error reading data for preferences file %@: %@ (%@ %ld: %@)" , objectPrefsPath , error , [ error domain ], ( long )[ error code ], [ error userInfo ]); AILogWithSignature ( @"Error reading data for preferences file %@: %@ (%@ %li: %@)" , objectPrefsPath , error , [ error domain ], [ error code ], [ error userInfo ]); if ([[ NSFileManager defaultManager ] fileExistsAtPath : objectPrefsPath ]) { AILogWithSignature ( @"Preferences file %@'s attributes: %@. Reattempting to read the file..." , globalPrefsName , [[ NSFileManager defaultManager ] attributesOfItemAtPath : objectPrefsPath error : NULL ]); data = [ NSData dataWithContentsOfFile : objectPrefsPath AILogWithSignature ( @"Error reading data for preferences file %@: %@ (%@ %li: %@)" , objectPrefsPath , error , [ error domain ], [ error code ], [ error userInfo ]); //We want to load a mutable dictioanry of mutable dictionaries. * myGlobalPrefs = [[ NSPropertyListSerialization propertyListFromData : data mutabilityOption : NSPropertyListMutableContainers errorDescription : & errorString ] retain ]; NSLog ( @"Error reading preferences file %@: %@" , objectPrefsPath , errorString ); AILogWithSignature ( @"Error reading preferences file %@: %@" , objectPrefsPath , errorString ); #ifdef PREFERENCE_CONTAINER_DEBUG AILogWithSignature ( @"I read in %@ with %li items" , globalPrefsName , [ * myGlobalPrefs count ]); /* If we don't get a dictionary, create a new one */ /* This wouldn't be an error if this were a new Adium installation; the below is temporary debug logging. */ NSLog ( @"WARNING: Unable to parse preference file %@ (data was %@)" , objectPrefsPath , data ); AILogWithSignature ( @"WARNING: Unable to parse preference file %@ (data was %@)" , objectPrefsPath , data ); * myGlobalPrefs = [[ NSMutableDictionary alloc ] init ]; - ( void ) setPrefValue: ( id ) value forKey: ( id ) key NSAssert ([ NSThread currentThread ] == [ NSThread mainThread ], @"AIPreferenceContainer is not threadsafe! Don't set prefs from non-main threads" ); NSMutableDictionary * prefDict = self . prefs ; if ( object && ! prefDict ) { //For compatibility with having loaded individual object prefs from previous version of Adium, we key by the safe filename string NSString * globalPrefsKey = [ object . internalObjectID safeFilenameString ]; prefs = [[ NSMutableDictionary alloc ] init ]; [ * myGlobalPrefs setObject : prefs [ self . prefs setValue : value forKey : key ]; * @brief Return a dictionary of our preferences, loading it from disk as needed - ( NSMutableDictionary * ) prefs NSString * userDirectory = adium . loginController . userDirectory ; //For compatibility with having loaded individual object prefs from previous version of Adium, we key by the safe filename string NSString * globalPrefsKey = [ object . internalObjectID safeFilenameString ]; prefs = [[ * myGlobalPrefs objectForKey : globalPrefsKey ] retain ]; prefs = [[ NSMutableDictionary dictionaryAtPath : userDirectory * @brief Return a dictionary of preferences and defaults, appropriately merged together - ( NSDictionary * ) dictionary NSDictionary * myPrefs = self . prefs ; if ( ! defaults ) return [[ myPrefs copy ] autorelease ]; NSMutableDictionary * prefsWithDefaults = [ defaults mutableCopy ]; //Add our own preferences to the defaults dictionary to get a dict with the set keys overriding the default keys if ( myPrefs ) [ prefsWithDefaults addEntriesFromDictionary : myPrefs ]; return [ prefsWithDefaults autorelease ]; * @brief Set value for key * This sets and saves a preference for the given key - ( void ) setValue: ( id ) value forKey: ( NSString * ) key /* Comparing pointers, numbers, and strings is far cheaper than writing out to disk; * check to see if we don't need to change anything at all. However, we still want to post notifications * for observers that we were set. id oldValue = [ self valueForKey : key ]; if (( ! value && ! oldValue ) || ( value && oldValue && [ value isEqual : oldValue ])) [ self willChangeValueForKey : key ]; [ self setPrefValue : value forKey : key ]; [ self didChangeValueForKey : key ]; //Now tell the preference controller if ( ! preferenceChangeDelays ) { [ adium . preferenceController informObserversOfChangedKey : key inGroup : group object : object ]; - ( id ) valueForKey: ( NSString * ) key result = [ self . prefs objectForKey : key ]; if ( ! result ) result = [ self . defaults objectForKey : key ]; * @brief Get a preference, possibly ignoring the defaults * @param ignoreDefaults If YES, the preferences are accessed diretly, without including the default values - ( id ) valueForKey: ( NSString * ) key ignoringDefaults: ( BOOL ) ignoreDefaults return [ self . prefs valueForKey : key ]; return [ self valueForKey : key ]; - ( id ) defaultValueForKey: ( NSString * ) key return [ self . defaults valueForKey : key ]; * @brief Set all preferences for this group * All existing preferences are removed for this group; the passed dictionary becomes the new preferences - ( void ) setPreferences: ( NSDictionary * ) inPreferences [ self setPreferenceChangedNotificationsEnabled : NO ]; [ self setValuesForKeysWithDictionary : inPreferences ]; [ self setPreferenceChangedNotificationsEnabled : YES ]; - ( void ) setPreferenceChangedNotificationsEnabled: ( BOOL ) inEnabled preferenceChangeDelays -- ; preferenceChangeDelays ++ ; if ( preferenceChangeDelays == 0 ) { [ adium . preferenceController informObserversOfChangedKey : nil inGroup : group object : object ]; - ( void ) performObjectPrefsSave: ( NSTimer * ) inTimer NSDictionary * immutablePrefsToWrite = [[[ NSDictionary alloc ] initWithDictionary : inTimer . userInfo copyItems : YES ] autorelease ]; #ifdef PREFERENCE_CONTAINER_DEBUG // NSData *data = [NSData dataWithContentsOfFile:[adium.loginController.userDirectory stringByAppendingPathComponent:[globalPrefsName stringByAppendingPathExtension:@"plist"]]]; // NSString *errorString = nil; // NSDictionary *theDict = [NSPropertyListSerialization propertyListFromData:data // mutabilityOption:NSPropertyListMutableContainers // errorDescription:&errorString]; // if (theDict && [theDict count] > 0 && [immutablePrefsToWrite count] == 0) // NSLog(@"Writing out an empty ByObjectPrefs when we have an existing non-empty one!"); if ([ immutablePrefsToWrite count ] > 0 ) { [ immutablePrefsToWrite asyncWriteToPath : adium . loginController . userDirectory withName : globalPrefsName ]; NSLog ( @"Attempted to write an empty ByObject Prefs. Uh oh!" ); if ( inTimer == timer_savingOfObjectCache ) { [ timer_savingOfObjectCache release ]; timer_savingOfObjectCache = nil ; } else if ( inTimer == timer_savingOfAccountCache ) { [ timer_savingOfAccountCache release ]; timer_savingOfAccountCache = nil ; //For an object's pref changes, batch all changes in a SAVE_OBJECT_PREFS_DELAY second period. We'll force an immediate save if Adium quits. if ( * myTimerForSavingGlobalPrefs ) { [ * myTimerForSavingGlobalPrefs setFireDate : [ NSDate dateWithTimeIntervalSinceNow : SAVE_OBJECT_PREFS_DELAY ]]; #ifdef PREFERENCE_CONTAINER_DEBUG // This shouldn't be happening at all. NSLog ( @"Attempted to detach to save for %@ [%@], but info was nil." , self , globalPrefsName ); AILogWithSignature ( @"Attempted to detach to save for %@ [%@], but info was nil." , self , globalPrefsName ); * myTimerForSavingGlobalPrefs = [[ NSTimer scheduledTimerWithTimeInterval : SAVE_OBJECT_PREFS_DELAY selector : @selector ( performObjectPrefsSave : ) //Save the preference change immediately [ self . prefs writeToPath : adium . loginController . userDirectory withName : group ]; - ( NSString * ) description return [ NSString stringWithFormat : @"<%@ %p: Group %@, object %@>" , NSStringFromClass ([ self class ]), self , group , object ];