Core/Source/GrowlPluginController.m
author Peter Hosey <hg@boredzo.org>
Thu Sep 24 09:24:33 2009 -0700 (2009-09-24)
changeset 4425 18f28b64e39d
parent 4423 051a29104de7
child 4435 ed4dae2a04fd
permissions -rw-r--r--
Rewrote the error message text for the this-plug-in-won't-work error to be more understandable for less-experienced users.
     1 //
     2 //  GrowlPluginController.m
     3 //  Growl
     4 //
     5 //  Created by Nelson Elhage on 8/25/04.
     6 //  Copyright 2004-2006 The Growl Project. All rights reserved.
     7 //
     8 // This file is under the BSD License, refer to License.txt for details
     9 
    10 #import "GrowlPluginController.h"
    11 #import "GrowlPreferencesController.h"
    12 #import "GrowlDisplayPlugin.h"
    13 #import "GrowlDefinesInternal.h"
    14 #include "CFDictionaryAdditions.h"
    15 #include "CFMutableDictionaryAdditions.h"
    16 
    17 #import "GrowlPathUtilities.h"
    18 #import "GrowlNonCopyingMutableDictionary.h"
    19 
    20 #import "NSSetAdditions.h"
    21 #import "NSWorkspaceAdditions.h"
    22 #import "GrowlWebKitPluginHandler.h"
    23 
    24 @interface GrowlPluginController (PRIVATE)
    25 - (void) registerDefaultPluginHandlers;
    26 - (void) findPluginsInDirectory:(NSString *)dir;
    27 - (void) pluginInstalledSelector:(NSWindow *)sheet returnCode:(int)returnCode contextInfo:(void *)contextInfo;
    28 - (void) pluginExistsSelector:(NSWindow *)sheet returnCode:(int)returnCode contextInfo:(void *)contextInfo;
    29 @end
    30 
    31 @interface WebCoreCache
    32 + (void) empty;
    33 @end
    34 
    35 //for use as CFSetCallBacks.equal
    36 static Boolean caseInsensitiveStringComparator(const void *value1, const void *value2);
    37 //for use as CFSetCallBacks.hash
    38 static CFHashCode passthroughStringHash(const void *value);
    39 //for use on array of matching plug-in handlers in -openPluginAtPath:
    40 NSInteger comparePluginHandlerRegistrationOrder(id a, id b, void *context);
    41 
    42 #pragma mark -
    43 
    44 NSString *GrowlPluginControllerWillAddPluginHandlerNotification = @"GrowlPluginControllerWillAddPluginHandlerNotification";
    45 NSString *GrowlPluginControllerDidAddPluginHandlerNotification = @"GrowlPluginControllerDidAddPluginHandlerNotification";
    46 NSString *GrowlPluginControllerWillRemovePluginHandlerNotification = @"GrowlPluginControllerWillRemovePluginHandlerNotification";
    47 NSString *GrowlPluginControllerDidRemovePluginHandlerNotification = @"GrowlPluginControllerDidRemovePluginHandlerNotification";
    48 
    49 //Info.plist keys for plug-in bundles.
    50 NSString *GrowlPluginInfoKeyName              = @"CFBundleName";
    51 NSString *GrowlPluginInfoKeyAuthor            = @"GrowlPluginAuthor";
    52 NSString *GrowlPluginInfoKeyVersion           = @"CFBundleVersion";
    53 NSString *GrowlPluginInfoKeyDescription       = @"GrowlPluginDescription";
    54 //keys in plug-in description dictionaries (also includes the above).
    55 NSString *GrowlPluginInfoKeyBundle            = @"GrowlPluginBundle";
    56 NSString *GrowlPluginInfoKeyTypes             = @"GrowlPluginType";
    57 NSString *GrowlPluginInfoKeyPath              = @"GrowlPluginPath";
    58 NSString *GrowlPluginInfoKeyHumanReadableName = @"GrowlPluginHumanReadableName";
    59 NSString *GrowlPluginInfoKeyIdentifier        = @"GrowlPluginIdentifier";
    60 NSString *GrowlPluginInfoKeyInstance          = @"GrowlPluginInstance";
    61 
    62 /*******************************************************************************
    63  *  _____ ___  ____   ___
    64  * |_   _/ _ \|  _ \ / _ \
    65  *   | || | | | | | | | | |
    66  *   | || |_| | |_| | |_| |
    67  *   |_| \___/|____/ \___/
    68  *
    69  *******************************************************************************
    70  *
    71  * 	-	Use identifier strings for all loaded plug-ins (simpler than using
    72  *		human-readable names) (DONE though this will probably only be used for
    73  *		storage of plug-in prefs)
    74  *	-	Use plug-in dictionaries (DONE)
    75  *	-	Use GrowlNonCopyingMutableDictionary instead of NSMapTable (DONE)
    76  *	-	Write the built-in plug-in handler (DONE)
    77  *	-	Add a WebKit plug-in handler (jkp)
    78  *	-	Better localize human-readable names
    79  */
    80 
    81 @implementation GrowlPluginController
    82 
    83 + (GrowlPluginController *) sharedController {
    84 	return [self sharedInstance];
    85 }
    86 
    87 - (id) initSingleton {
    88 	if ((self = [super initSingleton])) {
    89 		bundlesToLazilyInstantiateAnInstanceFrom = [[NSMutableSet alloc] init];
    90 
    91 		pluginsByIdentifier         = [[NSMutableDictionary alloc] init];
    92 		pluginIdentifiersByPath     = [[NSMutableDictionary alloc] init];
    93 		pluginIdentifiersByBundle   = [[GrowlNonCopyingMutableDictionary alloc] init];
    94 		pluginIdentifiersByInstance = [[GrowlNonCopyingMutableDictionary alloc] init];
    95 
    96 		pluginsByName     = [[NSMutableDictionary alloc] init];
    97 		pluginsByAuthor   = [[NSMutableDictionary alloc] init];
    98 		pluginsByVersion  = [[NSMutableDictionary alloc] init];
    99 		pluginsByFilename = [[NSMutableDictionary alloc] init];
   100 		pluginsByType     = [[NSMutableDictionary alloc] init];
   101 		pluginHumanReadableNames = [[NSCountedSet alloc] init];
   102 
   103 		loadedBundleIdentifiers = [[NSMutableSet alloc] init];
   104 		
   105 		allPluginHandlers = [[NSMutableArray alloc] init];
   106 		pluginHandlers  = [[NSMutableDictionary alloc] init];
   107 		handlersForPlugins = [[GrowlNonCopyingMutableDictionary alloc] init];
   108 
   109 		displayPlugins = [[NSMutableArray alloc] init];
   110 		disabledPlugins = [[NSMutableArray alloc] init];
   111 
   112 		[self registerDefaultPluginHandlers];
   113 
   114 		enum { builtInTypesCount = 4U };
   115 		NSString *builtInTypesArray[builtInTypesCount] = {
   116 			GROWL_STYLE_EXTENSION,
   117 			GROWL_VIEW_EXTENSION,
   118 			GROWL_PATHWAY_EXTENSION,
   119 			GROWL_PLUGIN_EXTENSION,
   120 		};
   121 		CFSetCallBacks callbacks = kCFCopyStringSetCallBacks;
   122 		callbacks.equal = caseInsensitiveStringComparator;
   123 		callbacks.hash = passthroughStringHash;
   124 		builtInTypes = (NSSet *)CFSetCreate(kCFAllocatorDefault,
   125 		                                    (const void **)builtInTypesArray,
   126 		                                    builtInTypesCount,
   127 		                                    &callbacks);
   128 
   129 		//Find plugins inside GHA itself first
   130 		[self findPluginsInDirectory:[[GrowlPathUtilities helperAppBundle] builtInPlugInsPath]];
   131 		
   132 		/* Then find plug-ins in Library/Application Support/Growl/Plugins directories. This allows GHA to override externally installed plugins,
   133 		 * which are fairly common as some 3rd party plugins have been rolled into the Growl distribution.
   134 		 */
   135 		NSArray *libraries = NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSAllDomainsMask, YES);
   136 		NSEnumerator *enumerator = [libraries objectEnumerator];
   137 		NSString *dir;
   138 		while ((dir = [enumerator nextObject])) {
   139 			dir = [dir stringByAppendingPathComponent:@"Application Support/Growl/Plugins"];
   140 			[self findPluginsInDirectory:dir];
   141 		}		
   142 	}
   143 
   144 	return self;
   145 }
   146 
   147 - (void) destroy {
   148 	[pluginsByIdentifier         release];
   149 	[pluginIdentifiersByPath     release];
   150 	[pluginIdentifiersByBundle   release];
   151 	[pluginIdentifiersByInstance release];
   152 
   153 	[pluginsByName     release];
   154 	[pluginsByAuthor   release];
   155 	[pluginsByVersion  release];
   156 	[pluginsByFilename release];
   157 	[pluginsByType     release];
   158 	[pluginHumanReadableNames release];
   159 
   160 	[loadedBundleIdentifiers release];
   161 
   162 	[bundlesToLazilyInstantiateAnInstanceFrom release];
   163 	[displayPlugins release];
   164 	[disabledPlugins release];
   165 
   166 	[allPluginHandlers release];
   167 	[pluginHandlers  release];
   168 	[handlersForPlugins release];
   169 
   170 	[builtInTypes release];
   171 
   172 	[cache_allPlugins release];
   173 	[cache_allPluginsArray release];
   174 	[cache_registeredPluginTypes release];
   175 	[cache_registeredPluginNames release];
   176 	[cache_registeredPluginNamesArray release];
   177 	[cache_allPluginInstances release];
   178 	[cache_displayPlugins release];
   179 
   180 	[super destroy];
   181 }
   182 
   183 #pragma mark -
   184 
   185 - (void) registerDefaultPluginHandlers {
   186 	//register ourselves for display plug-ins (non-WebKit), pathway plug-ins, and functional plug-ins.
   187 	NSSet *types = [[NSSet alloc] initWithObjects:
   188 		//display plug-ins
   189 		GROWL_VIEW_EXTENSION,
   190 		NSFileTypeForHFSTypeCode(FOUR_CHAR_CODE('DISP')),
   191 		//pathway plug-ins
   192 		GROWL_PATHWAY_EXTENSION,
   193 		NSFileTypeForHFSTypeCode(FOUR_CHAR_CODE('PWAY')),
   194 		//generic functional plug-ins
   195 		GROWL_PLUGIN_EXTENSION,
   196 		NSFileTypeForHFSTypeCode(FOUR_CHAR_CODE('GEXT')),
   197 		nil];
   198 
   199 	[self addPluginHandler:self forPluginTypes:types];
   200 
   201 	[types release];
   202 
   203 	[GrowlWebKitPluginHandler sharedInstance];		// Calling this here will cause the handler to register
   204 }
   205 
   206 #pragma mark -
   207 #pragma mark GrowlPluginHandler protocol conformance
   208 
   209 //the method that dispatches incoming plug-ins to plug-in handlers is -dispatchPluginAtPath:.
   210 //this is for handling plug-ins of the built-in types.
   211 - (BOOL)loadPluginWithBundle:(NSBundle *)bundle {
   212 	[self addPluginInstance:nil fromBundle:bundle];
   213 
   214 	return YES;
   215 }
   216 
   217 #pragma mark -
   218 #pragma mark Plugin-handler handling
   219 
   220 - (void) addPluginHandler:(id <GrowlPluginHandler>)handler forPluginTypes:(NSSet *)types {
   221 	NSParameterAssert(handler != nil);
   222 	NSParameterAssert(types != nil);
   223 
   224 	if (![types count]) {
   225 		NSLog(@"Warning: -[%@ addPluginHandler:forPluginTypes:] called with an empty set of file types. This may be indicative of a bug in Growl or a plug-in. The handler was %@.", [self class], handler);
   226 	} else {
   227 		//make sure nobody tries to register a plug-in handler for a built-in type.
   228 		if (builtInTypes) {
   229 			NSMutableSet *builtInMutable = [builtInTypes mutableCopy];
   230 			[builtInMutable intersectSet:types];
   231 
   232 			//the intersection must be empty; if it isn't, at least one of the types for which this handler is attempting to register is a built-in type.
   233 			NSAssert2([builtInMutable count] == 0U, @"Something attempted to register a plug-in handler for one or more reserved file types (%@). The handler was %@.", builtInMutable, handler);
   234 
   235 			[builtInMutable release];
   236 		}
   237 
   238 		NSEnumerator *typeEnum = [types objectEnumerator];
   239 		NSString *type;
   240 		while ((type = [typeEnum nextObject])) {
   241 			//normalise: strip leading ., if any.
   242 			NSUInteger i = 0U, len = [type length];
   243 			for (; i < len; ++i) {
   244 				if ([type characterAtIndex:i] != '.')
   245 					break;
   246 			}
   247 			NSAssert2(i < len, @"Something tried to register a plug-in handler for a file type consisting entirely of %u full stops ('.'). The handler was %@.", len, handler);
   248 			if (i)
   249 				type = [type substringFromIndex:i];
   250 
   251 			NSMutableArray *handlers = [pluginHandlers objectForKey:type];
   252 			if (!handlers) {
   253 				handlers = [[NSMutableArray alloc] initWithCapacity:1U];
   254 				[pluginHandlers setObject:handlers forKey:type];
   255 				[handlers release];
   256 			}
   257 			[handlers addObject:handler];
   258 		}
   259 
   260 		[allPluginHandlers addObject:handler];
   261 
   262 		NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
   263 		NSDictionary *notificationUserInfo = [[[NSDictionary alloc] initWithObjectsAndKeys:
   264 			handler, @"GrowlPluginHandler",
   265 			nil] autorelease];
   266 
   267 		[nc postNotificationName:GrowlPluginControllerWillAddPluginHandlerNotification
   268 						  object:self
   269 						userInfo:notificationUserInfo];
   270 
   271 		[allPluginHandlers addObject:handler];
   272 
   273 		//add the handler as an observer for various notifications.
   274 		if ([handler respondsToSelector:@selector(growlPluginControllerWillAddPluginHandler:)]) {
   275 			[nc addObserver:handler
   276 				   selector:@selector(growlPluginControllerWillAddPluginHandler:)
   277 					   name:GrowlPluginControllerWillAddPluginHandlerNotification
   278 					 object:self];
   279 		}
   280 		if ([handler respondsToSelector:@selector(growlPluginControllerDidAddPluginHandler:)]) {
   281 			[nc addObserver:handler
   282 				   selector:@selector(growlPluginControllerDidAddPluginHandler:)
   283 					   name:GrowlPluginControllerDidAddPluginHandlerNotification
   284 					 object:self];
   285 		}
   286 		if ([handler respondsToSelector:@selector(growlPluginControllerWillRemovePluginHandler:)]) {
   287 			[nc addObserver:handler
   288 				   selector:@selector(growlPluginControllerWillRemovePluginHandler:)
   289 					   name:GrowlPluginControllerWillRemovePluginHandlerNotification
   290 					 object:self];
   291 		}
   292 		if ([handler respondsToSelector:@selector(growlPluginControllerDidRemovePluginHandler:)]) {
   293 			[nc addObserver:handler
   294 				   selector:@selector(growlPluginControllerDidRemovePluginHandler:)
   295 					   name:GrowlPluginControllerDidRemovePluginHandlerNotification
   296 					 object:self];
   297 		}
   298 
   299 		[nc postNotificationName:GrowlPluginControllerDidAddPluginHandlerNotification
   300 						  object:self
   301 						userInfo:notificationUserInfo];
   302 	}
   303 }
   304 
   305 - (void) removePluginHandler:(id <GrowlPluginHandler>)handler forPluginTypes:(NSSet *)extensions {
   306 	NSParameterAssert(handler != nil);
   307 
   308 	[allPluginHandlers removeObjectIdenticalTo:handler];
   309 
   310 	if (!extensions)
   311 		extensions = (NSSet *)[pluginHandlers allKeysForObject:handler];
   312 
   313 	NSEnumerator *extEnum = [extensions objectEnumerator];
   314 	NSString *ext;
   315 	while ((ext = [extEnum nextObject])) {
   316 		NSMutableArray *handlers = [pluginHandlers objectForKey:ext];
   317 		if (handlers) {
   318 			NSUInteger idx = [handlers indexOfObjectIdenticalTo:handler];
   319 			if (idx != NSNotFound)
   320 				[handlers removeObjectAtIndex:idx];
   321 		}
   322 	}
   323 }
   324 
   325 - (NSArray *) allPluginHandlers {
   326 	return [[allPluginHandlers copy] autorelease];
   327 }
   328 
   329 #pragma mark -
   330 
   331 //private method.
   332 - (NSDictionary *) addPluginInstance:(GrowlPlugin *)plugin fromPath:(NSString *)path bundle:(NSBundle *)bundle {
   333 	//If we're passed a bundle, refuse to load it if we've already loaded a bundle with the same identifier, instead returning early.
   334 	NSString *bundleIdentifier = (bundle ? [bundle objectForInfoDictionaryKey:(NSString *)kCFBundleIdentifierKey] : nil);
   335 	if (bundleIdentifier && [loadedBundleIdentifiers containsObject:[bundle objectForInfoDictionaryKey:(NSString *)kCFBundleIdentifierKey]]) {
   336 		return nil;
   337 	}
   338 	
   339 	//Look up the identifier for the plugin. We try to look up the identifier by the instance, by the bundle; and by the pathname, in that order.
   340 	NSString *identifier = nil;
   341 	if (plugin)
   342 		identifier = [pluginIdentifiersByInstance objectForKey:plugin];
   343 	else if (bundle)
   344 		identifier = [pluginIdentifiersByBundle   objectForKey:bundle];
   345 	else if (path)
   346 		identifier = [pluginIdentifiersByPath     objectForKey:path];
   347 
   348 	/* If we have an identifier, look up the plug-in dictionary.
   349 	 * If we have a plug-in dictionary but no instance (the identifier was retrieved by bundle or by path), attempt to retrieve the instance from the dictionary.
   350 	 */
   351 	NSMutableDictionary *pluginDict = identifier ? [pluginsByIdentifier objectForKey:identifier] : nil;
   352 	if (pluginDict && !plugin)
   353 		plugin = [pluginDict pluginInstance];
   354 
   355 	//Assert that we have an instance OR a bundle. We need at least one to proceed.
   356 	NSAssert1(plugin || bundle, @"Cannot load plug-ins lazily without a bundle (path: %@)", path);
   357 
   358 	//Get the plug-in's name, author, and version. All three come from the plug-in instance if it exists and responds to -name/-author/-version; if both requirements are not satisfied, the information is retrieved from the bundle's Info.plist.
   359 	NSString *name    = plugin ? ([plugin respondsToSelector:@selector(name)] ? [plugin name] : [bundle objectForInfoDictionaryKey:(NSString *)kCFBundleNameKey]) 
   360 								: [bundle objectForInfoDictionaryKey:(NSString *)kCFBundleNameKey];
   361 	NSString *author  = plugin ? ([plugin respondsToSelector:@selector(author)] ? [plugin author] : [bundle objectForInfoDictionaryKey:GrowlPluginInfoKeyAuthor])
   362 							    : [bundle objectForInfoDictionaryKey:GrowlPluginInfoKeyAuthor];
   363 	NSString *version = plugin ? ([plugin respondsToSelector:@selector(version)] ? [plugin version] : [bundle objectForInfoDictionaryKey:(NSString *)kCFBundleVersionKey])
   364 							    : [bundle objectForInfoDictionaryKey:(NSString *)kCFBundleVersionKey];
   365 
   366 	//If we don't have a pathname, get it as the bundle's pathname.
   367 	if (!path)
   368 		path = [bundle bundlePath];
   369 	NSString *extension = [path pathExtension];
   370 	NSString *fileType = nil;
   371 
   372 	//Assert that we have a name, author, and version. (We got the path first so we can use it in the assertion message.)
   373 	NSAssert5((name != nil) && (author != nil) && (version != nil),
   374 			  @"Cannot load plug-in at path %@ (plug-in instance's class: %@). One of these is (null), but they must all not be:\n"
   375 			  @"\t"@"   name: %@\n"
   376 			  @"\t"@" author: %@\n"
   377 			  @"\t"@"version: %@\n",
   378 			  path, [plugin class], name, author, version);
   379 
   380 	//In case we got the names from the plug-in instance and it gave us a mutable string for some reason, make copies for ourselves.
   381 	//Note: This isn't a performance hit when the strings are immutable. -copy = -retain in that situation. Thanks, Apple!
   382 	name    = [name    copy];
   383 	author  = [author  copy];
   384 	version = [version copy];
   385 
   386 	//If we don't have an identifier yet, forge it.
   387 	if (!identifier)
   388 		identifier = [NSString stringWithFormat:@"Name: %@ Author: %@ Path: %@", name, author, path];
   389 
   390 	//If we don't have an instance but we do have a bundle, see if we've previously queued the bundle for lazy instantiation.
   391 	if (!plugin && bundle) {
   392 		if (![bundlesToLazilyInstantiateAnInstanceFrom containsObject:bundle]) {
   393 			//We haven't previously queued it: Queue it.
   394 			[bundlesToLazilyInstantiateAnInstanceFrom addObject:bundle];
   395 		} else {
   396 			//We have: This is our cue to instantiate it.
   397 			plugin = [[[bundle principalClass] alloc] init];
   398 			//Dequeue it, because we don't want to hit this branch again for this plug-in.
   399 			[bundlesToLazilyInstantiateAnInstanceFrom removeObject:bundle];
   400 			//Stash the plug-in instance in the plug-in dictionary. This retains the instance and means that we'll never hit the lazy-instantiation machinery again (because plugin will be non-nil).
   401 			[pluginDict setObject:plugin forKey:GrowlPluginInfoKeyInstance];
   402 		}
   403 	}
   404 
   405 	if (!plugin && bundle) {
   406 		//*Still* no plug-in! Again we check whether it's queued for instantiation (bug?).
   407 		if (![bundlesToLazilyInstantiateAnInstanceFrom containsObject:bundle])
   408 			[bundlesToLazilyInstantiateAnInstanceFrom addObject:bundle];
   409 		else {
   410 			//Apparently it is. Instantiate it, but don't stash the plug-in instance in the plug-in dictionary (why not?).
   411 			plugin = [[[bundle principalClass] alloc] init];
   412 			[bundlesToLazilyInstantiateAnInstanceFrom removeObject:bundle];
   413 		}
   414 	}
   415 
   416 	/*If we don't actually have a plug-in dictionary, create it.
   417 	 *Elements of a plug-in dictionary:
   418 	 *	Plug-in name
   419 	 *	Author
   420 	 *	Version
   421 	 *	Pathname
   422 	 *	Identifier
   423 	 *	Instance (later; see above lazy-instantiation code)
   424 	 *	Plug-in's HFS type and filename extension (combined in a set)
   425 	 */
   426 	BOOL pluginDictIsNew = !pluginDict;
   427 	if (!pluginDict) {
   428 		pluginDict = [NSMutableDictionary dictionaryWithObjectsAndKeys:
   429 			name,                 GrowlPluginInfoKeyName,
   430 			author,               GrowlPluginInfoKeyAuthor,
   431 			version,              GrowlPluginInfoKeyVersion,
   432 			path,                 GrowlPluginInfoKeyPath,
   433 			identifier,           GrowlPluginInfoKeyIdentifier,
   434 			nil];
   435 		NSString *description = ([plugin respondsToSelector:@selector(pluginDescription)] ? [plugin pluginDescription] : nil);
   436 		
   437 		if (description)
   438 			[pluginDict setObject:description forKey:GrowlPluginInfoKeyDescription];
   439 
   440 		[[NSWorkspace sharedWorkspace] getFileType:&fileType creatorCode:NULL forFile:path];
   441 
   442 		//Record the file types (HFS and filename extension) that the plug-in possessed at this time. These help determine what kind of plug-in it is (e.g. .growlView = custom view; .growlStyle = WebKit display).
   443 		NSSet *types = nil;
   444 		if (extension) {
   445 #warning problem here...
   446 			///XXX when there is no file type it is coming back as \'\'...im guessing this means no type, but it still tests as true so each plugin is registered against that type...wrong???
   447 			if (fileType)
   448 				types = [NSSet setWithObjects:extension, fileType, nil];
   449 			else
   450 				types = [NSSet setWithObject:extension];
   451 		} else if (fileType)
   452 			types = [NSSet setWithObject:fileType];
   453 
   454 		if (types)
   455 			[pluginDict setObject:types forKey:GrowlPluginInfoKeyTypes];
   456 	}
   457 
   458 	//We have a bundle. If no previous bundle was stored in the plug-in dictionary (why wouldn't there be?), store this bundle there. Also register the identifier as being the one for this bundle.
   459 	if (bundle) {
   460 		if (![pluginDict objectForKey:GrowlPluginInfoKeyBundle])
   461 			[pluginDict setObject:bundle forKey:GrowlPluginInfoKeyBundle];
   462 		[pluginIdentifiersByBundle setObject:identifier forKey:bundle];
   463 	}
   464 	//We have an instance. If no previous instance was stored in the plug-in dictionary (why wouldn't there be?), store this instance there. Also register the identifier as being the one for this instance.
   465 	if (plugin) {
   466 		if (![pluginDict objectForKey:GrowlPluginInfoKeyInstance])
   467 			[pluginDict setObject:plugin forKey:GrowlPluginInfoKeyInstance];
   468 		[pluginIdentifiersByInstance setObject:identifier forKey:plugin];
   469 	}
   470 
   471 	//If we just created the dictionary (and got done filling it out), start storing it in places.
   472 	if (pluginDictIsNew) {
   473 		[pluginsByIdentifier setObject:pluginDict forKey:identifier];
   474 		[pluginIdentifiersByPath setObject:identifier forKey:path];
   475 
   476 	#define ADD_TO_DICT(dictName, key, value)                                          \
   477 			do {                                                                        \
   478 				NSMutableSet *plugins = [dictName objectForKey:key];                     \
   479 				if (plugins)                                                              \
   480 					[plugins addObject:value];                                             \
   481 				else                                                                        \
   482 					[dictName setObject:[NSMutableSet setWithObject:value] forKey:key];      \
   483 			} while(0)
   484 		ADD_TO_DICT(pluginsByName,     name,                     pluginDict);
   485 		ADD_TO_DICT(pluginsByAuthor,   author,                   pluginDict);
   486 		ADD_TO_DICT(pluginsByVersion,  version,                  pluginDict);
   487 		ADD_TO_DICT(pluginsByFilename, [path lastPathComponent], pluginDict);
   488 
   489 		ADD_TO_DICT(pluginsByType, extension, pluginDict);
   490 		ADD_TO_DICT(pluginsByType, fileType,  pluginDict);
   491 	#undef ADD_TO_DICT
   492 	}
   493 
   494 	//Release our copies.
   495 	[name    release];
   496 	[author  release];
   497 	[version release];
   498 
   499 	//Invalidate non-display plug-in caches.
   500 	[cache_allPlugins release];
   501 	 cache_allPlugins = nil;
   502 	[cache_allPluginsArray release];
   503 	 cache_allPluginsArray = nil;
   504 	[cache_registeredPluginTypes release];
   505 	 cache_registeredPluginTypes = nil;
   506 	[cache_registeredPluginNames release];
   507 	 cache_registeredPluginNames = nil;
   508 	[cache_registeredPluginNamesArray release];
   509 	 cache_registeredPluginNamesArray = nil;
   510 
   511 	//Special handling if this plug-in is a display.
   512 	if ([self pluginWithDictionaryIsDisplayPlugin:pluginDict]) {
   513 		//If it doesn't respond to -requiresPositioning, it's old. Add it as a disabled plug-in.
   514 		if(![[pluginDict valueForKey:GrowlPluginInfoKeyInstance] respondsToSelector:@selector(requiresPositioning)]) {
   515 			[disabledPlugins addObject:[pluginDict valueForKey:GrowlPluginInfoKeyName]];
   516 		} 
   517 		else {
   518 			//It responds to -requiresPositioning, so add it as a(n enabled) display plug-in.
   519 			[displayPlugins addObject:pluginDict];
   520 		}
   521 		
   522 		//Invalidate display plug-in cache.
   523 		[cache_displayPlugins release];
   524 		 cache_displayPlugins = nil;
   525 	}
   526 
   527 	//Store the bundle identifier so we know we've loaded it.
   528 	if (bundleIdentifier) {
   529 		[loadedBundleIdentifiers addObject:bundleIdentifier];
   530 	}
   531 
   532 	return pluginDict;
   533 }
   534 
   535 - (NSArray *) disabledPlugins {
   536 	return disabledPlugins;
   537 }
   538 
   539 - (BOOL) disabledPluginsPresent {
   540 	return ([disabledPlugins count] > 0);
   541 }
   542 
   543 - (NSDictionary *) addPluginInstance:(GrowlPlugin *)plugin fromBundle:(NSBundle *)bundle {
   544 	return [self addPluginInstance:plugin fromPath:nil bundle:bundle];
   545 }
   546 - (NSDictionary *) addPluginInstance:(GrowlPlugin *)plugin fromPath:(NSString *)path {
   547 	return [self addPluginInstance:plugin fromPath:path bundle:nil];
   548 }
   549 
   550 #pragma mark -
   551 
   552 - (NSSet *) registeredPluginTypes {
   553 	if (!cache_registeredPluginTypes)
   554 		cache_registeredPluginTypes = [[NSSet alloc] initWithArray:[pluginHandlers allKeys]];
   555 
   556 	return cache_registeredPluginTypes;
   557 }
   558 
   559 - (NSSet *) registeredPluginNames {
   560 	if (!cache_registeredPluginNames)
   561 		cache_registeredPluginNames = [[NSSet alloc] initWithArray:[self registeredPluginNamesArray]];
   562 	return cache_registeredPluginNames;
   563 }
   564 
   565 - (NSArray *) registeredPluginNamesArray {
   566 	if (!cache_registeredPluginNamesArray) {
   567 		cache_registeredPluginNamesArray = [[[pluginsByIdentifier allValues] valueForKey:GrowlPluginInfoKeyName] retain];
   568 	}
   569 	return cache_registeredPluginNamesArray;
   570 }
   571 
   572 - (NSArray *) registeredPluginNamesArrayForType:(NSString *)type {
   573 #warning this should be cached per type
   574 	return [[[pluginsByType valueForKey:type] allObjects] valueForKey:GrowlPluginInfoKeyName];
   575 }
   576 
   577 
   578 #pragma mark -
   579 
   580 - (NSArray *) allPluginDictionariesArray {
   581 	if (!cache_allPluginsArray)
   582 		cache_allPluginsArray = [[pluginsByIdentifier allValues] copy];
   583 	return cache_allPluginsArray;
   584 }
   585 - (NSSet *) allPluginDictionaries {
   586 	if (!cache_allPlugins)
   587 		cache_allPlugins = [[NSMutableSet alloc] initWithArray:[self allPluginDictionariesArray]];
   588 	return cache_allPlugins;
   589 }
   590 - (NSArray *) allPluginInstances {
   591 	if (!cache_allPluginInstances)
   592 		cache_allPluginInstances = [[[self allPluginDictionaries] valueForKey:GrowlPluginInfoKeyInstance] retain];
   593 	return cache_allPluginInstances;
   594 }
   595 
   596 - (NSSet *) pluginDictionariesWithName:(NSString *)name author:(NSString *)author version:(NSString *)version type:(NSString *)type {
   597 	NSMutableSet *matches = [[[self allPluginDictionaries] mutableCopy] autorelease];
   598 
   599 	if ([matches count]) {
   600 		if (name)
   601 			[matches intersectSet:[pluginsByName objectForKey:name]];
   602 		if (author)
   603 			[matches intersectSet:[pluginsByAuthor objectForKey:author]];
   604 		if (version)
   605 			[matches intersectSet:[pluginsByVersion objectForKey:version]];
   606 		if (type)
   607 			[matches intersectSet:[pluginsByType objectForKey:type]];
   608 	}
   609 
   610 	return matches;
   611 }
   612 - (NSDictionary *) pluginDictionaryWithName:(NSString *)name author:(NSString *)author version:(NSString *)version type:(NSString *)type {
   613 	NSSet *matches = [self pluginDictionariesWithName:name author:author version:version type:type];
   614 	if ([matches count] == 1U)
   615 		return [matches anyObject];
   616 	else
   617 		return nil;
   618 }
   619 - (NSDictionary *) pluginDictionaryWithName:(NSString *)name {
   620 	return [self pluginDictionaryWithName:name author:nil version:nil type:nil];
   621 }
   622 - (GrowlPlugin *) pluginInstanceWithName:(NSString *)name author:(NSString *)author version:(NSString *)version type:(NSString *)type {
   623 	NSDictionary *pluginDict = [self pluginDictionaryWithName:name author:author version:version type:type];
   624 	GrowlPlugin *instance = [pluginDict pluginInstance];
   625 	if (!instance) {
   626 		NSBundle *bundle = [pluginDict pluginBundle];
   627 		if (bundle) {
   628 			[self addPluginInstance:nil fromBundle:bundle]; //causes instantiation
   629 			instance = [pluginDict pluginInstance];
   630 		}
   631 	}
   632 	return instance;
   633 }
   634 - (GrowlPlugin *) pluginInstanceWithName:(NSString *)name {
   635 	return [self pluginInstanceWithName:name author:nil version:nil type:nil];
   636 }
   637 
   638 #pragma mark -
   639 
   640 - (NSString *) humanReadableNameForPluginWithDictionary:(NSDictionary *)pluginDict {
   641 	NSString *humanReadableName = [pluginDict pluginHumanReadableName];
   642 
   643 	if (!humanReadableName) {
   644 		NSString *name = [pluginDict pluginName];
   645 		if ([[pluginsByName objectForKey:name] count] == 1U)
   646 			humanReadableName = name;
   647 		else {
   648 			NSString *author = [pluginDict pluginAuthor];
   649 			if ([[pluginsByAuthor objectForKey:author] count] == 1U)
   650 				humanReadableName = [NSString stringWithFormat:@"%@ (by %@)", name, author]; //XXX LOCALIZEME
   651 			else {
   652 				NSString *filename = [[pluginDict pluginPath] lastPathComponent];
   653 				if ([[pluginsByFilename objectForKey:filename] count] == 1U)
   654 					humanReadableName = [NSString stringWithFormat:@"%@ (filename %@)", name, filename]; //XXX LOCALIZEME
   655 				else {
   656 					humanReadableName = [NSString stringWithFormat:@"%@ (by %@, filename %@)", name, author, filename]; //XXX LOCALIZEME
   657 					NSUInteger count = [pluginHumanReadableNames countForObject:humanReadableName];
   658 					[pluginHumanReadableNames addObject:humanReadableName];
   659 					if (count > 1U)
   660 						humanReadableName = [NSString stringWithFormat:@"%@ %u", humanReadableName, count];
   661 				}
   662 			}
   663 		}
   664 
   665 		if ([pluginDict isKindOfClass:[NSMutableDictionary class]]) {
   666 			//save the name for later retrieval.
   667 			[(NSMutableDictionary *)pluginDict setObject:humanReadableName forKey:GrowlPluginInfoKeyHumanReadableName];
   668 		}
   669 	}
   670 
   671 	return humanReadableName;
   672 }
   673 
   674 - (BOOL) pluginWithDictionaryIsDisplayPlugin:(NSDictionary *)pluginDict {
   675 	GrowlPlugin *instance = [pluginDict pluginInstance];
   676 	if (instance)
   677 		return [instance isKindOfClass:[GrowlDisplayPlugin class]];
   678 	else {
   679 		NSBundle *bundle = [pluginDict pluginBundle];
   680 		NSAssert1(bundle, @"no instance or bundle in plug-in dictionary! description of dictionary follows\n%@", pluginDict);
   681 		Class principalClass = [bundle principalClass];
   682 		NSAssert1(bundle, @"bundle in plug-in dictionary has no principal class! description of dictionary follows\n%@", pluginDict);
   683 		return [principalClass isSubclassOfClass:[GrowlDisplayPlugin class]];
   684 	}
   685 }
   686 
   687 #pragma mark -
   688 #warning XXX all of this could potentially go bye-bye if it is not needed
   689 
   690 - (NSArray *) displayPlugins {
   691 	if (!cache_displayPlugins)
   692 		cache_displayPlugins = [[NSArray alloc] initWithArray:displayPlugins];
   693 	return cache_displayPlugins;
   694 }
   695 
   696 #pragma mark -
   697 
   698 - (NSSet *) displayPluginDictionariesWithName:(NSString *)name author:(NSString *)author version:(NSString *)version type:(NSString *)type {
   699 	NSMutableSet *matches = (NSMutableSet *)[self pluginDictionariesWithName:name
   700 																	  author:author
   701 																	 version:version
   702 																		type:type];
   703 
   704 	NSSet *copyForIteration = [matches copy];
   705 	NSEnumerator *matchesEnum = [copyForIteration objectEnumerator];
   706 	NSDictionary *pluginDict;
   707 	while ((pluginDict = [matchesEnum nextObject])) {
   708 		if (![self pluginWithDictionaryIsDisplayPlugin:pluginDict])
   709 			[matches removeObject:pluginDict];
   710 	}
   711 	[copyForIteration release];
   712 
   713 	return matches;
   714 }
   715 
   716 - (NSDictionary *) displayPluginDictionaryWithName:(NSString *)name author:(NSString *)author version:(NSString *)version type:(NSString *)type {
   717 	NSSet *matches = [self displayPluginDictionariesWithName:name
   718 													  author:author
   719 													 version:version
   720 														type:type];
   721 	if ([matches count] == 1U)
   722 		return [matches anyObject];
   723 	else
   724 		return nil;
   725 }
   726 
   727 - (GrowlDisplayPlugin *) displayPluginInstanceWithName:(NSString *)name author:(NSString *)author version:(NSString *)version type:(NSString *)type {
   728 	GrowlPlugin *plugin = [self pluginInstanceWithName:name author:author version:version type:type];
   729 	if (plugin && [plugin isKindOfClass:[GrowlDisplayPlugin class]])
   730 		return (GrowlDisplayPlugin *)plugin;
   731 	else
   732 		return nil;
   733 }
   734 
   735 #pragma mark -
   736 #pragma mark Finding and using installed plug-ins
   737 
   738 - (void) findPluginsInDirectory:(NSString *)dir {
   739 	NSDirectoryEnumerator *enumerator = [[NSFileManager defaultManager] enumeratorAtPath:dir];
   740 	NSString *file;
   741 	while ((file = [enumerator nextObject])) {
   742 		NSString *fullPath = [dir stringByAppendingPathComponent:file];
   743 
   744 		NSString *pathExtension = [file pathExtension];
   745 		NSString *fileType;
   746 		[[NSWorkspace sharedWorkspace] getFileType:&fileType creatorCode:NULL forFile:fullPath];
   747 		if ([pluginHandlers objectForKey:pathExtension] || (fileType && [pluginHandlers objectForKey:fileType])) {
   748 			[self dispatchPluginAtPath:fullPath];
   749 			[enumerator skipDescendents];
   750 		}
   751 	}
   752 }
   753 
   754 - (void) dispatchPluginAtPath:(NSString *)path {
   755 	//get all the relevant handlers, by extension and by type.
   756 	NSArray *handlersByExtension = [pluginHandlers objectForKey:[path pathExtension]];
   757 	NSString *fileType;
   758 	[[NSWorkspace sharedWorkspace] getFileType:&fileType creatorCode:NULL forFile:path];
   759 	NSArray *handlersByType      = [pluginHandlers objectForKey:fileType];
   760 
   761 	//strip duplicates by making a set from both arrays.
   762 	NSMutableSet *allMatchingHandlersSet = handlersByExtension ? [NSMutableSet setWithArray:handlersByExtension] : [NSMutableSet set];
   763 	if (handlersByType)
   764 		[allMatchingHandlersSet unionSet:[NSSet setWithArray:handlersByType]];
   765 
   766 	NSMutableArray *allMatchingHandlers = [[[allMatchingHandlersSet allObjects] mutableCopy] autorelease];
   767 	[allMatchingHandlers sortUsingFunction:comparePluginHandlerRegistrationOrder context:self];
   768 
   769 	NSBundle *pluginBundle = [[NSBundle alloc] initWithPath:path];
   770 
   771 	NSEnumerator *handlersEnum = [allMatchingHandlers objectEnumerator];
   772 	id <GrowlPluginHandler> handler;
   773 	while ((handler = [handlersEnum nextObject])) {
   774 		BOOL success = NO;
   775 		if (pluginBundle && [handler respondsToSelector:@selector(loadPluginWithBundle:)])
   776 			success = (NSUInteger)[handler performSelector:@selector(loadPluginWithBundle:) withObject:pluginBundle];
   777 		else if ([handler respondsToSelector:@selector(loadPluginAtPath:)])
   778 			success = (NSUInteger)[handler performSelector:@selector(loadPluginAtPath:) withObject:path];
   779 		else if ([handler respondsToSelector:@selector(loadPluginAtURL:)])
   780 			success = (NSUInteger)[handler performSelector:@selector(loadPluginAtURL:) withObject:[NSURL fileURLWithPath:path]];
   781 		else
   782 			NSLog(@"warning: while loading plug-in at %@, tried to use plug-in handler %@, but it appears incapable of handling a plug-in", path, handler); //XXX should do this diagnostic when adding the handler
   783 	}
   784 
   785 	[pluginBundle release];
   786 }
   787 
   788 #pragma mark -
   789 #pragma mark Installing plug-ins
   790 
   791 - (void) pluginInstalledSelector:(NSWindow *)sheet returnCode:(int)returnCode contextInfo:(void *)contextInfo {
   792 #pragma unused(sheet, contextInfo)
   793 	if (returnCode == NSAlertAlternateReturn) {
   794 		NSBundle *prefPane = [GrowlPathUtilities growlPrefPaneBundle];
   795 
   796 		if (prefPane && ![[NSWorkspace sharedWorkspace] openFile:[prefPane bundlePath]])
   797 			NSLog(@"Could not open Growl PrefPane");
   798 	}
   799 }
   800 
   801 - (void) pluginExistsSelector:(NSWindow *)sheet returnCode:(int)returnCode contextInfo:(void *)contextInfo {
   802 #pragma unused(sheet)
   803 	NSString *filename = (NSString *)contextInfo;
   804 
   805 	if (returnCode == NSAlertAlternateReturn) {
   806 		//'Yes' to 'Do you want to overwrite [the installed plug-in with the version you double-clicked]?'
   807 		NSString *pluginFile = [filename lastPathComponent];
   808 		NSString *destination = [[NSHomeDirectory()
   809 			stringByAppendingPathComponent:@"Library/Application Support/Growl/Plugins"]
   810 			stringByAppendingPathComponent:pluginFile];
   811 		NSFileManager *fileManager = [NSFileManager defaultManager];
   812 
   813 		// first remove old copy if present
   814 		[fileManager removeFileAtPath:destination handler:nil];
   815 
   816 		// copy new version to destination
   817 		if ([fileManager copyPath:filename toPath:destination handler:nil]) {
   818 			[self dispatchPluginAtPath:destination];
   819 			[[NSApplication sharedApplication] activateIgnoringOtherApps:YES];
   820 			if([self _hasNativeArchitecture:destination])
   821 				NSBeginInformationalAlertSheet( NSLocalizedString( @"Plugin installed", @"" ),
   822 											NSLocalizedString( @"No",  @"" ),
   823 											NSLocalizedString( @"Yes", @"" ),
   824 											nil, nil, self,
   825 											@selector(pluginInstalledSelector:returnCode:contextInfo:),
   826 											NULL, NULL,
   827 											NSLocalizedString( @"Plugin '%@' has been installed successfully. Do you want to configure it now?", @"" ),
   828 											[pluginFile stringByDeletingPathExtension] );
   829 		} else {
   830 			[[NSApplication sharedApplication] activateIgnoringOtherApps:YES];
   831 			NSBeginCriticalAlertSheet( NSLocalizedString( @"Plugin not installed", @"" ),
   832 									   NSLocalizedString( @"OK", @"" ),
   833 									   nil, nil, nil, self, NULL, NULL, NULL,
   834 									   NSLocalizedString( @"There was an error while installing the plugin '%@'.", @"" ),
   835 									   [pluginFile stringByDeletingPathExtension] );
   836 		}
   837 	}
   838 
   839 	[filename release];
   840 }
   841 
   842 - (void) installPluginFromPath:(NSString *)filename {
   843 	NSString *pluginFile = [filename lastPathComponent];
   844 	NSString *destination = [[NSHomeDirectory()
   845 		stringByAppendingPathComponent:@"Library/Application Support/Growl/Plugins"]
   846 		stringByAppendingPathComponent:pluginFile];
   847 	// retain a copy of the filename because it is passed as context to the sheetDidEnd selectors
   848 	NSString *filenameCopy = [[NSString alloc] initWithString:filename];
   849 
   850 	//Check to see if we've got valid architectures in this plugin for our use, if not, bail.
   851 	if(![self _hasNativeArchitecture:filenameCopy]) {
   852 		NSBeginAlertSheet( NSLocalizedString( @"Plugin missing native architecture", @"" ),
   853 						  NSLocalizedString( @"No", @"" ),
   854 						  NSLocalizedString( @"Yes", @"" ), nil, nil, self,
   855 						  NULL, @selector(pluginExistsSelector:returnCode:contextInfo:),
   856 						  filenameCopy,
   857 						  NSLocalizedString( @"Plugin '%@' will not work on this Mac running this version of Mac OS X. Install it anyway?", @"" ),
   858 						  [pluginFile stringByDeletingPathExtension] );		
   859 	}
   860 	else {
   861 		if ([[NSFileManager defaultManager] fileExistsAtPath:destination]) {
   862 			// plugin already exists at destination
   863 			[[NSApplication sharedApplication] activateIgnoringOtherApps:YES];
   864 			NSBeginAlertSheet( NSLocalizedString( @"Plugin already exists", @"" ),
   865 							  NSLocalizedString( @"No", @"" ),
   866 							  NSLocalizedString( @"Yes", @"" ), nil, nil, self,
   867 							  NULL, @selector(pluginExistsSelector:returnCode:contextInfo:),
   868 							  filenameCopy,
   869 							  NSLocalizedString( @"Plugin '%@' is already installed, do you want to overwrite it?", @"" ),
   870 							  [pluginFile stringByDeletingPathExtension] );
   871 		} else {
   872 			[self pluginExistsSelector:nil returnCode:NSAlertAlternateReturn contextInfo:filenameCopy];
   873 		}
   874 	}
   875 }
   876 
   877 - (BOOL)_hasNativeArchitecture:(NSString*)filename {	
   878 	BOOL result = NO;
   879 	NSInteger currentArchitecture = 0;
   880 #if defined(__ppc__) && __ppc__
   881 	currentArchitecture = NSBundleExecutableArchitecturePPC;
   882 #elif defined(__i386__) && __i386__
   883 	currentArchitecture = NSBundleExecutableArchitectureI386;
   884 #elif defined(__x86_64__) && __x86_64__
   885 	currentArchitecture = NSBundleExecutableArchitectureX86_64;
   886 #else
   887 	#error unsupported architecture
   888 #endif
   889 	NSBundle *pluginBundle = [NSBundle bundleWithPath:filename];
   890 	NSString *executablePath = [pluginBundle executablePath];
   891 	//we check to see if there is actually an executable in this plugin, it could be a growlStyle, under which we accept it as valid.
   892 	if(executablePath && [[NSFileManager defaultManager] fileExistsAtPath:executablePath]) {
   893 		NSArray *pluginArchitectures = [pluginBundle executableArchitectures];
   894 		if([pluginArchitectures containsObject:[NSNumber numberWithInteger:currentArchitecture]])
   895 			result = YES;
   896 	}
   897 	else {
   898 		result = YES;
   899 	}
   900 
   901 	return result;
   902 }
   903 
   904 @end
   905 
   906 static Boolean caseInsensitiveStringComparator(const void *value1, const void *value2) {
   907 	Class NSStringClass = [NSString class];
   908 	return [(id)value1 isKindOfClass:NSStringClass] \
   909 	    && [(id)value2 isKindOfClass:NSStringClass]  \
   910 	    && ([(NSString *)value1 caseInsensitiveCompare:(NSString *)value2] == NSOrderedSame);
   911 }
   912 
   913 static CFHashCode passthroughStringHash(const void *value) {
   914 	return [[(NSString *)value lowercaseString] hash];
   915 }
   916 
   917 #pragma mark -
   918 
   919 @implementation NSDictionary (GrowlPluginKeys)
   920 
   921 - (NSString *) pluginName {
   922 	return [self objectForKey:GrowlPluginInfoKeyName];
   923 }
   924 - (NSString *) pluginAuthor {
   925 	return [self objectForKey:GrowlPluginInfoKeyAuthor];
   926 }
   927 - (NSString *) pluginDescription {
   928 	return [self objectForKey:GrowlPluginInfoKeyDescription];
   929 }
   930 - (NSString *) pluginVersion {
   931 	return [self objectForKey:GrowlPluginInfoKeyVersion];
   932 }
   933 - (NSBundle *) pluginBundle {
   934 	return [self objectForKey:GrowlPluginInfoKeyBundle];
   935 }
   936 - (NSString *) pluginPath {
   937 	return [self objectForKey:GrowlPluginInfoKeyPath];
   938 }
   939 - (NSSet *) pluginTypes {
   940 	return [self objectForKey:GrowlPluginInfoKeyTypes];
   941 }
   942 - (NSString *) pluginHumanReadableName {
   943 	return [self objectForKey:GrowlPluginInfoKeyHumanReadableName];
   944 }
   945 - (NSString *) pluginIdentifier {
   946 	return [self objectForKey:GrowlPluginInfoKeyIdentifier];
   947 }
   948 - (GrowlPlugin *) pluginInstance {
   949 	return [self objectForKey:GrowlPluginInfoKeyInstance];
   950 }
   951 
   952 @end
   953 
   954 #define ASSERT_IN_FUNCTION(condition, desc, ...)                                                      \
   955 	[[NSAssertionHandler currentHandler] handleFailureInFunction:[NSString stringWithCString:__func__] \
   956 															file:[NSString stringWithCString:__FILE__]  \
   957 													  lineNumber:__LINE__                                \
   958 													 description:desc, __VA_ARGS__];
   959 
   960 NSInteger comparePluginHandlerRegistrationOrder(id a, id b, void *context) {
   961 	GrowlPluginController *self = (GrowlPluginController *)context;
   962 	NSArray *allPluginHandlers = [self allPluginHandlers];
   963 
   964 	NSUInteger aIndex = [allPluginHandlers indexOfObjectIdenticalTo:a];
   965 	NSUInteger bIndex = [allPluginHandlers indexOfObjectIdenticalTo:b];
   966 
   967 	ASSERT_IN_FUNCTION(aIndex != NSNotFound, @"Attempted to compare two plug-in handlers, but the first object was not a (registered) plug-in handler! Description of object: %@", a);
   968 	ASSERT_IN_FUNCTION(bIndex != NSNotFound, @"Attempted to compare two plug-in handlers, but the second object was not a (registered) plug-in handler! Description of object: %@", b);
   969 
   970 	if (aIndex < bIndex)
   971 		return NSOrderedAscending;
   972 	else if (aIndex > bIndex)
   973 		return NSOrderedDescending;
   974 	else
   975 		return NSOrderedSame;
   976 }