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