//
// 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
// ...
//
// CFBundleURLTypes
//
//
// CFBundleURLName
// Google Suggestion URL
// GTMBundleURLClass
// GoogleSuggestURLHandler
// CFBundleURLSchemes
//
// googlesuggest
// googlesuggestextreme
//
//
//
//
//
// Example implementation
// @interface GoogleSuggestURLHandler
// @end
// @implementation GoogleSuggestURLHandler
// + (BOOL)gtm_openURL:(NSURL*)url {
// NSLog(@"%@", url);
// }
// @end
#import
#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)getUrl:(NSAppleEventDescriptor *)event
withReplyEvent:(NSAppleEventDescriptor *)replyEvent;
@end
@implementation GTMGetURLHandler
GTM_METHOD_CHECK(NSNumber, gtm_appleEventDescriptor);
GTM_METHOD_CHECK(NSString, gtm_appleEventDescriptor);
+ (void)load {
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
NSAppleEventManager *man = [NSAppleEventManager sharedAppleEventManager];
[man setEventHandler:self
andSelector:@selector(getUrl:withReplyEvent:)
forEventClass:kInternetEventClass
andEventID:kAEGetURL];
[pool release];
}
+ (void)getUrl:(NSAppleEventDescriptor *)event
withReplyEvent:(NSAppleEventDescriptor *)replyEvent {
static GTMGetURLHandler *sHandler = nil;
if (!sHandler) {
NSBundle *bundle = [NSBundle mainBundle];
sHandler = [GTMGetURLHandler handlerForBundle:bundle];
if (sHandler) {
[sHandler retain];
GTMNSMakeUncollectable(sHandler);
}
}
[sHandler getUrl:event withReplyEvent:replyEvent];
}
+ (id)handlerForBundle:(NSBundle *)bundle {
GTMGetURLHandler *handler = nil;
NSArray *urlTypes
= [bundle objectForInfoDictionaryKey:kGTMCFBundleURLTypesKey];
if (urlTypes) {
handler = [[[GTMGetURLHandler alloc] initWithTypes:urlTypes] autorelease];
} else {
// COV_NF_START
// Hard to test it if we don't have it.
_GTMDevLog(@"If you don't have CFBundleURLTypes in your plist, you may want"
@" to remove GTMGetURLHandler.m from your project");
// COV_NF_END
}
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. We check here instead of at init in case some of the
// handlers are being handled by plugins or other imported code that are
// loaded after we have been initialized.
NSDictionary *urlType;
GTM_FOREACH_OBJECT(urlType, urlTypes_) {
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;
}
// COV_NF_START
// Singleton is never dealloc'd
- (void)dealloc {
[urlTypes_ release];
[super dealloc];
}
// COV_NF_END
- (NSURL*)extractURLFromEvent:(NSAppleEventDescriptor*)event
withReplyEvent:(NSAppleEventDescriptor *)replyEvent {
NSAppleEventDescriptor *desc
= [event paramDescriptorForKeyword:keyDirectObject];
NSString *urlstring = [desc stringValue];
NSURL *url = [NSURL URLWithString:urlstring];
if (!url) {
// COV_NF_START
// Can't convince the OS to give me a bad URL
[self addError:errAECoercionFail
withDescription:@"Unable to extract url from key direct object."
toDescriptor:replyEvent];
// COV_NF_END
}
return url;
}
- (Class)getClassForScheme:(NSString *)scheme
withReplyEvent:(NSAppleEventDescriptor*)replyEvent {
NSDictionary *urlType;
Class cls = nil;
NSString *typeScheme = nil;
GTM_FOREACH_OBJECT(urlType, urlTypes_) {
NSArray *schemes = [urlType objectForKey:kGTMCFBundleURLSchemesKey];
NSString *aScheme;
GTM_FOREACH_OBJECT(aScheme, schemes) {
if ([aScheme caseInsensitiveCompare:scheme] == NSOrderedSame) {
typeScheme = aScheme;
break;
}
}
if (typeScheme) {
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 {
if (![cls respondsToSelector:@selector(gtm_openURL:)]) {
NSString *errorString
= [NSString stringWithFormat:@"Class %@:%@ for scheme:%@ does not"
@"respond to gtm_openURL:",
kGTMBundleURLClassKey, class, typeScheme];
[self addError:errAECorruptData
withDescription:errorString
toDescriptor:replyEvent];
cls = Nil;
}
}
} else {
// COV_NF_START
// Don't know how to force an URL that we don't respond to upon ourselves.
NSString *errorString
= [NSString stringWithFormat:@"Unable to find handler for scheme %@.",
scheme];
[self addError:errAECorruptData
withDescription:errorString
toDescriptor:replyEvent];
// COV_NF_END
}
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