PageRenderTime 149ms CodeModel.GetById 50ms app.highlight 92ms RepoModel.GetById 1ms app.codeStats 0ms

/core/externals/google-toolbox-for-mac/AppKit/GTMGoogleSearch.m

http://macfuse.googlecode.com/
Objective C | 541 lines | 414 code | 58 blank | 69 comment | 56 complexity | 45343677cbdc2b7bc08819044d616336 MD5 | raw file
  1// 
  2// GTMGoogleSearch.m
  3//
  4//  Copyright 2006-2009 Google Inc.
  5//
  6//  Licensed under the Apache License, Version 2.0 (the "License"); you may not
  7//  use this file except in compliance with the License.  You may obtain a copy
  8//  of the License at
  9// 
 10//  http://www.apache.org/licenses/LICENSE-2.0
 11// 
 12//  Unless required by applicable law or agreed to in writing, software
 13//  distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 14//  WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
 15//  License for the specific language governing permissions and limitations under
 16//  the License.
 17//
 18
 19#import "GTMGoogleSearch.h"
 20#import "GTMDefines.h"
 21
 22#if GTM_IPHONE_SDK
 23#import <UIKit/UIKit.h>
 24#else
 25#import <AppKit/AppKit.h>
 26#endif  // GTM_IPHONE_SDK
 27
 28typedef struct {
 29  NSString *language;
 30  NSString *country;
 31  // we don't include a language, we'll use what we get from the OS
 32  NSString *defaultDomain;
 33} LanguageDefaultInfo;
 34
 35//
 36// this is a seed mapping from languages to domains for google search.
 37// this doesn't have to be complete, as it is just a seed.
 38//
 39//
 40static LanguageDefaultInfo kLanguageListDefaultMappingTable[] = {
 41  // order is important, first match is taken
 42  // if country is |nil|, then only language has to match
 43  { @"en", @"US", @"com" },    // english - united states
 44  { @"en", @"GB", @"co.uk" },  // english - united kingdom
 45  { @"en", @"CA", @"ca" },     // english - canada
 46  { @"en", @"AU", @"com.au" }, // english - australia
 47  { @"en", @"NZ", @"com" },    // english - new zealand
 48  { @"en", @"IE", @"ie" },     // english - ireland
 49  { @"en", @"IN", @"co.in" },  // english - india
 50  { @"en", @"PH", @"com.ph" }, // english - philippines
 51  { @"en", @"SG", @"com.sg" }, // english - singapore
 52  { @"en", @"ZA", @"co.za" },  // english - south africa
 53  { @"en", @"IL", @"co.il" },  // english - israel
 54  { @"en", nil  , @"com" },    // english (catch all)
 55  { @"fr", @"CA", @"ca" },     // french - canada
 56  { @"fr", @"CH", @"ch" },   // french - switzerland
 57  { @"fr", nil  , @"fr" },     // france
 58  { @"it", nil  , @"it" },     // italy
 59  { @"de", @"AT", @"at" },     // german - austria
 60  { @"de", nil  , @"de" },     // germany
 61  { @"es", @"MX", @"com.mx" }, // spanish - mexico
 62  { @"es", @"AR", @"com.ar" }, // spanish - argentina
 63  { @"es", @"CL", @"cl" },     // spanish - chile
 64  { @"es", @"CO", @"com.co" }, // spanish - colombia
 65  { @"es", @"PE", @"com.pe" }, // spanish - peru
 66  { @"es", @"VE", @"co.ve" },  // venezuela
 67  { @"es", nil  , @"es" },     // spain
 68  { @"zh", @"TW", @"com.tw" }, // taiwan
 69  { @"zh", @"HK", @"com.hk" }, // hong kong
 70  { @"zh", nil  , @"cn" },    // chinese (catch all)
 71  { @"ja", nil  , @"co.jp" },  // japan
 72  { @"ko", nil  , @"co.kr" },  // korea
 73  { @"nl", @"BE", @"be" },     // dutch - belgium
 74  { @"nl", nil  , @"nl" },     // (dutch) netherlands
 75  { @"ru", nil  , @"ru" },     // russia
 76  { @"pt", @"BZ", @"com.br"},  // portuguese - brazil
 77  { @"pt", nil  , @"pt" },     // portugal
 78  { @"sv", nil  , @"se" },     // sweden
 79  { @"nn", nil  , @"no" },     // norway (two variants)
 80  { @"nb", nil  , @"no" },     // norway (two variants)
 81  { @"da", nil  , @"dk" },     // denmark
 82  { @"fi", nil  , @"fi" },     // finland
 83  { @"bg", nil  , @"bg" },     // bulgaria
 84  { @"hr", nil  , @"hr" },     // croatia
 85  { @"cx", nil  , @"cz" },    // czech republic
 86  { @"el", nil  , @"gr" },     // greece
 87  { @"hu", nil  , @"co.hu" },  // hungary
 88  { @"ro", nil  , @"ro" },     // romania
 89  { @"sk", nil  , @"sk" },     // slovakia
 90  { @"sl", nil  , @"si" },    // slovenia
 91  { @"tr", nil  , @"com.tr" }, // turkey
 92  { @"my", nil  , @"com.my" }, // malaysia
 93  { @"th", nil  , @"co.th" },  // thailand
 94  { @"uk", nil  , @"com.ua" }, // ukraine
 95  { @"vi", nil  , @"com.vn" }, // vietnam
 96  { @"af", nil  , @"com.za" }, // south africa (afrikaans)
 97  { @"hi", nil  , @"co.in" },  // india (hindi)
 98  { @"id", nil  , @"co.id" },  // indonesia 
 99  { @"pl", nil  , @"pl" },     // poland 
100};
101
102// the notification we use for syncing up instances in different processes
103static NSString *const kNotificationName 
104  = @"com.google.GoogleSearchAllApps.prefsWritten";
105
106// this is the bundle id we use for the pref file used for all apps
107static CFStringRef const kAllAppsBuildIdentifier 
108  = CFSTR("com.google.GoogleSearchAllApps");
109
110static CFStringRef const kPreferredDomainPrefKey 
111  = CFSTR("com.google.PreferredDomain");
112static CFStringRef const kPreferredLanguagePrefKey 
113  = CFSTR("com.google.PreferredLanguage");
114
115static NSString *const kDefaultDomain = @"com";
116static NSString *const kDefaultLanguage = @"en";
117
118#define SEARCH_URL_TEMPLATE @"http://www.google.%@/%@?%@"
119
120@interface GTMGoogleSearch (PrivateMethods)
121- (void)defaultDomain:(NSString**)preferedDomain 
122             language:(NSString**)preferredLanguage;
123- (void)reloadAllAppCachedValues:(NSNotification*)notification;
124- (void)updateAllAppsDomain:(NSString*)domain language:(NSString*)language;
125@end
126
127
128@implementation GTMGoogleSearch
129
130+ (GTMGoogleSearch *)sharedInstance {
131  static GTMGoogleSearch *obj;
132  if (!obj) {
133    obj = [[self alloc] init];
134  }
135  return obj;
136}
137
138- (id)init {
139  self = [super init];
140  if (self != nil) {
141#if GTM_GOOGLE_SEARCH_SUPPORTS_DISTRIBUTED_NOTIFICATIONS
142    // register for the notification
143    NSDistributedNotificationCenter *distCenter =
144      [NSDistributedNotificationCenter defaultCenter];
145    [distCenter addObserver:self
146                   selector:@selector(reloadAllAppCachedValues:)
147                       name:kNotificationName
148                     object:nil];
149#endif  // GTM_GOOGLE_SEARCH_SUPPORTS_DISTRIBUTED_NOTIFICATIONS
150    // load the allApps value
151    [self reloadAllAppCachedValues:nil];
152
153    // load the cur app value
154    CFStringRef domain 
155      = CFPreferencesCopyValue(kPreferredDomainPrefKey,
156                               kCFPreferencesCurrentApplication,
157                               kCFPreferencesCurrentUser,
158                               kCFPreferencesAnyHost);
159    CFStringRef lang = CFPreferencesCopyValue(kPreferredLanguagePrefKey,
160                                              kCFPreferencesCurrentApplication,
161                                              kCFPreferencesCurrentUser,
162                                              kCFPreferencesAnyHost);
163
164    // make sure we got values for both and domain is not empty
165    if (domain && CFStringGetLength(domain) == 0) {
166      CFRelease(domain);
167      domain = nil;
168      if (lang) {
169        CFRelease(lang);
170        lang = nil;
171      }
172    }
173    
174    curAppCachedDomain_ = (NSString *)domain;
175    curAppCachedLanguage_ = (NSString *)lang;
176    
177    NSBundle *bundle = [NSBundle mainBundle];
178    
179    NSDictionary *appArgs 
180      = [bundle objectForInfoDictionaryKey:GTMGoogleSearchClientAppArgsKey];
181    globalSearchArguments_ = [appArgs retain];
182  }
183  return self;
184}
185
186#if GTM_GOOGLE_SEARCH_SUPPORTS_DISTRIBUTED_NOTIFICATIONS
187- (void)finalize {
188  [[NSDistributedNotificationCenter defaultCenter] removeObserver:self];
189  [super finalize];
190}
191#endif  // GTM_GOOGLE_SEARCH_SUPPORTS_DISTRIBUTED_NOTIFICATIONS
192
193- (void)dealloc {
194#if GTM_GOOGLE_SEARCH_SUPPORTS_DISTRIBUTED_NOTIFICATIONS
195  [[NSDistributedNotificationCenter defaultCenter] removeObserver:self];
196#endif  // GTM_GOOGLE_SEARCH_SUPPORTS_DISTRIBUTED_NOTIFICATIONS
197  [allAppsCachedDomain_ release];
198  [allAppsCachedLanguage_ release];
199  [curAppCachedDomain_ release];
200  [curAppCachedLanguage_ release];
201  [globalSearchArguments_ release];
202  [super dealloc];
203}
204
205- (void)preferredDomain:(NSString **)domain
206               language:(NSString**)language 
207      areCurrentAppOnly:(BOOL*)currentAppOnly {
208  BOOL localCurrentAppOnly = YES;
209  NSString *localDomain = curAppCachedDomain_;
210  NSString *localLanguage = curAppCachedLanguage_;
211
212  // if either one wasn't there, drop both, and use any app if we can
213  if (!localDomain || !localLanguage) {
214    localCurrentAppOnly = NO;
215    localDomain = allAppsCachedDomain_;
216    localLanguage = allAppsCachedLanguage_;
217
218    // if we didn't get anything from the prefs, go with the defaults
219    if (!localDomain || !localLanguage) {
220      // if either one wasn't there, drop both, and use defaults
221      [self defaultDomain:&localDomain language:&localLanguage];
222    }
223  }
224  if (!localDomain || !localLanguage) {
225    _GTMDevLog(@"GTMGoogleSearch: Failed to get the preferred domain/language "
226               @"from prefs or defaults");
227  }
228  if (language) {
229    *language = [[localLanguage retain] autorelease];
230  }
231  if (domain) {
232    *domain = [[localDomain retain] autorelease];
233  }
234  if (currentAppOnly) {
235    *currentAppOnly = localCurrentAppOnly;
236  }
237}
238
239- (void)updatePreferredDomain:(NSString*)domain
240                     language:(NSString*)language
241       currentApplicationOnly:(BOOL)currentAppOnly {
242  // valid inputs?
243  if (!domain || ![domain length] || !language) {
244    return;
245  }
246
247  if (currentAppOnly) {
248    // if they are the same, don't do anything
249    if ((domain == nil && curAppCachedDomain_ == nil &&
250         language == nil && curAppCachedLanguage_ == nil) ||
251        ([domain isEqualToString:curAppCachedDomain_] &&
252         [language isEqualToString:curAppCachedLanguage_])) {
253      return;
254    }
255
256    // save them out
257    CFPreferencesSetValue(kPreferredDomainPrefKey,
258                          (CFStringRef)domain,
259                           kCFPreferencesCurrentApplication,
260                           kCFPreferencesCurrentUser,
261                           kCFPreferencesAnyHost);
262    CFPreferencesSetValue(kPreferredLanguagePrefKey,
263                           (CFStringRef)language,
264                           kCFPreferencesCurrentApplication,
265                           kCFPreferencesCurrentUser,
266                           kCFPreferencesAnyHost);
267    CFPreferencesSynchronize(kCFPreferencesCurrentApplication,
268                             kCFPreferencesCurrentUser,
269                             kCFPreferencesAnyHost);
270    // update our locals
271    [curAppCachedDomain_ release];
272    [curAppCachedLanguage_ release];
273    curAppCachedDomain_ = [domain copy];
274    curAppCachedLanguage_ = [language copy];
275  } else {
276    // Set the "any application" values
277    [self updateAllAppsDomain:domain language:language];
278
279    // Clear the current application values (if there were any)
280    [self clearPreferredDomainAndLanguageForCurrentApplication];
281  }
282}
283
284- (void)clearPreferredDomainAndLanguageForCurrentApplication {
285  // flush what's in the file
286  CFPreferencesSetValue(kPreferredDomainPrefKey,
287                        NULL,
288                        kCFPreferencesCurrentApplication,
289                        kCFPreferencesCurrentUser,
290                        kCFPreferencesAnyHost);
291  CFPreferencesSetValue(kPreferredLanguagePrefKey,
292                        NULL,
293                        kCFPreferencesCurrentApplication,
294                        kCFPreferencesCurrentUser,
295                        kCFPreferencesAnyHost);
296  CFPreferencesSynchronize(kCFPreferencesCurrentApplication,
297                           kCFPreferencesCurrentUser,
298                           kCFPreferencesAnyHost);
299  // clear our locals
300  [curAppCachedDomain_ release];
301  [curAppCachedLanguage_ release];
302  curAppCachedDomain_ = nil;
303  curAppCachedLanguage_ = nil;
304}
305
306- (void)clearPreferredDomainAndLanguageForAllApps {
307  // nil/nil to clear things out, this will also update our cached values.
308  [self updateAllAppsDomain:nil language:nil];
309}
310
311- (NSDictionary *)globalSearchArguments {
312  return globalSearchArguments_;
313}
314
315- (void)setGlobalSearchArguments:(NSDictionary *)args {
316  [globalSearchArguments_ autorelease];
317  globalSearchArguments_ = [args copy];
318}
319
320- (NSString*)searchURLFor:(NSString*)queryText
321                   ofType:(NSString*)type 
322                arguments:(NSDictionary *)localArgs {
323  if (!type) {
324    return nil;
325  }
326  
327  NSString *language;
328  NSString *domain;
329  [self preferredDomain:&domain
330               language:&language
331      areCurrentAppOnly:NULL];
332
333  NSMutableDictionary *args 
334    = [NSMutableDictionary dictionaryWithObjectsAndKeys:
335       @"UTF-8", @"ie",
336       @"UTF-8", @"oe",
337       language, @"hl", 
338       nil];
339  if (queryText) {
340    [args setObject:queryText forKey:@"q"];
341  }
342  
343  NSDictionary *globalSearchArgs = [self globalSearchArguments];
344  if (globalSearchArgs) {
345    [args addEntriesFromDictionary:globalSearchArgs];
346  }
347  if (localArgs) {
348    [args addEntriesFromDictionary:localArgs];
349  }
350  
351  NSMutableArray *clientArgs = [NSMutableArray array];
352  NSString *key;
353  NSNull *nsNull = [NSNull null];
354  GTM_FOREACH_KEY(key, args) {
355    NSString *object = [args objectForKey:key];
356    if (![object isEqual:nsNull]) {
357#if DEBUG
358      // In debug we check key and object for things that should be escaped.
359      // Note that percent is not in there because escaped strings will have
360      // percents in them
361      NSCharacterSet *cs = [NSCharacterSet characterSetWithCharactersInString:
362                            @"!*'();:@&=+$,/?#[] "];
363      NSRange range = [key rangeOfCharacterFromSet:cs];
364      if (range.location != NSNotFound) {
365        _GTMDevLog(@"Unescaped string %@ in argument pair {%@, %@} in -[%@ %@]", 
366                   key, key, object, [self class], NSStringFromSelector(_cmd));
367      }
368      range = [object rangeOfCharacterFromSet:cs];
369      if (range.location != NSNotFound) {
370        _GTMDevLog(@"Unescaped string %@ in argument pair {%@,%@ } in -[%@ %@]",
371                   object, key, object, [self class], 
372                   NSStringFromSelector(_cmd));
373      }
374#endif  // DEBUG
375      NSString *arg = [NSString stringWithFormat:@"%@=%@", key, object];
376      [clientArgs addObject:arg];
377    }
378  }
379  NSString *clientArg = [clientArgs componentsJoinedByString:@"&"];
380  NSString *url = [NSString stringWithFormat:SEARCH_URL_TEMPLATE,
381                   domain, type, clientArg];
382  return url;
383}
384
385- (BOOL)performQuery:(NSString*)queryText
386              ofType:(NSString *)type
387           arguments:(NSDictionary *)localArgs {
388  BOOL success = NO;
389  NSString *urlString = [self searchURLFor:queryText 
390                                    ofType:type 
391                                 arguments:localArgs];
392  if (urlString) {
393    NSURL *url = [NSURL URLWithString:urlString];
394    if (url) {
395#if GTM_IPHONE_SDK
396      success = [[UIApplication sharedApplication] openURL:url];
397#else  // GTM_IPHONE_SDK
398      success = [[NSWorkspace sharedWorkspace] openURL:url];
399#endif  // GTM_IPHONE_SDK
400    }
401  }
402  return success;
403}
404
405@end
406
407
408@implementation GTMGoogleSearch (PrivateMethods)
409
410- (void)defaultDomain:(NSString**)preferredDomain 
411             language:(NSString**)preferredLanguage {
412  // must have both
413  if (!preferredDomain || !preferredLanguage) {
414    return;
415  }
416
417  // make sure they are clear to start
418  *preferredDomain = nil;
419  *preferredLanguage = nil;
420
421  // loop over their language list trying to find something we have in
422  // out default table.
423
424  NSUserDefaults* defs = [NSUserDefaults standardUserDefaults];
425  NSArray* languages = [defs objectForKey:@"AppleLanguages"];
426  // the current locale is only based on what languages the running apps is
427  // localized to, so we stick that at the end in case we weren't able to
428  // find anything else as a match, we'll match that.
429  languages =
430    [languages arrayByAddingObject:[[NSLocale currentLocale] localeIdentifier]];
431
432  NSEnumerator *enumerator = [languages objectEnumerator];
433  NSString *localeIdentifier;
434  while ((localeIdentifier = [enumerator nextObject])) {
435    NSDictionary *localeParts 
436      = [NSLocale componentsFromLocaleIdentifier:localeIdentifier];
437    NSString *localeLanguage = [localeParts objectForKey:NSLocaleLanguageCode];
438    // we don't use NSLocaleScriptCode for now
439    NSString *localeCountry = [localeParts objectForKey:NSLocaleCountryCode];
440
441    LanguageDefaultInfo *scan = kLanguageListDefaultMappingTable;
442    LanguageDefaultInfo *end = (scan + (sizeof(kLanguageListDefaultMappingTable) 
443                                        / sizeof(LanguageDefaultInfo)));
444    // find a match
445    // check language, and if country is not nil, check that
446    for ( ; scan < end ; ++scan) {
447      if ([localeLanguage isEqualToString:scan->language] &&
448          (!(scan->country) || [localeCountry isEqualToString:scan->country])) {
449        *preferredDomain = scan->defaultDomain;
450        *preferredLanguage = localeLanguage;
451        return; // out of here
452      }
453    }
454  }
455
456  *preferredDomain = kDefaultDomain;
457  *preferredLanguage = kDefaultLanguage;
458}
459
460// -reloadAllAppCachedValues:
461//
462- (void)reloadAllAppCachedValues:(NSNotification*)notification {
463  // drop the old...
464  [allAppsCachedDomain_ release];
465  [allAppsCachedLanguage_ release];
466  allAppsCachedDomain_ = nil;
467  allAppsCachedLanguage_ = nil;
468
469  // load the new
470  CFPreferencesSynchronize(kAllAppsBuildIdentifier,
471                           kCFPreferencesCurrentUser,
472                           kCFPreferencesAnyHost);
473  CFStringRef domain = CFPreferencesCopyValue(kPreferredDomainPrefKey,
474                                              kAllAppsBuildIdentifier,
475                                              kCFPreferencesCurrentUser,
476                                              kCFPreferencesAnyHost);
477  CFStringRef lang = CFPreferencesCopyValue(kPreferredLanguagePrefKey,
478                                            kAllAppsBuildIdentifier,
479                                            kCFPreferencesCurrentUser,
480                                            kCFPreferencesAnyHost);
481
482  // make sure we got values for both and domain is not empty
483  if (domain && CFStringGetLength(domain) == 0) {
484    CFRelease(domain);
485    domain = nil;
486    if (lang) {
487      CFRelease(lang);
488      lang = nil;
489    }
490  }
491
492  allAppsCachedDomain_ = (NSString *)domain;
493  allAppsCachedLanguage_ = (NSString *)lang;
494}
495
496// -updateAllAppsDomain:language:
497//
498- (void)updateAllAppsDomain:(NSString*)domain language:(NSString*)language {
499  // domain and language can be nil to clear the values
500
501  // if they are the same, don't do anything
502  if ((domain == nil && allAppsCachedDomain_ == nil &&
503       language == nil && allAppsCachedLanguage_ == nil) ||
504      ([domain isEqualToString:allAppsCachedDomain_] &&
505       [language isEqualToString:allAppsCachedLanguage_])) {
506    return;
507  }
508
509  // write it to the file
510  CFPreferencesSetValue(kPreferredDomainPrefKey,
511                        (CFStringRef)domain,
512                        kAllAppsBuildIdentifier,
513                        kCFPreferencesCurrentUser,
514                        kCFPreferencesAnyHost);
515  CFPreferencesSetValue(kPreferredLanguagePrefKey,
516                        (CFStringRef)language,
517                        kAllAppsBuildIdentifier,
518                        kCFPreferencesCurrentUser,
519                        kCFPreferencesAnyHost);
520  CFPreferencesSynchronize(kAllAppsBuildIdentifier,
521                           kCFPreferencesCurrentUser,
522                           kCFPreferencesAnyHost);
523
524  // update our values
525  [allAppsCachedDomain_ release];
526  [allAppsCachedLanguage_ release];
527  allAppsCachedDomain_ = [domain copy];
528  allAppsCachedLanguage_ = [language copy];
529
530#if GTM_GOOGLE_SEARCH_SUPPORTS_DISTRIBUTED_NOTIFICATIONS
531  // NOTE: we'll go ahead and reload when this comes back to ourselves since
532  // there is a race here if two folks wrote at about the same time.
533  NSDistributedNotificationCenter *distCenter =
534    [NSDistributedNotificationCenter defaultCenter];
535  [distCenter postNotificationName:kNotificationName
536                            object:nil
537                          userInfo:nil];
538#endif  // GTM_GOOGLE_SEARCH_SUPPORTS_DISTRIBUTED_NOTIFICATIONS
539}
540
541@end