/** Implementation for NSUserDefaults for GNUstep Copyright (C) 1995-2001 Free Software Foundation, Inc. Written by: Georg Tuparev EMBL & Academia Naturalis, Heidelberg, Germany Modified by: Richard Frith-Macdonald This file is part of the GNUstep Base Library. This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02111 USA. NSUserDefaults class reference $Date$ $Revision$ */ #include "config.h" #include "GNUstepBase/preface.h" #include #include #include #include "Foundation/NSUserDefaults.h" #include "Foundation/NSArchiver.h" #include "Foundation/NSArray.h" #include "Foundation/NSBundle.h" #include "Foundation/NSData.h" #include "Foundation/NSDate.h" #include "Foundation/NSDictionary.h" #include "Foundation/NSDistributedLock.h" #include "Foundation/NSException.h" #include "Foundation/NSFileManager.h" #include "Foundation/NSLock.h" #include "Foundation/NSNotification.h" #include "Foundation/NSPathUtilities.h" #include "Foundation/NSProcessInfo.h" #include "Foundation/NSRunLoop.h" #include "Foundation/NSSet.h" #include "Foundation/NSThread.h" #include "Foundation/NSTimer.h" #include "Foundation/NSUtilities.h" #include "Foundation/NSValue.h" #include "Foundation/NSDebug.h" #include "GNUstepBase/GSLocale.h" #include "GNUstepBase/GSLock.h" #if defined(__MINGW32__) @class NSUserDefaultsWin32; #endif #ifdef HAVE_LOCALE_H #include #endif #include "GSPrivate.h" /* Wait for access */ #define _MAX_COUNT 5 /* Max 10 sec. */ /************************************************************************* *** Class variables *************************************************************************/ static SEL nextObjectSel; static SEL objectForKeySel; static SEL addSel; static Class NSArrayClass; static Class NSDataClass; static Class NSDateClass; static Class NSDictionaryClass; static Class NSNumberClass; static Class NSMutableDictionaryClass; static Class NSStringClass; static NSString *defaultsFile = @".GNUstepDefaults"; static NSUserDefaults *sharedDefaults = nil; static NSMutableString *processName = nil; static NSMutableArray *userLanguages = nil; static BOOL invalidatedLanguages = NO; static NSRecursiveLock *classLock = nil; /* * Caching some defaults. */ static BOOL flags[GSUserDefaultMaxFlag] = { 0 }; static void updateCache(NSUserDefaults *self) { if (self == sharedDefaults) { NSArray *debug; /** * If there is an array NSUserDefault called GNU-Debug, * we add its contents to the set of active debug levels. */ debug = [self arrayForKey: @"GNU-Debug"]; if (debug != nil) { unsigned c = [debug count]; NSMutableSet *s; s = [[NSProcessInfo processInfo] debugSet]; while (c-- > 0) { NSString *level = [debug objectAtIndex: c]; [s addObject: level]; } } flags[GSMacOSXCompatible] = [self boolForKey: @"GSMacOSXCompatible"]; flags[GSOldStyleGeometry] = [self boolForKey: @"GSOldStyleGeometry"]; flags[GSLogSyslog] = [self boolForKey: @"GSLogSyslog"]; flags[GSLogThread] = [self boolForKey: @"GSLogThread"]; flags[NSWriteOldStylePropertyLists] = [self boolForKey: @"NSWriteOldStylePropertyLists"]; } } /************************************************************************* *** Local method definitions *************************************************************************/ @interface NSUserDefaults (__local_NSUserDefaults) - (NSDictionary*) __createArgumentDictionary; - (void) __changePersistentDomain: (NSString*)domainName; - (NSMutableDictionary*) readDefaults; - (BOOL) writeDefaults: (NSDictionary*)defaults oldData: (NSDictionary*)oldData; @end /** *

* NSUserDefaults provides an interface to the defaults system, * which allows an application access to global and/or application * specific defaults set by the user. A particular instance of * NSUserDefaults, standardUserDefaults, is provided as a * convenience. Most of the information described below * pertains to the standardUserDefaults. It is unlikely * that you would want to instantiate your own userDefaults * object, since it would not be set up in the same way as the * standardUserDefaults. *

*

* Defaults are managed based on domains. Certain * domains, such as NSGlobalDomain, are * persistent. These domains have defaults that are stored * externally. Other domains are volatile. The defaults in * these domains remain in effect only during the existence of * the application and may in fact be different for * applications running at the same time. When asking for a * default value from standardUserDefaults, NSUserDefaults * looks through the various domains in a particular order. *

