aboutsummaryrefslogtreecommitdiffhomepage
path: root/example/common/gtm-oauth2/HTTPFetcher
diff options
context:
space:
mode:
Diffstat (limited to 'example/common/gtm-oauth2/HTTPFetcher')
-rwxr-xr-xexample/common/gtm-oauth2/HTTPFetcher/GTMHTTPFetchHistory.h187
-rwxr-xr-xexample/common/gtm-oauth2/HTTPFetcher/GTMHTTPFetchHistory.m605
-rwxr-xr-xexample/common/gtm-oauth2/HTTPFetcher/GTMHTTPFetcher.h766
-rwxr-xr-xexample/common/gtm-oauth2/HTTPFetcher/GTMHTTPFetcher.m1956
4 files changed, 3514 insertions, 0 deletions
diff --git a/example/common/gtm-oauth2/HTTPFetcher/GTMHTTPFetchHistory.h b/example/common/gtm-oauth2/HTTPFetcher/GTMHTTPFetchHistory.h
new file mode 100755
index 00000000..96018f5d
--- /dev/null
+++ b/example/common/gtm-oauth2/HTTPFetcher/GTMHTTPFetchHistory.h
@@ -0,0 +1,187 @@
+/* Copyright (c) 2011 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.
+ */
+
+//
+// GTMHTTPFetchHistory.h
+//
+
+//
+// Users of the GTMHTTPFetcher class may optionally create and set a fetch
+// history object. The fetch history provides "memory" between subsequent
+// fetches, including:
+//
+// - For fetch responses with Etag headers, the fetch history
+// remembers the response headers. Future fetcher requests to the same URL
+// will be given an "If-None-Match" header, telling the server to return
+// a 304 Not Modified status if the response is unchanged, reducing the
+// server load and network traffic.
+//
+// - Optionally, the fetch history can cache the ETagged data that was returned
+// in the responses that contained Etag headers. If a later fetch
+// results in a 304 status, the fetcher will return the cached ETagged data
+// to the client along with a 200 status, hiding the 304.
+//
+// - The fetch history can track cookies.
+//
+
+#pragma once
+
+#import <Foundation/Foundation.h>
+
+#import "GTMHTTPFetcher.h"
+
+#undef _EXTERN
+#undef _INITIALIZE_AS
+#ifdef GTMHTTPFETCHHISTORY_DEFINE_GLOBALS
+ #define _EXTERN
+ #define _INITIALIZE_AS(x) =x
+#else
+ #if defined(__cplusplus)
+ #define _EXTERN extern "C"
+ #else
+ #define _EXTERN extern
+ #endif
+ #define _INITIALIZE_AS(x)
+#endif
+
+
+// default data cache size for when we're caching responses to handle "not
+// modified" errors for the client
+#if GTM_IPHONE
+// iPhone: up to 1MB memory
+_EXTERN const NSUInteger kGTMDefaultETaggedDataCacheMemoryCapacity _INITIALIZE_AS(1*1024*1024);
+#else
+// Mac OS X: up to 15MB memory
+_EXTERN const NSUInteger kGTMDefaultETaggedDataCacheMemoryCapacity _INITIALIZE_AS(15*1024*1024);
+#endif
+
+// forward declarations
+@class GTMURLCache;
+@class GTMCookieStorage;
+
+@interface GTMHTTPFetchHistory : NSObject <GTMHTTPFetchHistoryProtocol> {
+ @private
+ GTMURLCache *etaggedDataCache_;
+ BOOL shouldRememberETags_;
+ BOOL shouldCacheETaggedData_; // if NO, then only headers are cached
+ GTMCookieStorage *cookieStorage_;
+}
+
+// With caching enabled, previously-cached data will be returned instead of
+// 304 Not Modified responses when repeating a fetch of an URL that previously
+// included an ETag header in its response
+@property (assign) BOOL shouldRememberETags; // default: NO
+@property (assign) BOOL shouldCacheETaggedData; // default: NO
+
+// the default ETag data cache capacity is kGTMDefaultETaggedDataCacheMemoryCapacity
+@property (assign) NSUInteger memoryCapacity;
+
+@property (retain) GTMCookieStorage *cookieStorage;
+
+- (id)initWithMemoryCapacity:(NSUInteger)totalBytes
+ shouldCacheETaggedData:(BOOL)shouldCacheETaggedData;
+
+- (void)updateRequest:(NSMutableURLRequest *)request isHTTPGet:(BOOL)isHTTPGet;
+
+- (void)clearETaggedDataCache;
+- (void)clearHistory;
+
+- (void)removeAllCookies;
+
+@end
+
+
+// GTMURLCache and GTMCachedURLResponse have interfaces similar to their
+// NSURLCache counterparts, in hopes that someday the NSURLCache versions
+// can be used. But in 10.5.8, those are not reliable enough except when
+// used with +setSharedURLCache. Our goal here is just to cache
+// responses for handling If-None-Match requests that return
+// "Not Modified" responses, not for replacing the general URL
+// caches.
+
+@interface GTMCachedURLResponse : NSObject {
+ @private
+ NSURLResponse *response_;
+ NSData *data_;
+ NSDate *useDate_; // date this response was last saved or used
+ NSDate *reservationDate_; // date this response's ETag was used
+}
+
+@property (readonly) NSURLResponse* response;
+@property (readonly) NSData* data;
+
+// date the response was saved or last accessed
+@property (retain) NSDate *useDate;
+
+// date the response's ETag header was last used for a fetch request
+@property (retain) NSDate *reservationDate;
+
+- (id)initWithResponse:(NSURLResponse *)response data:(NSData *)data;
+@end
+
+@interface GTMURLCache : NSObject {
+ NSMutableDictionary *responses_; // maps request URL to GTMCachedURLResponse
+ NSUInteger memoryCapacity_; // capacity of NSDatas in the responses
+ NSUInteger totalDataSize_; // sum of sizes of NSDatas of all responses
+ NSTimeInterval reservationInterval_; // reservation expiration interval
+}
+
+@property (assign) NSUInteger memoryCapacity;
+
+- (id)initWithMemoryCapacity:(NSUInteger)totalBytes;
+
+- (GTMCachedURLResponse *)cachedResponseForRequest:(NSURLRequest *)request;
+- (void)storeCachedResponse:(GTMCachedURLResponse *)cachedResponse forRequest:(NSURLRequest *)request;
+- (void)removeCachedResponseForRequest:(NSURLRequest *)request;
+- (void)removeAllCachedResponses;
+
+// for unit testing
+- (void)setReservationInterval:(NSTimeInterval)secs;
+- (NSDictionary *)responses;
+- (NSUInteger)totalDataSize;
+@end
+
+@interface GTMCookieStorage : NSObject <GTMCookieStorageProtocol> {
+ @private
+ // The cookie storage object manages an array holding cookies, but the array
+ // is allocated externally (it may be in a fetcher object or the static
+ // fetcher cookie array.) See the fetcher's setCookieStorageMethod:
+ // for allocation of this object and assignment of its cookies array.
+ NSMutableArray *cookies_;
+}
+
+// add all NSHTTPCookies in the supplied array to the storage array,
+// replacing cookies in the storage array as appropriate
+// Side effect: removes expired cookies from the storage array
+- (void)setCookies:(NSArray *)newCookies;
+
+// retrieve all cookies appropriate for the given URL, considering
+// domain, path, cookie name, expiration, security setting.
+// Side effect: removes expired cookies from the storage array
+- (NSArray *)cookiesForURL:(NSURL *)theURL;
+
+// return a cookie with the same name, domain, and path as the
+// given cookie, or else return nil if none found
+//
+// Both the cookie being tested and all stored cookies should
+// be valid (non-nil name, domains, paths)
+- (NSHTTPCookie *)cookieMatchingCookie:(NSHTTPCookie *)cookie;
+
+// remove any expired cookies, excluding cookies with nil expirations
+- (void)removeExpiredCookies;
+
+- (void)removeAllCookies;
+
+@end
diff --git a/example/common/gtm-oauth2/HTTPFetcher/GTMHTTPFetchHistory.m b/example/common/gtm-oauth2/HTTPFetcher/GTMHTTPFetchHistory.m
new file mode 100755
index 00000000..2c859230
--- /dev/null
+++ b/example/common/gtm-oauth2/HTTPFetcher/GTMHTTPFetchHistory.m
@@ -0,0 +1,605 @@
+/* Copyright (c) 2010 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.
+ */
+
+//
+// GTMHTTPFetchHistory.m
+//
+
+#define GTMHTTPFETCHHISTORY_DEFINE_GLOBALS 1
+
+#import "GTMHTTPFetchHistory.h"
+
+const NSTimeInterval kCachedURLReservationInterval = 60.0; // 1 minute
+static NSString* const kGTMIfNoneMatchHeader = @"If-None-Match";
+static NSString* const kGTMETagHeader = @"Etag";
+
+@implementation GTMCookieStorage
+
+- (id)init {
+ self = [super init];
+ if (self != nil) {
+ cookies_ = [[NSMutableArray alloc] init];
+ }
+ return self;
+}
+
+- (void)dealloc {
+ [cookies_ release];
+ [super dealloc];
+}
+
+// Add all cookies in the new cookie array to the storage,
+// replacing stored cookies as appropriate.
+//
+// Side effect: removes expired cookies from the storage array.
+- (void)setCookies:(NSArray *)newCookies {
+
+ @synchronized(cookies_) {
+ [self removeExpiredCookies];
+
+ for (NSHTTPCookie *newCookie in newCookies) {
+ if ([[newCookie name] length] > 0
+ && [[newCookie domain] length] > 0
+ && [[newCookie path] length] > 0) {
+
+ // remove the cookie if it's currently in the array
+ NSHTTPCookie *oldCookie = [self cookieMatchingCookie:newCookie];
+ if (oldCookie) {
+ [cookies_ removeObjectIdenticalTo:oldCookie];
+ }
+
+ // make sure the cookie hasn't already expired
+ NSDate *expiresDate = [newCookie expiresDate];
+ if ((!expiresDate) || [expiresDate timeIntervalSinceNow] > 0) {
+ [cookies_ addObject:newCookie];
+ }
+
+ } else {
+ NSAssert1(NO, @"Cookie incomplete: %@", newCookie);
+ }
+ }
+ }
+}
+
+- (void)deleteCookie:(NSHTTPCookie *)cookie {
+ @synchronized(cookies_) {
+ NSHTTPCookie *foundCookie = [self cookieMatchingCookie:cookie];
+ if (foundCookie) {
+ [cookies_ removeObjectIdenticalTo:foundCookie];
+ }
+ }
+}
+
+// Retrieve all cookies appropriate for the given URL, considering
+// domain, path, cookie name, expiration, security setting.
+// Side effect: removed expired cookies from the storage array.
+- (NSArray *)cookiesForURL:(NSURL *)theURL {
+
+ NSMutableArray *foundCookies = nil;
+
+ @synchronized(cookies_) {
+ [self removeExpiredCookies];
+
+ // We'll prepend "." to the desired domain, since we want the
+ // actual domain "nytimes.com" to still match the cookie domain
+ // ".nytimes.com" when we check it below with hasSuffix.
+ NSString *host = [[theURL host] lowercaseString];
+ NSString *path = [theURL path];
+ NSString *scheme = [theURL scheme];
+
+ NSString *domain = nil;
+ BOOL isLocalhostRetrieval = NO;
+
+ if ([host isEqual:@"localhost"]) {
+ isLocalhostRetrieval = YES;
+ } else {
+ if (host) {
+ domain = [@"." stringByAppendingString:host];
+ }
+ }
+
+ NSUInteger numberOfCookies = [cookies_ count];
+ for (NSUInteger idx = 0; idx < numberOfCookies; idx++) {
+
+ NSHTTPCookie *storedCookie = [cookies_ objectAtIndex:idx];
+
+ NSString *cookieDomain = [[storedCookie domain] lowercaseString];
+ NSString *cookiePath = [storedCookie path];
+ BOOL cookieIsSecure = [storedCookie isSecure];
+
+ BOOL isDomainOK;
+
+ if (isLocalhostRetrieval) {
+ // prior to 10.5.6, the domain stored into NSHTTPCookies for localhost
+ // is "localhost.local"
+ isDomainOK = [cookieDomain isEqual:@"localhost"]
+ || [cookieDomain isEqual:@"localhost.local"];
+ } else {
+ isDomainOK = [domain hasSuffix:cookieDomain];
+ }
+
+ BOOL isPathOK = [cookiePath isEqual:@"/"] || [path hasPrefix:cookiePath];
+ BOOL isSecureOK = (!cookieIsSecure) || [scheme isEqual:@"https"];
+
+ if (isDomainOK && isPathOK && isSecureOK) {
+ if (foundCookies == nil) {
+ foundCookies = [NSMutableArray arrayWithCapacity:1];
+ }
+ [foundCookies addObject:storedCookie];
+ }
+ }
+ }
+ return foundCookies;
+}
+
+// Return a cookie from the array with the same name, domain, and path as the
+// given cookie, or else return nil if none found.
+//
+// Both the cookie being tested and all cookies in the storage array should
+// be valid (non-nil name, domains, paths).
+//
+// Note: this should only be called from inside a @synchronized(cookies_) block
+- (NSHTTPCookie *)cookieMatchingCookie:(NSHTTPCookie *)cookie {
+
+ NSUInteger numberOfCookies = [cookies_ count];
+ NSString *name = [cookie name];
+ NSString *domain = [cookie domain];
+ NSString *path = [cookie path];
+
+ NSAssert3(name && domain && path, @"Invalid cookie (name:%@ domain:%@ path:%@)",
+ name, domain, path);
+
+ for (NSUInteger idx = 0; idx < numberOfCookies; idx++) {
+
+ NSHTTPCookie *storedCookie = [cookies_ objectAtIndex:idx];
+
+ if ([[storedCookie name] isEqual:name]
+ && [[storedCookie domain] isEqual:domain]
+ && [[storedCookie path] isEqual:path]) {
+
+ return storedCookie;
+ }
+ }
+ return nil;
+}
+
+
+// Internal routine to remove any expired cookies from the array, excluding
+// cookies with nil expirations.
+//
+// Note: this should only be called from inside a @synchronized(cookies_) block
+- (void)removeExpiredCookies {
+
+ // count backwards since we're deleting items from the array
+ for (NSInteger idx = (NSInteger)[cookies_ count] - 1; idx >= 0; idx--) {
+
+ NSHTTPCookie *storedCookie = [cookies_ objectAtIndex:(NSUInteger)idx];
+
+ NSDate *expiresDate = [storedCookie expiresDate];
+ if (expiresDate && [expiresDate timeIntervalSinceNow] < 0) {
+ [cookies_ removeObjectAtIndex:(NSUInteger)idx];
+ }
+ }
+}
+
+- (void)removeAllCookies {
+ @synchronized(cookies_) {
+ [cookies_ removeAllObjects];
+ }
+}
+@end
+
+//
+// GTMCachedURLResponse
+//
+
+@implementation GTMCachedURLResponse
+
+@synthesize response = response_;
+@synthesize data = data_;
+@synthesize reservationDate = reservationDate_;
+@synthesize useDate = useDate_;
+
+- (id)initWithResponse:(NSURLResponse *)response data:(NSData *)data {
+ self = [super init];
+ if (self != nil) {
+ response_ = [response retain];
+ data_ = [data retain];
+ useDate_ = [[NSDate alloc] init];
+ }
+ return self;
+}
+
+- (void)dealloc {
+ [response_ release];
+ [data_ release];
+ [useDate_ release];
+ [reservationDate_ release];
+ [super dealloc];
+}
+
+- (NSString *)description {
+ NSString *reservationStr = reservationDate_ ?
+ [NSString stringWithFormat:@" resDate:%@", reservationDate_] : @"";
+
+ return [NSString stringWithFormat:@"%@ %p: {bytes:%@ useDate:%@%@}",
+ [self class], self,
+ data_ ? [NSNumber numberWithInt:(int)[data_ length]] : nil,
+ useDate_,
+ reservationStr];
+}
+
+- (NSComparisonResult)compareUseDate:(GTMCachedURLResponse *)other {
+ return [useDate_ compare:[other useDate]];
+}
+
+@end
+
+//
+// GTMURLCache
+//
+
+@implementation GTMURLCache
+
+@dynamic memoryCapacity;
+
+- (id)init {
+ return [self initWithMemoryCapacity:kGTMDefaultETaggedDataCacheMemoryCapacity];
+}
+
+- (id)initWithMemoryCapacity:(NSUInteger)totalBytes {
+ self = [super init];
+ if (self != nil) {
+ memoryCapacity_ = totalBytes;
+
+ responses_ = [[NSMutableDictionary alloc] initWithCapacity:5];
+
+ reservationInterval_ = kCachedURLReservationInterval;
+ }
+ return self;
+}
+
+- (void)dealloc {
+ [responses_ release];
+ [super dealloc];
+}
+
+- (NSString *)description {
+ return [NSString stringWithFormat:@"%@ %p: {responses:%@}",
+ [self class], self, [responses_ allValues]];
+}
+
+// Setters/getters
+
+- (void)pruneCacheResponses {
+ // Internal routine to remove the least-recently-used responses when the
+ // cache has grown too large
+ if (memoryCapacity_ >= totalDataSize_) return;
+
+ // Sort keys by date
+ SEL sel = @selector(compareUseDate:);
+ NSArray *sortedKeys = [responses_ keysSortedByValueUsingSelector:sel];
+
+ // The least-recently-used keys are at the beginning of the sorted array;
+ // remove those (except ones still reserved) until the total data size is
+ // reduced sufficiently
+ for (NSURL *key in sortedKeys) {
+ GTMCachedURLResponse *response = [responses_ objectForKey:key];
+
+ NSDate *resDate = [response reservationDate];
+ BOOL isResponseReserved = (resDate != nil)
+ && ([resDate timeIntervalSinceNow] > -reservationInterval_);
+
+ if (!isResponseReserved) {
+ // We can remove this response from the cache
+ NSUInteger storedSize = [[response data] length];
+ totalDataSize_ -= storedSize;
+ [responses_ removeObjectForKey:key];
+ }
+
+ // If we've removed enough response data, then we're done
+ if (memoryCapacity_ >= totalDataSize_) break;
+ }
+}
+
+- (void)storeCachedResponse:(GTMCachedURLResponse *)cachedResponse
+ forRequest:(NSURLRequest *)request {
+ @synchronized(self) {
+ // Remove any previous entry for this request
+ [self removeCachedResponseForRequest:request];
+
+ // cache this one only if it's not bigger than our cache
+ NSUInteger storedSize = [[cachedResponse data] length];
+ if (storedSize < memoryCapacity_) {
+
+ NSURL *key = [request URL];
+ [responses_ setObject:cachedResponse forKey:key];
+ totalDataSize_ += storedSize;
+
+ [self pruneCacheResponses];
+ }
+ }
+}
+
+- (GTMCachedURLResponse *)cachedResponseForRequest:(NSURLRequest *)request {
+ GTMCachedURLResponse *response;
+
+ @synchronized(self) {
+ NSURL *key = [request URL];
+ response = [[[responses_ objectForKey:key] retain] autorelease];
+
+ // Touch the date to indicate this was recently retrieved
+ [response setUseDate:[NSDate date]];
+ }
+ return response;
+}
+
+- (void)removeCachedResponseForRequest:(NSURLRequest *)request {
+ @synchronized(self) {
+ NSURL *key = [request URL];
+ totalDataSize_ -= [[[responses_ objectForKey:key] data] length];
+ [responses_ removeObjectForKey:key];
+ }
+}
+
+- (void)removeAllCachedResponses {
+ @synchronized(self) {
+ [responses_ removeAllObjects];
+ totalDataSize_ = 0;
+ }
+}
+
+- (NSUInteger)memoryCapacity {
+ return memoryCapacity_;
+}
+
+- (void)setMemoryCapacity:(NSUInteger)totalBytes {
+ @synchronized(self) {
+ BOOL didShrink = (totalBytes < memoryCapacity_);
+ memoryCapacity_ = totalBytes;
+
+ if (didShrink) {
+ [self pruneCacheResponses];
+ }
+ }
+}
+
+// Methods for unit testing.
+- (void)setReservationInterval:(NSTimeInterval)secs {
+ reservationInterval_ = secs;
+}
+
+- (NSDictionary *)responses {
+ return responses_;
+}
+
+- (NSUInteger)totalDataSize {
+ return totalDataSize_;
+}
+
+@end
+
+//
+// GTMHTTPFetchHistory
+//
+
+@interface GTMHTTPFetchHistory ()
+- (NSString *)cachedETagForRequest:(NSURLRequest *)request;
+- (void)removeCachedDataForRequest:(NSURLRequest *)request;
+@end
+
+@implementation GTMHTTPFetchHistory
+
+@synthesize cookieStorage = cookieStorage_;
+
+@dynamic shouldRememberETags;
+@dynamic shouldCacheETaggedData;
+@dynamic memoryCapacity;
+
+- (id)init {
+ return [self initWithMemoryCapacity:kGTMDefaultETaggedDataCacheMemoryCapacity
+ shouldCacheETaggedData:NO];
+}
+
+- (id)initWithMemoryCapacity:(NSUInteger)totalBytes
+ shouldCacheETaggedData:(BOOL)shouldCacheETaggedData {
+ self = [super init];
+ if (self != nil) {
+ etaggedDataCache_ = [[GTMURLCache alloc] initWithMemoryCapacity:totalBytes];
+ shouldRememberETags_ = shouldCacheETaggedData;
+ shouldCacheETaggedData_ = shouldCacheETaggedData;
+ cookieStorage_ = [[GTMCookieStorage alloc] init];
+ }
+ return self;
+}
+
+- (void)dealloc {
+ [etaggedDataCache_ release];
+ [cookieStorage_ release];
+ [super dealloc];
+}
+
+- (void)updateRequest:(NSMutableURLRequest *)request isHTTPGet:(BOOL)isHTTPGet {
+ @synchronized(self) {
+ if ([self shouldRememberETags]) {
+ // If this URL is in the history, and no ETag has been set, then
+ // set the ETag header field
+
+ // If we have a history, we're tracking across fetches, so we don't
+ // want to pull results from any other cache
+ [request setCachePolicy:NSURLRequestReloadIgnoringCacheData];
+
+ if (isHTTPGet) {
+ // We'll only add an ETag if there's no ETag specified in the user's
+ // request
+ NSString *specifiedETag = [request valueForHTTPHeaderField:kGTMIfNoneMatchHeader];
+ if (specifiedETag == nil) {
+ // No ETag: extract the previous ETag for this request from the
+ // fetch history, and add it to the request
+ NSString *cachedETag = [self cachedETagForRequest:request];
+
+ if (cachedETag != nil) {
+ [request addValue:cachedETag forHTTPHeaderField:kGTMIfNoneMatchHeader];
+ }
+ } else {
+ // Has an ETag: remove any stored response in the fetch history
+ // for this request, as the If-None-Match header could lead to
+ // a 304 Not Modified, and we want that error delivered to the
+ // user since they explicitly specified the ETag
+ [self removeCachedDataForRequest:request];
+ }
+ }
+ }
+ }
+}
+
+- (void)updateFetchHistoryWithRequest:(NSURLRequest *)request
+ response:(NSURLResponse *)response
+ downloadedData:(NSData *)downloadedData {
+ @synchronized(self) {
+ if (![self shouldRememberETags]) return;
+
+ if (![response respondsToSelector:@selector(allHeaderFields)]) return;
+
+ NSInteger statusCode = [(NSHTTPURLResponse *)response statusCode];
+
+ if (statusCode != kGTMHTTPFetcherStatusNotModified) {
+ // Save this ETag string for successful results (<300)
+ // If there's no last modified string, clear the dictionary
+ // entry for this URL. Also cache or delete the data, if appropriate
+ // (when etaggedDataCache is non-nil.)
+ NSDictionary *headers = [(NSHTTPURLResponse *)response allHeaderFields];
+ NSString* etag = [headers objectForKey:kGTMETagHeader];
+
+ if (etag != nil && statusCode < 300) {
+
+ // we want to cache responses for the headers, even if the client
+ // doesn't want the response body data caches
+ NSData *dataToStore = shouldCacheETaggedData_ ? downloadedData : nil;
+
+ GTMCachedURLResponse *cachedResponse;
+ cachedResponse = [[[GTMCachedURLResponse alloc] initWithResponse:response
+ data:dataToStore] autorelease];
+ [etaggedDataCache_ storeCachedResponse:cachedResponse
+ forRequest:request];
+ } else {
+ [etaggedDataCache_ removeCachedResponseForRequest:request];
+ }
+ }
+ }
+}
+
+- (NSString *)cachedETagForRequest:(NSURLRequest *)request {
+ // Internal routine.
+ GTMCachedURLResponse *cachedResponse;
+ cachedResponse = [etaggedDataCache_ cachedResponseForRequest:request];
+
+ NSURLResponse *response = [cachedResponse response];
+ NSDictionary *headers = [(NSHTTPURLResponse *)response allHeaderFields];
+ NSString *cachedETag = [headers objectForKey:kGTMETagHeader];
+ if (cachedETag) {
+ // Since the request having an ETag implies this request is about
+ // to be fetched again, reserve the cached response to ensure that
+ // that it will be around at least until the fetch completes.
+ //
+ // When the fetch completes, either the cached response will be replaced
+ // with a new response, or the cachedDataForRequest: method below will
+ // clear the reservation.
+ [cachedResponse setReservationDate:[NSDate date]];
+ }
+ return cachedETag;
+}
+
+- (NSData *)cachedDataForRequest:(NSURLRequest *)request {
+ @synchronized(self) {
+ GTMCachedURLResponse *cachedResponse;
+ cachedResponse = [etaggedDataCache_ cachedResponseForRequest:request];
+
+ NSData *cachedData = [cachedResponse data];
+
+ // Since the data for this cached request is being obtained from the cache,
+ // we can clear the reservation as the fetch has completed.
+ [cachedResponse setReservationDate:nil];
+
+ return cachedData;
+ }
+}
+
+- (void)removeCachedDataForRequest:(NSURLRequest *)request {
+ @synchronized(self) {
+ [etaggedDataCache_ removeCachedResponseForRequest:request];
+ }
+}
+
+- (void)clearETaggedDataCache {
+ @synchronized(self) {
+ [etaggedDataCache_ removeAllCachedResponses];
+ }
+}
+
+- (void)clearHistory {
+ @synchronized(self) {
+ [self clearETaggedDataCache];
+ [cookieStorage_ removeAllCookies];
+ }
+}
+
+- (void)removeAllCookies {
+ @synchronized(self) {
+ [cookieStorage_ removeAllCookies];
+ }
+}
+
+- (BOOL)shouldRememberETags {
+ return shouldRememberETags_;
+}
+
+- (void)setShouldRememberETags:(BOOL)flag {
+ BOOL wasRemembering = shouldRememberETags_;
+ shouldRememberETags_ = flag;
+
+ if (wasRemembering && !flag) {
+ // Free up the cache memory
+ [self clearETaggedDataCache];
+ }
+}
+
+- (BOOL)shouldCacheETaggedData {
+ return shouldCacheETaggedData_;
+}
+
+- (void)setShouldCacheETaggedData:(BOOL)flag {
+ BOOL wasCaching = shouldCacheETaggedData_;
+ shouldCacheETaggedData_ = flag;
+
+ if (flag) {
+ self.shouldRememberETags = YES;
+ }
+
+ if (wasCaching && !flag) {
+ // users expect turning off caching to free up the cache memory
+ [self clearETaggedDataCache];
+ }
+}
+
+- (NSUInteger)memoryCapacity {
+ return [etaggedDataCache_ memoryCapacity];
+}
+
+- (void)setMemoryCapacity:(NSUInteger)totalBytes {
+ [etaggedDataCache_ setMemoryCapacity:totalBytes];
+}
+
+@end
diff --git a/example/common/gtm-oauth2/HTTPFetcher/GTMHTTPFetcher.h b/example/common/gtm-oauth2/HTTPFetcher/GTMHTTPFetcher.h
new file mode 100755
index 00000000..cd9a9ff6
--- /dev/null
+++ b/example/common/gtm-oauth2/HTTPFetcher/GTMHTTPFetcher.h
@@ -0,0 +1,766 @@
+/* Copyright (c) 2011 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.
+ */
+
+//
+// GTMHTTPFetcher.h
+//
+
+// This is essentially a wrapper around NSURLConnection for POSTs and GETs.
+// If setPostData: is called, then POST is assumed.
+//
+// When would you use this instead of NSURLConnection?
+//
+// - When you just want the result from a GET, POST, or PUT
+// - When you want the "standard" behavior for connections (redirection handling
+// an so on)
+// - When you want automatic retry on failures
+// - When you want to avoid cookie collisions with Safari and other applications
+// - When you are fetching resources with ETags and want to avoid the overhead
+// of repeated fetches of unchanged data
+// - When you need to set a credential for the http operation
+//
+// This is assumed to be a one-shot fetch request; don't reuse the object
+// for a second fetch.
+//
+// The fetcher may be created auto-released, in which case it will release
+// itself after the fetch completion callback. The fetcher is implicitly
+// retained as long as a connection is pending.
+//
+// But if you may need to cancel the fetcher, retain it and have the delegate
+// release the fetcher in the callbacks.
+//
+// Sample usage:
+//
+// NSURLRequest *request = [NSURLRequest requestWithURL:myURL];
+// GTMHTTPFetcher* myFetcher = [GTMHTTPFetcher fetcherWithRequest:request];
+//
+// // optional upload body data
+// [myFetcher setPostData:[postString dataUsingEncoding:NSUTF8StringEncoding]];
+//
+// [myFetcher beginFetchWithDelegate:self
+// didFinishSelector:@selector(myFetcher:finishedWithData:error:)];
+//
+// Upon fetch completion, the callback selector is invoked; it should have
+// this signature (you can use any callback method name you want so long as
+// the signature matches this):
+//
+// - (void)myFetcher:(GTMHTTPFetcher *)fetcher finishedWithData:(NSData *)retrievedData error:(NSError *)error;
+//
+// The block callback version looks like:
+//
+// [myFetcher beginFetchWithCompletionHandler:^(NSData *retrievedData, NSError *error) {
+// if (error != nil) {
+// // status code or network error
+// } else {
+// // succeeded
+// }
+// }];
+
+//
+// NOTE: Fetches may retrieve data from the server even though the server
+// returned an error. The failure selector is called when the server
+// status is >= 300, with an NSError having domain
+// kGTMHTTPFetcherStatusDomain and code set to the server status.
+//
+// Status codes are at <http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html>
+//
+//
+// Threading and queue support:
+//
+// Callbacks require either that the thread used to start the fetcher have a run
+// loop spinning (typically the main thread), or that an NSOperationQueue be
+// provided upon which the delegate callbacks will be called. Starting with
+// iOS 6 and Mac OS X 10.7, clients may simply create an operation queue for
+// callbacks on a background thread:
+//
+// NSOperationQueue *queue = [[[NSOperationQueue alloc] init] autorelease];
+// [queue setMaxConcurrentOperationCount:1];
+// fetcher.delegateQueue = queue;
+//
+// or specify the main queue for callbacks on the main thread:
+//
+// fetcher.delegateQueue = [NSOperationQueue mainQueue];
+//
+// The client may also re-dispatch from the callbacks and notifications to
+// a known dispatch queue:
+//
+// [myFetcher beginFetchWithCompletionHandler:^(NSData *retrievedData, NSError *error) {
+// if (error == nil) {
+// dispatch_async(myDispatchQueue, ^{
+// ...
+// });
+// }
+// }];
+//
+//
+//
+// Downloading to disk:
+//
+// To have downloaded data saved directly to disk, specify either a path for the
+// downloadPath property, or a file handle for the downloadFileHandle property.
+// When downloading to disk, callbacks will be passed a nil for the NSData*
+// arguments.
+//
+//
+// HTTP methods and headers:
+//
+// Alternative HTTP methods, like PUT, and custom headers can be specified by
+// creating the fetcher with an appropriate NSMutableURLRequest
+//
+//
+// Proxies:
+//
+// Proxy handling is invisible so long as the system has a valid credential in
+// the keychain, which is normally true (else most NSURL-based apps would have
+// difficulty.) But when there is a proxy authetication error, the the fetcher
+// will call the failedWithError: method with the NSURLChallenge in the error's
+// userInfo. The error method can get the challenge info like this:
+//
+// NSURLAuthenticationChallenge *challenge
+// = [[error userInfo] objectForKey:kGTMHTTPFetcherErrorChallengeKey];
+// BOOL isProxyChallenge = [[challenge protectionSpace] isProxy];
+//
+// If a proxy error occurs, you can ask the user for the proxy username/password
+// and call fetcher's setProxyCredential: to provide those for the
+// next attempt to fetch.
+//
+//
+// Cookies:
+//
+// There are three supported mechanisms for remembering cookies between fetches.
+//
+// By default, GTMHTTPFetcher uses a mutable array held statically to track
+// cookies for all instantiated fetchers. This avoids server cookies being set
+// by servers for the application from interfering with Safari cookie settings,
+// and vice versa. The fetcher cookies are lost when the application quits.
+//
+// To rely instead on WebKit's global NSHTTPCookieStorage, call
+// setCookieStorageMethod: with kGTMHTTPFetcherCookieStorageMethodSystemDefault.
+//
+// If the fetcher is created from a GTMHTTPFetcherService object
+// then the cookie storage mechanism is set to use the cookie storage in the
+// service object rather than the static storage.
+//
+//
+// Fetching for periodic checks:
+//
+// The fetcher object tracks ETag headers from responses and
+// provide an "If-None-Match" header. This allows the server to save
+// bandwidth by providing a status message instead of repeated response
+// data.
+//
+// To get this behavior, create the fetcher from an GTMHTTPFetcherService object
+// and look for a fetch callback error with code 304
+// (kGTMHTTPFetcherStatusNotModified) like this:
+//
+// - (void)myFetcher:(GTMHTTPFetcher *)fetcher finishedWithData:(NSData *)data error:(NSError *)error {
+// if ([error code] == kGTMHTTPFetcherStatusNotModified) {
+// // |data| is empty; use the data from the previous finishedWithData: for this URL
+// } else {
+// // handle other server status code
+// }
+// }
+//
+//
+// Monitoring received data
+//
+// The optional received data selector can be set with setReceivedDataSelector:
+// and should have the signature
+//
+// - (void)myFetcher:(GTMHTTPFetcher *)fetcher receivedData:(NSData *)dataReceivedSoFar;
+//
+// The number bytes received so far is available as [fetcher downloadedLength].
+// This number may go down if a redirect causes the download to begin again from
+// a new server.
+//
+// If supplied by the server, the anticipated total download size is available
+// as [[myFetcher response] expectedContentLength] (and may be -1 for unknown
+// download sizes.)
+//
+//
+// Automatic retrying of fetches
+//
+// The fetcher can optionally create a timer and reattempt certain kinds of
+// fetch failures (status codes 408, request timeout; 503, service unavailable;
+// 504, gateway timeout; networking errors NSURLErrorTimedOut and
+// NSURLErrorNetworkConnectionLost.) The user may set a retry selector to
+// customize the type of errors which will be retried.
+//
+// Retries are done in an exponential-backoff fashion (that is, after 1 second,
+// 2, 4, 8, and so on.)
+//
+// Enabling automatic retries looks like this:
+// [myFetcher setRetryEnabled:YES];
+//
+// With retries enabled, the success or failure callbacks are called only
+// when no more retries will be attempted. Calling the fetcher's stopFetching
+// method will terminate the retry timer, without the finished or failure
+// selectors being invoked.
+//
+// Optionally, the client may set the maximum retry interval:
+// [myFetcher setMaxRetryInterval:60.0]; // in seconds; default is 60 seconds
+// // for downloads, 600 for uploads
+//
+// Also optionally, the client may provide a callback selector to determine
+// if a status code or other error should be retried.
+// [myFetcher setRetrySelector:@selector(myFetcher:willRetry:forError:)];
+//
+// If set, the retry selector should have the signature:
+// -(BOOL)fetcher:(GTMHTTPFetcher *)fetcher willRetry:(BOOL)suggestedWillRetry forError:(NSError *)error
+// and return YES to set the retry timer or NO to fail without additional
+// fetch attempts.
+//
+// The retry method may return the |suggestedWillRetry| argument to get the
+// default retry behavior. Server status codes are present in the
+// error argument, and have the domain kGTMHTTPFetcherStatusDomain. The
+// user's method may look something like this:
+//
+// -(BOOL)myFetcher:(GTMHTTPFetcher *)fetcher willRetry:(BOOL)suggestedWillRetry forError:(NSError *)error {
+//
+// // perhaps examine [error domain] and [error code], or [fetcher retryCount]
+// //
+// // return YES to start the retry timer, NO to proceed to the failure
+// // callback, or |suggestedWillRetry| to get default behavior for the
+// // current error domain and code values.
+// return suggestedWillRetry;
+// }
+
+
+
+#pragma once
+
+#import <Foundation/Foundation.h>
+
+#if defined(GTL_TARGET_NAMESPACE)
+ // we're using target namespace macros
+ #import "GTLDefines.h"
+#elif defined(GDATA_TARGET_NAMESPACE)
+ #import "GDataDefines.h"
+#else
+ #if TARGET_OS_IPHONE
+ #ifndef GTM_FOUNDATION_ONLY
+ #define GTM_FOUNDATION_ONLY 1
+ #endif
+ #ifndef GTM_IPHONE
+ #define GTM_IPHONE 1
+ #endif
+ #endif
+#endif
+
+#if TARGET_OS_IPHONE && (__IPHONE_OS_VERSION_MAX_ALLOWED >= 40000)
+ #define GTM_BACKGROUND_FETCHING 1
+#endif
+
+#undef _EXTERN
+#undef _INITIALIZE_AS
+#ifdef GTMHTTPFETCHER_DEFINE_GLOBALS
+ #define _EXTERN
+ #define _INITIALIZE_AS(x) =x
+#else
+ #if defined(__cplusplus)
+ #define _EXTERN extern "C"
+ #else
+ #define _EXTERN extern
+ #endif
+ #define _INITIALIZE_AS(x)
+#endif
+
+// notifications
+//
+// fetch started and stopped, and fetch retry delay started and stopped
+_EXTERN NSString* const kGTMHTTPFetcherStartedNotification _INITIALIZE_AS(@"kGTMHTTPFetcherStartedNotification");
+_EXTERN NSString* const kGTMHTTPFetcherStoppedNotification _INITIALIZE_AS(@"kGTMHTTPFetcherStoppedNotification");
+_EXTERN NSString* const kGTMHTTPFetcherRetryDelayStartedNotification _INITIALIZE_AS(@"kGTMHTTPFetcherRetryDelayStartedNotification");
+_EXTERN NSString* const kGTMHTTPFetcherRetryDelayStoppedNotification _INITIALIZE_AS(@"kGTMHTTPFetcherRetryDelayStoppedNotification");
+
+// callback constants
+_EXTERN NSString* const kGTMHTTPFetcherErrorDomain _INITIALIZE_AS(@"com.google.GTMHTTPFetcher");
+_EXTERN NSString* const kGTMHTTPFetcherStatusDomain _INITIALIZE_AS(@"com.google.HTTPStatus");
+_EXTERN NSString* const kGTMHTTPFetcherErrorChallengeKey _INITIALIZE_AS(@"challenge");
+_EXTERN NSString* const kGTMHTTPFetcherStatusDataKey _INITIALIZE_AS(@"data"); // data returned with a kGTMHTTPFetcherStatusDomain error
+
+enum {
+ kGTMHTTPFetcherErrorDownloadFailed = -1,
+ kGTMHTTPFetcherErrorAuthenticationChallengeFailed = -2,
+ kGTMHTTPFetcherErrorChunkUploadFailed = -3,
+ kGTMHTTPFetcherErrorFileHandleException = -4,
+ kGTMHTTPFetcherErrorBackgroundExpiration = -6,
+
+ // The code kGTMHTTPFetcherErrorAuthorizationFailed (-5) has been removed;
+ // look for status 401 instead.
+
+ kGTMHTTPFetcherStatusNotModified = 304,
+ kGTMHTTPFetcherStatusBadRequest = 400,
+ kGTMHTTPFetcherStatusUnauthorized = 401,
+ kGTMHTTPFetcherStatusForbidden = 403,
+ kGTMHTTPFetcherStatusPreconditionFailed = 412
+};
+
+// cookie storage methods
+enum {
+ kGTMHTTPFetcherCookieStorageMethodStatic = 0,
+ kGTMHTTPFetcherCookieStorageMethodFetchHistory = 1,
+ kGTMHTTPFetcherCookieStorageMethodSystemDefault = 2,
+ kGTMHTTPFetcherCookieStorageMethodNone = 3
+};
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+void GTMAssertSelectorNilOrImplementedWithArgs(id obj, SEL sel, ...);
+
+// Utility functions for applications self-identifying to servers via a
+// user-agent header
+
+// Make a proper app name without whitespace from the given string, removing
+// whitespace and other characters that may be special parsed marks of
+// the full user-agent string.
+NSString *GTMCleanedUserAgentString(NSString *str);
+
+// Make an identifier like "MacOSX/10.7.1" or "iPod_Touch/4.1"
+NSString *GTMSystemVersionString(void);
+
+// Make a generic name and version for the current application, like
+// com.example.MyApp/1.2.3 relying on the bundle identifier and the
+// CFBundleShortVersionString or CFBundleVersion. If no bundle ID
+// is available, the process name preceded by "proc_" is used.
+NSString *GTMApplicationIdentifier(NSBundle *bundle);
+
+#ifdef __cplusplus
+} // extern "C"
+#endif
+
+@class GTMHTTPFetcher;
+
+@protocol GTMCookieStorageProtocol <NSObject>
+// This protocol allows us to call into the service without requiring
+// GTMCookieStorage sources in this project
+//
+// The public interface for cookie handling is the GTMCookieStorage class,
+// accessible from a fetcher service object's fetchHistory or from the fetcher's
+// +staticCookieStorage method.
+- (NSArray *)cookiesForURL:(NSURL *)theURL;
+- (void)setCookies:(NSArray *)newCookies;
+@end
+
+@protocol GTMHTTPFetchHistoryProtocol <NSObject>
+// This protocol allows us to call the fetch history object without requiring
+// GTMHTTPFetchHistory sources in this project
+- (void)updateRequest:(NSMutableURLRequest *)request isHTTPGet:(BOOL)isHTTPGet;
+- (BOOL)shouldCacheETaggedData;
+- (NSData *)cachedDataForRequest:(NSURLRequest *)request;
+- (id <GTMCookieStorageProtocol>)cookieStorage;
+- (void)updateFetchHistoryWithRequest:(NSURLRequest *)request
+ response:(NSURLResponse *)response
+ downloadedData:(NSData *)downloadedData;
+- (void)removeCachedDataForRequest:(NSURLRequest *)request;
+@end
+
+@protocol GTMHTTPFetcherServiceProtocol <NSObject>
+// This protocol allows us to call into the service without requiring
+// GTMHTTPFetcherService sources in this project
+
+@property (retain) NSOperationQueue *delegateQueue;
+
+- (BOOL)fetcherShouldBeginFetching:(GTMHTTPFetcher *)fetcher;
+- (void)fetcherDidStop:(GTMHTTPFetcher *)fetcher;
+
+- (GTMHTTPFetcher *)fetcherWithRequest:(NSURLRequest *)request;
+- (BOOL)isDelayingFetcher:(GTMHTTPFetcher *)fetcher;
+@end
+
+@protocol GTMFetcherAuthorizationProtocol <NSObject>
+@required
+// This protocol allows us to call the authorizer without requiring its sources
+// in this project.
+- (void)authorizeRequest:(NSMutableURLRequest *)request
+ delegate:(id)delegate
+ didFinishSelector:(SEL)sel;
+
+- (void)stopAuthorization;
+
+- (void)stopAuthorizationForRequest:(NSURLRequest *)request;
+
+- (BOOL)isAuthorizingRequest:(NSURLRequest *)request;
+
+- (BOOL)isAuthorizedRequest:(NSURLRequest *)request;
+
+@property (retain, readonly) NSString *userEmail;
+
+@optional
+
+// Indicate if authorization may be attempted. Even if this succeeds,
+// authorization may fail if the user's permissions have been revoked.
+@property (readonly) BOOL canAuthorize;
+
+// For development only, allow authorization of non-SSL requests, allowing
+// transmission of the bearer token unencrypted.
+@property (assign) BOOL shouldAuthorizeAllRequests;
+
+#if NS_BLOCKS_AVAILABLE
+- (void)authorizeRequest:(NSMutableURLRequest *)request
+ completionHandler:(void (^)(NSError *error))handler;
+#endif
+
+@property (assign) id <GTMHTTPFetcherServiceProtocol> fetcherService; // WEAK
+
+- (BOOL)primeForRefresh;
+
+@end
+
+// GTMHTTPFetcher objects are used for async retrieval of an http get or post
+//
+// See additional comments at the beginning of this file
+@interface GTMHTTPFetcher : NSObject {
+ @protected
+ NSMutableURLRequest *request_;
+ NSURLConnection *connection_;
+ NSMutableData *downloadedData_;
+ NSString *downloadPath_;
+ NSString *temporaryDownloadPath_;
+ NSFileHandle *downloadFileHandle_;
+ unsigned long long downloadedLength_;
+ NSURLCredential *credential_; // username & password
+ NSURLCredential *proxyCredential_; // credential supplied to proxy servers
+ NSData *postData_;
+ NSInputStream *postStream_;
+ NSMutableData *loggedStreamData_;
+ NSURLResponse *response_; // set in connection:didReceiveResponse:
+ id delegate_;
+ SEL finishedSel_; // should by implemented by delegate
+ SEL sentDataSel_; // optional, set with setSentDataSelector
+ SEL receivedDataSel_; // optional, set with setReceivedDataSelector
+#if NS_BLOCKS_AVAILABLE
+ void (^completionBlock_)(NSData *, NSError *);
+ void (^receivedDataBlock_)(NSData *);
+ void (^sentDataBlock_)(NSInteger, NSInteger, NSInteger);
+ BOOL (^retryBlock_)(BOOL, NSError *);
+#elif !__LP64__
+ // placeholders: for 32-bit builds, keep the size of the object's ivar section
+ // the same with and without blocks
+ id completionPlaceholder_;
+ id receivedDataPlaceholder_;
+ id sentDataPlaceholder_;
+ id retryPlaceholder_;
+#endif
+ BOOL hasConnectionEnded_; // set if the connection need not be cancelled
+ BOOL isCancellingChallenge_; // set only when cancelling an auth challenge
+ BOOL isStopNotificationNeeded_; // set when start notification has been sent
+ BOOL shouldFetchInBackground_;
+#if GTM_BACKGROUND_FETCHING
+ NSUInteger backgroundTaskIdentifer_; // UIBackgroundTaskIdentifier
+#endif
+ id userData_; // retained, if set by caller
+ NSMutableDictionary *properties_; // more data retained for caller
+ NSArray *runLoopModes_; // optional
+ NSOperationQueue *delegateQueue_; // optional; available iOS 6/10.7 and later
+ id <GTMHTTPFetchHistoryProtocol> fetchHistory_; // if supplied by the caller, used for Last-Modified-Since checks and cookies
+ NSInteger cookieStorageMethod_; // constant from above
+ id <GTMCookieStorageProtocol> cookieStorage_;
+
+ id <GTMFetcherAuthorizationProtocol> authorizer_;
+
+ // the service object that created and monitors this fetcher, if any
+ id <GTMHTTPFetcherServiceProtocol> service_;
+ NSString *serviceHost_;
+ NSInteger servicePriority_;
+ NSThread *thread_;
+
+ BOOL isRetryEnabled_; // user wants auto-retry
+ SEL retrySel_; // optional; set with setRetrySelector
+ NSTimer *retryTimer_;
+ NSUInteger retryCount_;
+ NSTimeInterval maxRetryInterval_; // default 600 seconds
+ NSTimeInterval minRetryInterval_; // random between 1 and 2 seconds
+ NSTimeInterval retryFactor_; // default interval multiplier is 2
+ NSTimeInterval lastRetryInterval_;
+ BOOL hasAttemptedAuthRefresh_;
+
+ NSString *comment_; // comment for log
+ NSString *log_;
+#if !STRIP_GTM_FETCH_LOGGING
+ NSURL *redirectedFromURL_;
+ NSString *logRequestBody_;
+ NSString *logResponseBody_;
+ BOOL shouldDeferResponseBodyLogging_;
+#endif
+}
+
+// Create a fetcher
+//
+// fetcherWithRequest will return an autoreleased fetcher, but if
+// the connection is successfully created, the connection should retain the
+// fetcher for the life of the connection as well. So the caller doesn't have
+// to retain the fetcher explicitly unless they want to be able to cancel it.
++ (GTMHTTPFetcher *)fetcherWithRequest:(NSURLRequest *)request;
+
+// Convenience methods that make a request, like +fetcherWithRequest
++ (GTMHTTPFetcher *)fetcherWithURL:(NSURL *)requestURL;
++ (GTMHTTPFetcher *)fetcherWithURLString:(NSString *)requestURLString;
+
+// Designated initializer
+- (id)initWithRequest:(NSURLRequest *)request;
+
+// Fetcher request
+//
+// The underlying request is mutable and may be modified by the caller
+@property (retain) NSMutableURLRequest *mutableRequest;
+
+// Setting the credential is optional; it is used if the connection receives
+// an authentication challenge
+@property (retain) NSURLCredential *credential;
+
+// Setting the proxy credential is optional; it is used if the connection
+// receives an authentication challenge from a proxy
+@property (retain) NSURLCredential *proxyCredential;
+
+// If post data or stream is not set, then a GET retrieval method is assumed
+@property (retain) NSData *postData;
+@property (retain) NSInputStream *postStream;
+
+// The default cookie storage method is kGTMHTTPFetcherCookieStorageMethodStatic
+// without a fetch history set, and kGTMHTTPFetcherCookieStorageMethodFetchHistory
+// with a fetch history set
+//
+// Applications needing control of cookies across a sequence of fetches should
+// create fetchers from a GTMHTTPFetcherService object (which encapsulates
+// fetch history) for a well-defined cookie store
+@property (assign) NSInteger cookieStorageMethod;
+
++ (id <GTMCookieStorageProtocol>)staticCookieStorage;
+
+// Object to add authorization to the request, if needed
+@property (retain) id <GTMFetcherAuthorizationProtocol> authorizer;
+
+// The service object that created and monitors this fetcher, if any
+@property (retain) id <GTMHTTPFetcherServiceProtocol> service;
+
+// The host, if any, used to classify this fetcher in the fetcher service
+@property (copy) NSString *serviceHost;
+
+// The priority, if any, used for starting fetchers in the fetcher service
+//
+// Lower values are higher priority; the default is 0, and values may
+// be negative or positive. This priority affects only the start order of
+// fetchers that are being delayed by a fetcher service.
+@property (assign) NSInteger servicePriority;
+
+// The thread used to run this fetcher in the fetcher service when no operation
+// queue is provided.
+@property (retain) NSThread *thread;
+
+// The delegate is retained during the connection
+@property (retain) id delegate;
+
+// On iOS 4 and later, the fetch may optionally continue while the app is in the
+// background until finished or stopped by OS expiration
+//
+// The default value is NO
+//
+// For Mac OS X, background fetches are always supported, and this property
+// is ignored
+@property (assign) BOOL shouldFetchInBackground;
+
+// The delegate's optional sentData selector may be used to monitor upload
+// progress. It should have a signature like:
+// - (void)myFetcher:(GTMHTTPFetcher *)fetcher
+// didSendBytes:(NSInteger)bytesSent
+// totalBytesSent:(NSInteger)totalBytesSent
+// totalBytesExpectedToSend:(NSInteger)totalBytesExpectedToSend;
+//
+// +doesSupportSentDataCallback indicates if this delegate method is supported
++ (BOOL)doesSupportSentDataCallback;
+
+@property (assign) SEL sentDataSelector;
+
+// The delegate's optional receivedData selector may be used to monitor download
+// progress. It should have a signature like:
+// - (void)myFetcher:(GTMHTTPFetcher *)fetcher
+// receivedData:(NSData *)dataReceivedSoFar;
+//
+// The dataReceived argument will be nil when downloading to a path or to a
+// file handle.
+//
+// Applications should not use this method to accumulate the received data;
+// the callback method or block supplied to the beginFetch call will have
+// the complete NSData received.
+@property (assign) SEL receivedDataSelector;
+
+#if NS_BLOCKS_AVAILABLE
+// The full interface to the block is provided rather than just a typedef for
+// its parameter list in order to get more useful code completion in the Xcode
+// editor
+@property (copy) void (^sentDataBlock)(NSInteger bytesSent, NSInteger totalBytesSent, NSInteger bytesExpectedToSend);
+
+// The dataReceived argument will be nil when downloading to a path or to
+// a file handle
+@property (copy) void (^receivedDataBlock)(NSData *dataReceivedSoFar);
+#endif
+
+// retrying; see comments at the top of the file. Calling
+// setRetryEnabled(YES) resets the min and max retry intervals.
+@property (assign, getter=isRetryEnabled) BOOL retryEnabled;
+
+// Retry selector or block is optional for retries.
+//
+// If present, it should have the signature:
+// -(BOOL)fetcher:(GTMHTTPFetcher *)fetcher willRetry:(BOOL)suggestedWillRetry forError:(NSError *)error
+// and return YES to cause a retry. See comments at the top of this file.
+@property (assign) SEL retrySelector;
+
+#if NS_BLOCKS_AVAILABLE
+@property (copy) BOOL (^retryBlock)(BOOL suggestedWillRetry, NSError *error);
+#endif
+
+// Retry intervals must be strictly less than maxRetryInterval, else
+// they will be limited to maxRetryInterval and no further retries will
+// be attempted. Setting maxRetryInterval to 0.0 will reset it to the
+// default value, 600 seconds.
+
+@property (assign) NSTimeInterval maxRetryInterval;
+
+// Starting retry interval. Setting minRetryInterval to 0.0 will reset it
+// to a random value between 1.0 and 2.0 seconds. Clients should normally not
+// call this except for unit testing.
+@property (assign) NSTimeInterval minRetryInterval;
+
+// Multiplier used to increase the interval between retries, typically 2.0.
+// Clients should not need to call this.
+@property (assign) double retryFactor;
+
+// Number of retries attempted
+@property (readonly) NSUInteger retryCount;
+
+// interval delay to precede next retry
+@property (readonly) NSTimeInterval nextRetryInterval;
+
+// Begin fetching the request
+//
+// The delegate can optionally implement the finished selectors or pass NULL
+// for it.
+//
+// Returns YES if the fetch is initiated. The delegate is retained between
+// the beginFetch call until after the finish callback.
+//
+// An error is passed to the callback for server statuses 300 or
+// higher, with the status stored as the error object's code.
+//
+// finishedSEL has a signature like:
+// - (void)fetcher:(GTMHTTPFetcher *)fetcher finishedWithData:(NSData *)data error:(NSError *)error;
+//
+// If the application has specified a downloadPath or downloadFileHandle
+// for the fetcher, the data parameter passed to the callback will be nil.
+
+- (BOOL)beginFetchWithDelegate:(id)delegate
+ didFinishSelector:(SEL)finishedSEL;
+
+#if NS_BLOCKS_AVAILABLE
+- (BOOL)beginFetchWithCompletionHandler:(void (^)(NSData *data, NSError *error))handler;
+#endif
+
+
+// Returns YES if this is in the process of fetching a URL
+- (BOOL)isFetching;
+
+// Cancel the fetch of the request that's currently in progress
+- (void)stopFetching;
+
+// Return the status code from the server response
+@property (readonly) NSInteger statusCode;
+
+// Return the http headers from the response
+@property (retain, readonly) NSDictionary *responseHeaders;
+
+// The response, once it's been received
+@property (retain) NSURLResponse *response;
+
+// Bytes downloaded so far
+@property (readonly) unsigned long long downloadedLength;
+
+// Buffer of currently-downloaded data
+@property (readonly, retain) NSData *downloadedData;
+
+// Path in which to non-atomically create a file for storing the downloaded data
+//
+// The path must be set before fetching begins. The download file handle
+// will be created for the path, and can be used to monitor progress. If a file
+// already exists at the path, it will be overwritten.
+@property (copy) NSString *downloadPath;
+
+// If downloadFileHandle is set, data received is immediately appended to
+// the file handle rather than being accumulated in the downloadedData property
+//
+// The file handle supplied must allow writing and support seekToFileOffset:,
+// and must be set before fetching begins. Setting a download path will
+// override the file handle property.
+@property (retain) NSFileHandle *downloadFileHandle;
+
+// The optional fetchHistory object is used for a sequence of fetchers to
+// remember ETags, cache ETagged data, and store cookies. Typically, this
+// is set by a GTMFetcherService object when it creates a fetcher.
+//
+// Side effect: setting fetch history implicitly calls setCookieStorageMethod:
+@property (retain) id <GTMHTTPFetchHistoryProtocol> fetchHistory;
+
+// userData is retained for the convenience of the caller
+@property (retain) id userData;
+
+// Stored property values are retained for the convenience of the caller
+@property (copy) NSMutableDictionary *properties;
+
+- (void)setProperty:(id)obj forKey:(NSString *)key; // pass nil obj to remove property
+- (id)propertyForKey:(NSString *)key;
+
+- (void)addPropertiesFromDictionary:(NSDictionary *)dict;
+
+// Comments are useful for logging
+@property (copy) NSString *comment;
+
+- (void)setCommentWithFormat:(NSString *)format, ... NS_FORMAT_FUNCTION(1, 2);
+
+// Log of request and response, if logging is enabled
+@property (copy) NSString *log;
+
+// Callbacks can be invoked on an operation queue rather than via the run loop,
+// starting on 10.7 and iOS 6. If a delegate queue is supplied. the run loop
+// modes are ignored.
+@property (retain) NSOperationQueue *delegateQueue;
+
+// Using the fetcher while a modal dialog is displayed requires setting the
+// run-loop modes to include NSModalPanelRunLoopMode
+@property (retain) NSArray *runLoopModes;
+
+// Users who wish to replace GTMHTTPFetcher's use of NSURLConnection
+// can do so globally here. The replacement should be a subclass of
+// NSURLConnection.
++ (Class)connectionClass;
++ (void)setConnectionClass:(Class)theClass;
+
+// Spin the run loop, discarding events, until the fetch has completed
+//
+// This is only for use in testing or in tools without a user interface.
+//
+// Synchronous fetches should never be done by shipping apps; they are
+// sufficient reason for rejection from the app store.
+- (void)waitForCompletionWithTimeout:(NSTimeInterval)timeoutInSeconds;
+
+#if STRIP_GTM_FETCH_LOGGING
+// if logging is stripped, provide a stub for the main method
+// for controlling logging
++ (void)setLoggingEnabled:(BOOL)flag;
+#endif // STRIP_GTM_FETCH_LOGGING
+
+@end
diff --git a/example/common/gtm-oauth2/HTTPFetcher/GTMHTTPFetcher.m b/example/common/gtm-oauth2/HTTPFetcher/GTMHTTPFetcher.m
new file mode 100755
index 00000000..ebbdd0de
--- /dev/null
+++ b/example/common/gtm-oauth2/HTTPFetcher/GTMHTTPFetcher.m
@@ -0,0 +1,1956 @@
+/* Copyright (c) 2011 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.
+ */
+
+//
+// GTMHTTPFetcher.m
+//
+
+#define GTMHTTPFETCHER_DEFINE_GLOBALS 1
+
+#import "GTMHTTPFetcher.h"
+
+#if GTM_BACKGROUND_FETCHING
+#import <UIKit/UIKit.h>
+#endif
+
+static id <GTMCookieStorageProtocol> gGTMFetcherStaticCookieStorage = nil;
+static Class gGTMFetcherConnectionClass = nil;
+
+// The default max retry interview is 10 minutes for uploads (POST/PUT/PATCH),
+// 1 minute for downloads.
+static const NSTimeInterval kUnsetMaxRetryInterval = -1;
+static const NSTimeInterval kDefaultMaxDownloadRetryInterval = 60.0;
+static const NSTimeInterval kDefaultMaxUploadRetryInterval = 60.0 * 10.;
+
+// delegateQueue callback parameters
+static NSString *const kCallbackData = @"data";
+static NSString *const kCallbackError = @"error";
+
+//
+// GTMHTTPFetcher
+//
+
+@interface GTMHTTPFetcher ()
+
+@property (copy) NSString *temporaryDownloadPath;
+@property (retain) id <GTMCookieStorageProtocol> cookieStorage;
+@property (readwrite, retain) NSData *downloadedData;
+#if NS_BLOCKS_AVAILABLE
+@property (copy) void (^completionBlock)(NSData *, NSError *);
+#endif
+
+- (BOOL)beginFetchMayDelay:(BOOL)mayDelay
+ mayAuthorize:(BOOL)mayAuthorize;
+- (void)failToBeginFetchWithError:(NSError *)error;
+- (void)failToBeginFetchDeferWithError:(NSError *)error;
+
+#if GTM_BACKGROUND_FETCHING
+- (void)endBackgroundTask;
+- (void)backgroundFetchExpired;
+#endif
+
+- (BOOL)authorizeRequest;
+- (void)authorizer:(id <GTMFetcherAuthorizationProtocol>)auth
+ request:(NSMutableURLRequest *)request
+ finishedWithError:(NSError *)error;
+
+- (NSString *)createTempDownloadFilePathForPath:(NSString *)targetPath;
+- (void)stopFetchReleasingCallbacks:(BOOL)shouldReleaseCallbacks;
+- (BOOL)shouldReleaseCallbacksUponCompletion;
+
+- (void)addCookiesToRequest:(NSMutableURLRequest *)request;
+- (void)handleCookiesForResponse:(NSURLResponse *)response;
+
+- (void)invokeFetchCallbacksWithData:(NSData *)data
+ error:(NSError *)error;
+- (void)invokeFetchCallback:(SEL)sel
+ target:(id)target
+ data:(NSData *)data
+ error:(NSError *)error;
+- (void)invokeFetchCallbacksOnDelegateQueueWithData:(NSData *)data
+ error:(NSError *)error;
+- (void)releaseCallbacks;
+
+- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error;
+
+- (BOOL)shouldRetryNowForStatus:(NSInteger)status error:(NSError *)error;
+- (void)destroyRetryTimer;
+- (void)beginRetryTimer;
+- (void)primeRetryTimerWithNewTimeInterval:(NSTimeInterval)secs;
+- (void)sendStopNotificationIfNeeded;
+- (void)retryFetch;
+- (void)retryTimerFired:(NSTimer *)timer;
+@end
+
+@interface GTMHTTPFetcher (GTMHTTPFetcherLoggingInternal)
+- (void)setupStreamLogging;
+- (void)logFetchWithError:(NSError *)error;
+@end
+
+@implementation GTMHTTPFetcher
+
++ (GTMHTTPFetcher *)fetcherWithRequest:(NSURLRequest *)request {
+ return [[[[self class] alloc] initWithRequest:request] autorelease];
+}
+
++ (GTMHTTPFetcher *)fetcherWithURL:(NSURL *)requestURL {
+ return [self fetcherWithRequest:[NSURLRequest requestWithURL:requestURL]];
+}
+
++ (GTMHTTPFetcher *)fetcherWithURLString:(NSString *)requestURLString {
+ return [self fetcherWithURL:[NSURL URLWithString:requestURLString]];
+}
+
++ (void)initialize {
+ // initialize is guaranteed by the runtime to be called in a
+ // thread-safe manner
+ if (!gGTMFetcherStaticCookieStorage) {
+ Class cookieStorageClass = NSClassFromString(@"GTMCookieStorage");
+ if (cookieStorageClass) {
+ gGTMFetcherStaticCookieStorage = [[cookieStorageClass alloc] init];
+ }
+ }
+}
+
+- (id)init {
+ return [self initWithRequest:nil];
+}
+
+- (id)initWithRequest:(NSURLRequest *)request {
+ self = [super init];
+ if (self) {
+ request_ = [request mutableCopy];
+
+ if (gGTMFetcherStaticCookieStorage != nil) {
+ // The user has compiled with the cookie storage class available;
+ // default to static cookie storage, so our cookies are independent
+ // of the cookies of other apps.
+ [self setCookieStorageMethod:kGTMHTTPFetcherCookieStorageMethodStatic];
+ } else {
+ // Default to system default cookie storage
+ [self setCookieStorageMethod:kGTMHTTPFetcherCookieStorageMethodSystemDefault];
+ }
+#if !STRIP_GTM_FETCH_LOGGING
+ // Encourage developers to set the comment property or use
+ // setCommentWithFormat: by providing a default string.
+ comment_ = @"(No fetcher comment set)";
+#endif
+ }
+ return self;
+}
+
+- (id)copyWithZone:(NSZone *)zone {
+ // disallow use of fetchers in a copy property
+ [self doesNotRecognizeSelector:_cmd];
+ return nil;
+}
+
+- (NSString *)description {
+ return [NSString stringWithFormat:@"%@ %p (%@)",
+ [self class], self, [self.mutableRequest URL]];
+}
+
+#if !GTM_IPHONE
+- (void)finalize {
+ [self stopFetchReleasingCallbacks:YES]; // releases connection_, destroys timers
+ [super finalize];
+}
+#endif
+
+- (void)dealloc {
+#if DEBUG
+ NSAssert(!isStopNotificationNeeded_,
+ @"unbalanced fetcher notification for %@", [request_ URL]);
+#endif
+
+ // Note: if a connection or a retry timer was pending, then this instance
+ // would be retained by those so it wouldn't be getting dealloc'd,
+ // hence we don't need to stopFetch here
+ [request_ release];
+ [connection_ release];
+ [downloadedData_ release];
+ [downloadPath_ release];
+ [temporaryDownloadPath_ release];
+ [downloadFileHandle_ release];
+ [credential_ release];
+ [proxyCredential_ release];
+ [postData_ release];
+ [postStream_ release];
+ [loggedStreamData_ release];
+ [response_ release];
+#if NS_BLOCKS_AVAILABLE
+ [completionBlock_ release];
+ [receivedDataBlock_ release];
+ [sentDataBlock_ release];
+ [retryBlock_ release];
+#endif
+ [userData_ release];
+ [properties_ release];
+ [delegateQueue_ release];
+ [runLoopModes_ release];
+ [fetchHistory_ release];
+ [cookieStorage_ release];
+ [authorizer_ release];
+ [service_ release];
+ [serviceHost_ release];
+ [thread_ release];
+ [retryTimer_ release];
+ [comment_ release];
+ [log_ release];
+#if !STRIP_GTM_FETCH_LOGGING
+ [redirectedFromURL_ release];
+ [logRequestBody_ release];
+ [logResponseBody_ release];
+#endif
+
+ [super dealloc];
+}
+
+#pragma mark -
+
+// Begin fetching the URL (or begin a retry fetch). The delegate is retained
+// for the duration of the fetch connection.
+
+- (BOOL)beginFetchWithDelegate:(id)delegate
+ didFinishSelector:(SEL)finishedSelector {
+ GTMAssertSelectorNilOrImplementedWithArgs(delegate, finishedSelector, @encode(GTMHTTPFetcher *), @encode(NSData *), @encode(NSError *), 0);
+ GTMAssertSelectorNilOrImplementedWithArgs(delegate, receivedDataSel_, @encode(GTMHTTPFetcher *), @encode(NSData *), 0);
+ GTMAssertSelectorNilOrImplementedWithArgs(delegate, retrySel_, @encode(GTMHTTPFetcher *), @encode(BOOL), @encode(NSError *), 0);
+
+ // We'll retain the delegate only during the outstanding connection (similar
+ // to what Cocoa does with performSelectorOnMainThread:) and during
+ // authorization or delays, since the app would crash
+ // if the delegate was released before the fetch calls back
+ [self setDelegate:delegate];
+ finishedSel_ = finishedSelector;
+
+ return [self beginFetchMayDelay:YES
+ mayAuthorize:YES];
+}
+
+- (BOOL)beginFetchMayDelay:(BOOL)mayDelay
+ mayAuthorize:(BOOL)mayAuthorize {
+ // This is the internal entry point for re-starting fetches
+ NSError *error = nil;
+
+ if (connection_ != nil) {
+ NSAssert1(connection_ != nil, @"fetch object %@ being reused; this should never happen", self);
+ goto CannotBeginFetch;
+ }
+
+ if (request_ == nil || [request_ URL] == nil) {
+ NSAssert(request_ != nil, @"beginFetchWithDelegate requires a request with a URL");
+ goto CannotBeginFetch;
+ }
+
+ self.downloadedData = nil;
+ downloadedLength_ = 0;
+
+ if (mayDelay && service_) {
+ BOOL shouldFetchNow = [service_ fetcherShouldBeginFetching:self];
+ if (!shouldFetchNow) {
+ // the fetch is deferred, but will happen later
+ return YES;
+ }
+ }
+
+ NSString *effectiveHTTPMethod = [request_ valueForHTTPHeaderField:@"X-HTTP-Method-Override"];
+ if (effectiveHTTPMethod == nil) {
+ effectiveHTTPMethod = [request_ HTTPMethod];
+ }
+ BOOL isEffectiveHTTPGet = (effectiveHTTPMethod == nil
+ || [effectiveHTTPMethod isEqual:@"GET"]);
+
+ if (postData_ || postStream_) {
+ if (isEffectiveHTTPGet) {
+ [request_ setHTTPMethod:@"POST"];
+ isEffectiveHTTPGet = NO;
+ }
+
+ if (postData_) {
+ [request_ setHTTPBody:postData_];
+ } else {
+ if ([self respondsToSelector:@selector(setupStreamLogging)]) {
+ [self performSelector:@selector(setupStreamLogging)];
+ }
+
+ [request_ setHTTPBodyStream:postStream_];
+ }
+ }
+
+ // We authorize after setting up the http method and body in the request
+ // because OAuth 1 may need to sign the request body
+ if (mayAuthorize && authorizer_) {
+ BOOL isAuthorized = [authorizer_ isAuthorizedRequest:request_];
+ if (!isAuthorized) {
+ // authorization needed
+ return [self authorizeRequest];
+ }
+ }
+
+ [fetchHistory_ updateRequest:request_ isHTTPGet:isEffectiveHTTPGet];
+
+ // set the default upload or download retry interval, if necessary
+ if (isRetryEnabled_
+ && maxRetryInterval_ <= kUnsetMaxRetryInterval) {
+ if (isEffectiveHTTPGet || [effectiveHTTPMethod isEqual:@"HEAD"]) {
+ [self setMaxRetryInterval:kDefaultMaxDownloadRetryInterval];
+ } else {
+ [self setMaxRetryInterval:kDefaultMaxUploadRetryInterval];
+ }
+ }
+
+ [self addCookiesToRequest:request_];
+
+ if (downloadPath_ != nil) {
+ // downloading to a path, so create a temporary file and a file handle for
+ // downloading
+ NSString *tempPath = [self createTempDownloadFilePathForPath:downloadPath_];
+
+ BOOL didCreate = [[NSData data] writeToFile:tempPath
+ options:0
+ error:&error];
+ if (!didCreate) goto CannotBeginFetch;
+
+ [self setTemporaryDownloadPath:tempPath];
+
+ NSFileHandle *fh = [NSFileHandle fileHandleForWritingAtPath:tempPath];
+ if (fh == nil) goto CannotBeginFetch;
+
+ [self setDownloadFileHandle:fh];
+ }
+
+ // finally, start the connection
+
+ Class connectionClass = [[self class] connectionClass];
+
+ NSOperationQueue *delegateQueue = delegateQueue_;
+ if (delegateQueue &&
+ ![connectionClass instancesRespondToSelector:@selector(setDelegateQueue:)]) {
+ // NSURLConnection has no setDelegateQueue: on iOS 4 and Mac OS X 10.5.
+ delegateQueue = nil;
+ self.delegateQueue = nil;
+ }
+
+#if DEBUG && TARGET_OS_IPHONE
+ BOOL isPreIOS6 = (NSFoundationVersionNumber <= 890.1);
+ if (isPreIOS6 && delegateQueue) {
+ NSLog(@"GTMHTTPFetcher delegateQueue not safe in iOS 5");
+ }
+#endif
+
+ if (downloadFileHandle_ != nil) {
+ // Downloading to a file, so downloadedData_ remains nil.
+ } else {
+ self.downloadedData = [NSMutableData data];
+ }
+
+ hasConnectionEnded_ = NO;
+ if ([runLoopModes_ count] == 0 && delegateQueue == nil) {
+ // No custom callback modes or queue were specified, so start the connection
+ // on the current run loop in the current mode
+ connection_ = [[connectionClass connectionWithRequest:request_
+ delegate:self] retain];
+ } else {
+ // Specify callbacks be on an operation queue or on the current run loop
+ // in the specified modes
+ connection_ = [[connectionClass alloc] initWithRequest:request_
+ delegate:self
+ startImmediately:NO];
+ if (delegateQueue) {
+ [connection_ performSelector:@selector(setDelegateQueue:)
+ withObject:delegateQueue];
+ } else if (runLoopModes_) {
+ NSRunLoop *rl = [NSRunLoop currentRunLoop];
+ for (NSString *mode in runLoopModes_) {
+ [connection_ scheduleInRunLoop:rl forMode:mode];
+ }
+ }
+ [connection_ start];
+ }
+
+ if (!connection_) {
+ NSAssert(connection_ != nil, @"beginFetchWithDelegate could not create a connection");
+ self.downloadedData = nil;
+ goto CannotBeginFetch;
+ }
+
+#if GTM_BACKGROUND_FETCHING
+ backgroundTaskIdentifer_ = 0; // UIBackgroundTaskInvalid is 0 on iOS 4
+ if (shouldFetchInBackground_) {
+ // For iOS 3 compatibility, ensure that UIApp supports backgrounding
+ UIApplication *app = [UIApplication sharedApplication];
+ if ([app respondsToSelector:@selector(beginBackgroundTaskWithExpirationHandler:)]) {
+ // Tell UIApplication that we want to continue even when the app is in the
+ // background.
+ NSThread *thread = delegateQueue_ ? nil : [NSThread currentThread];
+ backgroundTaskIdentifer_ = [app beginBackgroundTaskWithExpirationHandler:^{
+ // Background task expiration callback - this block is always invoked by
+ // UIApplication on the main thread.
+ if (thread) {
+ // Run the user's callbacks on the thread used to start the
+ // fetch.
+ [self performSelector:@selector(backgroundFetchExpired)
+ onThread:thread
+ withObject:nil
+ waitUntilDone:YES];
+ } else {
+ // backgroundFetchExpired invokes callbacks on the provided delegate
+ // queue.
+ [self backgroundFetchExpired];
+ }
+ }];
+ }
+ }
+#endif
+
+ // Once connection_ is non-nil we can send the start notification
+ isStopNotificationNeeded_ = YES;
+ NSNotificationCenter *defaultNC = [NSNotificationCenter defaultCenter];
+ [defaultNC postNotificationName:kGTMHTTPFetcherStartedNotification
+ object:self];
+ return YES;
+
+CannotBeginFetch:
+ [self failToBeginFetchDeferWithError:error];
+ return NO;
+}
+
+- (void)failToBeginFetchDeferWithError:(NSError *)error {
+ if (delegateQueue_) {
+ // Deferring will happen by the callback being invoked on the specified
+ // queue.
+ [self failToBeginFetchWithError:error];
+ } else {
+ // No delegate queue has been specified, so put the callback
+ // on an appropriate run loop.
+ NSArray *modes = (runLoopModes_ ? runLoopModes_ :
+ [NSArray arrayWithObject:NSRunLoopCommonModes]);
+ [self performSelector:@selector(failToBeginFetchWithError:)
+ onThread:[NSThread currentThread]
+ withObject:error
+ waitUntilDone:NO
+ modes:modes];
+ }
+}
+
+- (void)failToBeginFetchWithError:(NSError *)error {
+ if (error == nil) {
+ error = [NSError errorWithDomain:kGTMHTTPFetcherErrorDomain
+ code:kGTMHTTPFetcherErrorDownloadFailed
+ userInfo:nil];
+ }
+
+ [[self retain] autorelease]; // In case the callback releases us
+
+ [self invokeFetchCallbacksOnDelegateQueueWithData:nil
+ error:error];
+
+ [self releaseCallbacks];
+
+ [service_ fetcherDidStop:self];
+
+ self.authorizer = nil;
+
+ if (temporaryDownloadPath_) {
+ [[NSFileManager defaultManager] removeItemAtPath:temporaryDownloadPath_
+ error:NULL];
+ self.temporaryDownloadPath = nil;
+ }
+}
+
+#if GTM_BACKGROUND_FETCHING
+- (void)backgroundFetchExpired {
+ // On background expiration, we stop the fetch and invoke the callbacks
+ NSError *error = [NSError errorWithDomain:kGTMHTTPFetcherErrorDomain
+ code:kGTMHTTPFetcherErrorBackgroundExpiration
+ userInfo:nil];
+ [self invokeFetchCallbacksOnDelegateQueueWithData:nil
+ error:error];
+ @synchronized(self) {
+ // Stopping the fetch here will indirectly call endBackgroundTask
+ [self stopFetchReleasingCallbacks:NO];
+
+ [self releaseCallbacks];
+ self.authorizer = nil;
+ }
+}
+
+- (void)endBackgroundTask {
+ @synchronized(self) {
+ // Whenever the connection stops or background execution expires,
+ // we need to tell UIApplication we're done
+ if (backgroundTaskIdentifer_) {
+ // If backgroundTaskIdentifer_ is non-zero, we know we're on iOS 4
+ UIApplication *app = [UIApplication sharedApplication];
+ [app endBackgroundTask:backgroundTaskIdentifer_];
+
+ backgroundTaskIdentifer_ = 0;
+ }
+ }
+}
+#endif // GTM_BACKGROUND_FETCHING
+
+- (BOOL)authorizeRequest {
+ id authorizer = self.authorizer;
+ SEL asyncAuthSel = @selector(authorizeRequest:delegate:didFinishSelector:);
+ if ([authorizer respondsToSelector:asyncAuthSel]) {
+ SEL callbackSel = @selector(authorizer:request:finishedWithError:);
+ [authorizer authorizeRequest:request_
+ delegate:self
+ didFinishSelector:callbackSel];
+ return YES;
+ } else {
+ NSAssert(authorizer == nil, @"invalid authorizer for fetch");
+
+ // No authorizing possible, and authorizing happens only after any delay;
+ // just begin fetching
+ return [self beginFetchMayDelay:NO
+ mayAuthorize:NO];
+ }
+}
+
+- (void)authorizer:(id <GTMFetcherAuthorizationProtocol>)auth
+ request:(NSMutableURLRequest *)request
+ finishedWithError:(NSError *)error {
+ if (error != nil) {
+ // We can't fetch without authorization
+ [self failToBeginFetchDeferWithError:error];
+ } else {
+ [self beginFetchMayDelay:NO
+ mayAuthorize:NO];
+ }
+}
+
+#if NS_BLOCKS_AVAILABLE
+- (BOOL)beginFetchWithCompletionHandler:(void (^)(NSData *data, NSError *error))handler {
+ self.completionBlock = handler;
+
+ // The user may have called setDelegate: earlier if they want to use other
+ // delegate-style callbacks during the fetch; otherwise, the delegate is nil,
+ // which is fine.
+ return [self beginFetchWithDelegate:[self delegate]
+ didFinishSelector:nil];
+}
+#endif
+
+- (NSString *)createTempDownloadFilePathForPath:(NSString *)targetPath {
+ NSString *tempDir = nil;
+
+#if (!TARGET_OS_IPHONE && (MAC_OS_X_VERSION_MAX_ALLOWED >= 1060))
+ // Find an appropriate directory for the download, ideally on the same disk
+ // as the final target location so the temporary file won't have to be moved
+ // to a different disk.
+ //
+ // Available in SDKs for 10.6 and iOS 4
+ //
+ // Oct 2011: We previously also used URLForDirectory for
+ // (TARGET_OS_IPHONE && (__IPHONE_OS_VERSION_MAX_ALLOWED >= 40000))
+ // but that is returning a non-temporary directory for iOS, unfortunately
+
+ SEL sel = @selector(URLForDirectory:inDomain:appropriateForURL:create:error:);
+ if ([NSFileManager instancesRespondToSelector:sel]) {
+ NSError *error = nil;
+ NSURL *targetURL = [NSURL fileURLWithPath:targetPath];
+ NSFileManager *fileMgr = [NSFileManager defaultManager];
+
+ NSURL *tempDirURL = [fileMgr URLForDirectory:NSItemReplacementDirectory
+ inDomain:NSUserDomainMask
+ appropriateForURL:targetURL
+ create:YES
+ error:&error];
+ tempDir = [tempDirURL path];
+ }
+#endif
+
+ if (tempDir == nil) {
+ tempDir = NSTemporaryDirectory();
+ }
+
+ static unsigned int counter = 0;
+ NSString *name = [NSString stringWithFormat:@"gtmhttpfetcher_%u_%u",
+ ++counter, (unsigned int) arc4random()];
+ NSString *result = [tempDir stringByAppendingPathComponent:name];
+ return result;
+}
+
+- (void)addCookiesToRequest:(NSMutableURLRequest *)request {
+ // Get cookies for this URL from our storage array, if
+ // we have a storage array
+ if (cookieStorageMethod_ != kGTMHTTPFetcherCookieStorageMethodSystemDefault
+ && cookieStorageMethod_ != kGTMHTTPFetcherCookieStorageMethodNone) {
+
+ NSArray *cookies = [cookieStorage_ cookiesForURL:[request URL]];
+ if ([cookies count] > 0) {
+
+ NSDictionary *headerFields = [NSHTTPCookie requestHeaderFieldsWithCookies:cookies];
+ NSString *cookieHeader = [headerFields objectForKey:@"Cookie"]; // key used in header dictionary
+ if (cookieHeader) {
+ [request addValue:cookieHeader forHTTPHeaderField:@"Cookie"]; // header name
+ }
+ }
+ }
+}
+
+// Returns YES if this is in the process of fetching a URL, or waiting to
+// retry, or waiting for authorization, or waiting to be issued by the
+// service object
+- (BOOL)isFetching {
+ if (connection_ != nil || retryTimer_ != nil) return YES;
+
+ BOOL isAuthorizing = [authorizer_ isAuthorizingRequest:request_];
+ if (isAuthorizing) return YES;
+
+ BOOL isDelayed = [service_ isDelayingFetcher:self];
+ return isDelayed;
+}
+
+// Returns the status code set in connection:didReceiveResponse:
+- (NSInteger)statusCode {
+
+ NSInteger statusCode;
+
+ if (response_ != nil
+ && [response_ respondsToSelector:@selector(statusCode)]) {
+
+ statusCode = [(NSHTTPURLResponse *)response_ statusCode];
+ } else {
+ // Default to zero, in hopes of hinting "Unknown" (we can't be
+ // sure that things are OK enough to use 200).
+ statusCode = 0;
+ }
+ return statusCode;
+}
+
+- (NSDictionary *)responseHeaders {
+ if (response_ != nil
+ && [response_ respondsToSelector:@selector(allHeaderFields)]) {
+
+ NSDictionary *headers = [(NSHTTPURLResponse *)response_ allHeaderFields];
+ return headers;
+ }
+ return nil;
+}
+
+- (void)releaseCallbacks {
+ [delegate_ autorelease];
+ delegate_ = nil;
+
+ [delegateQueue_ autorelease];
+ delegateQueue_ = nil;
+
+#if NS_BLOCKS_AVAILABLE
+ self.completionBlock = nil;
+ self.sentDataBlock = nil;
+ self.receivedDataBlock = nil;
+ self.retryBlock = nil;
+#endif
+}
+
+// Cancel the fetch of the URL that's currently in progress.
+- (void)stopFetchReleasingCallbacks:(BOOL)shouldReleaseCallbacks {
+ id <GTMHTTPFetcherServiceProtocol> service;
+
+ // if the connection or the retry timer is all that's retaining the fetcher,
+ // we want to be sure this instance survives stopping at least long enough for
+ // the stack to unwind
+ [[self retain] autorelease];
+
+ [self destroyRetryTimer];
+
+ @synchronized(self) {
+ service = [[service_ retain] autorelease];
+
+ if (connection_) {
+ // in case cancelling the connection calls this recursively, we want
+ // to ensure that we'll only release the connection and delegate once,
+ // so first set connection_ to nil
+ NSURLConnection* oldConnection = connection_;
+ connection_ = nil;
+
+ if (!hasConnectionEnded_) {
+ [oldConnection cancel];
+ }
+
+ // this may be called in a callback from the connection, so use autorelease
+ [oldConnection autorelease];
+ }
+ } // @synchronized(self)
+
+ // send the stopped notification
+ [self sendStopNotificationIfNeeded];
+
+ @synchronized(self) {
+ [authorizer_ stopAuthorizationForRequest:request_];
+
+ if (shouldReleaseCallbacks) {
+ [self releaseCallbacks];
+
+ self.authorizer = nil;
+ }
+
+ if (temporaryDownloadPath_) {
+ [[NSFileManager defaultManager] removeItemAtPath:temporaryDownloadPath_
+ error:NULL];
+ self.temporaryDownloadPath = nil;
+ }
+ } // @synchronized(self)
+
+ [service fetcherDidStop:self];
+
+#if GTM_BACKGROUND_FETCHING
+ [self endBackgroundTask];
+#endif
+}
+
+// External stop method
+- (void)stopFetching {
+ [self stopFetchReleasingCallbacks:YES];
+}
+
+- (void)sendStopNotificationIfNeeded {
+ BOOL sendNow = NO;
+ @synchronized(self) {
+ if (isStopNotificationNeeded_) {
+ isStopNotificationNeeded_ = NO;
+ sendNow = YES;
+ }
+ }
+
+ if (sendNow) {
+ NSNotificationCenter *defaultNC = [NSNotificationCenter defaultCenter];
+ [defaultNC postNotificationName:kGTMHTTPFetcherStoppedNotification
+ object:self];
+ }
+}
+
+- (void)retryFetch {
+ [self stopFetchReleasingCallbacks:NO];
+
+ [self beginFetchWithDelegate:delegate_
+ didFinishSelector:finishedSel_];
+}
+
+- (void)waitForCompletionWithTimeout:(NSTimeInterval)timeoutInSeconds {
+ NSDate* giveUpDate = [NSDate dateWithTimeIntervalSinceNow:timeoutInSeconds];
+
+ // Loop until the callbacks have been called and released, and until
+ // the connection is no longer pending, or until the timeout has expired
+ BOOL isMainThread = [NSThread isMainThread];
+
+ while ((!hasConnectionEnded_
+#if NS_BLOCKS_AVAILABLE
+ || completionBlock_ != nil
+#endif
+ || delegate_ != nil)
+ && [giveUpDate timeIntervalSinceNow] > 0) {
+
+ // Run the current run loop 1/1000 of a second to give the networking
+ // code a chance to work
+ if (isMainThread || delegateQueue_ == nil) {
+ NSDate *stopDate = [NSDate dateWithTimeIntervalSinceNow:0.001];
+ [[NSRunLoop currentRunLoop] runUntilDate:stopDate];
+ } else {
+ [NSThread sleepForTimeInterval:0.001];
+ }
+ }
+}
+
+#pragma mark NSURLConnection Delegate Methods
+
+//
+// NSURLConnection Delegate Methods
+//
+
+// This method just says "follow all redirects", which _should_ be the default behavior,
+// According to file:///Developer/ADC%20Reference%20Library/documentation/Cocoa/Conceptual/URLLoadingSystem
+// but the redirects were not being followed until I added this method. May be
+// a bug in the NSURLConnection code, or the documentation.
+//
+// In OS X 10.4.8 and earlier, the redirect request doesn't
+// get the original's headers and body. This causes POSTs to fail.
+// So we construct a new request, a copy of the original, with overrides from the
+// redirect.
+//
+// Docs say that if redirectResponse is nil, just return the redirectRequest.
+
+- (NSURLRequest *)connection:(NSURLConnection *)connection
+ willSendRequest:(NSURLRequest *)redirectRequest
+ redirectResponse:(NSURLResponse *)redirectResponse {
+ @synchronized(self) {
+ if (redirectRequest && redirectResponse) {
+ // save cookies from the response
+ [self handleCookiesForResponse:redirectResponse];
+
+ NSMutableURLRequest *newRequest = [[request_ mutableCopy] autorelease];
+ // copy the URL
+ NSURL *redirectURL = [redirectRequest URL];
+ NSURL *url = [newRequest URL];
+
+ // disallow scheme changes (say, from https to http)
+ NSString *redirectScheme = [url scheme];
+ NSString *newScheme = [redirectURL scheme];
+ NSString *newResourceSpecifier = [redirectURL resourceSpecifier];
+
+ if ([redirectScheme caseInsensitiveCompare:@"http"] == NSOrderedSame
+ && newScheme != nil
+ && [newScheme caseInsensitiveCompare:@"https"] == NSOrderedSame) {
+
+ // allow the change from http to https
+ redirectScheme = newScheme;
+ }
+
+ NSString *newUrlString = [NSString stringWithFormat:@"%@:%@",
+ redirectScheme, newResourceSpecifier];
+
+ NSURL *newURL = [NSURL URLWithString:newUrlString];
+ [newRequest setURL:newURL];
+
+ // any headers in the redirect override headers in the original.
+ NSDictionary *redirectHeaders = [redirectRequest allHTTPHeaderFields];
+ for (NSString *key in redirectHeaders) {
+ NSString *value = [redirectHeaders objectForKey:key];
+ [newRequest setValue:value forHTTPHeaderField:key];
+ }
+
+ [self addCookiesToRequest:newRequest];
+
+ redirectRequest = newRequest;
+
+ // log the response we just received
+ [self setResponse:redirectResponse];
+ [self logNowWithError:nil];
+
+ // update the request for future logging
+ NSMutableURLRequest *mutable = [[redirectRequest mutableCopy] autorelease];
+ [self setMutableRequest:mutable];
+ }
+ return redirectRequest;
+ } // @synchronized(self)
+}
+
+- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {
+ @synchronized(self) {
+ // This method is called when the server has determined that it
+ // has enough information to create the NSURLResponse
+ // it can be called multiple times, for example in the case of a
+ // redirect, so each time we reset the data.
+ [downloadedData_ setLength:0];
+ [downloadFileHandle_ truncateFileAtOffset:0];
+ downloadedLength_ = 0;
+
+ [self setResponse:response];
+
+ // Save cookies from the response
+ [self handleCookiesForResponse:response];
+ }
+}
+
+
+// handleCookiesForResponse: handles storage of cookies for responses passed to
+// connection:willSendRequest:redirectResponse: and connection:didReceiveResponse:
+- (void)handleCookiesForResponse:(NSURLResponse *)response {
+
+ if (cookieStorageMethod_ == kGTMHTTPFetcherCookieStorageMethodSystemDefault
+ || cookieStorageMethod_ == kGTMHTTPFetcherCookieStorageMethodNone) {
+
+ // do nothing special for NSURLConnection's default storage mechanism
+ // or when we're ignoring cookies
+
+ } else if ([response respondsToSelector:@selector(allHeaderFields)]) {
+
+ // grab the cookies from the header as NSHTTPCookies and store them either
+ // into our static array or into the fetchHistory
+
+ NSDictionary *responseHeaderFields = [(NSHTTPURLResponse *)response allHeaderFields];
+ if (responseHeaderFields) {
+
+ NSArray *cookies = [NSHTTPCookie cookiesWithResponseHeaderFields:responseHeaderFields
+ forURL:[response URL]];
+ if ([cookies count] > 0) {
+ [cookieStorage_ setCookies:cookies];
+ }
+ }
+ }
+}
+
+-(void)connection:(NSURLConnection *)connection
+didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge {
+ @synchronized(self) {
+ if ([challenge previousFailureCount] <= 2) {
+
+ NSURLCredential *credential = credential_;
+
+ if ([[challenge protectionSpace] isProxy] && proxyCredential_ != nil) {
+ credential = proxyCredential_;
+ }
+
+ // Here, if credential is still nil, then we *could* try to get it from
+ // NSURLCredentialStorage's defaultCredentialForProtectionSpace:.
+ // We don't, because we're assuming:
+ //
+ // - for server credentials, we only want ones supplied by the program
+ // calling http fetcher
+ // - for proxy credentials, if one were necessary and available in the
+ // keychain, it would've been found automatically by NSURLConnection
+ // and this challenge delegate method never would've been called
+ // anyway
+
+ if (credential) {
+ // try the credential
+ [[challenge sender] useCredential:credential
+ forAuthenticationChallenge:challenge];
+ return;
+ }
+ } // @synchronized(self)
+
+ // If we don't have credentials, or we've already failed auth 3x,
+ // report the error, putting the challenge as a value in the userInfo
+ // dictionary.
+#if DEBUG
+ NSAssert(!isCancellingChallenge_, @"isCancellingChallenge_ unexpected");
+#endif
+ NSDictionary *userInfo = [NSDictionary dictionaryWithObject:challenge
+ forKey:kGTMHTTPFetcherErrorChallengeKey];
+ NSError *error = [NSError errorWithDomain:kGTMHTTPFetcherErrorDomain
+ code:kGTMHTTPFetcherErrorAuthenticationChallengeFailed
+ userInfo:userInfo];
+
+ // cancelAuthenticationChallenge seems to indirectly call
+ // connection:didFailWithError: now, though that isn't documented
+ //
+ // We'll use an ivar to make the indirect invocation of the
+ // delegate method do nothing.
+ isCancellingChallenge_ = YES;
+ [[challenge sender] cancelAuthenticationChallenge:challenge];
+ isCancellingChallenge_ = NO;
+
+ [self connection:connection didFailWithError:error];
+ }
+}
+
+- (void)invokeFetchCallbacksWithData:(NSData *)data
+ error:(NSError *)error {
+ // To avoid deadlocks, this should not be called inside of @synchronized(self)
+ id target;
+ SEL sel;
+#if NS_BLOCKS_AVAILABLE
+ void (^block)(NSData *, NSError *);
+#endif
+ @synchronized(self) {
+ target = delegate_;
+ sel = finishedSel_;
+ block = completionBlock_;
+ }
+
+ [[self retain] autorelease]; // In case the callback releases us
+
+ [self invokeFetchCallback:sel
+ target:target
+ data:data
+ error:error];
+
+#if NS_BLOCKS_AVAILABLE
+ if (block) {
+ block(data, error);
+ }
+#endif
+}
+
+- (void)invokeFetchCallback:(SEL)sel
+ target:(id)target
+ data:(NSData *)data
+ error:(NSError *)error {
+ // This method is available to subclasses which may provide a customized
+ // target pointer.
+ if (target && sel) {
+ NSMethodSignature *sig = [target methodSignatureForSelector:sel];
+ NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:sig];
+ [invocation setSelector:sel];
+ [invocation setTarget:target];
+ [invocation setArgument:&self atIndex:2];
+ [invocation setArgument:&data atIndex:3];
+ [invocation setArgument:&error atIndex:4];
+ [invocation invoke];
+ }
+}
+
+- (void)invokeFetchCallbacksOnDelegateQueueWithData:(NSData *)data
+ error:(NSError *)error {
+ // This is called by methods that are not already on the delegateQueue
+ // (as NSURLConnection callbacks should already be, but other failures
+ // are not.)
+ if (!delegateQueue_) {
+ [self invokeFetchCallbacksWithData:data error:error];
+ }
+
+ // Values may be nil.
+ NSMutableDictionary *dict = [NSMutableDictionary dictionaryWithCapacity:2];
+ [dict setValue:data forKey:kCallbackData];
+ [dict setValue:error forKey:kCallbackError];
+ NSInvocationOperation *op =
+ [[[NSInvocationOperation alloc] initWithTarget:self
+ selector:@selector(invokeOnQueueWithDictionary:)
+ object:dict] autorelease];
+ [delegateQueue_ addOperation:op];
+}
+
+- (void)invokeOnQueueWithDictionary:(NSDictionary *)dict {
+ NSData *data = [dict objectForKey:kCallbackData];
+ NSError *error = [dict objectForKey:kCallbackError];
+
+ [self invokeFetchCallbacksWithData:data error:error];
+}
+
+
+- (void)invokeSentDataCallback:(SEL)sel
+ target:(id)target
+ didSendBodyData:(NSInteger)bytesWritten
+ totalBytesWritten:(NSInteger)totalBytesWritten
+ totalBytesExpectedToWrite:(NSInteger)totalBytesExpectedToWrite {
+ if (target && sel) {
+ NSMethodSignature *sig = [target methodSignatureForSelector:sel];
+ NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:sig];
+ [invocation setSelector:sel];
+ [invocation setTarget:target];
+ [invocation setArgument:&self atIndex:2];
+ [invocation setArgument:&bytesWritten atIndex:3];
+ [invocation setArgument:&totalBytesWritten atIndex:4];
+ [invocation setArgument:&totalBytesExpectedToWrite atIndex:5];
+ [invocation invoke];
+ }
+}
+
+- (BOOL)invokeRetryCallback:(SEL)sel
+ target:(id)target
+ willRetry:(BOOL)willRetry
+ error:(NSError *)error {
+ if (target && sel) {
+ NSMethodSignature *sig = [target methodSignatureForSelector:sel];
+ NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:sig];
+ [invocation setSelector:sel];
+ [invocation setTarget:target];
+ [invocation setArgument:&self atIndex:2];
+ [invocation setArgument:&willRetry atIndex:3];
+ [invocation setArgument:&error atIndex:4];
+ [invocation invoke];
+
+ [invocation getReturnValue:&willRetry];
+ }
+ return willRetry;
+}
+
+- (void)connection:(NSURLConnection *)connection
+ didSendBodyData:(NSInteger)bytesWritten
+ totalBytesWritten:(NSInteger)totalBytesWritten
+totalBytesExpectedToWrite:(NSInteger)totalBytesExpectedToWrite {
+ @synchronized(self) {
+ SEL sel = [self sentDataSelector];
+ [self invokeSentDataCallback:sel
+ target:delegate_
+ didSendBodyData:bytesWritten
+ totalBytesWritten:totalBytesWritten
+ totalBytesExpectedToWrite:totalBytesExpectedToWrite];
+
+#if NS_BLOCKS_AVAILABLE
+ if (sentDataBlock_) {
+ sentDataBlock_(bytesWritten, totalBytesWritten, totalBytesExpectedToWrite);
+ }
+#endif
+ }
+}
+
+- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
+ @synchronized(self) {
+#if DEBUG
+ NSAssert(!hasConnectionEnded_, @"Connection received data after ending");
+
+ // The download file handle should be set or the data object allocated
+ // before the fetch is started.
+ NSAssert((downloadFileHandle_ == nil) != (downloadedData_ == nil),
+ @"received data accumulates as either NSData (%d) or"
+ @" NSFileHandle (%d)",
+ (downloadedData_ != nil), (downloadFileHandle_ != nil));
+#endif
+ // Hopefully, we'll never see this execute out-of-order, receiving data
+ // after we've received the finished or failed callback.
+ if (hasConnectionEnded_) return;
+
+ if (downloadFileHandle_ != nil) {
+ // Append to file
+ @try {
+ [downloadFileHandle_ writeData:data];
+
+ downloadedLength_ = [downloadFileHandle_ offsetInFile];
+ }
+ @catch (NSException *exc) {
+ // Couldn't write to file, probably due to a full disk
+ NSDictionary *userInfo = [NSDictionary dictionaryWithObject:[exc reason]
+ forKey:NSLocalizedDescriptionKey];
+ NSError *error = [NSError errorWithDomain:kGTMHTTPFetcherStatusDomain
+ code:kGTMHTTPFetcherErrorFileHandleException
+ userInfo:userInfo];
+ [self connection:connection didFailWithError:error];
+ return;
+ }
+ } else {
+ // append to mutable data
+ [downloadedData_ appendData:data];
+
+ downloadedLength_ = [downloadedData_ length];
+ }
+
+ if (receivedDataSel_) {
+ [delegate_ performSelector:receivedDataSel_
+ withObject:self
+ withObject:downloadedData_];
+ }
+
+#if NS_BLOCKS_AVAILABLE
+ if (receivedDataBlock_) {
+ receivedDataBlock_(downloadedData_);
+ }
+#endif
+ } // @synchronized(self)
+}
+
+// For error 304's ("Not Modified") where we've cached the data, return
+// status 200 ("OK") to the caller (but leave the fetcher status as 304)
+// and copy the cached data.
+//
+// For other errors or if there's no cached data, just return the actual status.
+- (NSData *)cachedDataForStatus {
+ if ([self statusCode] == kGTMHTTPFetcherStatusNotModified
+ && [fetchHistory_ shouldCacheETaggedData]) {
+ NSData *cachedData = [fetchHistory_ cachedDataForRequest:request_];
+ return cachedData;
+ }
+ return nil;
+}
+
+- (NSInteger)statusAfterHandlingNotModifiedError {
+ NSInteger status = [self statusCode];
+ NSData *cachedData = [self cachedDataForStatus];
+ if (cachedData) {
+ // Forge the status to pass on to the delegate
+ status = 200;
+
+ // Copy our stored data
+ if (downloadFileHandle_ != nil) {
+ @try {
+ // Downloading to a file handle won't save to the cache (the data is
+ // likely inappropriately large for caching), but will still read from
+ // the cache, on the unlikely chance that the response was Not Modified
+ // and the URL response was indeed present in the cache.
+ [downloadFileHandle_ truncateFileAtOffset:0];
+ [downloadFileHandle_ writeData:cachedData];
+ downloadedLength_ = [downloadFileHandle_ offsetInFile];
+ }
+ @catch (NSException *) {
+ // Failed to write data, likely due to lack of disk space
+ status = kGTMHTTPFetcherErrorFileHandleException;
+ }
+ } else {
+ [downloadedData_ setData:cachedData];
+ downloadedLength_ = [cachedData length];
+ }
+ }
+ return status;
+}
+
+- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
+ BOOL shouldStopFetching = YES;
+ BOOL shouldSendStopNotification = NO;
+ NSError *error = nil;
+ NSData *downloadedData;
+#if !STRIP_GTM_FETCH_LOGGING
+ BOOL shouldDeferLogging = NO;
+#endif
+ BOOL shouldBeginRetryTimer = NO;
+ BOOL hasLogged = NO;
+
+ @synchronized(self) {
+ // We no longer need to cancel the connection
+ hasConnectionEnded_ = YES;
+
+ // Skip caching ETagged results when the data is being saved to a file
+ if (downloadFileHandle_ == nil) {
+ [fetchHistory_ updateFetchHistoryWithRequest:request_
+ response:response_
+ downloadedData:downloadedData_];
+ } else {
+ [fetchHistory_ removeCachedDataForRequest:request_];
+ }
+
+ [[self retain] autorelease]; // in case the callback releases us
+
+ NSInteger status = [self statusCode];
+ if ([self cachedDataForStatus] != nil) {
+ // Log the pre-cache response.
+ [self logNowWithError:nil];
+ hasLogged = YES;
+ status = [self statusAfterHandlingNotModifiedError];
+ }
+
+ shouldSendStopNotification = YES;
+
+ if (status >= 0 && status < 300) {
+ // success
+ if (downloadPath_) {
+ // Avoid deleting the downloaded file when the fetch stops
+ [downloadFileHandle_ closeFile];
+ self.downloadFileHandle = nil;
+
+ NSFileManager *fileMgr = [NSFileManager defaultManager];
+ [fileMgr removeItemAtPath:downloadPath_
+ error:NULL];
+
+ if ([fileMgr moveItemAtPath:temporaryDownloadPath_
+ toPath:downloadPath_
+ error:&error]) {
+ self.temporaryDownloadPath = nil;
+ }
+ }
+ } else {
+ // unsuccessful
+ if (!hasLogged) {
+ [self logNowWithError:nil];
+ hasLogged = YES;
+ }
+ // Status over 300; retry or notify the delegate of failure
+ if ([self shouldRetryNowForStatus:status error:nil]) {
+ // retrying
+ shouldBeginRetryTimer = YES;
+ shouldStopFetching = NO;
+ } else {
+ NSDictionary *userInfo = nil;
+ if ([downloadedData_ length] > 0) {
+ userInfo = [NSDictionary dictionaryWithObject:downloadedData_
+ forKey:kGTMHTTPFetcherStatusDataKey];
+ }
+ error = [NSError errorWithDomain:kGTMHTTPFetcherStatusDomain
+ code:status
+ userInfo:userInfo];
+ }
+ }
+ downloadedData = downloadedData_;
+#if !STRIP_GTM_FETCH_LOGGING
+ shouldDeferLogging = shouldDeferResponseBodyLogging_;
+#endif
+ } // @synchronized(self)
+
+ if (shouldBeginRetryTimer) {
+ [self beginRetryTimer];
+ }
+
+ if (shouldSendStopNotification) {
+ // We want to send the stop notification before calling the delegate's
+ // callback selector, since the callback selector may release all of
+ // the fetcher properties that the client is using to track the fetches.
+ //
+ // We'll also stop now so that, to any observers watching the notifications,
+ // it doesn't look like our wait for a retry (which may be long,
+ // 30 seconds or more) is part of the network activity.
+ [self sendStopNotificationIfNeeded];
+ }
+
+ if (shouldStopFetching) {
+ // Call the callbacks (outside of the @synchronized to avoid deadlocks.)
+ [self invokeFetchCallbacksWithData:downloadedData
+ error:error];
+ BOOL shouldRelease = [self shouldReleaseCallbacksUponCompletion];
+ [self stopFetchReleasingCallbacks:shouldRelease];
+ }
+
+ @synchronized(self) {
+ BOOL shouldLogNow = !hasLogged;
+#if !STRIP_GTM_FETCH_LOGGING
+ if (shouldDeferLogging) shouldLogNow = NO;
+#endif
+ if (shouldLogNow) {
+ [self logNowWithError:nil];
+ }
+ }
+}
+
+- (BOOL)shouldReleaseCallbacksUponCompletion {
+ // A subclass can override this to keep callbacks around after the
+ // connection has finished successfully
+ return YES;
+}
+
+- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
+ @synchronized(self) {
+ // Prevent the failure callback from being called twice, since the stopFetch
+ // call below (either the explicit one at the end of this method, or the
+ // implicit one when the retry occurs) will release the delegate.
+ if (connection_ == nil) return;
+
+ // If this method was invoked indirectly by cancellation of an authentication
+ // challenge, defer this until it is called again with the proper error object
+ if (isCancellingChallenge_) return;
+
+ // We no longer need to cancel the connection
+ hasConnectionEnded_ = YES;
+
+ [self logNowWithError:error];
+ }
+
+ // See comment about sendStopNotificationIfNeeded
+ // in connectionDidFinishLoading:
+ [self sendStopNotificationIfNeeded];
+
+ if ([self shouldRetryNowForStatus:0 error:error]) {
+ [self beginRetryTimer];
+ } else {
+ [[self retain] autorelease]; // in case the callback releases us
+
+ [self invokeFetchCallbacksWithData:nil
+ error:error];
+
+ [self stopFetchReleasingCallbacks:YES];
+ }
+}
+
+- (void)logNowWithError:(NSError *)error {
+ // If the logging category is available, then log the current request,
+ // response, data, and error
+ if ([self respondsToSelector:@selector(logFetchWithError:)]) {
+ [self performSelector:@selector(logFetchWithError:) withObject:error];
+ }
+}
+
+#pragma mark Retries
+
+- (BOOL)isRetryError:(NSError *)error {
+
+ struct retryRecord {
+ NSString *const domain;
+ int code;
+ };
+
+ struct retryRecord retries[] = {
+ { kGTMHTTPFetcherStatusDomain, 408 }, // request timeout
+ { kGTMHTTPFetcherStatusDomain, 503 }, // service unavailable
+ { kGTMHTTPFetcherStatusDomain, 504 }, // request timeout
+ { NSURLErrorDomain, NSURLErrorTimedOut },
+ { NSURLErrorDomain, NSURLErrorNetworkConnectionLost },
+ { nil, 0 }
+ };
+
+ // NSError's isEqual always returns false for equal but distinct instances
+ // of NSError, so we have to compare the domain and code values explicitly
+
+ for (int idx = 0; retries[idx].domain != nil; idx++) {
+
+ if ([[error domain] isEqual:retries[idx].domain]
+ && [error code] == retries[idx].code) {
+
+ return YES;
+ }
+ }
+ return NO;
+}
+
+
+// shouldRetryNowForStatus:error: returns YES if the user has enabled retries
+// and the status or error is one that is suitable for retrying. "Suitable"
+// means either the isRetryError:'s list contains the status or error, or the
+// user's retrySelector: is present and returns YES when called, or the
+// authorizer may be able to fix.
+- (BOOL)shouldRetryNowForStatus:(NSInteger)status
+ error:(NSError *)error {
+ // Determine if a refreshed authorizer may avoid an authorization error
+ BOOL shouldRetryForAuthRefresh = NO;
+ BOOL isFirstAuthError = (authorizer_ != nil)
+ && !hasAttemptedAuthRefresh_
+ && (status == kGTMHTTPFetcherStatusUnauthorized); // 401
+
+ if (isFirstAuthError) {
+ if ([authorizer_ respondsToSelector:@selector(primeForRefresh)]) {
+ BOOL hasPrimed = [authorizer_ primeForRefresh];
+ if (hasPrimed) {
+ shouldRetryForAuthRefresh = YES;
+ hasAttemptedAuthRefresh_ = YES;
+ [request_ setValue:nil forHTTPHeaderField:@"Authorization"];
+ }
+ }
+ }
+
+ // Determine if we're doing exponential backoff retries
+ BOOL shouldDoIntervalRetry = [self isRetryEnabled]
+ && ([self nextRetryInterval] < [self maxRetryInterval]);
+
+ BOOL willRetry = NO;
+ BOOL canRetry = shouldRetryForAuthRefresh || shouldDoIntervalRetry;
+ if (canRetry) {
+ // Check if this is a retryable error
+ if (error == nil) {
+ // Make an error for the status
+ NSDictionary *userInfo = nil;
+ if ([downloadedData_ length] > 0) {
+ userInfo = [NSDictionary dictionaryWithObject:downloadedData_
+ forKey:kGTMHTTPFetcherStatusDataKey];
+ }
+ error = [NSError errorWithDomain:kGTMHTTPFetcherStatusDomain
+ code:status
+ userInfo:userInfo];
+ }
+
+ willRetry = shouldRetryForAuthRefresh || [self isRetryError:error];
+
+ // If the user has installed a retry callback, consult that
+ willRetry = [self invokeRetryCallback:retrySel_
+ target:delegate_
+ willRetry:willRetry
+ error:error];
+#if NS_BLOCKS_AVAILABLE
+ if (retryBlock_) {
+ willRetry = retryBlock_(willRetry, error);
+ }
+#endif
+ }
+ return willRetry;
+}
+
+- (void)beginRetryTimer {
+ @synchronized(self) {
+ if (delegateQueue_ != nil && ![NSThread isMainThread]) {
+ // A delegate queue is set, so the thread we're running on may not
+ // have a run loop. We'll defer creating and starting the timer
+ // until we're on the main thread to ensure it has a run loop.
+ // (If we weren't supporting 10.5, we could use dispatch_after instead
+ // of an NSTimer.)
+ [self performSelectorOnMainThread:_cmd
+ withObject:nil
+ waitUntilDone:NO];
+ return;
+ }
+ }
+
+ NSTimeInterval nextInterval = [self nextRetryInterval];
+ NSTimeInterval maxInterval = [self maxRetryInterval];
+ NSTimeInterval newInterval = MIN(nextInterval, maxInterval);
+
+ [self primeRetryTimerWithNewTimeInterval:newInterval];
+
+ NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
+ [nc postNotificationName:kGTMHTTPFetcherRetryDelayStartedNotification
+ object:self];
+}
+
+- (void)primeRetryTimerWithNewTimeInterval:(NSTimeInterval)secs {
+
+ [self destroyRetryTimer];
+
+ @synchronized(self) {
+ lastRetryInterval_ = secs;
+
+ retryTimer_ = [NSTimer timerWithTimeInterval:secs
+ target:self
+ selector:@selector(retryTimerFired:)
+ userInfo:nil
+ repeats:NO];
+ [retryTimer_ retain];
+
+ NSRunLoop *timerRL = (self.delegateQueue ?
+ [NSRunLoop mainRunLoop] : [NSRunLoop currentRunLoop]);
+ [timerRL addTimer:retryTimer_
+ forMode:NSDefaultRunLoopMode];
+ }
+}
+
+- (void)retryTimerFired:(NSTimer *)timer {
+ [self destroyRetryTimer];
+
+ @synchronized(self) {
+ retryCount_++;
+
+ [self retryFetch];
+ }
+}
+
+- (void)destroyRetryTimer {
+ BOOL shouldNotify = NO;
+ @synchronized(self) {
+ if (retryTimer_) {
+ [retryTimer_ invalidate];
+ [retryTimer_ autorelease];
+ retryTimer_ = nil;
+ shouldNotify = YES;
+ }
+ } // @synchronized(self)
+
+ if (shouldNotify) {
+ NSNotificationCenter *defaultNC = [NSNotificationCenter defaultCenter];
+ [defaultNC postNotificationName:kGTMHTTPFetcherRetryDelayStoppedNotification
+ object:self];
+ }
+}
+
+- (NSUInteger)retryCount {
+ return retryCount_;
+}
+
+- (NSTimeInterval)nextRetryInterval {
+ // The next wait interval is the factor (2.0) times the last interval,
+ // but never less than the minimum interval.
+ NSTimeInterval secs = lastRetryInterval_ * retryFactor_;
+ secs = MIN(secs, maxRetryInterval_);
+ secs = MAX(secs, minRetryInterval_);
+
+ return secs;
+}
+
+- (BOOL)isRetryEnabled {
+ return isRetryEnabled_;
+}
+
+- (void)setRetryEnabled:(BOOL)flag {
+
+ if (flag && !isRetryEnabled_) {
+ // We defer initializing these until the user calls setRetryEnabled
+ // to avoid using the random number generator if it's not needed.
+ // However, this means min and max intervals for this fetcher are reset
+ // as a side effect of calling setRetryEnabled.
+ //
+ // Make an initial retry interval random between 1.0 and 2.0 seconds
+ [self setMinRetryInterval:0.0];
+ [self setMaxRetryInterval:kUnsetMaxRetryInterval];
+ [self setRetryFactor:2.0];
+ lastRetryInterval_ = 0.0;
+ }
+ isRetryEnabled_ = flag;
+};
+
+- (NSTimeInterval)maxRetryInterval {
+ return maxRetryInterval_;
+}
+
+- (void)setMaxRetryInterval:(NSTimeInterval)secs {
+ if (secs > 0) {
+ maxRetryInterval_ = secs;
+ } else {
+ maxRetryInterval_ = kUnsetMaxRetryInterval;
+ }
+}
+
+- (double)minRetryInterval {
+ return minRetryInterval_;
+}
+
+- (void)setMinRetryInterval:(NSTimeInterval)secs {
+ if (secs > 0) {
+ minRetryInterval_ = secs;
+ } else {
+ // Set min interval to a random value between 1.0 and 2.0 seconds
+ // so that if multiple clients start retrying at the same time, they'll
+ // repeat at different times and avoid overloading the server
+ minRetryInterval_ = 1.0 + ((double)(arc4random() & 0x0FFFF) / (double) 0x0FFFF);
+ }
+}
+
+#pragma mark Getters and Setters
+
+@dynamic cookieStorageMethod,
+ retryEnabled,
+ maxRetryInterval,
+ minRetryInterval,
+ retryCount,
+ nextRetryInterval,
+ statusCode,
+ responseHeaders,
+ fetchHistory,
+ userData,
+ properties;
+
+@synthesize mutableRequest = request_,
+ credential = credential_,
+ proxyCredential = proxyCredential_,
+ postData = postData_,
+ postStream = postStream_,
+ delegate = delegate_,
+ authorizer = authorizer_,
+ service = service_,
+ serviceHost = serviceHost_,
+ servicePriority = servicePriority_,
+ thread = thread_,
+ sentDataSelector = sentDataSel_,
+ receivedDataSelector = receivedDataSel_,
+ retrySelector = retrySel_,
+ retryFactor = retryFactor_,
+ response = response_,
+ downloadedLength = downloadedLength_,
+ downloadedData = downloadedData_,
+ downloadPath = downloadPath_,
+ temporaryDownloadPath = temporaryDownloadPath_,
+ downloadFileHandle = downloadFileHandle_,
+ delegateQueue = delegateQueue_,
+ runLoopModes = runLoopModes_,
+ comment = comment_,
+ log = log_,
+ cookieStorage = cookieStorage_;
+
+#if NS_BLOCKS_AVAILABLE
+@synthesize completionBlock = completionBlock_,
+ sentDataBlock = sentDataBlock_,
+ receivedDataBlock = receivedDataBlock_,
+ retryBlock = retryBlock_;
+#endif
+
+@synthesize shouldFetchInBackground = shouldFetchInBackground_;
+
+- (NSInteger)cookieStorageMethod {
+ return cookieStorageMethod_;
+}
+
+- (void)setCookieStorageMethod:(NSInteger)method {
+
+ cookieStorageMethod_ = method;
+
+ if (method == kGTMHTTPFetcherCookieStorageMethodSystemDefault) {
+ // System default
+ [request_ setHTTPShouldHandleCookies:YES];
+
+ // No need for a cookie storage object
+ self.cookieStorage = nil;
+
+ } else {
+ // Not system default
+ [request_ setHTTPShouldHandleCookies:NO];
+
+ if (method == kGTMHTTPFetcherCookieStorageMethodStatic) {
+ // Store cookies in the static array
+ NSAssert(gGTMFetcherStaticCookieStorage != nil,
+ @"cookie storage requires GTMHTTPFetchHistory");
+
+ self.cookieStorage = gGTMFetcherStaticCookieStorage;
+ } else if (method == kGTMHTTPFetcherCookieStorageMethodFetchHistory) {
+ // store cookies in the fetch history
+ self.cookieStorage = [fetchHistory_ cookieStorage];
+ } else {
+ // kGTMHTTPFetcherCookieStorageMethodNone - ignore cookies
+ self.cookieStorage = nil;
+ }
+ }
+}
+
++ (id <GTMCookieStorageProtocol>)staticCookieStorage {
+ return gGTMFetcherStaticCookieStorage;
+}
+
++ (BOOL)doesSupportSentDataCallback {
+#if GTM_IPHONE
+ // NSURLConnection's didSendBodyData: delegate support appears to be
+ // available starting in iPhone OS 3.0
+ return (NSFoundationVersionNumber >= 678.47);
+#else
+ // Per WebKit's MaxFoundationVersionWithoutdidSendBodyDataDelegate
+ //
+ // Indicates if NSURLConnection will invoke the didSendBodyData: delegate
+ // method
+ return (NSFoundationVersionNumber > 677.21);
+#endif
+}
+
+- (id <GTMHTTPFetchHistoryProtocol>)fetchHistory {
+ return fetchHistory_;
+}
+
+- (void)setFetchHistory:(id <GTMHTTPFetchHistoryProtocol>)fetchHistory {
+ [fetchHistory_ autorelease];
+ fetchHistory_ = [fetchHistory retain];
+
+ if (fetchHistory_ != nil) {
+ // set the fetch history's cookie array to be the cookie store
+ [self setCookieStorageMethod:kGTMHTTPFetcherCookieStorageMethodFetchHistory];
+
+ } else {
+ // The fetch history was removed
+ if (cookieStorageMethod_ == kGTMHTTPFetcherCookieStorageMethodFetchHistory) {
+ // Fall back to static storage
+ [self setCookieStorageMethod:kGTMHTTPFetcherCookieStorageMethodStatic];
+ }
+ }
+}
+
+- (id)userData {
+ @synchronized(self) {
+ return userData_;
+ }
+}
+
+- (void)setUserData:(id)theObj {
+ @synchronized(self) {
+ [userData_ autorelease];
+ userData_ = [theObj retain];
+ }
+}
+
+- (void)setProperties:(NSMutableDictionary *)dict {
+ @synchronized(self) {
+ [properties_ autorelease];
+
+ // This copies rather than retains the parameter for compatiblity with
+ // an earlier version that took an immutable parameter and copied it.
+ properties_ = [dict mutableCopy];
+ }
+}
+
+- (NSMutableDictionary *)properties {
+ @synchronized(self) {
+ return properties_;
+ }
+}
+
+- (void)setProperty:(id)obj forKey:(NSString *)key {
+ @synchronized(self) {
+ if (properties_ == nil && obj != nil) {
+ [self setProperties:[NSMutableDictionary dictionary]];
+ }
+ [properties_ setValue:obj forKey:key];
+ }
+}
+
+- (id)propertyForKey:(NSString *)key {
+ @synchronized(self) {
+ return [properties_ objectForKey:key];
+ }
+}
+
+- (void)addPropertiesFromDictionary:(NSDictionary *)dict {
+ @synchronized(self) {
+ if (properties_ == nil && dict != nil) {
+ [self setProperties:[[dict mutableCopy] autorelease]];
+ } else {
+ [properties_ addEntriesFromDictionary:dict];
+ }
+ }
+}
+
+- (void)setCommentWithFormat:(id)format, ... {
+#if !STRIP_GTM_FETCH_LOGGING
+ NSString *result = format;
+ if (format) {
+ va_list argList;
+ va_start(argList, format);
+
+ result = [[[NSString alloc] initWithFormat:format
+ arguments:argList] autorelease];
+ va_end(argList);
+ }
+ [self setComment:result];
+#endif
+}
+
++ (Class)connectionClass {
+ if (gGTMFetcherConnectionClass == nil) {
+ gGTMFetcherConnectionClass = [NSURLConnection class];
+ }
+ return gGTMFetcherConnectionClass;
+}
+
++ (void)setConnectionClass:(Class)theClass {
+ gGTMFetcherConnectionClass = theClass;
+}
+
+#if STRIP_GTM_FETCH_LOGGING
++ (void)setLoggingEnabled:(BOOL)flag {
+}
+#endif // STRIP_GTM_FETCH_LOGGING
+
+@end
+
+void GTMAssertSelectorNilOrImplementedWithArgs(id obj, SEL sel, ...) {
+
+ // Verify that the object's selector is implemented with the proper
+ // number and type of arguments
+#if DEBUG
+ va_list argList;
+ va_start(argList, sel);
+
+ if (obj && sel) {
+ // Check that the selector is implemented
+ if (![obj respondsToSelector:sel]) {
+ NSLog(@"\"%@\" selector \"%@\" is unimplemented or misnamed",
+ NSStringFromClass([obj class]),
+ NSStringFromSelector(sel));
+ NSCAssert(0, @"callback selector unimplemented or misnamed");
+ } else {
+ const char *expectedArgType;
+ unsigned int argCount = 2; // skip self and _cmd
+ NSMethodSignature *sig = [obj methodSignatureForSelector:sel];
+
+ // Check that each expected argument is present and of the correct type
+ while ((expectedArgType = va_arg(argList, const char*)) != 0) {
+
+ if ([sig numberOfArguments] > argCount) {
+ const char *foundArgType = [sig getArgumentTypeAtIndex:argCount];
+
+ if(0 != strncmp(foundArgType, expectedArgType, strlen(expectedArgType))) {
+ NSLog(@"\"%@\" selector \"%@\" argument %d should be type %s",
+ NSStringFromClass([obj class]),
+ NSStringFromSelector(sel), (argCount - 2), expectedArgType);
+ NSCAssert(0, @"callback selector argument type mistake");
+ }
+ }
+ argCount++;
+ }
+
+ // Check that the proper number of arguments are present in the selector
+ if (argCount != [sig numberOfArguments]) {
+ NSLog( @"\"%@\" selector \"%@\" should have %d arguments",
+ NSStringFromClass([obj class]),
+ NSStringFromSelector(sel), (argCount - 2));
+ NSCAssert(0, @"callback selector arguments incorrect");
+ }
+ }
+ }
+
+ va_end(argList);
+#endif
+}
+
+NSString *GTMCleanedUserAgentString(NSString *str) {
+ // Reference http://www.w3.org/Protocols/rfc2616/rfc2616-sec2.html
+ // and http://www-archive.mozilla.org/build/user-agent-strings.html
+
+ if (str == nil) return nil;
+
+ NSMutableString *result = [NSMutableString stringWithString:str];
+
+ // Replace spaces with underscores
+ [result replaceOccurrencesOfString:@" "
+ withString:@"_"
+ options:0
+ range:NSMakeRange(0, [result length])];
+
+ // Delete http token separators and remaining whitespace
+ static NSCharacterSet *charsToDelete = nil;
+ if (charsToDelete == nil) {
+ // Make a set of unwanted characters
+ NSString *const kSeparators = @"()<>@,;:\\\"/[]?={}";
+
+ NSMutableCharacterSet *mutableChars;
+ mutableChars = [[[NSCharacterSet whitespaceAndNewlineCharacterSet] mutableCopy] autorelease];
+ [mutableChars addCharactersInString:kSeparators];
+ charsToDelete = [mutableChars copy]; // hang on to an immutable copy
+ }
+
+ while (1) {
+ NSRange separatorRange = [result rangeOfCharacterFromSet:charsToDelete];
+ if (separatorRange.location == NSNotFound) break;
+
+ [result deleteCharactersInRange:separatorRange];
+ };
+
+ return result;
+}
+
+NSString *GTMSystemVersionString(void) {
+ NSString *systemString = @"";
+
+#if TARGET_OS_MAC && !TARGET_OS_IPHONE
+ // Mac build
+ static NSString *savedSystemString = nil;
+ if (savedSystemString == nil) {
+ // With Gestalt inexplicably deprecated in 10.8, we're reduced to reading
+ // the system plist file.
+ NSString *const kPath = @"/System/Library/CoreServices/SystemVersion.plist";
+ NSDictionary *plist = [NSDictionary dictionaryWithContentsOfFile:kPath];
+ NSString *versString = [plist objectForKey:@"ProductVersion"];
+ if ([versString length] == 0) {
+ versString = @"10.?.?";
+ }
+ savedSystemString = [[NSString alloc] initWithFormat:@"MacOSX/%@", versString];
+ }
+ systemString = savedSystemString;
+#elif TARGET_OS_IPHONE
+ // Compiling against the iPhone SDK
+
+ static NSString *savedSystemString = nil;
+ if (savedSystemString == nil) {
+ // Avoid the slowness of calling currentDevice repeatedly on the iPhone
+ UIDevice* currentDevice = [UIDevice currentDevice];
+
+ NSString *rawModel = [currentDevice model];
+ NSString *model = GTMCleanedUserAgentString(rawModel);
+
+ NSString *systemVersion = [currentDevice systemVersion];
+
+ savedSystemString = [[NSString alloc] initWithFormat:@"%@/%@",
+ model, systemVersion]; // "iPod_Touch/2.2"
+ }
+ systemString = savedSystemString;
+
+#elif (GTL_IPHONE || GDATA_IPHONE)
+ // Compiling iOS libraries against the Mac SDK
+ systemString = @"iPhone/x.x";
+
+#elif defined(_SYS_UTSNAME_H)
+ // Foundation-only build
+ struct utsname unameRecord;
+ uname(&unameRecord);
+
+ systemString = [NSString stringWithFormat:@"%s/%s",
+ unameRecord.sysname, unameRecord.release]; // "Darwin/8.11.1"
+#endif
+
+ return systemString;
+}
+
+// Return a generic name and version for the current application; this avoids
+// anonymous server transactions.
+NSString *GTMApplicationIdentifier(NSBundle *bundle) {
+ static NSString *sAppID = nil;
+ if (sAppID != nil) return sAppID;
+
+ // If there's a bundle ID, use that; otherwise, use the process name
+ if (bundle == nil) {
+ bundle = [NSBundle mainBundle];
+ }
+
+ NSString *identifier;
+ NSString *bundleID = [bundle bundleIdentifier];
+ if ([bundleID length] > 0) {
+ identifier = bundleID;
+ } else {
+ // Fall back on the procname, prefixed by "proc" to flag that it's
+ // autogenerated and perhaps unreliable
+ NSString *procName = [[NSProcessInfo processInfo] processName];
+ identifier = [NSString stringWithFormat:@"proc_%@", procName];
+ }
+
+ // Clean up whitespace and special characters
+ identifier = GTMCleanedUserAgentString(identifier);
+
+ // If there's a version number, append that
+ NSString *version = [bundle objectForInfoDictionaryKey:@"CFBundleShortVersionString"];
+ if ([version length] == 0) {
+ version = [bundle objectForInfoDictionaryKey:@"CFBundleVersion"];
+ }
+
+ // Clean up whitespace and special characters
+ version = GTMCleanedUserAgentString(version);
+
+ // Glue the two together (cleanup done above or else cleanup would strip the
+ // slash)
+ if ([version length] > 0) {
+ identifier = [identifier stringByAppendingFormat:@"/%@", version];
+ }
+
+ sAppID = [identifier copy];
+ return sAppID;
+}