diff options
author | gtm.daemon <gtm.daemon@7dc7ac4e-7543-0410-b95c-c1676fc8e2a3> | 2009-05-22 21:30:34 +0000 |
---|---|---|
committer | gtm.daemon <gtm.daemon@7dc7ac4e-7543-0410-b95c-c1676fc8e2a3> | 2009-05-22 21:30:34 +0000 |
commit | c036015092f737da4ef8e2a5df6dd9f63f1c3b3f (patch) | |
tree | ead3869d96aac4abc0e81c625608201514f69b09 | |
parent | 2bed905feae3f45a28237d20b075ef13dfb55a87 (diff) |
[Author: dmaclach]
Added GTMGoogleSearch.
DELTA=853 (853 added, 0 deleted, 0 changed)
TBR=thomasvl
-rw-r--r-- | Foundation/GTMGoogleSearch.h | 136 | ||||
-rw-r--r-- | Foundation/GTMGoogleSearch.m | 527 | ||||
-rw-r--r-- | Foundation/GTMGoogleSearchTest.m | 180 | ||||
-rw-r--r-- | GTM.xcodeproj/project.pbxproj | 12 | ||||
-rw-r--r-- | GTMiPhone.xcodeproj/project.pbxproj | 10 | ||||
-rw-r--r-- | ReleaseNotes.txt | 1 |
6 files changed, 866 insertions, 0 deletions
diff --git a/Foundation/GTMGoogleSearch.h b/Foundation/GTMGoogleSearch.h new file mode 100644 index 0000000..b8f4b5b --- /dev/null +++ b/Foundation/GTMGoogleSearch.h @@ -0,0 +1,136 @@ +// +// GTMGoogleSearch.h +// +// Copyright 2006-2009 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. +// + +#import <Foundation/Foundation.h> + +// Key for Info.plist for default global search args +#define GTMGoogleSearchClientAppArgsKey @"GTMGoogleSearchClientAppArgs" + +// Types to pass in to searchForURL:ofType:arguments +// and performQuery:ofType:arguments +#define GTMGoogleSearchFroogle @"products" +#define GTMGoogleSearchGroups @"groups" +#define GTMGoogleSearchImages @"images" +#define GTMGoogleSearchLocal @"local" +#define GTMGoogleSearchNews @"news" +#define GTMGoogleSearchFinance @"finance" +#define GTMGoogleSearchBooks @"books" +#define GTMGoogleSearchWeb @"search" + +// Composes URLs and searches for google properties in the correct language and domain. +@interface GTMGoogleSearch : NSObject { + // the cached values + NSString *allAppsCachedDomain_; + NSString *allAppsCachedLanguage_; + NSString *curAppCachedDomain_; + NSString *curAppCachedLanguage_; + NSDictionary *globalSearchArguments_; +} + +// +// +sharedInstance +// +// fetches the common shared object for accessing this users preference +// ++ (GTMGoogleSearch*)sharedInstance; + +// +// searchURLFor:ofType:arguments: +// +// creates a search url of type |type| for |queryText| using the user's +// preferred domain and language settings. |args| is a set of arguments +// that will be added into your query, and you can use it to complement +// or override settings stored in globalSearchArguments. +// example dictionary to do an I'm feeling lucky search would be: +// [NSDictionary dictionaryWithObject:@"1" key:@"btnI"]; +// +- (NSString*)searchURLFor:(NSString *)queryText + ofType:(NSString *)type + arguments:(NSDictionary *)args; + +// +// performQuery:ofType:arguments: +// +// Asks NSWorkspace to open up a query for an url created by passing +// the args to searchURLFor:ofType:arguments: above. +// +- (BOOL)performQuery:(NSString *)queryText + ofType:(NSString *)type + arguments:(NSDictionary *)localArgs; + +// Global search arguments are initially picked up from your main bundle +// info.plist if there is a dictionary entry at the top level with the key +// "GTMGoogleSearchClientAppArgs". This dictionary should be a map of strings +// to strings where they are the args you want passed to all Google searches. +// You can override these with your localArgs when you actually perform the +// search if you wish. +// This arguments will affect all searches. +- (void)setGlobalSearchArguments:(NSDictionary *)args; + +// Returns the global search arguments. +- (NSDictionary *)globalSearchArguments; + +// +// -preferredDomainAndLanguage:areCurrentAppOnly +// +// fetches the user's preferred domain and language, and whether the values +// that were grabbed were from the anyapplication domain, or from the current +// application domain. You may pass in nil for |language| if you don't want +// a language back, and you may pass in NULL for |currentAppOnly| if you don't +// care about where it came from. +// +- (void)preferredDomain:(NSString **)domain + language:(NSString **)language + areCurrentAppOnly:(BOOL*)currentAppOnly; + +// +// -updatePreferredDomain:language:currentApplicationOnly: +// +// updated the users preferred domain and language to copies of |domain| and +// |language| respectively. |domain| can't be nil or an empty string, but +// |language| can't be nil, but can be an empty string to signify no language +// pref. If |currentAppOnly| is YES, only updates the preferred settings for the +// current app, otherwise updates them for all apps. +// +- (void)updatePreferredDomain:(NSString *)domain + language:(NSString *)language + currentApplicationOnly:(BOOL)currentAppOnly; + +// +// -clearPreferredDomainAndLanguageForCurrentApplication +// +// clears the setting for the current applications preferred domain and +// language so future fetches will get the system level ones. +// +- (void)clearPreferredDomainAndLanguageForCurrentApplication; + +// +// -clearPreferredDomainAndLanguageForAllApps +// +// clears the "AllApps" setting for preferred domain and language so future +// fetches end up having to use the default. Odds are this is only +// used by the unittests. +// NOTE: this doesn't do anything to any setting that's set in an individual +// apps preferences, so those settings will still override inplace of the +// "all apps" value (or default). +// +- (void)clearPreferredDomainAndLanguageForAllApps; + + + +@end diff --git a/Foundation/GTMGoogleSearch.m b/Foundation/GTMGoogleSearch.m new file mode 100644 index 0000000..6bb5d58 --- /dev/null +++ b/Foundation/GTMGoogleSearch.m @@ -0,0 +1,527 @@ +// +// GTMGoogleSearch.m +// +// Copyright 2006-2009 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. +// + +#import "GTMGoogleSearch.h" +#import "GTMObjectSingleton.h" +#import "GTMNSString+URLArguments.h" +#import "GTMMethodCheck.h" +#import "GTMGarbageCollection.h" + +#if GTM_IPHONE_SDK +#import <UIKit/UIKit.h> +#endif // GTM_IPHONE_SDK + +typedef struct { + NSString *language; + NSString *country; + // we don't include a language, we'll use what we get from the OS + NSString *defaultDomain; +} LanguageDefaultInfo; + +// +// this is a seed mapping from languages to domains for google search. +// this doesn't have to be complete, as it is just a seed. +// +// initial values for the table were taken from the GDWin code. +// (/googleclient/totalrecall/common/url_tools.cpp, but moved to +// /google3/java/com/google/totalrecall/production/config/gpac.xml) +// +static LanguageDefaultInfo kLanguageListDefaultMappingTable[] = { + // order is important, first match is taken + // if country is |nil|, then only language has to match + { @"en", @"US", @"com" }, // english - united states + { @"en", @"GB", @"co.uk" }, // english - united kingdom + { @"en", @"CA", @"ca" }, // english - canada + { @"en", @"AU", @"com.au" }, // english - australia + { @"en", @"NZ", @"com" }, // english - new zealand + { @"en", @"IE", @"ie" }, // english - ireland + { @"en", @"IN", @"co.in" }, // english - india + { @"en", @"PH", @"com.ph" }, // english - philippines + { @"en", @"SG", @"com.sg" }, // english - singapore + { @"en", @"ZA", @"co.za" }, // english - south africa + { @"en", @"IL", @"co.il" }, // english - israel + { @"en", nil , @"com" }, // english (catch all) + { @"fr", @"CA", @"ca" }, // french - canada + { @"fr", @"CH", @"ch" }, // french - switzerland + { @"fr", nil , @"fr" }, // france + { @"it", nil , @"it" }, // italy + { @"de", @"AT", @"at" }, // german - austria + { @"de", nil , @"de" }, // germany + { @"es", @"MX", @"com.mx" }, // spanish - mexico + { @"es", @"AR", @"com.ar" }, // spanish - argentina + { @"es", @"CL", @"cl" }, // spanish - chile + { @"es", @"CO", @"com.co" }, // spanish - colombia + { @"es", @"PE", @"com.pe" }, // spanish - peru + { @"es", @"VE", @"co.ve" }, // venezuela + { @"es", nil , @"es" }, // spain + { @"zh", @"TW", @"com.tw" }, // taiwan + { @"zh", @"HK", @"com.hk" }, // hong kong + { @"zh", nil , @"cn" }, // chinese (catch all) + { @"ja", nil , @"co.jp" }, // japan + { @"ko", nil , @"co.kr" }, // korea + { @"nl", @"BE", @"be" }, // dutch - belgium + { @"nl", nil , @"nl" }, // (dutch) netherlands + { @"ru", nil , @"ru" }, // russia + { @"pt", @"BZ", @"com.br"}, // portuguese - brazil + { @"pt", nil , @"pt" }, // portugal + { @"sv", nil , @"se" }, // sweden + { @"nn", nil , @"no" }, // norway (two variants) + { @"nb", nil , @"no" }, // norway (two variants) + { @"da", nil , @"dk" }, // denmark + { @"fi", nil , @"fi" }, // finland + { @"bg", nil , @"bg" }, // bulgaria + { @"hr", nil , @"hr" }, // croatia + { @"cx", nil , @"cz" }, // czech republic + { @"el", nil , @"gr" }, // greece + { @"hu", nil , @"co.hu" }, // hungary + { @"ro", nil , @"ro" }, // romania + { @"sk", nil , @"sk" }, // slovakia + { @"sl", nil , @"si" }, // slovenia + { @"tr", nil , @"com.tr" }, // turkey + { @"my", nil , @"com.my" }, // malaysia + { @"th", nil , @"co.th" }, // thailand + { @"uk", nil , @"com.ua" }, // ukraine + { @"vi", nil , @"com.vn" }, // vietnam + { @"af", nil , @"com.za" }, // south africa (afrikaans) + { @"hi", nil , @"co.in" }, // india (hindi) + { @"id", nil , @"co.id" }, // indonesia + { @"pl", nil , @"pl" }, // poland +}; + +// the notification we use for syncing up instances in different processes +static NSString *const kNotificationName + = @"com.google.GoogleSearchAllApps.prefsWritten"; + +// this is the bundle id we use for the pref file used for all apps +static CFStringRef const kAllAppsBuildIdentifier + = CFSTR("com.google.GoogleSearchAllApps"); + +static CFStringRef const kPreferredDomainPrefKey + = CFSTR("com.google.PreferredDomain"); +static CFStringRef const kPreferredLanguagePrefKey + = CFSTR("com.google.PreferredLanguage"); + +static NSString *const kDefaultDomain = @"com"; +static NSString *const kDefaultLanguage = @"en"; + +static NSString *const kSearchURLTemplate = @"http://www.google.%@/%@?%@"; + +@interface GTMGoogleSearch (PrivateMethods) +- (void)defaultDomain:(NSString**)preferedDomain + language:(NSString**)preferredLanguage; +- (void)reloadAllAppCachedValues:(NSNotification*)notification; +- (void)updateAllAppsDomain:(NSString*)domain language:(NSString*)language; +- (NSDictionary *)normalizeGoogleArguments:(NSDictionary *)args; +@end + + +@implementation GTMGoogleSearch + +GTM_METHOD_CHECK(NSString, gtm_stringByEscapingForURLArgument); + +GTMOBJECT_SINGLETON_BOILERPLATE(GTMGoogleSearch, sharedInstance); + +- (id)init { + self = [super init]; + if (self != nil) { + // register for the notification + NSDistributedNotificationCenter *distCenter = + [NSDistributedNotificationCenter defaultCenter]; + [distCenter addObserver:self + selector:@selector(reloadAllAppCachedValues:) + name:kNotificationName + object:nil]; + + // load the allApps value + [self reloadAllAppCachedValues:nil]; + + // load the cur app value + CFStringRef domain + = CFPreferencesCopyValue(kPreferredDomainPrefKey, + kCFPreferencesCurrentApplication, + kCFPreferencesCurrentUser, + kCFPreferencesAnyHost); + CFStringRef lang = CFPreferencesCopyValue(kPreferredLanguagePrefKey, + kCFPreferencesCurrentApplication, + kCFPreferencesCurrentUser, + kCFPreferencesAnyHost); + + // make sure we got values for both and domain is not empty + if (domain && CFStringGetLength(domain) == 0) { + CFRelease(domain); + domain = nil; + if (lang) { + CFRelease(lang); + lang = nil; + } + } + + curAppCachedDomain_ = GTMNSMakeCollectable(domain); + curAppCachedLanguage_ = GTMNSMakeCollectable(lang); + + NSBundle *bundle = [NSBundle mainBundle]; + + NSDictionary *appArgs + = [bundle objectForInfoDictionaryKey:GTMGoogleSearchClientAppArgsKey]; + globalSearchArguments_ = [[self normalizeGoogleArguments:appArgs] retain]; + } + return self; +} + +- (void)finalize { + [[NSDistributedNotificationCenter defaultCenter] removeObject:self]; + [super finalize]; +} + +- (void)dealloc { + [[NSDistributedNotificationCenter defaultCenter] removeObject:self]; + [allAppsCachedDomain_ release]; + [allAppsCachedLanguage_ release]; + [curAppCachedDomain_ release]; + [curAppCachedLanguage_ release]; + [globalSearchArguments_ release]; + [super dealloc]; +} + +- (void)preferredDomain:(NSString **)domain + language:(NSString**)language + areCurrentAppOnly:(BOOL*)currentAppOnly { + BOOL localCurrentAppOnly = YES; + NSString *localDomain = curAppCachedDomain_; + NSString *localLanguage = curAppCachedLanguage_; + + // if either one wasn't there, drop both, and use any app if we can + if (!localDomain || !localLanguage) { + localCurrentAppOnly = NO; + localDomain = allAppsCachedDomain_; + localLanguage = allAppsCachedLanguage_; + + // if we didn't get anything from the prefs, go with the defaults + if (!localDomain || !localLanguage) { + // if either one wasn't there, drop both, and use defaults + [self defaultDomain:&localDomain language:&localLanguage]; + } + } + if (!localDomain || !localLanguage) { + _GTMDevLog(@"GTMGoogleSearch: Failed to get the preferred domain/language " + @"from prefs or defaults"); + } + if (language) { + *language = [[localLanguage retain] autorelease]; + } + if (domain) { + *domain = [[localDomain retain] autorelease]; + } + if (currentAppOnly) { + *currentAppOnly = localCurrentAppOnly; + } +} + +- (void)updatePreferredDomain:(NSString*)domain + language:(NSString*)language + currentApplicationOnly:(BOOL)currentAppOnly { + // valid inputs? + if (!domain || ![domain length] || !language) { + return; + } + + if (currentAppOnly) { + // if they are the same, don't do anything + if ((domain == nil && curAppCachedDomain_ == nil && + language == nil && curAppCachedLanguage_ == nil) || + ([domain isEqualToString:curAppCachedDomain_] && + [language isEqualToString:curAppCachedLanguage_])) { + return; + } + + // save them out + CFPreferencesSetValue(kPreferredDomainPrefKey, + (CFStringRef)domain, + kCFPreferencesCurrentApplication, + kCFPreferencesCurrentUser, + kCFPreferencesAnyHost); + CFPreferencesSetValue(kPreferredLanguagePrefKey, + (CFStringRef)language, + kCFPreferencesCurrentApplication, + kCFPreferencesCurrentUser, + kCFPreferencesAnyHost); + CFPreferencesSynchronize(kCFPreferencesCurrentApplication, + kCFPreferencesCurrentUser, + kCFPreferencesAnyHost); + // update our locals + [curAppCachedDomain_ release]; + [curAppCachedLanguage_ release]; + curAppCachedDomain_ = [domain copy]; + curAppCachedLanguage_ = [language copy]; + } else { + // Set the "any application" values + [self updateAllAppsDomain:domain language:language]; + + // Clear the current application values (if there were any) + [self clearPreferredDomainAndLanguageForCurrentApplication]; + } +} + +- (void)clearPreferredDomainAndLanguageForCurrentApplication { + // flush what's in the file + CFPreferencesSetValue(kPreferredDomainPrefKey, + NULL, + kCFPreferencesCurrentApplication, + kCFPreferencesCurrentUser, + kCFPreferencesAnyHost); + CFPreferencesSetValue(kPreferredLanguagePrefKey, + NULL, + kCFPreferencesCurrentApplication, + kCFPreferencesCurrentUser, + kCFPreferencesAnyHost); + CFPreferencesSynchronize(kCFPreferencesCurrentApplication, + kCFPreferencesCurrentUser, + kCFPreferencesAnyHost); + // clear our locals + [curAppCachedDomain_ release]; + [curAppCachedLanguage_ release]; + curAppCachedDomain_ = nil; + curAppCachedLanguage_ = nil; +} + +- (void)clearPreferredDomainAndLanguageForAllApps { + // nil/nil to clear things out, this will also update our cached values. + [self updateAllAppsDomain:nil language:nil]; +} + +- (NSDictionary *)globalSearchArguments { + return globalSearchArguments_; +} + +- (void)setGlobalSearchArguments:(NSDictionary *)args { + args = [self normalizeGoogleArguments:args]; + [globalSearchArguments_ autorelease]; + globalSearchArguments_ = [args retain]; +} + +- (NSString*)searchURLFor:(NSString*)queryText + ofType:(NSString*)type + arguments:(NSDictionary *)localArgs { + NSString *url = nil; + if (!queryText) { + return nil; + } + + NSString *language; + NSString *domain; + [self preferredDomain:&domain + language:&language + areCurrentAppOnly:NULL]; + + NSMutableDictionary *args + = [NSMutableDictionary dictionaryWithObjectsAndKeys: + @"UTF-8", @"ie", + @"UTF-8", @"oe", + language, @"hl", + [queryText gtm_stringByEscapingForURLArgument], @"q", + nil]; + + NSDictionary *globalSearchArgs = [self globalSearchArguments]; + if (globalSearchArgs) { + [args addEntriesFromDictionary:globalSearchArgs]; + } + if (localArgs) { + localArgs = [self normalizeGoogleArguments:localArgs]; + [args addEntriesFromDictionary:localArgs]; + } + + NSMutableArray *clientArgs = [NSMutableArray array]; + NSString *key; + GTM_FOREACH_KEY(key, args) { + NSString *object = [args objectForKey:key]; + NSString *arg = [NSString stringWithFormat:@"%@=%@", key, object]; + [clientArgs addObject:arg]; + } + NSString *clientArg = [clientArgs componentsJoinedByString:@"&"]; + url = [NSString stringWithFormat:kSearchURLTemplate, + domain, type, clientArg]; + return url; +} + +- (BOOL)performQuery:(NSString*)queryText + ofType:(NSString *)type + arguments:(NSDictionary *)localArgs { + BOOL success = NO; + NSString *urlString = [self searchURLFor:queryText + ofType:type + arguments:localArgs]; + if (urlString) { + NSURL *url = [NSURL URLWithString:urlString]; + if (url) { +#if GTM_IPHONE_SDK + success = [[UIApplication sharedApplication] openURL:url]; +#else // GTM_IPHONE_SDK + success = [[NSWorkspace sharedWorkspace] openURL:url]; +#endif // GTM_IPHONE_SDK + } + } + return success; +} + +@end + + +@implementation GTMGoogleSearch (PrivateMethods) + +- (void)defaultDomain:(NSString**)preferredDomain + language:(NSString**)preferredLanguage { + // must have both + if (!preferredDomain || !preferredLanguage) { + return; + } + + // make sure they are clear to start + *preferredDomain = nil; + *preferredLanguage = nil; + + // loop over their language list trying to find something we have in + // out default table. + + NSUserDefaults* defs = [NSUserDefaults standardUserDefaults]; + NSArray* languages = [defs objectForKey:@"AppleLanguages"]; + // the current locale is only based on what languages the running apps is + // localized to, so we stick that at the end in case we weren't able to + // find anything else as a match, we'll match that. + languages = + [languages arrayByAddingObject:[[NSLocale currentLocale] localeIdentifier]]; + + NSEnumerator *enumerator = [languages objectEnumerator]; + NSString *localeIdentifier; + while ((localeIdentifier = [enumerator nextObject])) { + NSDictionary *localeParts + = [NSLocale componentsFromLocaleIdentifier:localeIdentifier]; + NSString *localeLanguage = [localeParts objectForKey:NSLocaleLanguageCode]; + // we don't use NSLocaleScriptCode for now + NSString *localeCountry = [localeParts objectForKey:NSLocaleCountryCode]; + + LanguageDefaultInfo *scan = kLanguageListDefaultMappingTable; + LanguageDefaultInfo *end = (scan + (sizeof(kLanguageListDefaultMappingTable) + / sizeof(LanguageDefaultInfo))); + // find a match + // check language, and if country is not nil, check that + for ( ; scan < end ; ++scan) { + if ([localeLanguage isEqualToString:scan->language] && + (!(scan->country) || [localeCountry isEqualToString:scan->country])) { + *preferredDomain = scan->defaultDomain; + *preferredLanguage = localeLanguage; + return; // out of here + } + } + } + + *preferredDomain = kDefaultDomain; + *preferredLanguage = kDefaultLanguage; +} + +// -reloadAllAppCachedValues: +// +- (void)reloadAllAppCachedValues:(NSNotification*)notification { + // drop the old... + [allAppsCachedDomain_ release]; + [allAppsCachedLanguage_ release]; + allAppsCachedDomain_ = nil; + allAppsCachedLanguage_ = nil; + + // load the new + CFPreferencesSynchronize(kAllAppsBuildIdentifier, + kCFPreferencesCurrentUser, + kCFPreferencesAnyHost); + CFStringRef domain = CFPreferencesCopyValue(kPreferredDomainPrefKey, + kAllAppsBuildIdentifier, + kCFPreferencesCurrentUser, + kCFPreferencesAnyHost); + CFStringRef lang = CFPreferencesCopyValue(kPreferredLanguagePrefKey, + kAllAppsBuildIdentifier, + kCFPreferencesCurrentUser, + kCFPreferencesAnyHost); + + // make sure we got values for both and domain is not empty + if (domain && CFStringGetLength(domain) == 0) { + CFRelease(domain); + domain = nil; + if (lang) { + CFRelease(lang); + lang = nil; + } + } + + allAppsCachedDomain_ = GTMNSMakeCollectable(domain); + allAppsCachedLanguage_ = GTMNSMakeCollectable(lang); +} + +// -updateAllAppsDomain:language: +// +- (void)updateAllAppsDomain:(NSString*)domain language:(NSString*)language { + // domain and language can be nil to clear the values + + // if they are the same, don't do anything + if ((domain == nil && allAppsCachedDomain_ == nil && + language == nil && allAppsCachedLanguage_ == nil) || + ([domain isEqualToString:allAppsCachedDomain_] && + [language isEqualToString:allAppsCachedLanguage_])) { + return; + } + + // write it to the file + CFPreferencesSetValue(kPreferredDomainPrefKey, + (CFStringRef)domain, + kAllAppsBuildIdentifier, + kCFPreferencesCurrentUser, + kCFPreferencesAnyHost); + CFPreferencesSetValue(kPreferredLanguagePrefKey, + (CFStringRef)language, + kAllAppsBuildIdentifier, + kCFPreferencesCurrentUser, + kCFPreferencesAnyHost); + CFPreferencesSynchronize(kAllAppsBuildIdentifier, + kCFPreferencesCurrentUser, + kCFPreferencesAnyHost); + + // update our values + [allAppsCachedDomain_ release]; + [allAppsCachedLanguage_ release]; + allAppsCachedDomain_ = [domain copy]; + allAppsCachedLanguage_ = [language copy]; + + // NOTE: we'll go ahead and reload when this comes back to ourselves since + // there is a race here if two folks wrote at about the same time. + NSDistributedNotificationCenter *distCenter = + [NSDistributedNotificationCenter defaultCenter]; + [distCenter postNotificationName:kNotificationName + object:nil + userInfo:nil]; +} + +- (NSDictionary *)normalizeGoogleArguments:(NSDictionary *)args { + NSMutableDictionary *outArgs = [NSMutableDictionary dictionary]; + NSString *key; + GTM_FOREACH_KEY(key, args) { + NSString *object = [args objectForKey:key]; + key = [[key gtm_stringByEscapingForURLArgument] lowercaseString]; + object = [object gtm_stringByEscapingForURLArgument]; + [outArgs setObject:object forKey:key]; + } + return outArgs; +} + +@end diff --git a/Foundation/GTMGoogleSearchTest.m b/Foundation/GTMGoogleSearchTest.m new file mode 100644 index 0000000..3e2ec79 --- /dev/null +++ b/Foundation/GTMGoogleSearchTest.m @@ -0,0 +1,180 @@ +// +// GTMGoogleSearchTest.m +// +// Copyright 2006-2009 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. +// + +#import "GTMGoogleSearch.h" +#import "GTMSenTestCase.h" +#import <unistd.h> + +@interface GTMGoogleSearchTest : GTMTestCase +@end + +@implementation GTMGoogleSearchTest + +- (void)testSearches { + typedef struct { + NSString *type; + NSString *expectedPrefix; + } TestSearchDesc; + static TestSearchDesc testSearches[] = { + { GTMGoogleSearchFroogle, @"http://www.google.xxx/products?" }, + { GTMGoogleSearchGroups, @"http://www.google.xxx/groups?" }, + { GTMGoogleSearchImages, @"http://www.google.xxx/images?"}, + { GTMGoogleSearchLocal, @"http://www.google.xxx/local?"}, + { GTMGoogleSearchNews, @"http://www.google.xxx/news?"}, + { GTMGoogleSearchFinance, @"http://www.google.xxx/finance?"}, + { GTMGoogleSearchBooks, @"http://www.google.xxx/books?"}, + { GTMGoogleSearchWeb, @"http://www.google.xxx/search?"}, + }; + + GTMGoogleSearch *googleSearch = [GTMGoogleSearch sharedInstance]; + STAssertNotNil(googleSearch, nil); + + // force the current app values so we aren't at the mercy of the + // global setting the users locale. + [googleSearch updatePreferredDomain:@"xxx" + language:@"yyy" + currentApplicationOnly:TRUE]; + + size_t count = sizeof(testSearches) / sizeof(testSearches[0]); + NSDictionary *globalArgs + = [NSDictionary dictionaryWithObject:@"f" forKey:@"fo o"]; + [googleSearch setGlobalSearchArguments:globalArgs]; + NSDictionary *args = [NSDictionary dictionaryWithObject:@"Ba ba" + forKey:@"BaR"]; + NSString *expectedStrings[] = { + @"oe=UTF-8", @"hl=yyy", @"q=Foobar", + @"fo%20o=f", @"ie=UTF-8", @"bar=Ba%20ba" + }; + for (size_t i = 0; i < count; i++) { + // test building the url + NSString *urlString = [googleSearch searchURLFor:@"Foobar" + ofType:testSearches[i].type + arguments:args]; + STAssertTrue([urlString hasPrefix:testSearches[i].expectedPrefix], + @"Bad URL? URL:%@ Expected Prefix:%@", + urlString, testSearches[i].expectedPrefix); + for (size_t j = 0; + j < sizeof(expectedStrings) / sizeof(expectedStrings[0]); + ++j) { + STAssertGreaterThan([urlString rangeOfString:expectedStrings[j]].length, + (NSUInteger)0, @"URL: %@ expectedString: %@", + urlString, expectedStrings[j]); + } + } + + // clear what we just set for this test + [googleSearch setGlobalSearchArguments:nil]; + [googleSearch clearPreferredDomainAndLanguageForCurrentApplication]; +} + +- (void)testPreferredDefaults { + GTMGoogleSearch *googleSearch = [GTMGoogleSearch sharedInstance]; + STAssertNotNil(googleSearch, nil); + + // hey, we're a unit test, so start by blowing away what we have at the + // app level. + [googleSearch clearPreferredDomainAndLanguageForCurrentApplication]; + + // in theory, we could fetch now and save off what we get to reset at the + // end of this, but we can't tell if that was an "all apps" setting, or if + // it was the default, so...hey, we're a unit test, we'll just stomp what's + // there and clear it out when done... + [googleSearch clearPreferredDomainAndLanguageForAllApps]; + + // make sure the individual accessors work... + + // since they system level default can be set by any app, we just have to + // check for non nil here (also the users locale could control what + // we get if nothing is set). + NSString *domain; + NSString *lang; + // now do a detailed check... + BOOL areCurrentAppOnly = YES; + [googleSearch preferredDomain:&domain + language:&lang + areCurrentAppOnly:&areCurrentAppOnly]; + // should get something for defaults... + STAssertNotNil(domain, nil); + STAssertNotNil(lang, nil); + STAssertFalse(areCurrentAppOnly, nil); + + // test it for "all apps"... + [googleSearch updatePreferredDomain:@"domain" + language:@"lang" + currentApplicationOnly:NO]; + [googleSearch preferredDomain:&domain + language:&lang + areCurrentAppOnly:&areCurrentAppOnly]; + STAssertEqualObjects(domain, @"domain", nil); + STAssertEqualObjects(lang, @"lang", nil); + STAssertFalse(areCurrentAppOnly, nil); + + // test it for this app... + [googleSearch updatePreferredDomain:@"domainThisApp" + language:@"langThisApp" + currentApplicationOnly:YES]; + [googleSearch preferredDomain:&domain + language:&lang + areCurrentAppOnly:&areCurrentAppOnly]; + STAssertEqualObjects(domain, @"domainThisApp", nil); + STAssertEqualObjects(lang, @"langThisApp", nil); + STAssertTrue(areCurrentAppOnly, nil); + + // clear what we just set for this app + [googleSearch clearPreferredDomainAndLanguageForCurrentApplication]; + + // should get back what we set for all apps + [googleSearch preferredDomain:&domain + language:&lang + areCurrentAppOnly:&areCurrentAppOnly]; + STAssertEqualObjects(domain, @"domain", nil); + STAssertEqualObjects(lang, @"lang", nil); + STAssertFalse(areCurrentAppOnly, nil); + + // try changing the value directly in the plist file (as if another app had + // done it) and sending our notification. + [[NSTask launchedTaskWithLaunchPath:@"/usr/bin/defaults" + arguments:[NSArray arrayWithObjects:@"write", + @"com.google.GoogleSearchAllApps", + @"{ \"com.google.PreferredDomain\" = xxx; \"com.google.PreferredLanguage\" = yyy; }", + nil]] waitUntilExit]; + // Sleep for a moment to let things flush (seen rarely as a problem on aharper's machine) + sleep(1); + NSDistributedNotificationCenter *distCenter = + [NSDistributedNotificationCenter defaultCenter]; + [distCenter postNotificationName:@"com.google.GoogleSearchAllApps.prefsWritten" + object:nil + userInfo:nil + options:NSNotificationDeliverImmediately]; + + // Spin the runloop so the notifications fire. + [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:1.0]]; + // did we get what we expected? + [googleSearch preferredDomain:&domain + language:&lang + areCurrentAppOnly:&areCurrentAppOnly]; + STAssertEqualObjects(domain, @"xxx", nil); + STAssertEqualObjects(lang, @"yyy", nil); + STAssertFalse(areCurrentAppOnly, nil); + + // lastly, clean up what we set for all apps to leave the system at the + // default. + [googleSearch clearPreferredDomainAndLanguageForAllApps]; +} + +@end diff --git a/GTM.xcodeproj/project.pbxproj b/GTM.xcodeproj/project.pbxproj index f83228a..aae950d 100644 --- a/GTM.xcodeproj/project.pbxproj +++ b/GTM.xcodeproj/project.pbxproj @@ -185,6 +185,9 @@ 8BEEA90D0DA7446300894774 /* GTMUnitTestingImage.tiff in Resources */ = {isa = PBXBuildFile; fileRef = 8BEEA90A0DA7446300894774 /* GTMUnitTestingImage.tiff */; }; 8BEEA90E0DA7446300894774 /* GTMUnitTestingWindow.tiff in Resources */ = {isa = PBXBuildFile; fileRef = 8BEEA90B0DA7446300894774 /* GTMUnitTestingWindow.tiff */; }; 8BEEA90F0DA7446300894774 /* GTMUnitTestingView.tiff in Resources */ = {isa = PBXBuildFile; fileRef = 8BEEA90C0DA7446300894774 /* GTMUnitTestingView.tiff */; }; + 8BF4D2E60FC7073A009ABC3F /* GTMGoogleSearch.h in Headers */ = {isa = PBXBuildFile; fileRef = 8BF4D2E30FC7073A009ABC3F /* GTMGoogleSearch.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 8BF4D2E70FC7073A009ABC3F /* GTMGoogleSearch.m in Sources */ = {isa = PBXBuildFile; fileRef = 8BF4D2E40FC7073A009ABC3F /* GTMGoogleSearch.m */; }; + 8BF4D2E80FC70751009ABC3F /* GTMGoogleSearchTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 8BF4D2E20FC7073A009ABC3F /* GTMGoogleSearchTest.m */; }; 8BFE13B60FB0F2C0001BE894 /* GTMABAddressBook.h in Headers */ = {isa = PBXBuildFile; fileRef = 8BFE13B00FB0F2B9001BE894 /* GTMABAddressBook.h */; settings = {ATTRIBUTES = (Public, ); }; }; 8BFE13B70FB0F2C0001BE894 /* GTMABAddressBook.m in Sources */ = {isa = PBXBuildFile; fileRef = 8BFE13B10FB0F2B9001BE894 /* GTMABAddressBook.m */; }; 8BFE13ED0FB0F2D8001BE894 /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1058C7B1FEA5585E11CA2CBB /* Cocoa.framework */; }; @@ -496,6 +499,9 @@ 8BEEA90A0DA7446300894774 /* GTMUnitTestingImage.tiff */ = {isa = PBXFileReference; lastKnownFileType = image.tiff; path = GTMUnitTestingImage.tiff; sourceTree = "<group>"; }; 8BEEA90B0DA7446300894774 /* GTMUnitTestingWindow.tiff */ = {isa = PBXFileReference; lastKnownFileType = image.tiff; path = GTMUnitTestingWindow.tiff; sourceTree = "<group>"; }; 8BEEA90C0DA7446300894774 /* GTMUnitTestingView.tiff */ = {isa = PBXFileReference; lastKnownFileType = image.tiff; path = GTMUnitTestingView.tiff; sourceTree = "<group>"; }; + 8BF4D2E20FC7073A009ABC3F /* GTMGoogleSearchTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GTMGoogleSearchTest.m; sourceTree = "<group>"; }; + 8BF4D2E30FC7073A009ABC3F /* GTMGoogleSearch.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GTMGoogleSearch.h; sourceTree = "<group>"; }; + 8BF4D2E40FC7073A009ABC3F /* GTMGoogleSearch.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GTMGoogleSearch.m; sourceTree = "<group>"; }; 8BFE13B00FB0F2B9001BE894 /* GTMABAddressBook.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GTMABAddressBook.h; sourceTree = "<group>"; }; 8BFE13B10FB0F2B9001BE894 /* GTMABAddressBook.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GTMABAddressBook.m; sourceTree = "<group>"; }; 8BFE13B20FB0F2B9001BE894 /* GTMABAddressBook.strings */ = {isa = PBXFileReference; fileEncoding = 10; lastKnownFileType = text.plist.strings; path = GTMABAddressBook.strings; sourceTree = "<group>"; }; @@ -952,6 +958,9 @@ F48FE27E0D198D0E009257D2 /* GTMGeometryUtils.h */, F48FE27F0D198D0E009257D2 /* GTMGeometryUtils.m */, F48FE2800D198D0E009257D2 /* GTMGeometryUtilsTest.m */, + 8BF4D2E30FC7073A009ABC3F /* GTMGoogleSearch.h */, + 8BF4D2E40FC7073A009ABC3F /* GTMGoogleSearch.m */, + 8BF4D2E20FC7073A009ABC3F /* GTMGoogleSearchTest.m */, F47F1D2D0D4914AD00925B8F /* GTMCalculatedRange.h */, F47F1D2E0D4914AD00925B8F /* GTMCalculatedRange.m */, F47F1D2F0D4914AD00925B8F /* GTMCalculatedRangeTest.m */, @@ -1188,6 +1197,7 @@ 8B40994B0F93C5CC00DF540E /* GTMUILocalizer.h in Headers */, 8BFE13B60FB0F2C0001BE894 /* GTMABAddressBook.h in Headers */, 8BD35B910FB22980009058F5 /* GTMNSScanner+JSON.h in Headers */, + 8BF4D2E60FC7073A009ABC3F /* GTMGoogleSearch.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1606,6 +1616,7 @@ 10998F8B0F4B5F1B007F179D /* GTMTransientRootProxyTest.m in Sources */, 108930850F4CCB380018D4A0 /* GTMTransientRootPortProxyTest.m in Sources */, 8BD35B940FB22986009058F5 /* GTMNSScanner+JSONTest.m in Sources */, + 8BF4D2E80FC70751009ABC3F /* GTMGoogleSearchTest.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1671,6 +1682,7 @@ 8B40994C0F93C5CC00DF540E /* GTMUILocalizer.m in Sources */, 8BFE13B70FB0F2C0001BE894 /* GTMABAddressBook.m in Sources */, 8BD35B920FB22980009058F5 /* GTMNSScanner+JSON.m in Sources */, + 8BF4D2E70FC7073A009ABC3F /* GTMGoogleSearch.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/GTMiPhone.xcodeproj/project.pbxproj b/GTMiPhone.xcodeproj/project.pbxproj index 24c7597..d8c2f4e 100644 --- a/GTMiPhone.xcodeproj/project.pbxproj +++ b/GTMiPhone.xcodeproj/project.pbxproj @@ -73,6 +73,8 @@ 8BDA25140E759A6500C9769D /* GTMNSData+zlibTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 8BC047800DAE928A00C2D1CA /* GTMNSData+zlibTest.m */; }; 8BE839890E89C74B00C611B0 /* GTMDebugThreadValidation.m in Sources */ = {isa = PBXBuildFile; fileRef = 8BE839870E89C74A00C611B0 /* GTMDebugThreadValidation.m */; }; 8BE83A660E8B059A00C611B0 /* GTMDebugThreadValidationTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 8BE83A650E8B059A00C611B0 /* GTMDebugThreadValidationTest.m */; }; + 8BF4D4180FC74998009ABC3F /* GTMGoogleSearchTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 8BF4D3E20FC72A46009ABC3F /* GTMGoogleSearchTest.m */; }; + 8BF4D4190FC7499D009ABC3F /* GTMGoogleSearch.m in Sources */ = {isa = PBXBuildFile; fileRef = 8BF4D3E10FC72A46009ABC3F /* GTMGoogleSearch.m */; }; 8BFE15C60FB0F764001BE894 /* GTMABAddressBook.m in Sources */ = {isa = PBXBuildFile; fileRef = 8BFE15C10FB0F764001BE894 /* GTMABAddressBook.m */; }; 8BFE15C70FB0F764001BE894 /* GTMABAddressBook.strings in Resources */ = {isa = PBXBuildFile; fileRef = 8BFE15C20FB0F764001BE894 /* GTMABAddressBook.strings */; }; 8BFE15C80FB0F764001BE894 /* GTMABAddressBookTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 8BFE15C30FB0F764001BE894 /* GTMABAddressBookTest.m */; }; @@ -196,6 +198,9 @@ 8BE839870E89C74A00C611B0 /* GTMDebugThreadValidation.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GTMDebugThreadValidation.m; sourceTree = "<group>"; }; 8BE839880E89C74A00C611B0 /* GTMDebugThreadValidation.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GTMDebugThreadValidation.h; sourceTree = "<group>"; }; 8BE83A650E8B059A00C611B0 /* GTMDebugThreadValidationTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GTMDebugThreadValidationTest.m; sourceTree = "<group>"; }; + 8BF4D3E00FC72A46009ABC3F /* GTMGoogleSearch.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GTMGoogleSearch.h; sourceTree = "<group>"; }; + 8BF4D3E10FC72A46009ABC3F /* GTMGoogleSearch.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GTMGoogleSearch.m; sourceTree = "<group>"; }; + 8BF4D3E20FC72A46009ABC3F /* GTMGoogleSearchTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GTMGoogleSearchTest.m; sourceTree = "<group>"; }; 8BFE15C00FB0F764001BE894 /* GTMABAddressBook.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GTMABAddressBook.h; sourceTree = "<group>"; }; 8BFE15C10FB0F764001BE894 /* GTMABAddressBook.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GTMABAddressBook.m; sourceTree = "<group>"; }; 8BFE15C20FB0F764001BE894 /* GTMABAddressBook.strings */ = {isa = PBXFileReference; fileEncoding = 10; lastKnownFileType = text.plist.strings; path = GTMABAddressBook.strings; sourceTree = "<group>"; }; @@ -320,6 +325,9 @@ F439ADED0DBD3C4000BE9B91 /* GTMGeometryUtils.h */, F439ADEE0DBD3C4000BE9B91 /* GTMGeometryUtils.m */, F439ADEF0DBD3C4000BE9B91 /* GTMGeometryUtilsTest.m */, + 8BF4D3E00FC72A46009ABC3F /* GTMGoogleSearch.h */, + 8BF4D3E10FC72A46009ABC3F /* GTMGoogleSearch.m */, + 8BF4D3E20FC72A46009ABC3F /* GTMGoogleSearchTest.m */, 8B3AA91F0E033624007E31B5 /* GTMHTTPServer.h */, 8B3AA9200E033624007E31B5 /* GTMHTTPServer.m */, 8B3AA9210E033624007E31B5 /* GTMHTTPServerTest.m */, @@ -636,6 +644,8 @@ 8BFE15C80FB0F764001BE894 /* GTMABAddressBookTest.m in Sources */, 8BD35C920FB234E1009058F5 /* GTMNSScanner+JSON.m in Sources */, 8BD35C930FB234E1009058F5 /* GTMNSScanner+JSONTest.m in Sources */, + 8BF4D4180FC74998009ABC3F /* GTMGoogleSearchTest.m in Sources */, + 8BF4D4190FC7499D009ABC3F /* GTMGoogleSearch.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/ReleaseNotes.txt b/ReleaseNotes.txt index 6b3d195..c3e1036 100644 --- a/ReleaseNotes.txt +++ b/ReleaseNotes.txt @@ -297,6 +297,7 @@ Changes since 1.5.1 *WithCompositeNameWithPrefix methods as they can't do diacritic or width insensitive search on Tiger, but this shouldn't affect most users. +- Added GTMGoogleSearch to foundation to make doing google searches easier. Release 1.5.1 Changes since 1.5.0 |