* * NSArgumentDomain ... volatile * * Contains defaults read from the arguments provided * to the application at startup.
* Pairs of arguments are used for this, with the first argument in * each pair being the name of a default (with a hyphen prepended) * and the second argument of the pair being the value of the default.
* NB. In GNUstep special arguments of the form --GNU-Debug=... * are used to enable debugging. Despite beginning with a hyphen, these * are not treated as default keys. *
* Application (name of the current process) ... persistent * * Contains application specific defaults, * such as window positions. * NSGlobalDomain ... persistent * * Global defaults applicable to all applications. * * Language (name based on users's language) ... volatile * * Constants that help with localization to the users's * language. * * GSConfigDomain ... volatile * * Information retrieved from the GNUstep configuration system. * Usually the system wide and user specific GNUstep.conf files, * or from information compiled in when the base library was * built. * * NSRegistrationDomain ... volatile * * Temporary defaults set up by the application. * *
*

* The NSLanguages default value is used to set up the * constants for localization. GNUstep will also look for the * LANGUAGES environment variable if it is not set * in the defaults system. If it exists, it consists of an * array of languages that the user prefers. At least one of * the languages should have a corresponding localization file * (typically located in the Languages directory * of the GNUstep resources). *

*

* As a special extension, on systems that support locales * (e.g. GNU/Linux and Solaris), GNUstep will use information * from the user specified locale, if the NSLanguages * default value is not found. Typically the locale is * specified in the environment with the LANG * environment variable. *

*

* The first change to a persistent domain after a -synchronize * will cause an NSUserDefaultsDidChangeNotification to be posted * (as will any change caused by reading new values from disk), * so your application can keep track of changes made to the * defaults by other software. *

*

* NB. The GNUstep implementation differs from the Apple one in * that it is thread-safe while Apple's (as of MacOS-X 10.1) is not. *

*/ @implementation NSUserDefaults: NSObject static BOOL setSharedDefaults = NO; /* Flag to prevent infinite recursion */ + (void) initialize { if (self == [NSUserDefaults class]) { nextObjectSel = @selector(nextObject); objectForKeySel = @selector(objectForKey:); addSel = @selector(addEntriesFromDictionary:); /* * Cache class info for more rapid testing of the types of defaults. */ NSArrayClass = [NSArray class]; NSDataClass = [NSData class]; NSDateClass = [NSDate class]; NSDictionaryClass = [NSDictionary class]; NSNumberClass = [NSNumber class]; NSMutableDictionaryClass = [NSMutableDictionary class]; NSStringClass = [NSString class]; classLock = [GSLazyRecursiveLock new]; } } /** * Resets the shared user defaults object to reflect the current * user ID. Needed by setuid processes which change the user they * are running as.
* In GNUstep you should call GSSetUserName() when changing your * effective user ID, and that function will call this function for you. */ + (void) resetStandardUserDefaults { [classLock lock]; if (sharedDefaults != nil) { NSDictionary *regDefs; [sharedDefaults synchronize]; // Ensure changes are written. regDefs = RETAIN([sharedDefaults->_tempDomains objectForKey: NSRegistrationDomain]); setSharedDefaults = NO; DESTROY(sharedDefaults); if (regDefs != nil) { [self standardUserDefaults]; if (sharedDefaults != nil) { [sharedDefaults->_tempDomains setObject: regDefs forKey: NSRegistrationDomain]; } RELEASE(regDefs); } } [classLock unlock]; } /* Create a locale dictionary when we have absolutely no information about the locale. This method should go away, since it will never be called in a properly installed system. */ + (NSDictionary *) _unlocalizedDefaults { NSDictionary *registrationDefaults; NSArray *ampm; NSArray *long_day; NSArray *long_month; NSArray *short_day; NSArray *short_month; NSArray *earlyt; NSArray *latert; NSArray *hour_names; NSArray *ymw_names; ampm = [NSArray arrayWithObjects: @"AM", @"PM", nil]; short_month = [NSArray arrayWithObjects: @"Jan", @"Feb", @"Mar", @"Apr", @"May", @"Jun", @"Jul", @"Aug", @"Sep", @"Oct", @"Nov", @"Dec", nil]; long_month = [NSArray arrayWithObjects: @"January", @"February", @"March", @"April", @"May", @"June", @"July", @"August", @"September", @"October", @"November", @"December", nil]; short_day = [NSArray arrayWithObjects: @"Sun", @"Mon", @"Tue", @"Wed", @"Thu", @"Fri", @"Sat", nil]; long_day = [NSArray arrayWithObjects: @"Sunday", @"Monday", @"Tuesday", @"Wednesday", @"Thursday", @"Friday", @"Saturday", nil]; earlyt = [NSArray arrayWithObjects: @"prior", @"last", @"past", @"ago", nil]; latert = [NSArray arrayWithObjects: @"next", nil]; ymw_names = [NSArray arrayWithObjects: @"year", @"month", @"week", nil]; hour_names = [NSArray arrayWithObjects: [NSArray arrayWithObjects: @"0", @"midnight", nil], [NSArray arrayWithObjects: @"12", @"noon", @"lunch", nil], [NSArray arrayWithObjects: @"10", @"morning", nil], [NSArray arrayWithObjects: @"14", @"afternoon", nil], [NSArray arrayWithObjects: @"19", @"dinner", nil], nil]; registrationDefaults = [NSDictionary dictionaryWithObjectsAndKeys: ampm, NSAMPMDesignation, long_month, NSMonthNameArray, long_day, NSWeekDayNameArray, short_month, NSShortMonthNameArray, short_day, NSShortWeekDayNameArray, @"DMYH", NSDateTimeOrdering, [NSArray arrayWithObject: @"tomorrow"], NSNextDayDesignations, [NSArray arrayWithObject: @"nextday"], NSNextNextDayDesignations, [NSArray arrayWithObject: @"yesterday"], NSPriorDayDesignations, [NSArray arrayWithObject: @"today"], NSThisDayDesignations, earlyt, NSEarlierTimeDesignations, latert, NSLaterTimeDesignations, hour_names, NSHourNameDesignations, ymw_names, NSYearMonthWeekDesignations, nil]; return registrationDefaults; } /** * Returns the shared defaults object. If it doesn't exist yet, it's * created. The defaults are initialized for the current user. * The search list is guaranteed to be standard only the first time * this method is invoked. The shared instance is provided as a * convenience; other instances may also be created. */ + (NSUserDefaults*) standardUserDefaults { BOOL added_locale, added_lang; id lang; NSArray *uL; NSEnumerator *enumerator; /* * Calling +standardUserDefaults and +userLanguages is horribly interrelated. * Messing with the order of assignments and calls within these methods can * break things very easily ... take care. * * If +standardUserDefaults is called first, it sets up initial information * in sharedDefaults then calls +userLanguages to get language information. * +userLanguages then calls +standardUserDefaults to read NSLanguages * and returns the language information. * +standardUserDefaults then sets up language domains and loads localisation * information before returning. * * If +userLanguages is called first, it calls +standardUserDefaults * to obtain the NSLanguages array. * +standardUserDefaults loads basic information initialising the * sharedDefaults variable, then calls back to +userLanguages to set up * its search list. * +userLanguages calls +standardUserDefaults again to get NSLanguages. * +standardUserDefaults returns the partially initialised sharedDefaults. * +userLanguages uses this to create and return the user languages. * +standardUserDefaults uses the languages to update the search list * and load in localisation information then returns sharedDefaults. * +userLanguages uses this to rebuild language information and return it. */ [classLock lock]; /* * NB. The use of the setSharedDefaults flag ensures that a recursive * call to this method is quietly suppressed ... so we get a more * manageable problem. */ if (setSharedDefaults == YES) { RETAIN(sharedDefaults); [classLock unlock]; return AUTORELEASE(sharedDefaults); } setSharedDefaults = YES; // Create new sharedDefaults (NOTE: Not added to the autorelease pool!) #if defined(__MINGW32__) { NSString *path = GSDefaultsRootForUser(NSUserName()); NSRange r = [path rangeOfString: @":REGISTRY:"]; if (r.length > 0) { sharedDefaults = [[NSUserDefaultsWin32 alloc] init]; } else { sharedDefaults = [[self alloc] init]; } } #else sharedDefaults = [[self alloc] init]; #endif if (sharedDefaults == nil) { NSLog(@"WARNING - unable to create shared user defaults!\n"); [classLock unlock]; return nil; } /* * Set up search list (excluding language list, which we don't know yet) */ [sharedDefaults->_searchList addObject: NSArgumentDomain]; [sharedDefaults->_searchList addObject: processName]; [sharedDefaults->_searchList addObject: NSGlobalDomain]; [sharedDefaults->_searchList addObject: GSConfigDomain]; [sharedDefaults->_searchList addObject: NSRegistrationDomain]; /* * Look up user languages list and insert language specific domains * into search list before NSRegistrationDomain */ uL = [self userLanguages]; enumerator = [uL objectEnumerator]; while ((lang = [enumerator nextObject])) { unsigned index = [sharedDefaults->_searchList count] - 1; [sharedDefaults->_searchList insertObject: lang atIndex: index]; } /* Set up language constants */ added_locale = NO; added_lang = NO; enumerator = [uL objectEnumerator]; while ((lang = [enumerator nextObject])) { NSString *path; NSDictionary *dict; NSBundle *gbundle; gbundle = [NSBundle bundleForLibrary: @"gnustep-base"]; path = [gbundle pathForResource: lang ofType: nil inDirectory: @"Languages"]; dict = nil; if (path != nil) { dict = [NSDictionary dictionaryWithContentsOfFile: path]; } if (dict) { [sharedDefaults setVolatileDomain: dict forName: lang]; added_lang = YES; } else if (added_locale == NO) { NSString *locale = nil; #ifdef HAVE_LOCALE_H #ifdef LC_MESSAGES locale = GSSetLocale(LC_MESSAGES, nil); #endif #endif if (locale == nil) { continue; } /* See if we can get the dictionary from i18n functions. Note that we get the dict from the current locale regardless of what 'lang' is, since it should match anyway. */ /* Also, I don't think that the i18n routines can handle more than one locale, but tell me if I'm wrong... */ if (GSLanguageFromLocale(locale)) { lang = GSLanguageFromLocale(locale); } dict = GSDomainFromDefaultLocale(); if (dict != nil) { [sharedDefaults setVolatileDomain: dict forName: lang]; } added_locale = YES; } } if (added_lang == NO) { /* Ack! We should never get here */ NSWarnMLog(@"Improper installation: No language locale found"); [sharedDefaults registerDefaults: [self _unlocalizedDefaults]]; } RETAIN(sharedDefaults); updateCache(sharedDefaults); [classLock unlock]; return AUTORELEASE(sharedDefaults); } /** * Returns the array of user languages preferences. Uses the * NSLanguages user default if available, otherwise * tries to infer setup from operating system information etc * (in particular, uses the LANGUAGES environment variable). */ + (NSArray*) userLanguages { NSArray *result; /* * Calling +standardUserDefaults and +userLanguages is horribly interrelated. * Messing with the order of assignments and calls within these methods can * break things very easily ... take care. * * If +standardUserDefaults is called first, it sets up initial information * in sharedDefaults then calls +userLanguages to get language information. * +userLanguages then calls +standardUserDefaults to read NSLanguages * and returns the language information. * +standardUserDefaults then sets up language domains and loads localisation * information before returning. * * If +userLanguages is called first, it calls +standardUserDefaults * to obtain the NSLanguages array. * +standardUserDefaults loads basic information initialising the * sharedDefaults variable, then calls back to +userLanguages to set up * its search list. * +userLanguages calls +standardUserDefaults again to get NSLanguages. * +standardUserDefaults returns the partially initialised sharedDefaults. * +userLanguages uses this to create and return the user languages. * +standardUserDefaults uses the languages to update the search list * and load in localisation information then returns sharedDefaults. * +userLanguages uses this to rebuild language information and return it. */ [classLock lock]; if (invalidatedLanguages == YES) { invalidatedLanguages = NO; DESTROY(userLanguages); } if (userLanguages == nil) { NSArray *currLang = nil; NSString *locale = nil; #ifdef HAVE_LOCALE_H #ifdef LC_MESSAGES locale = GSSetLocale(LC_MESSAGES, nil); #endif #endif currLang = [[NSUserDefaults standardUserDefaults] stringArrayForKey: @"NSLanguages"]; userLanguages = [[NSMutableArray alloc] initWithCapacity: 5]; if (currLang == nil && locale != nil && GSLanguageFromLocale(locale)) { currLang = [NSArray arrayWithObject: GSLanguageFromLocale(locale)]; } #ifdef __MINGW32__ if (currLang == nil && locale != nil) { /* Check for language as the first part of the locale string */ NSRange under = [locale rangeOfString: @"_"]; if (under.location) currLang = [NSArray arrayWithObject: [locale substringToIndex: under.location]]; } #endif if (currLang == nil) { NSString *env; env = [[[NSProcessInfo processInfo] environment] objectForKey: @"LANGUAGES"]; if (env != nil) { currLang = [env componentsSeparatedByString: @";"]; } } if (currLang != nil) { NSMutableArray *a = [currLang mutableCopy]; unsigned c = [a count]; while (c-- > 0) { NSString *s = [[a objectAtIndex: c] stringByTrimmingSpaces]; if ([s length] == 0) { [a removeObjectAtIndex: c]; } else { [a replaceObjectAtIndex: c withObject: s]; } } [userLanguages addObjectsFromArray: a]; RELEASE(a); } /* Check if "English" is included. We do this to make sure all the required language constants are set somewhere if they aren't set in the default language */ if ([userLanguages containsObject: @"English"] == NO) { [userLanguages addObject: @"English"]; } } result = RETAIN(userLanguages); [classLock unlock]; return AUTORELEASE(result); } /** * Sets the array of user languages preferences. Places the specified * array in the NSLanguages user default. */ + (void) setUserLanguages: (NSArray*)languages { NSMutableDictionary *globDict; globDict = [[[self standardUserDefaults] persistentDomainForName: NSGlobalDomain] mutableCopy]; if (languages == nil) // Remove the entry [globDict removeObjectForKey: @"NSLanguages"]; else [globDict setObject: languages forKey: @"NSLanguages"]; [[self standardUserDefaults] setPersistentDomain: globDict forName: NSGlobalDomain]; RELEASE(globDict); } /************************************************************************* *** Initializing the User Defaults *************************************************************************/ /** * Initializes defaults for current user calling initWithUser: */ - (id) init { return [self initWithUser: NSUserName()]; } /** * Initializes defaults for the specified user calling -initWithContentsOfFile: */ - (id) initWithUser: (NSString*)userName { NSString *path; path = [GSDefaultsRootForUser(userName) stringByAppendingPathComponent: defaultsFile]; return [self initWithContentsOfFile: path]; } /** * * Initializes defaults for the specified path. Returns an object with * an empty search list. */ - (id) initWithContentsOfFile: (NSString*)path { NSFileManager *mgr = [NSFileManager defaultManager]; NSRange r; BOOL loadReadonly = NO; BOOL flag; self = [super init]; /* * Global variable. */ if (processName == nil) { processName = RETAIN([[NSProcessInfo processInfo] processName]); } if (path == nil || [path isEqual: @""] == YES) { path = [GSDefaultsRootForUser(NSUserName()) stringByAppendingPathComponent: defaultsFile]; } r = [path rangeOfString: @":INTERNAL:"]; #if defined(__MINGW32__) if (r.length == 0) { r = [path rangeOfString: @":REGISTRY:"]; } #endif if (r.length == 0) { _defaultsDatabase = [[path stringByStandardizingPath] copy]; path = [_defaultsDatabase stringByDeletingLastPathComponent]; if ([mgr isWritableFileAtPath: path] == YES && [mgr fileExistsAtPath: path isDirectory: &flag] == YES && flag == YES && [mgr fileExistsAtPath: _defaultsDatabase] == YES && [mgr isReadableFileAtPath: _defaultsDatabase] == YES) { _fileLock = [[NSDistributedLock alloc] initWithPath: [_defaultsDatabase stringByAppendingPathExtension: @"lck"]]; } else if ([mgr isReadableFileAtPath: _defaultsDatabase] == YES) { loadReadonly = YES; } } _lock = [GSLazyRecursiveLock new]; // Create an empty search list _searchList = [[NSMutableArray alloc] initWithCapacity: 10]; if (loadReadonly == YES) { // Load read-only defaults. ASSIGN(_lastSync, [NSDateClass date]); ASSIGN(_persDomains, [self readDefaults]); updateCache(self); [[NSNotificationCenter defaultCenter] postNotificationName: NSUserDefaultsDidChangeNotification object: self]; } else { // Initialize _persDomains from the archived user defaults (persistent) _persDomains = [[NSMutableDictionaryClass alloc] initWithCapacity: 10]; if ([self synchronize] == NO) { DESTROY(self); return self; } } // Check and if not existent add the Application and the Global domains if ([_persDomains objectForKey: processName] == nil) { [_persDomains setObject: [NSMutableDictionaryClass dictionaryWithCapacity: 10] forKey: processName]; [self __changePersistentDomain: processName]; } if ([_persDomains objectForKey: NSGlobalDomain] == nil) { [_persDomains setObject: [NSMutableDictionaryClass dictionaryWithCapacity: 10] forKey: NSGlobalDomain]; [self __changePersistentDomain: NSGlobalDomain]; } // Create volatile defaults and add the Argument and the Registration domains _tempDomains = [[NSMutableDictionaryClass alloc] initWithCapacity: 10]; [_tempDomains setObject: [self __createArgumentDictionary] forKey: NSArgumentDomain]; [_tempDomains setObject: [NSMutableDictionaryClass dictionaryWithCapacity: 10] forKey: NSRegistrationDomain]; [_tempDomains setObject: GNUstepConfig(nil) forKey: GSConfigDomain]; [[NSNotificationCenter defaultCenter] addObserver: self selector: @selector(synchronize) name: @"GSHousekeeping" object: nil]; return self; } - (void) dealloc { [[NSNotificationCenter defaultCenter] removeObserver: self]; RELEASE(_lastSync); RELEASE(_searchList); RELEASE(_persDomains); RELEASE(_tempDomains); RELEASE(_changedDomains); RELEASE(_dictionaryRep); RELEASE(_fileLock); RELEASE(_lock); [super dealloc]; } - (NSString*) description { NSMutableString *desc; [_lock lock]; desc = [NSMutableString stringWithFormat: @"%@", [super description]]; [desc appendFormat: @" SearchList: %@", _searchList]; [desc appendFormat: @" Persistant: %@", _persDomains]; [desc appendFormat: @" Temporary: %@", _tempDomains]; [_lock unlock]; return desc; } /** * Adds the domain names aName to the search list of the receiver.
* The domain is added after the application domain.
* Suites may be removed using the -removeSuiteNamed: method. */ - (void) addSuiteNamed: (NSString*)aName { unsigned index; if (aName == nil) { [NSException raise: NSInvalidArgumentException format: @"attempt to add suite with nil name"]; } [_lock lock]; DESTROY(_dictionaryRep); if (self == sharedDefaults) invalidatedLanguages = YES; [_searchList removeObject: aName]; index = [_searchList indexOfObject: processName]; index++; // NSNotFound wraps to zero ... insert at start. aName = [aName copy]; [_searchList insertObject: aName atIndex: index]; [_lock unlock]; RELEASE(aName); } /** * Looks up a value for a specified default using -objectForKey: * and checks that it is an NSArray object. Returns nil if it is not. */ - (NSArray*) arrayForKey: (NSString*)defaultName { id obj = [self objectForKey: defaultName]; if (obj != nil && [obj isKindOfClass: NSArrayClass]) return obj; return nil; } /** * Looks up a value for a specified default using -objectForKey: * and returns its boolean representation.
* Returns NO if it is not a boolean.
* The text 'yes' or 'true' or any non zero numeric value is considered * to be a boolean YES. Other string values are NO.
* NB. This differs slightly from the documented behavior for MacOS-X * (August 2002) in that the GNUstep version accepts the string 'TRUE' * as equivalent to 'YES'. */ - (BOOL) boolForKey: (NSString*)defaultName { id obj = [self objectForKey: defaultName]; if (obj != nil && ([obj isKindOfClass: NSStringClass] || [obj isKindOfClass: NSNumberClass])) { return [obj boolValue]; } return NO; } /** * Looks up a value for a specified default using -objectForKey: * and checks that it is an NSData object. Returns nil if it is not. */ - (NSData*) dataForKey: (NSString*)defaultName { id obj = [self objectForKey: defaultName]; if (obj != nil && [obj isKindOfClass: NSDataClass]) return obj; return nil; } /** * Looks up a value for a specified default using -objectForKey: * and checks that it is an NSDictionary object. Returns nil if it is not. */ - (NSDictionary*) dictionaryForKey: (NSString*)defaultName { id obj = [self objectForKey: defaultName]; if (obj != nil && [obj isKindOfClass: NSDictionaryClass]) { return obj; } return nil; } /** * Looks up a value for a specified default using -objectForKey: * and checks that it is a float. Returns 0.0 if it is not. */ - (float) floatForKey: (NSString*)defaultName { id obj = [self objectForKey: defaultName]; if (obj != nil && ([obj isKindOfClass: NSStringClass] || [obj isKindOfClass: NSNumberClass])) { return [obj floatValue]; } return 0.0; } /** * Looks up a value for a specified default using -objectForKey: * and checks that it is an integer. Returns 0 if it is not. */ - (int) integerForKey: (NSString*)defaultName { id obj = [self objectForKey: defaultName]; if (obj != nil && ([obj isKindOfClass: NSStringClass] || [obj isKindOfClass: NSNumberClass])) { return [obj intValue]; } return 0; } /** * Looks up a value for a specified default using. * The lookup is performed by accessing the domains in the order * given in the search list. *
Returns nil if defaultName cannot be found. */ - (id) objectForKey: (NSString*)defaultName { NSEnumerator *enumerator; IMP nImp; id object; id dN; IMP pImp; IMP tImp; [_lock lock]; enumerator = [_searchList objectEnumerator]; nImp = [enumerator methodForSelector: nextObjectSel]; object = nil; pImp = [_persDomains methodForSelector: objectForKeySel]; tImp = [_tempDomains methodForSelector: objectForKeySel]; while ((dN = (*nImp)(enumerator, nextObjectSel)) != nil) { NSDictionary *dict; dict = (*pImp)(_persDomains, objectForKeySel, dN); if (dict != nil && (object = [dict objectForKey: defaultName])) break; dict = (*tImp)(_tempDomains, objectForKeySel, dN); if (dict != nil && (object = [dict objectForKey: defaultName])) break; } RETAIN(object); [_lock unlock]; return AUTORELEASE(object); } /** * Removes the default with the specified name from the application * domain. */ - (void) removeObjectForKey: (NSString*)defaultName { id obj; [_lock lock]; obj = [_persDomains objectForKey: processName]; obj = [(NSDictionary*)obj objectForKey: defaultName]; if (obj != nil) { NSMutableDictionary *dict; id obj = [_persDomains objectForKey: processName]; if ([obj isKindOfClass: NSMutableDictionaryClass] == YES) { dict = obj; } else { dict = [obj mutableCopy]; [_persDomains setObject: dict forKey: processName]; } [dict removeObjectForKey: defaultName]; [self __changePersistentDomain: processName]; } [_lock unlock]; } /** * Sets a boolean value for defaultName in the application domain.
* The boolean value is stored as a string - either YES or NO. * Calls -setObject:forKey: to make the change. */ - (void) setBool: (BOOL)value forKey: (NSString*)defaultName { NSNumber *n = [NSNumberClass numberWithBool: value]; [self setObject: n forKey: defaultName]; } /** * Sets a float value for defaultName in the application domain. *
Calls -setObject:forKey: to make the change. */ - (void) setFloat: (float)value forKey: (NSString*)defaultName { NSNumber *n = [NSNumberClass numberWithFloat: value]; [self setObject: n forKey: defaultName]; } /** * Sets an integer value for defaultName in the application domain. *
Calls -setObject:forKey: to make the change. */ - (void) setInteger: (int)value forKey: (NSString*)defaultName { NSNumber *n = [NSNumberClass numberWithInt: value]; [self setObject: n forKey: defaultName]; } static BOOL isPlistObject(id o) { if ([o isKindOfClass: NSStringClass] == YES) { return YES; } if ([o isKindOfClass: NSDataClass] == YES) { return YES; } if ([o isKindOfClass: NSDateClass] == YES) { return YES; } if ([o isKindOfClass: NSNumberClass] == YES) { return YES; } if ([o isKindOfClass: NSArrayClass] == YES) { NSEnumerator *e = [o objectEnumerator]; id tmp; while ((tmp = [e nextObject]) != nil) { if (isPlistObject(tmp) == NO) { return NO; } } return YES; } if ([o isKindOfClass: NSDictionaryClass] == YES) { NSEnumerator *e = [o keyEnumerator]; id tmp; while ((tmp = [e nextObject]) != nil) { if (isPlistObject(tmp) == NO) { return NO; } tmp = [(NSDictionary*)o objectForKey: tmp]; if (isPlistObject(tmp) == NO) { return NO; } } return YES; } return NO; } /** * Sets an object value for defaultName in the application domain.
* The defaultName must be a non-empty string.
* The value must be an instance of one of the [NSString-propertyList] * classes.
*

Causes a NSUserDefaultsDidChangeNotification to be posted * if this is the first change to a persistent-domain since the * last -synchronize. *

* If value is nil, this is equivalent to the -removeObjectForKey: method. */ - (void) setObject: (id)value forKey: (NSString*)defaultName { NSMutableDictionary *dict; id obj; if (value == nil) { [self removeObjectForKey: defaultName]; } if ([defaultName isKindOfClass: [NSString class]] == NO || [defaultName length] == 0) { [NSException raise: NSInvalidArgumentException format: @"attempt to set object with bad key (%@)", defaultName]; } if (isPlistObject(value) == NO) { [NSException raise: NSInvalidArgumentException format: @"attempt to set non property list object for key (%@)", defaultName]; } [_lock lock]; obj = [_persDomains objectForKey: processName]; if ([obj isKindOfClass: NSMutableDictionaryClass] == YES) { dict = obj; } else { dict = [obj mutableCopy]; [_persDomains setObject: dict forKey: processName]; RELEASE(dict); } [dict setObject: value forKey: defaultName]; [self __changePersistentDomain: processName]; [_lock unlock]; } /** * Calls -arrayForKey: to get an array value for defaultName and checks * that the array contents are string objects ... if not, returns nil. */ - (NSArray*) stringArrayForKey: (NSString*)defaultName { id arr = [self arrayForKey: defaultName]; if (arr != nil) { NSEnumerator *enumerator = [arr objectEnumerator]; id obj; while ((obj = [enumerator nextObject])) { if ([obj isKindOfClass: NSStringClass] == NO) { return nil; } } return arr; } return nil; } /** * Looks up a value for a specified default using -objectForKey: * and checks that it is an NSString. Returns nil if it is not. */ - (NSString*) stringForKey: (NSString*)defaultName { id obj = [self objectForKey: defaultName]; if (obj != nil && [obj isKindOfClass: NSStringClass]) return obj; return nil; } /************************************************************************* *** Returning the Search List *************************************************************************/ /** * Returns an array listing the domains searched in order to look up * a value in the defaults system. The order of the names in the * array is the order in which the domains are searched. */ - (NSArray*) searchList { NSArray *copy; [_lock lock]; copy = [_searchList copy]; [_lock unlock]; return AUTORELEASE(copy); } /** * Sets the list of the domains searched in order to look up * a value in the defaults system. The order of the names in the * array is the order in which the domains are searched.
* On lookup, the first match is used. */ - (void) setSearchList: (NSArray*)newList { [_lock lock]; DESTROY(_dictionaryRep); if (self == sharedDefaults) invalidatedLanguages = YES; RELEASE(_searchList); _searchList = [newList mutableCopy]; [_lock unlock]; } /** * Returns the persistent domain specified by domainName. */ - (NSDictionary*) persistentDomainForName: (NSString*)domainName { NSDictionary *copy; [_lock lock]; copy = [[_persDomains objectForKey: domainName] copy]; [_lock unlock]; return AUTORELEASE(copy); } /** * Returns an array listing the name of all the persistent domains. */ - (NSArray*) persistentDomainNames { NSArray *keys; [_lock lock]; keys = [_persDomains allKeys]; [_lock unlock]; return keys; } /** * Removes the persistent domain specified by domainName from the * user defaults. *
Causes a NSUserDefaultsDidChangeNotification to be posted * if this is the first change to a persistent-domain since the * last -synchronize. */ - (void) removePersistentDomainForName: (NSString*)domainName { [_lock lock]; if ([_persDomains objectForKey: domainName]) { [_persDomains removeObjectForKey: domainName]; [self __changePersistentDomain: domainName]; } [_lock unlock]; } /** * Replaces the persistent-domain specified by domainName with * domain ... a dictionary containing keys and defaults values. *
Raises an NSInvalidArgumentException if domainName already * exists as a volatile-domain. *
Causes a NSUserDefaultsDidChangeNotification to be posted * if this is the first change to a persistent-domain since the * last -synchronize. */ - (void) setPersistentDomain: (NSDictionary*)domain forName: (NSString*)domainName { NSDictionary *dict; [_lock lock]; dict = [_tempDomains objectForKey: domainName]; if (dict != nil) { [_lock unlock]; [NSException raise: NSInvalidArgumentException format: @"a volatile domain called %@ exists", domainName]; } domain = [domain mutableCopy]; [_persDomains setObject: domain forKey: domainName]; RELEASE(domain); [self __changePersistentDomain: domainName]; [_lock unlock]; } - (BOOL) wantToReadDefaultsSince: (NSDate*)lastSyncDate { NSFileManager *mgr; NSDictionary *attr; if (_fileLock == nil) { return NO; // Database did not exist on startup. } mgr = [NSFileManager defaultManager]; attr = [mgr fileAttributesAtPath: _defaultsDatabase traverseLink: YES]; if (lastSyncDate == nil) { return YES; } else { if (attr == nil) { return YES; } else { NSDate *mod; /* * If the database was modified since the last synchronisation * we need to read it. */ mod = [attr objectForKey: NSFileModificationDate]; if (mod != nil && [lastSyncDate laterDate: mod] != lastSyncDate) { return YES; } } } return NO; } static BOOL isLocked = NO; - (BOOL) lockDefaultsFile: (BOOL*)wasLocked { BOOL firstTime = NO; if (_fileLock == nil) { NSFileManager *mgr; NSString *path; unsigned desired; NSDictionary *attr; BOOL isDir; path = [_defaultsDatabase stringByDeletingLastPathComponent]; mgr = [NSFileManager defaultManager]; #if !(defined(S_IRUSR) && defined(S_IWUSR) && defined(S_IXUSR) \ && defined(S_IRGRP) && defined(S_IXGRP) \ && defined(S_IROTH) && defined(S_IXOTH)) desired = 0755; #else desired = (S_IRUSR|S_IWUSR|S_IXUSR|S_IRGRP|S_IXGRP|S_IROTH|S_IXOTH); #endif attr = [NSDictionary dictionaryWithObjectsAndKeys: NSUserName(), NSFileOwnerAccountName, [NSNumberClass numberWithUnsignedLong: desired], NSFilePosixPermissions, nil]; if ([mgr fileExistsAtPath: path isDirectory: &isDir] == NO) { if ([mgr createDirectoryAtPath: path attributes: attr] == NO) { NSLog(@"Defaults path '%@' does not exist - failed to create it.", path); return NO; } else { NSLog(@"Defaults path '%@' did not exist - created it", path); isDir = YES; } } if (isDir == NO) { NSLog(@"ERROR - Defaults path '%@' is not a directory!", path); return NO; } _fileLock = [[NSDistributedLock alloc] initWithPath: [_defaultsDatabase stringByAppendingPathExtension: @"lck"]]; firstTime = YES; } *wasLocked = isLocked; if (isLocked == NO && _fileLock != nil) { NSDate *started = [NSDateClass date]; while ([_fileLock tryLock] == NO) { CREATE_AUTORELEASE_POOL(arp); NSDate *when; NSDate *lockDate; lockDate = [_fileLock lockDate]; when = [NSDateClass dateWithTimeIntervalSinceNow: 0.1]; /* * In case we have tried and failed to break the lock, * we give up after a while ... 16 seconds should give * us three lock breaks if we do them at 5 second * intervals. */ if ([when timeIntervalSinceDate: started] > 16.0) { NSLog(@"Failed to lock user defaults database even after " @"breaking old locks!"); return NO; } /* * If lockDate is nil, we should be able to lock again ... but we * wait a little anyway ... so that in the case of a locking * problem we do an idle wait rather than a busy one. */ if (lockDate != nil && [when timeIntervalSinceDate: lockDate] > 5.0) { [_fileLock breakLock]; } else { [NSThread sleepUntilDate: when]; } RELEASE(arp); } isLocked = YES; if (firstTime == YES) { NSFileManager *mgr = [NSFileManager defaultManager]; NSDictionary *attr; uint32_t desired; uint32_t attributes; /* * If the lock did not exist ... make sure the databsase exists. */ if ([mgr isReadableFileAtPath: _defaultsDatabase] == NO) { NSDictionary *empty = [NSDictionary new]; /* * Create empty database. */ if ([empty writeToFile: _defaultsDatabase atomically: NO] == NO) { NSLog(@"Failed to create defaults database file %@", _defaultsDatabase); } RELEASE(empty); } attr = [mgr fileAttributesAtPath: _defaultsDatabase traverseLink: YES]; attributes = [attr filePosixPermissions]; #if !(defined(S_IRUSR) && defined(S_IWUSR)) desired = 0600; #else desired = (S_IRUSR|S_IWUSR); #endif if (attributes != desired) { NSMutableDictionary *enforced_attributes; NSNumber *permissions; enforced_attributes = [NSMutableDictionary dictionaryWithDictionary: [mgr fileAttributesAtPath: _defaultsDatabase traverseLink: YES]]; permissions = [NSNumberClass numberWithUnsignedLong: desired]; [enforced_attributes setObject: permissions forKey: NSFilePosixPermissions]; [mgr changeFileAttributes: enforced_attributes atPath: _defaultsDatabase]; } } } return YES; } - (void) unlockDefaultsFile { [_fileLock unlock]; isLocked = NO; } - (NSMutableDictionary*) readDefaults { NSMutableDictionary *newDict = nil; // Read the changes if we have an external database file if (_defaultsDatabase != nil) { NSFileManager *mgr = [NSFileManager defaultManager]; if ([mgr isReadableFileAtPath: _defaultsDatabase] == YES) { newDict = AUTORELEASE([[NSMutableDictionaryClass allocWithZone: [self zone]] initWithContentsOfFile: _defaultsDatabase]); } if (newDict == nil) { newDict = AUTORELEASE([[NSMutableDictionaryClass allocWithZone: [self zone]] initWithCapacity: 10]); } } return newDict; } - (BOOL) writeDefaults: (NSDictionary*)defaults oldData: (NSDictionary*)oldData { // Save the changes if we have an external database file if (_fileLock != nil) { if ([defaults writeToFile: _defaultsDatabase atomically: YES] == NO) { return NO; } } return YES; } /** * Ensures that the in-memory and on-disk representations of the defaults * are in sync. You may call this yourself, but probably don't need to * since it is invoked at intervals whenever a runloop is running.
* If any persistent domain is changed by reading new values from disk, * an NSUserDefaultsDidChangeNotification is posted. */ - (BOOL) synchronize { NSMutableDictionary *newDict; BOOL wasLocked; [_lock lock]; /* * If we haven't changed anything, we only need to synchronise if * the on-disk database has been changed by someone else. */ if (_changedDomains == nil) { if ([self wantToReadDefaultsSince: _lastSync] == NO) { [_lock unlock]; return YES; } } DESTROY(_dictionaryRep); if (self == sharedDefaults) { invalidatedLanguages = YES; } if ([self lockDefaultsFile: &wasLocked] == NO) { return NO; } newDict = [self readDefaults]; if (newDict == nil) { if (wasLocked == NO) { [self unlockDefaultsFile]; } [_lock unlock]; return NO; } if (_changedDomains != nil) { // Synchronize both dictionaries NSEnumerator *enumerator = [_changedDomains objectEnumerator]; NSString *domainName; NSDictionary *domain; NSDictionary *oldData = AUTORELEASE([newDict copy]); DESTROY(_changedDomains); // Retained by enumerator. while ((domainName = [enumerator nextObject]) != nil) { domain = [_persDomains objectForKey: domainName]; if (domain != nil) // Domain was added or changed { [newDict setObject: domain forKey: domainName]; } else // Domain was removed { [newDict removeObjectForKey: domainName]; } } ASSIGN(_persDomains, newDict); if ([self writeDefaults: _persDomains oldData: oldData] == NO) { if (wasLocked == NO) { [self unlockDefaultsFile]; } [_lock unlock]; return NO; } ASSIGN(_lastSync, [NSDateClass date]); } else { ASSIGN(_lastSync, [NSDateClass date]); if ([_persDomains isEqual: newDict] == NO) { ASSIGN(_persDomains, newDict); updateCache(self); [[NSNotificationCenter defaultCenter] postNotificationName: NSUserDefaultsDidChangeNotification object: self]; } } if (wasLocked == NO) { [self unlockDefaultsFile]; } [_lock unlock]; return YES; } /** * Removes the volatile domain specified by domainName from the * user defaults. */ - (void) removeVolatileDomainForName: (NSString*)domainName { [_lock lock]; DESTROY(_dictionaryRep); if (self == sharedDefaults) invalidatedLanguages = YES; [_tempDomains removeObjectForKey: domainName]; [_lock unlock]; } /** * Sets the volatile-domain specified by domainName to * domain ... a dictionary containing keys and defaults values.
* Raises an NSInvalidArgumentException if domainName already * exists as either a volatile-domain or a persistent-domain. */ - (void) setVolatileDomain: (NSDictionary*)domain forName: (NSString*)domainName { id dict; [_lock lock]; dict = [_persDomains objectForKey: domainName]; if (dict != nil) { [_lock unlock]; [NSException raise: NSInvalidArgumentException format: @"a persistent domain called %@ exists", domainName]; } dict = [_tempDomains objectForKey: domainName]; if (dict != nil) { [_lock unlock]; [NSException raise: NSInvalidArgumentException format: @"the volatile domain %@ already exists", domainName]; } DESTROY(_dictionaryRep); if (self == sharedDefaults) invalidatedLanguages = YES; domain = [domain mutableCopy]; [_tempDomains setObject: domain forKey: domainName]; RELEASE(domain); [_lock unlock]; } /** * Returns the volatile domain specified by domainName. */ - (NSDictionary*) volatileDomainForName: (NSString*)domainName { NSDictionary *copy; [_lock lock]; copy = [[_tempDomains objectForKey: domainName] copy]; [_lock unlock]; return AUTORELEASE(copy); } /** * Returns an array listing the name of all the volatile domains. */ - (NSArray*) volatileDomainNames { NSArray *keys; [_lock lock]; keys = [_tempDomains allKeys]; [_lock unlock]; return keys; } /** * Returns a dictionary representing the current state of the defaults * system ... this is a merged version of all the domains in the * search list. */ - (NSDictionary*) dictionaryRepresentation { NSDictionary *rep; [_lock lock]; if (_dictionaryRep == nil) { NSEnumerator *enumerator; NSMutableDictionary *dictRep; id obj; id dict; IMP nImp; IMP pImp; IMP tImp; IMP addImp; pImp = [_persDomains methodForSelector: objectForKeySel]; tImp = [_tempDomains methodForSelector: objectForKeySel]; enumerator = [_searchList reverseObjectEnumerator]; nImp = [enumerator methodForSelector: nextObjectSel]; dictRep = [NSMutableDictionaryClass allocWithZone: NSDefaultMallocZone()]; dictRep = [dictRep initWithCapacity: 512]; addImp = [dictRep methodForSelector: addSel]; while ((obj = (*nImp)(enumerator, nextObjectSel)) != nil) { if ((dict = (*pImp)(_persDomains, objectForKeySel, obj)) != nil || (dict = (*tImp)(_tempDomains, objectForKeySel, obj)) != nil) (*addImp)(dictRep, addSel, dict); } _dictionaryRep = [dictRep copy]; RELEASE(dictRep); } rep = RETAIN(_dictionaryRep); [_lock unlock]; return AUTORELEASE(rep); } /** * Merges the contents of the dictionary newVals into the registration * domain. Registration defaults may be added to or replaced using this * method, but may never be removed. Thus, setting registration defaults * at any point in your program guarantees that the defaults will be * available thereafter. */ - (void) registerDefaults: (NSDictionary*)newVals { NSMutableDictionary *regDefs; [_lock lock]; regDefs = [_tempDomains objectForKey: NSRegistrationDomain]; if (regDefs == nil) { regDefs = [NSMutableDictionaryClass dictionaryWithCapacity: [newVals count]]; [_tempDomains setObject: regDefs forKey: NSRegistrationDomain]; } DESTROY(_dictionaryRep); if (self == sharedDefaults) invalidatedLanguages = YES; [regDefs addEntriesFromDictionary: newVals]; [_lock unlock]; } /** * Removes the named domain from the search list of the receiver.
* Suites may be added using the -addSuiteNamed: method. */ - (void) removeSuiteNamed: (NSString*)aName { if (aName == nil) { [NSException raise: NSInvalidArgumentException format: @"attempt to remove suite with nil name"]; } [_lock lock]; DESTROY(_dictionaryRep); if (self == sharedDefaults) invalidatedLanguages = YES; [_searchList removeObject: aName]; [_lock unlock]; } /************************************************************************* *** Accessing the User Defaults database *************************************************************************/ - (NSDictionary*) __createArgumentDictionary { NSArray *args; NSEnumerator *enumerator; NSMutableDictionary *argDict; BOOL done; id key, val; [_lock lock]; args = [[NSProcessInfo processInfo] arguments]; enumerator = [args objectEnumerator]; argDict = [NSMutableDictionaryClass dictionaryWithCapacity: 2]; [enumerator nextObject]; // Skip process name. done = ((key = [enumerator nextObject]) == nil) ? YES : NO; while (done == NO) { if ([key hasPrefix: @"-"] == YES && [key isEqual: @"-"] == NO) { NSString *old = nil; /* anything beginning with a '-' is a defaults key and we must strip the '-' from it. As a special case, we leave the '- in place for '-GS...' and '--GS...' for backward compatibility. */ if ([key hasPrefix: @"-GS"] == YES || [key hasPrefix: @"--GS"] == YES) { old = key; } key = [key substringFromIndex: 1]; val = [enumerator nextObject]; if (val == nil) { // No more args [argDict setObject: @"" forKey: key]; // arg is empty. if (old != nil) { [argDict setObject: @"" forKey: old]; } done = YES; continue; } else if ([val hasPrefix: @"-"] == YES && [val isEqual: @"-"] == NO) { // Yet another argument [argDict setObject: @"" forKey: key]; // arg is empty. if (old != nil) { [argDict setObject: @"" forKey: old]; } key = val; continue; } else { // Real parameter /* Parsing the argument as a property list is very delicate. We *MUST NOT* crash here just because a strange parameter (such as `(load "test.scm")`) is passed, otherwise the whole library is useless in a foreign environment. */ NSObject *plist_val; NS_DURING { plist_val = [val propertyList]; } NS_HANDLER { plist_val = val; } NS_ENDHANDLER /* Make sure we don't crash being caught adding nil to a dictionary. */ if (plist_val == nil) { plist_val = val; } [argDict setObject: plist_val forKey: key]; if (old != nil) { [argDict setObject: plist_val forKey: old]; } } } done = ((key = [enumerator nextObject]) == nil); } [_lock unlock]; return argDict; } - (void) __changePersistentDomain: (NSString*)domainName { [_lock lock]; DESTROY(_dictionaryRep); if (self == sharedDefaults) invalidatedLanguages = YES; if (_changedDomains == nil) { _changedDomains = [[NSMutableArray alloc] initWithObjects: &domainName count: 1]; updateCache(self); [[NSNotificationCenter defaultCenter] postNotificationName: NSUserDefaultsDidChangeNotification object: self]; } else if ([_changedDomains containsObject: domainName] == NO) { [_changedDomains addObject: domainName]; } [_lock unlock]; } @end /* * Get one of several potentially useful flags. */ BOOL GSUserDefaultsFlag(GSUserDefaultFlagType type) { if (sharedDefaults == nil) { [NSUserDefaults standardUserDefaults]; } return flags[type]; }