aboutsummaryrefslogtreecommitdiff
path: root/AppKit/GTMGetURLHandler.m
diff options
context:
space:
mode:
Diffstat (limited to 'AppKit/GTMGetURLHandler.m')
-rw-r--r--AppKit/GTMGetURLHandler.m284
1 files changed, 284 insertions, 0 deletions
diff --git a/AppKit/GTMGetURLHandler.m b/AppKit/GTMGetURLHandler.m
new file mode 100644
index 0000000..a35dd95
--- /dev/null
+++ b/AppKit/GTMGetURLHandler.m
@@ -0,0 +1,284 @@
+//
+// GTMGetURLHandler.m
+//
+// Copyright 2008 Google Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy
+// of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+//
+
+// Add this class to your app to have get URL handled almost automatically for
+// you. For each entry in your CFBundleURLTypes dictionaries, add a new
+// key/object pair of GTMBundleURLClass/the name of the class you want
+// to have handle the scheme(s).
+// Then have that class respond to the class method:
+// + (BOOL)gtm_openURL:(NSURL*)url
+// and voila, it will just work.
+// Note that in Debug mode we will do extensive testing to make sure that this
+// is all hooked up correctly, and will spew out to the console if we
+// find anything amiss.
+//
+// Example plist entry
+// ...
+//
+// <key>CFBundleURLTypes</key>
+// <array>
+// <dict>
+// <key>CFBundleURLName</key>
+// <string>Google Suggestion URL</string>
+// <key>GTMBundleURLClass</key>
+// <string>GoogleSuggestURLHandler</string>
+// <key>CFBundleURLSchemes</key>
+// <array>
+// <string>googlesuggest</string>
+// <string>googlesuggestextreme</string>
+// </array>
+// </dict>
+// </array>
+//
+//
+// Example implementation
+// @interface GoogleSuggestURLHandler
+// @end
+// @implementation GoogleSuggestURLHandler
+// + (BOOL)gtm_openURL:(NSURL*)url {
+// NSLog(@"%@", url);
+// }
+// @end
+
+#import <AppKit/AppKit.h>
+#import "GTMGarbageCollection.h"
+#import "GTMNSAppleEventDescriptor+Foundation.h"
+#import "GTMMethodCheck.h"
+
+static NSString *const kGTMBundleURLClassKey = @"GTMBundleURLClass";
+// A variety of constants Apple really should have defined somewhere to
+// allow the compiler to find your typos.
+static NSString *const kGTMCFBundleURLSchemesKey = @"CFBundleURLSchemes";
+static NSString *const kGTMCFBundleURLNameKey = @"CFBundleURLName";
+static NSString *const kGTMCFBundleTypeRoleKey = @"CFBundleTypeRole";
+static NSString *const kGTMCFBundleURLTypesKey = @"CFBundleURLTypes";
+static NSString *const kGTMCFBundleViewerRole = @"Viewer";
+static NSString *const kGTMCFBundleEditorRole = @"Editor";
+
+// Set this macro elsewhere is you want to force the
+// bundle checks on/off. They are nice for debugging
+// problems, but shouldn't be required in a release version
+// unless you are paranoid about your users messing with your
+// Info.plist
+#ifndef GTM_CHECK_BUNDLE_URL_CLASSES
+#define GTM_CHECK_BUNDLE_URL_CLASSES DEBUG
+#endif // GTM_CHECK_BUNDLE_URL_CLASSES
+
+@protocol GTMGetURLHandlerProtocol
++ (BOOL)gtm_openURL:(NSURL*)url;
+@end
+
+@interface GTMGetURLHandler : NSObject {
+ NSArray *urlTypes_;
+}
+- (id)initWithTypes:(NSArray*)urlTypes;
+- (void)getUrl:(NSAppleEventDescriptor *)event
+withReplyEvent:(NSAppleEventDescriptor *)replyEvent;
+- (void)addError:(OSStatus)error
+ withDescription:(NSString*)string
+ toDescriptor:(NSAppleEventDescriptor *)desc;
++ (id)handlerForBundle:(NSBundle *)bundle;
++ (void)appFinishedLaunchingHandler:(NSNotification*)notification;
+@end
+
+@implementation GTMGetURLHandler
+GTM_METHOD_CHECK(NSNumber, gtm_appleEventDescriptor);
+GTM_METHOD_CHECK(NSString, gtm_appleEventDescriptor);
+
++ (void)load {
+ NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
+ NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
+ [nc addObserver:self
+ selector:@selector(appFinishedLaunchingHandler:)
+ name:NSApplicationDidFinishLaunchingNotification
+ object:nil];
+ [pool release];
+}
+
++ (void)appFinishedLaunchingHandler:(NSNotification*)notification {
+ NSBundle *bundle = [NSBundle mainBundle];
+ GTMGetURLHandler *handler = [GTMGetURLHandler handlerForBundle:bundle];
+ if (handler) {
+ [handler retain];
+ GTMNSMakeUncollectable(handler);
+ NSAppleEventManager *man = [NSAppleEventManager sharedAppleEventManager];
+ [man setEventHandler:handler
+ andSelector:@selector(getUrl:withReplyEvent:)
+ forEventClass:kInternetEventClass
+ andEventID:kAEGetURL];
+ }
+ NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
+ [nc removeObserver:self
+ name:NSApplicationDidFinishLaunchingNotification
+ object:nil];
+}
+
++ (id)handlerForBundle:(NSBundle *)bundle {
+ GTMGetURLHandler *handler = nil;
+ NSArray *urlTypes
+ = [bundle objectForInfoDictionaryKey:kGTMCFBundleURLTypesKey];
+ if (urlTypes) {
+ handler = [[[GTMGetURLHandler alloc] initWithTypes:urlTypes] autorelease];
+ } else {
+ _GTMDevLog(@"If you don't have CFBundleURLTypes in your plist, you may want"
+ @" to remove GTMGetURLHandler.m from your project");
+ }
+ return handler;
+}
+
+- (id)initWithTypes:(NSArray*)urlTypes {
+ if ((self = [super init])) {
+ urlTypes_ = [urlTypes retain];
+#if GTM_CHECK_BUNDLE_URL_CLASSES
+ // Some debug handling to check to make sure we can handle the
+ // classes properly.
+ NSEnumerator *enumerator = [urlTypes_ objectEnumerator];
+ NSDictionary *urlType;
+ while ((urlType = [enumerator nextObject])) {
+ NSString *className = [urlType objectForKey:kGTMBundleURLClassKey];
+ if ([className length]) {
+ Class cls = NSClassFromString(className);
+ if (cls) {
+ if (![cls respondsToSelector:@selector(gtm_openURL:)]) {
+ _GTMDevLog(@"Class %@ for URL handler %@ "
+ "(URL schemes: %@) doesn't respond to openURL:",
+ className,
+ [urlType objectForKey:kGTMCFBundleURLNameKey],
+ [urlType objectForKey:kGTMCFBundleURLSchemesKey]);
+ }
+ } else {
+ _GTMDevLog(@"Unable to get class %@ for URL handler %@ "
+ "(URL schemes: %@)",
+ className,
+ [urlType objectForKey:kGTMCFBundleURLNameKey],
+ [urlType objectForKey:kGTMCFBundleURLSchemesKey]);
+ }
+ } else {
+ NSString *role = [urlType objectForKey:kGTMCFBundleTypeRoleKey];
+ if ([role caseInsensitiveCompare:kGTMCFBundleViewerRole] == NSOrderedSame ||
+ [role caseInsensitiveCompare:kGTMCFBundleEditorRole] == NSOrderedSame) {
+ _GTMDevLog(@"Missing %@ for URL handler %@ "
+ "(URL schemes: %@)",
+ kGTMBundleURLClassKey,
+ [urlType objectForKey:kGTMCFBundleURLNameKey],
+ [urlType objectForKey:kGTMCFBundleURLSchemesKey]);
+ }
+ }
+ }
+#endif // GTM_CHECK_BUNDLE_URL_CLASSES
+ }
+ return self;
+}
+
+- (void)dealloc {
+ [urlTypes_ release];
+ [super dealloc];
+}
+
+- (NSURL*)extractURLFromEvent:(NSAppleEventDescriptor*)event
+ withReplyEvent:(NSAppleEventDescriptor *)replyEvent {
+ NSAppleEventDescriptor *desc
+ = [event paramDescriptorForKeyword:keyDirectObject];
+ NSString *urlstring = [desc stringValue];
+ NSURL *url = [NSURL URLWithString:urlstring];
+ if (!url) {
+ [self addError:errAECoercionFail
+ withDescription:@"Unable to extract url from key direct object."
+ toDescriptor:replyEvent];
+ }
+ return url;
+}
+
+- (Class)getClassForScheme:(NSString *)scheme
+ withReplyEvent:(NSAppleEventDescriptor*)replyEvent {
+ NSEnumerator *typeEnumerator = [urlTypes_ objectEnumerator];
+ NSDictionary *urlType;
+ Class cls = nil;
+ NSString *typeScheme = nil;
+ while (!typeScheme && (urlType = [typeEnumerator nextObject])) {
+ NSArray *schemes = [urlType objectForKey:kGTMCFBundleURLSchemesKey];
+ NSEnumerator *schemeEnumerator = [schemes objectEnumerator];
+ while ((typeScheme = [schemeEnumerator nextObject])) {
+ if ([typeScheme caseInsensitiveCompare:scheme] == NSOrderedSame) {
+ break;
+ }
+ }
+ }
+ if (typeScheme) {
+ NSString *class = [urlType objectForKey:kGTMBundleURLClassKey];
+ if (class) {
+ cls = NSClassFromString(class);
+ }
+ if (!cls) {
+ NSString *errorString
+ = [NSString stringWithFormat:@"Unable to instantiate class for "
+ "%@:%@ for scheme:%@.",
+ kGTMBundleURLClassKey, class, typeScheme];
+ [self addError:errAECorruptData
+ withDescription:errorString
+ toDescriptor:replyEvent];
+ }
+ } else {
+ NSString *errorString
+ = [NSString stringWithFormat:@"Unable to find handler for scheme %@.",
+ scheme];
+ [self addError:errAECorruptData
+ withDescription:errorString
+ toDescriptor:replyEvent];
+ }
+ return cls;
+}
+
+- (void)getUrl:(NSAppleEventDescriptor *)event
+withReplyEvent:(NSAppleEventDescriptor *)replyEvent {
+ NSURL *url = [self extractURLFromEvent:event withReplyEvent:replyEvent];
+ if (!url) {
+ return;
+ }
+ NSString *scheme = [url scheme];
+ Class cls = [self getClassForScheme:scheme withReplyEvent:replyEvent];
+ if (!cls) {
+ return;
+ }
+ BOOL wasGood = [cls gtm_openURL:url];
+ if (!wasGood) {
+ NSString *errorString
+ = [NSString stringWithFormat:@"[%@ gtm_openURL:] failed to handle %@",
+ NSStringFromClass(cls), url];
+ [self addError:errAEEventNotHandled
+ withDescription:errorString
+ toDescriptor:replyEvent];
+ }
+}
+
+- (void)addError:(OSStatus)error
+ withDescription:(NSString*)string
+ toDescriptor:(NSAppleEventDescriptor *)desc {
+ NSAppleEventDescriptor *errorDesc = nil;
+ if (error != noErr) {
+ NSNumber *errNum = [NSNumber numberWithLong:error];
+ errorDesc = [errNum gtm_appleEventDescriptor];
+ [desc setParamDescriptor:errorDesc forKeyword:keyErrorNumber];
+ }
+ if (string) {
+ errorDesc = [string gtm_appleEventDescriptor];
+ [desc setParamDescriptor:errorDesc forKeyword:keyErrorString];
+ }
+}
+@end
+