PageRenderTime 94ms CodeModel.GetById 15ms app.highlight 73ms RepoModel.GetById 1ms app.codeStats 0ms

/core/externals/update-engine/externals/gdata-objectivec-client/Source/HTTPFetcher/GTMHTTPFetchHistory.m

http://macfuse.googlecode.com/
Objective C | 612 lines | 394 code | 121 blank | 97 comment | 69 complexity | 596ba1fc2a20d7227a3c75ef8c13196c MD5 | raw file
  1/* Copyright (c) 2010 Google Inc.
  2 *
  3 * Licensed under the Apache License, Version 2.0 (the "License");
  4 * you may not use this file except in compliance with the License.
  5 * You may obtain a copy of the License at
  6 *
  7 *     http://www.apache.org/licenses/LICENSE-2.0
  8 *
  9 * Unless required by applicable law or agreed to in writing, software
 10 * distributed under the License is distributed on an "AS IS" BASIS,
 11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 12 * See the License for the specific language governing permissions and
 13 * limitations under the License.
 14 */
 15
 16//
 17//  GTMHTTPFetchHistory.m
 18//
 19
 20#import "GTMHTTPFetchHistory.h"
 21
 22const NSTimeInterval kCachedURLReservationInterval = 60.0; // 1 minute
 23static NSString* const kGTMIfNoneMatchHeader = @"If-None-Match";
 24static NSString* const kGTMETagHeader = @"Etag";
 25
 26#if GTM_IPHONE
 27// iPhone: up to 1MB memory
 28const NSUInteger kGTMDefaultETaggedDataCacheMemoryCapacity = 1 * 1024 * 1024;
 29#else
 30// Mac OS X: up to 15MB memory
 31const NSUInteger kGTMDefaultETaggedDataCacheMemoryCapacity = 15 * 1024 * 1024;
 32#endif
 33
 34
 35@implementation GTMCookieStorage
 36
 37- (id)init {
 38  self = [super init];
 39  if (self != nil) {
 40    cookies_ = [[NSMutableArray alloc] init];
 41  }
 42  return self;
 43}
 44
 45- (void)dealloc {
 46  [cookies_ release];
 47  [super dealloc];
 48}
 49
 50// Add all cookies in the new cookie array to the storage,
 51// replacing stored cookies as appropriate.
 52//
 53// Side effect: removes expired cookies from the storage array.
 54- (void)setCookies:(NSArray *)newCookies {
 55
 56  @synchronized(cookies_) {
 57    [self removeExpiredCookies];
 58
 59    for (NSHTTPCookie *newCookie in newCookies) {
 60      if ([[newCookie name] length] > 0
 61          && [[newCookie domain] length] > 0
 62          && [[newCookie path] length] > 0) {
 63
 64        // remove the cookie if it's currently in the array
 65        NSHTTPCookie *oldCookie = [self cookieMatchingCookie:newCookie];
 66        if (oldCookie) {
 67          [cookies_ removeObjectIdenticalTo:oldCookie];
 68        }
 69
 70        // make sure the cookie hasn't already expired
 71        NSDate *expiresDate = [newCookie expiresDate];
 72        if ((!expiresDate) || [expiresDate timeIntervalSinceNow] > 0) {
 73          [cookies_ addObject:newCookie];
 74        }
 75
 76      } else {
 77        NSAssert1(NO, @"Cookie incomplete: %@", newCookie);
 78      }
 79    }
 80  }
 81}
 82
 83- (void)deleteCookie:(NSHTTPCookie *)cookie {
 84  @synchronized(cookies_) {
 85    NSHTTPCookie *foundCookie = [self cookieMatchingCookie:cookie];
 86    if (foundCookie) {
 87      [cookies_ removeObjectIdenticalTo:foundCookie];
 88    }
 89  }
 90}
 91
 92// Retrieve all cookies appropriate for the given URL, considering
 93// domain, path, cookie name, expiration, security setting.
 94// Side effect: removed expired cookies from the storage array.
 95- (NSArray *)cookiesForURL:(NSURL *)theURL {
 96
 97  NSMutableArray *foundCookies = nil;
 98
 99  @synchronized(cookies_) {
100    [self removeExpiredCookies];
101
102    // We'll prepend "." to the desired domain, since we want the
103    // actual domain "nytimes.com" to still match the cookie domain
104    // ".nytimes.com" when we check it below with hasSuffix.
105    NSString *host = [[theURL host] lowercaseString];
106    NSString *path = [theURL path];
107    NSString *scheme = [theURL scheme];
108
109    NSString *domain = nil;
110    BOOL isLocalhostRetrieval = NO;
111
112    if ([host isEqual:@"localhost"]) {
113      isLocalhostRetrieval = YES;
114    } else {
115      if (host) {
116        domain = [@"." stringByAppendingString:host];
117      }
118    }
119
120    NSUInteger numberOfCookies = [cookies_ count];
121    for (NSUInteger idx = 0; idx < numberOfCookies; idx++) {
122
123      NSHTTPCookie *storedCookie = [cookies_ objectAtIndex:idx];
124
125      NSString *cookieDomain = [[storedCookie domain] lowercaseString];
126      NSString *cookiePath = [storedCookie path];
127      BOOL cookieIsSecure = [storedCookie isSecure];
128
129      BOOL isDomainOK;
130
131      if (isLocalhostRetrieval) {
132        // prior to 10.5.6, the domain stored into NSHTTPCookies for localhost
133        // is "localhost.local"
134        isDomainOK = [cookieDomain isEqual:@"localhost"]
135          || [cookieDomain isEqual:@"localhost.local"];
136      } else {
137        isDomainOK = [domain hasSuffix:cookieDomain];
138      }
139
140      BOOL isPathOK = [cookiePath isEqual:@"/"] || [path hasPrefix:cookiePath];
141      BOOL isSecureOK = (!cookieIsSecure) || [scheme isEqual:@"https"];
142
143      if (isDomainOK && isPathOK && isSecureOK) {
144        if (foundCookies == nil) {
145          foundCookies = [NSMutableArray arrayWithCapacity:1];
146        }
147        [foundCookies addObject:storedCookie];
148      }
149    }
150  }
151  return foundCookies;
152}
153
154// Return a cookie from the array with the same name, domain, and path as the
155// given cookie, or else return nil if none found.
156//
157// Both the cookie being tested and all cookies in the storage array should
158// be valid (non-nil name, domains, paths).
159//
160// Note: this should only be called from inside a @synchronized(cookies_) block
161- (NSHTTPCookie *)cookieMatchingCookie:(NSHTTPCookie *)cookie {
162
163  NSUInteger numberOfCookies = [cookies_ count];
164  NSString *name = [cookie name];
165  NSString *domain = [cookie domain];
166  NSString *path = [cookie path];
167
168  NSAssert3(name && domain && path, @"Invalid cookie (name:%@ domain:%@ path:%@)",
169            name, domain, path);
170
171  for (NSUInteger idx = 0; idx < numberOfCookies; idx++) {
172
173    NSHTTPCookie *storedCookie = [cookies_ objectAtIndex:idx];
174
175    if ([[storedCookie name] isEqual:name]
176        && [[storedCookie domain] isEqual:domain]
177        && [[storedCookie path] isEqual:path]) {
178
179      return storedCookie;
180    }
181  }
182  return nil;
183}
184
185
186// Internal routine to remove any expired cookies from the array, excluding
187// cookies with nil expirations.
188//
189// Note: this should only be called from inside a @synchronized(cookies_) block
190- (void)removeExpiredCookies {
191
192  // count backwards since we're deleting items from the array
193  for (NSInteger idx = (NSInteger)[cookies_ count] - 1; idx >= 0; idx--) {
194
195    NSHTTPCookie *storedCookie = [cookies_ objectAtIndex:(NSUInteger)idx];
196
197    NSDate *expiresDate = [storedCookie expiresDate];
198    if (expiresDate && [expiresDate timeIntervalSinceNow] < 0) {
199      [cookies_ removeObjectAtIndex:(NSUInteger)idx];
200    }
201  }
202}
203
204- (void)removeAllCookies {
205  @synchronized(cookies_) {
206    [cookies_ removeAllObjects];
207  }
208}
209@end
210
211//
212// GTMCachedURLResponse
213//
214
215@implementation GTMCachedURLResponse
216
217@synthesize response = response_;
218@synthesize data = data_;
219@synthesize reservationDate = reservationDate_;
220@synthesize useDate = useDate_;
221
222- (id)initWithResponse:(NSURLResponse *)response data:(NSData *)data {
223  self = [super init];
224  if (self != nil) {
225    response_ = [response retain];
226    data_ = [data retain];
227    useDate_ = [[NSDate alloc] init];
228  }
229  return self;
230}
231
232- (void)dealloc {
233  [response_ release];
234  [data_ release];
235  [useDate_ release];
236  [reservationDate_ release];
237  [super dealloc];
238}
239
240- (NSString *)description {
241  NSString *reservationStr = reservationDate_ ?
242    [NSString stringWithFormat:@" resDate:%@", reservationDate_] : @"";
243
244  return [NSString stringWithFormat:@"%@ %p: {bytes:%@ useDate:%@%@}",
245          [self class], self,
246          data_ ? [NSNumber numberWithInt:(int)[data_ length]] : nil,
247          useDate_,
248          reservationStr];
249}
250
251- (NSComparisonResult)compareUseDate:(GTMCachedURLResponse *)other {
252  return [useDate_ compare:[other useDate]];
253}
254
255@end
256
257//
258// GTMURLCache
259//
260
261@implementation GTMURLCache
262
263@dynamic memoryCapacity;
264
265- (id)init {
266  return [self initWithMemoryCapacity:kGTMDefaultETaggedDataCacheMemoryCapacity];
267}
268
269- (id)initWithMemoryCapacity:(NSUInteger)totalBytes {
270  self = [super init];
271  if (self != nil) {
272    memoryCapacity_ = totalBytes;
273
274    responses_ = [[NSMutableDictionary alloc] initWithCapacity:5];
275
276    reservationInterval_ = kCachedURLReservationInterval;
277  }
278  return self;
279}
280
281- (void)dealloc {
282  [responses_ release];
283  [super dealloc];
284}
285
286- (NSString *)description {
287  return [NSString stringWithFormat:@"%@ %p: {responses:%@}",
288          [self class], self, [responses_ allValues]];
289}
290
291// Setters/getters
292
293- (void)pruneCacheResponses {
294  // Internal routine to remove the least-recently-used responses when the
295  // cache has grown too large
296  if (memoryCapacity_ >= totalDataSize_) return;
297
298  // Sort keys by date
299  SEL sel = @selector(compareUseDate:);
300  NSArray *sortedKeys = [responses_ keysSortedByValueUsingSelector:sel];
301
302  // The least-recently-used keys are at the beginning of the sorted array;
303  // remove those (except ones still reserved) until the total data size is
304  // reduced sufficiently
305  for (NSURL *key in sortedKeys) {
306    GTMCachedURLResponse *response = [responses_ objectForKey:key];
307
308    NSDate *resDate = [response reservationDate];
309    BOOL isResponseReserved = (resDate != nil)
310      && ([resDate timeIntervalSinceNow] > -reservationInterval_);
311
312    if (!isResponseReserved) {
313      // We can remove this response from the cache
314      NSUInteger storedSize = [[response data] length];
315      totalDataSize_ -= storedSize;
316      [responses_ removeObjectForKey:key];
317    }
318
319    // If we've removed enough response data, then we're done
320    if (memoryCapacity_ >= totalDataSize_) break;
321  }
322}
323
324- (void)storeCachedResponse:(GTMCachedURLResponse *)cachedResponse
325                 forRequest:(NSURLRequest *)request {
326  @synchronized(self) {
327    // Remove any previous entry for this request
328    [self removeCachedResponseForRequest:request];
329
330    // cache this one only if it's not bigger than our cache
331    NSUInteger storedSize = [[cachedResponse data] length];
332    if (storedSize < memoryCapacity_) {
333
334      NSURL *key = [request URL];
335      [responses_ setObject:cachedResponse forKey:key];
336      totalDataSize_ += storedSize;
337
338      [self pruneCacheResponses];
339    }
340  }
341}
342
343- (GTMCachedURLResponse *)cachedResponseForRequest:(NSURLRequest *)request {
344  GTMCachedURLResponse *response;
345
346  @synchronized(self) {
347    NSURL *key = [request URL];
348    response = [[[responses_ objectForKey:key] retain] autorelease];
349
350    // Touch the date to indicate this was recently retrieved
351    [response setUseDate:[NSDate date]];
352  }
353  return response;
354}
355
356- (void)removeCachedResponseForRequest:(NSURLRequest *)request {
357  @synchronized(self) {
358    NSURL *key = [request URL];
359    totalDataSize_ -= [[[responses_ objectForKey:key] data] length];
360    [responses_ removeObjectForKey:key];
361  }
362}
363
364- (void)removeAllCachedResponses {
365  @synchronized(self) {
366    [responses_ removeAllObjects];
367    totalDataSize_ = 0;
368  }
369}
370
371- (NSUInteger)memoryCapacity {
372  return memoryCapacity_;
373}
374
375- (void)setMemoryCapacity:(NSUInteger)totalBytes {
376  @synchronized(self) {
377    BOOL didShrink = (totalBytes < memoryCapacity_);
378    memoryCapacity_ = totalBytes;
379
380    if (didShrink) {
381      [self pruneCacheResponses];
382    }
383  }
384}
385
386// Methods for unit testing.
387- (void)setReservationInterval:(NSTimeInterval)secs {
388  reservationInterval_ = secs;
389}
390
391- (NSDictionary *)responses {
392  return responses_;
393}
394
395- (NSUInteger)totalDataSize {
396  return totalDataSize_;
397}
398
399@end
400
401//
402// GTMHTTPFetchHistory
403//
404
405@interface GTMHTTPFetchHistory ()
406- (NSString *)cachedETagForRequest:(NSURLRequest *)request;
407- (void)removeCachedDataForRequest:(NSURLRequest *)request;
408@end
409
410@implementation GTMHTTPFetchHistory
411
412@synthesize cookieStorage = cookieStorage_;
413
414@dynamic shouldRememberETags;
415@dynamic shouldCacheETaggedData;
416@dynamic memoryCapacity;
417
418- (id)init {
419 return [self initWithMemoryCapacity:kGTMDefaultETaggedDataCacheMemoryCapacity
420              shouldCacheETaggedData:NO];
421}
422
423- (id)initWithMemoryCapacity:(NSUInteger)totalBytes
424      shouldCacheETaggedData:(BOOL)shouldCacheETaggedData {
425  self = [super init];
426  if (self != nil) {
427    etaggedDataCache_ = [[GTMURLCache alloc] initWithMemoryCapacity:totalBytes];
428    shouldRememberETags_ = shouldCacheETaggedData;
429    shouldCacheETaggedData_ = shouldCacheETaggedData;
430    cookieStorage_ = [[GTMCookieStorage alloc] init];
431  }
432  return self;
433}
434
435- (void)dealloc {
436  [etaggedDataCache_ release];
437  [cookieStorage_ release];
438  [super dealloc];
439}
440
441- (void)updateRequest:(NSMutableURLRequest *)request isHTTPGet:(BOOL)isHTTPGet {
442  @synchronized(self) {
443    if ([self shouldRememberETags]) {
444      // If this URL is in the history, and no ETag has been set, then
445      // set the ETag header field
446
447      // If we have a history, we're tracking across fetches, so we don't
448      // want to pull results from any other cache
449      [request setCachePolicy:NSURLRequestReloadIgnoringCacheData];
450
451      if (isHTTPGet) {
452        // We'll only add an ETag if there's no ETag specified in the user's
453        // request
454        NSString *specifiedETag = [request valueForHTTPHeaderField:kGTMIfNoneMatchHeader];
455        if (specifiedETag == nil) {
456          // No ETag: extract the previous ETag for this request from the
457          // fetch history, and add it to the request
458          NSString *cachedETag = [self cachedETagForRequest:request];
459
460          if (cachedETag != nil) {
461            [request addValue:cachedETag forHTTPHeaderField:kGTMIfNoneMatchHeader];
462          }
463        } else {
464          // Has an ETag: remove any stored response in the fetch history
465          // for this request, as the If-None-Match header could lead to
466          // a 304 Not Modified, and we want that error delivered to the
467          // user since they explicitly specified the ETag
468          [self removeCachedDataForRequest:request];
469        }
470      }
471    }
472  }
473}
474
475- (void)updateFetchHistoryWithRequest:(NSURLRequest *)request
476                             response:(NSURLResponse *)response
477                       downloadedData:(NSData *)downloadedData {
478  @synchronized(self) {
479    if (![self shouldRememberETags]) return;
480
481    if (![response respondsToSelector:@selector(allHeaderFields)]) return;
482
483    NSInteger statusCode = [(NSHTTPURLResponse *)response statusCode];
484
485    if (statusCode != kGTMHTTPFetcherStatusNotModified) {
486      // Save this ETag string for successful results (<300)
487      // If there's no last modified string, clear the dictionary
488      // entry for this URL. Also cache or delete the data, if appropriate
489      // (when etaggedDataCache is non-nil.)
490      NSDictionary *headers = [(NSHTTPURLResponse *)response allHeaderFields];
491      NSString* etag = [headers objectForKey:kGTMETagHeader];
492
493      if (etag != nil && statusCode < 300) {
494
495        // we want to cache responses for the headers, even if the client
496        // doesn't want the response body data caches
497        NSData *dataToStore = shouldCacheETaggedData_ ? downloadedData : nil;
498
499        GTMCachedURLResponse *cachedResponse;
500        cachedResponse = [[[GTMCachedURLResponse alloc] initWithResponse:response
501                                                                      data:dataToStore] autorelease];
502        [etaggedDataCache_ storeCachedResponse:cachedResponse
503                                  forRequest:request];
504      } else {
505        [etaggedDataCache_ removeCachedResponseForRequest:request];
506      }
507    }
508  }
509}
510
511- (NSString *)cachedETagForRequest:(NSURLRequest *)request {
512  // Internal routine.
513  GTMCachedURLResponse *cachedResponse;
514  cachedResponse = [etaggedDataCache_ cachedResponseForRequest:request];
515
516  NSURLResponse *response = [cachedResponse response];
517  NSDictionary *headers = [(NSHTTPURLResponse *)response allHeaderFields];
518  NSString *cachedETag = [headers objectForKey:kGTMETagHeader];
519  if (cachedETag) {
520    // Since the request having an ETag implies this request is about
521    // to be fetched again, reserve the cached response to ensure that
522    // that it will be around at least until the fetch completes.
523    //
524    // When the fetch completes, either the cached response will be replaced
525    // with a new response, or the cachedDataForRequest: method below will
526    // clear the reservation.
527    [cachedResponse setReservationDate:[NSDate date]];
528  }
529  return cachedETag;
530}
531
532- (NSData *)cachedDataForRequest:(NSURLRequest *)request {
533  @synchronized(self) {
534    GTMCachedURLResponse *cachedResponse;
535    cachedResponse = [etaggedDataCache_ cachedResponseForRequest:request];
536
537    NSData *cachedData = [cachedResponse data];
538
539    // Since the data for this cached request is being obtained from the cache,
540    // we can clear the reservation as the fetch has completed.
541    [cachedResponse setReservationDate:nil];
542
543    return cachedData;
544  }
545}
546
547- (void)removeCachedDataForRequest:(NSURLRequest *)request {
548  @synchronized(self) {
549    [etaggedDataCache_ removeCachedResponseForRequest:request];
550  }
551}
552
553- (void)clearETaggedDataCache {
554  @synchronized(self) {
555    [etaggedDataCache_ removeAllCachedResponses];
556  }
557}
558
559- (void)clearHistory {
560  @synchronized(self) {
561    [self clearETaggedDataCache];
562    [cookieStorage_ removeAllCookies];
563  }
564}
565
566- (void)removeAllCookies {
567  @synchronized(self) {
568    [cookieStorage_ removeAllCookies];
569  }
570}
571
572- (BOOL)shouldRememberETags {
573  return shouldRememberETags_;
574}
575
576- (void)setShouldRememberETags:(BOOL)flag {
577  BOOL wasRemembering = shouldRememberETags_;
578  shouldRememberETags_ = flag;
579
580  if (wasRemembering && !flag) {
581    // Free up the cache memory
582    [self clearETaggedDataCache];
583  }
584}
585
586- (BOOL)shouldCacheETaggedData {
587  return shouldCacheETaggedData_;
588}
589
590- (void)setShouldCacheETaggedData:(BOOL)flag {
591  BOOL wasCaching = shouldCacheETaggedData_;
592  shouldCacheETaggedData_ = flag;
593
594  if (flag) {
595    self.shouldRememberETags = YES;
596  }
597
598  if (wasCaching && !flag) {
599    // users expect turning off caching to free up the cache memory
600    [self clearETaggedDataCache];
601  }
602}
603
604- (NSUInteger)memoryCapacity {
605  return [etaggedDataCache_ memoryCapacity];
606}
607
608- (void)setMemoryCapacity:(NSUInteger)totalBytes {
609  [etaggedDataCache_ setMemoryCapacity:totalBytes];
610}
611
612@end