diff options
Diffstat (limited to 'Foundation')
-rw-r--r-- | Foundation/GTMHTTPFetcher.h | 494 | ||||
-rw-r--r-- | Foundation/GTMHTTPFetcher.m | 1978 | ||||
-rw-r--r-- | Foundation/GTMHTTPFetcherTest.m | 516 | ||||
-rw-r--r-- | Foundation/GTMProgressMonitorInputStream.h | 59 | ||||
-rw-r--r-- | Foundation/GTMProgressMonitorInputStream.m | 217 | ||||
-rw-r--r-- | Foundation/GTMProgressMonitorInputStreamTest.m | 262 | ||||
-rw-r--r-- | Foundation/TestData/GTMHTTPFetcherTestPage.html | 9 |
7 files changed, 8 insertions, 3527 deletions
diff --git a/Foundation/GTMHTTPFetcher.h b/Foundation/GTMHTTPFetcher.h index 5abbe1a..7377d1c 100644 --- a/Foundation/GTMHTTPFetcher.h +++ b/Foundation/GTMHTTPFetcher.h @@ -16,493 +16,7 @@ // the License. // -// 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 or POST -// - When you want the "standard" behavior for connections (redirection handling -// an so on) -// - When you want to avoid cookie collisions with Safari and other applications -// - When you want to provide if-modified-since headers -// - When you need to set a credential for the http -// - When you want to avoid changing WebKit's cookies -// -// 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, allocate it with initWithRequest: -// and have the delegate release the fetcher in the callbacks. -// -// Sample usage: -// -// NSURLRequest *request = [NSURLRequest requestWithURL:myURL]; -// GTMHTTPFetcher* myFetcher = [GTMHTTPFetcher httpFetcherWithRequest:request]; -// -// [myFetcher setPostData:[postString dataUsingEncoding:NSUTF8StringEncoding]]; // for POSTs -// -// [myFetcher setCredential:[NSURLCredential authCredentialWithUsername:@"foo" -// password:@"bar"]]; // optional http credential -// -// [myFetcher setFetchHistory:myMutableDictionary]; // optional, for persisting modified-dates -// -// [myFetcher beginFetchWithDelegate:self -// didFinishSelector:@selector(myFetcher:finishedWithData:) -// didFailSelector:@selector(myFetcher:failedWithError:)]; -// -// Upon fetch completion, the callback selectors are invoked; they should have -// these signatures (you can use any callback method names you want so long as -// the signatures match these): -// -// - (void)myFetcher:(GTMHTTPFetcher *)fetcher finishedWithData:(NSData *)retrievedData; -// - (void)myFetcher:(GTMHTTPFetcher *)fetcher failedWithError:(NSError *)error; -// -// NOTE: Fetches may retrieve data from the server even though the server -// returned an error. The failWithError selector is called when the server -// status is >= 300 (along with any server-supplied data, usually -// some html explaining the error). -// Status codes are at <http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html> -// -// -// 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 you provide a fetch history (such as for periodic checks, described -// below) then the cookie storage mechanism is set to use the fetch -// history rather than the static storage. -// -// -// Fetching for periodic checks: -// -// The fetcher object can track "Last-modified" dates on returned data and -// provide an "If-modified-since" header. This allows the server to save -// bandwidth by providing a "Nothing changed" status message instead of response -// data. -// -// To get this behavior, provide a persistent mutable dictionary to setFetchHistory:, -// and look for the failedWithError: callback with code 304 -// (kGTMHTTPFetcherStatusNotModified) like this: -// -// - (void)myFetcher:(GTMHTTPFetcher *)fetcher failedWithError:(NSError *)error { -// if ([[error domain] isEqual:kGTMHTTPFetcherStatusDomain] && -// ([error code] == kGTMHTTPFetcherStatusNotModified)) { -// // [[error userInfo] objectForKey:kGTMHTTPFetcherStatusDataKey] is -// // empty; use the data from the previous finishedWithData: for this URL -// } else { -// // handle other server status code -// } -// } -// -// The fetchHistory mutable dictionary should be maintained by the client between -// fetches and given to each fetcher intended to have the If-modified-since header -// or the same cookie storage. -// -// -// Monitoring received data -// -// The optional received data selector should have the signature -// -// - (void)myFetcher:(GTMHTTPFetcher *)fetcher receivedData:(NSData *)dataReceivedSoFar; -// -// The bytes received so far are [dataReceivedSoFar length]. 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] (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 setIsRetryEnabled: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.]; // in seconds; default is 600 seconds -// -// 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> - -#import "GTMDefines.h" - -#undef _EXTERN -#undef _INITIALIZE_AS -#ifdef GTMHTTPFETCHER_DEFINE_GLOBALS -#define _EXTERN -#define _INITIALIZE_AS(x) =x -#else -#define _EXTERN GTM_EXTERN -#define _INITIALIZE_AS(x) -#endif - -// notifications & errors -_EXTERN NSString* const kGTMHTTPFetcherErrorDomain _INITIALIZE_AS(@"com.google.mactoolbox.HTTPFetcher"); -_EXTERN NSString* const kGTMHTTPFetcherStatusDomain _INITIALIZE_AS(@"com.google.mactoolbox.HTTPStatus"); -_EXTERN NSString* const kGTMHTTPFetcherErrorChallengeKey _INITIALIZE_AS(@"challenge"); -_EXTERN NSString* const kGTMHTTPFetcherStatusDataKey _INITIALIZE_AS(@"data"); // any data returns w/ a kGTMHTTPFetcherStatusDomain error - - -// fetch history mutable dictionary keys -_EXTERN NSString* const kGTMHTTPFetcherHistoryLastModifiedKey _INITIALIZE_AS(@"FetchHistoryLastModified"); -_EXTERN NSString* const kGTMHTTPFetcherHistoryDatedDataKey _INITIALIZE_AS(@"FetchHistoryDatedDataCache"); -_EXTERN NSString* const kGTMHTTPFetcherHistoryCookiesKey _INITIALIZE_AS(@"FetchHistoryCookies"); - -enum { - kGTMHTTPFetcherErrorDownloadFailed = -1, - kGTMHTTPFetcherErrorAuthenticationChallengeFailed = -2, - - kGTMHTTPFetcherStatusNotModified = 304 -}; - -enum { - kGTMHTTPFetcherCookieStorageMethodStatic = 0, - kGTMHTTPFetcherCookieStorageMethodFetchHistory = 1, - kGTMHTTPFetcherCookieStorageMethodSystemDefault = 2 -}; -typedef NSUInteger GTMHTTPFetcherCookieStorageMethod; - -/// async retrieval of an http get or post -@interface GTMHTTPFetcher : NSObject { - NSMutableURLRequest *request_; - NSURLConnection *connection_; // while connection_ is non-nil, delegate_ is retained - NSMutableData *downloadedData_; - 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_; // WEAK (though retained during an open connection) - SEL finishedSEL_; // should by implemented by delegate - SEL failedSEL_; // should be implemented by delegate - SEL receivedDataSEL_; // optional, set with setReceivedDataSelector - id userData_; // retained, if set by caller - NSMutableDictionary *properties_; // more data retained for caller - NSArray *runLoopModes_; // optional, for 10.5 and later - NSMutableDictionary *fetchHistory_; // if supplied by the caller, used for Last-Modified-Since checks and cookies - BOOL shouldCacheDatedData_; // if true, remembers and returns data marked with a last-modified date - GTMHTTPFetcherCookieStorageMethod cookieStorageMethod_; // constant from above - - BOOL isRetryEnabled_; // user wants auto-retry - SEL retrySEL_; // optional; set with setRetrySelector - NSTimer *retryTimer_; - unsigned int retryCount_; - NSTimeInterval maxRetryInterval_; // default 600 seconds - NSTimeInterval minRetryInterval_; // random between 1 and 2 seconds - NSTimeInterval retryFactor_; // default interval multiplier is 2 - NSTimeInterval lastRetryInterval_; -} - -/// create a fetcher -// -// httpFetcherWithRequest 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 *)httpFetcherWithRequest:(NSURLRequest *)request; - -// designated initializer -- (id)initWithRequest:(NSURLRequest *)request; - -- (NSMutableURLRequest *)request; -- (void)setRequest:(NSURLRequest *)theRequest; - -// setting the credential is optional; it is used if the connection receives -// an authentication challenge -- (NSURLCredential *)credential; -- (void)setCredential:(NSURLCredential *)theCredential; - -// setting the proxy credential is optional; it is used if the connection -// receives an authentication challenge from a proxy -- (NSURLCredential *)proxyCredential; -- (void)setProxyCredential:(NSURLCredential *)theCredential; - - -// if post data or stream is not set, then a GET retrieval method is assumed -- (NSData *)postData; -- (void)setPostData:(NSData *)theData; - -// beware: In 10.4, NSInputStream fails to copy or retain -// the data it was initialized with, contrary to docs. -// NOTE: if logging is enabled and GTM_HTTPFETCHER_ENABLE_INPUTSTREAM_LOGGING is -// 1, postStream will return a GTMProgressMonitorInputStream that wraps your -// stream (so the upload can be logged). -- (NSInputStream *)postStream; -- (void)setPostStream:(NSInputStream *)theStream; - -- (GTMHTTPFetcherCookieStorageMethod)cookieStorageMethod; -- (void)setCookieStorageMethod:(GTMHTTPFetcherCookieStorageMethod)method; - -// returns cookies from the currently appropriate cookie storage -- (NSArray *)cookiesForURL:(NSURL *)theURL; - -// the delegate is not retained except during the connection -- (id)delegate; -- (void)setDelegate:(id)theDelegate; - -// the delegate's optional receivedData selector has a signature like: -// - (void)myFetcher:(GTMHTTPFetcher *)fetcher receivedData:(NSData *)dataReceivedSoFar; -- (SEL)receivedDataSelector; -- (void)setReceivedDataSelector:(SEL)theSelector; - - -// retrying; see comments at the top of the file. Calling -// setIsRetryEnabled(YES) resets the min and max retry intervals. -- (BOOL)isRetryEnabled; -- (void)setIsRetryEnabled:(BOOL)flag; - -// retry selector 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. -- (SEL)retrySelector; -- (void)setRetrySelector:(SEL)theSel; - -// 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. -- (NSTimeInterval)maxRetryInterval; -- (void)setMaxRetryInterval:(NSTimeInterval)secs; - -// 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. -- (NSTimeInterval)minRetryInterval; -- (void)setMinRetryInterval:(NSTimeInterval)secs; - -// Multiplier used to increase the interval between retries, typically 2.0. -// Clients should not need to call this. -- (double)retryFactor; -- (void)setRetryFactor:(double)multiplier; - -// number of retries attempted -- (unsigned int)retryCount; - -// interval delay to precede next retry -- (NSTimeInterval)nextRetryInterval; - -/// Begin fetching the request. -// -/// |delegate| can optionally implement the two selectors |finishedSEL| and -/// |networkFailedSEL| or pass nil for them. -/// Returns YES if the fetch is initiated. Delegate is retained between -/// the beginFetch call until after the finish/fail callbacks. -// -// finishedSEL has a signature like: -// - (void)fetcher:(GTMHTTPFetcher *)fetcher finishedWithData:(NSData *)data -// failedSEL has a signature like: -// - (void)fetcher:(GTMHTTPFetcher *)fetcher failedWithError:(NSError *)error -// - -- (BOOL)beginFetchWithDelegate:(id)delegate - didFinishSelector:(SEL)finishedSEL - didFailSelector:(SEL)networkFailedSEL; - -// 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 -- (NSInteger)statusCode; - -/// the response, once it's been received -- (NSURLResponse *)response; -- (void)setResponse:(NSURLResponse *)response; - -// Fetch History is a useful, but a little complex at times... -// -// The caller should provide a mutable dictionary that can be used for storing -// Last-Modified-Since checks and cookie storage (setFetchHistory implicity -// calls setCookieStorageMethod w/ -// kGTMHTTPFetcherCookieStorageMethodFetchHistory if you passed a dictionary, -// kGTMHTTPFetcherCookieStorageMethodStatic if you passed nil. -// -// The caller can hold onto the dictionary to reuse the modification dates and -// cookies across multiple fetcher instances. -// -// With a fetch history dictionary setup, the http fetcher cache has the -// modification dates returned by the servers cached, and future fetches will -// return 304 to indicate the data hasn't changed since then (the data in the -// NSError object will be of length zero to show nothing was fetched). This -// reduces load on the server when the response data has not changed. See -// shouldCacheDatedData below for additional 304 support. -// -// Side effect: setFetchHistory: implicitly calls setCookieStorageMethod: -- (NSMutableDictionary *)fetchHistory; -- (void)setFetchHistory:(NSMutableDictionary *)fetchHistory; - -// For fetched data with a last-modified date, the fetcher can optionally cache -// the response data in the fetch history and return cached data instead of a -// 304 error. Set this to NO if you want to manually handle last-modified and -// status 304 (Not changed) rather than be delivered cached data from previous -// fetches. Default is NO. When a cache result is returned, the didFinish -// selector is called with the data, and [fetcher status] returns 200. -// -// If the caller has already provided a fetchHistory dictionary, they can also -// enable fetcher handling of 304 (not changed) status responses. By setting -// shouldCacheDatedData to YES, the fetcher will save any response that has a -// last modifed reply header into the fetchHistory. Then any future fetches -// using that same fetchHistory will automatically load the cached response and -// return it to the caller (with a status of 200) in place of the 304 server -// reply. -- (BOOL)shouldCacheDatedData; -- (void)setShouldCacheDatedData:(BOOL)flag; - -// Delete the last-modified dates and cached data from the fetch history. -- (void)clearDatedDataHistory; - -/// userData is retained for the convenience of the caller -- (id)userData; -- (void)setUserData:(id)theObj; - -// properties are retained for the convenience of the caller -- (void)setProperties:(NSDictionary *)dict; -- (NSDictionary *)properties; - -- (void)setProperty:(id)obj forKey:(NSString *)key; // pass nil obj to remove property -- (id)propertyForKey:(NSString *)key; - -// using the fetcher while a modal dialog is displayed requires setting the -// run-loop modes to include NSModalPanelRunLoopMode -// -// setting run loop modes does nothing if they are not supported, -// such as on 10.4 -- (NSArray *)runLoopModes; -- (void)setRunLoopModes:(NSArray *)modes; - -+ (BOOL)doesSupportRunLoopModes; -+ (NSArray *)defaultRunLoopModes; -+ (void)setDefaultRunLoopModes:(NSArray *)modes; - -// 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; - -@end - -// GTM HTTP Logging -// -// All traffic using GTMHTTPFetcher can be easily logged. Call -// -// [GTMHTTPFetcher setIsLoggingEnabled:YES]; -// -// to begin generating log files. -// -// Log files are put into a folder on the desktop called "GTMHTTPDebugLogs" -// unless another directory is specified with +setLoggingDirectory. -// -// Each run of an application gets a separate set of log files. An html -// file is generated to simplify browsing the run's http transactions. -// The html file includes javascript links for inline viewing of uploaded -// and downloaded data. -// -// A symlink is created in the logs folder to simplify finding the html file -// for the latest run of the application; the symlink is called -// -// AppName_http_log_newest.html -// -// For better viewing of XML logs, use Camino or Firefox rather than Safari. -// -// Projects may define GTM_HTTPFETCHER_ENABLE_LOGGING to 0 to remove all of the -// logging code (it defaults to 1). By default, any data uploaded via PUT/POST -// w/ and NSInputStream will not be logged. You can enable this logging by -// defining GTM_HTTPFETCHER_ENABLE_INPUTSTREAM_LOGGING to 1 (it defaults to 0). -// - -@interface GTMHTTPFetcher (GTMHTTPFetcherLogging) - -// Note: the default logs directory is ~/Desktop/GTMHTTPDebugLogs; it will be -// created as needed. If a custom directory is set, the directory should -// already exist. -+ (void)setLoggingDirectory:(NSString *)path; -+ (NSString *)loggingDirectory; - -// client apps can turn logging on and off -+ (void)setIsLoggingEnabled:(BOOL)flag; -+ (BOOL)isLoggingEnabled; - -// client apps can optionally specify process name and date string used in -// log file names -+ (void)setLoggingProcessName:(NSString *)str; -+ (NSString *)loggingProcessName; - -+ (void)setLoggingDateStamp:(NSString *)str; -+ (NSString *)loggingDateStamp; -@end +// This class is no more. If you want something like it's functionality, look +// at using the version in the Objective-C GData Client +// (http://code.google.com/p/gdata-objectivec-client/). It provides the same +// functionality and will continue to be maintained. diff --git a/Foundation/GTMHTTPFetcher.m b/Foundation/GTMHTTPFetcher.m deleted file mode 100644 index 1c6720a..0000000 --- a/Foundation/GTMHTTPFetcher.m +++ /dev/null @@ -1,1978 +0,0 @@ -// -// GTMHTTPFetcher.m -// -// Copyright 2007-2008 Google Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); you may not -// use this file except in compliance with the License. You may obtain a copy -// of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -// License for the specific language governing permissions and limitations under -// the License. -// - -#define GTMHTTPFETCHER_DEFINE_GLOBALS 1 - -#import "GTMHTTPFetcher.h" -#import "GTMDebugSelectorValidation.h" -#import "GTMGarbageCollection.h" -#import "GTMSystemVersion.h" - -@interface GTMHTTPFetcher (GTMHTTPFetcherLoggingInternal) -- (void)logFetchWithError:(NSError *)error; -- (void)logCapturePostStream; -@end - -// Make sure that if logging is disabled, the InputStream logging is also -// diabled. -#if !GTM_HTTPFETCHER_ENABLE_LOGGING -# undef GTM_HTTPFETCHER_ENABLE_INPUTSTREAM_LOGGING -# define GTM_HTTPFETCHER_ENABLE_INPUTSTREAM_LOGGING 0 -#endif // GTM_HTTPFETCHER_ENABLE_LOGGING - -#if GTM_HTTPFETCHER_ENABLE_INPUTSTREAM_LOGGING -#import "GTMProgressMonitorInputStream.h" -@interface GTMInputStreamLogger : GTMProgressMonitorInputStream -// GTMInputStreamLogger wraps any NSInputStream used for uploading so we can -// capture a copy of the data for the log -@end -#endif // !GTM_HTTPFETCHER_ENABLE_INPUTSTREAM_LOGGING - -#if MAC_OS_X_VERSION_MAX_ALLOWED <= MAC_OS_X_VERSION_10_4 -@interface NSURLConnection (LeopardMethodsOnTigerBuilds) -- (id)initWithRequest:(NSURLRequest *)request - delegate:(id)delegate - startImmediately:(BOOL)startImmediately; -- (void)start; -- (void)scheduleInRunLoop:(NSRunLoop *)aRunLoop forMode:(NSString *)mode; -@end -#endif - -NSString* const kGTMLastModifiedHeader = @"Last-Modified"; -NSString* const kGTMIfModifiedSinceHeader = @"If-Modified-Since"; - - -static NSMutableArray* gGTMFetcherStaticCookies = nil; -static Class gGTMFetcherConnectionClass = nil; -static NSArray *gGTMFetcherDefaultRunLoopModes = nil; - -const NSTimeInterval kDefaultMaxRetryInterval = 60. * 10.; // 10 minutes - -@interface GTMHTTPFetcher (PrivateMethods) -- (void)setCookies:(NSArray *)newCookies - inArray:(NSMutableArray *)cookieStorageArray; -- (NSArray *)cookiesForURL:(NSURL *)theURL - inArray:(NSMutableArray *)cookieStorageArray; -- (void)handleCookiesForResponse:(NSURLResponse *)response; -- (BOOL)shouldRetryNowForStatus:(NSInteger)status error:(NSError *)error; -- (void)retryTimerFired:(NSTimer *)timer; -- (void)destroyRetryTimer; -- (void)beginRetryTimer; -- (void)primeTimerWithNewTimeInterval:(NSTimeInterval)secs; -- (void)retryFetch; -@end - -@implementation GTMHTTPFetcher - -+ (GTMHTTPFetcher *)httpFetcherWithRequest:(NSURLRequest *)request { - return [[[GTMHTTPFetcher alloc] initWithRequest:request] autorelease]; -} - -+ (void)initialize { - if (!gGTMFetcherStaticCookies) { - gGTMFetcherStaticCookies = [[NSMutableArray alloc] init]; - GTMNSMakeUncollectable(gGTMFetcherStaticCookies); - } -} - -- (id)init { - return [self initWithRequest:nil]; -} - -- (id)initWithRequest:(NSURLRequest *)request { - if ((self = [super init]) != nil) { - - request_ = [request mutableCopy]; - - [self setCookieStorageMethod:kGTMHTTPFetcherCookieStorageMethodStatic]; - } - return self; -} - -// TODO: do we need finalize to call stopFetching? - -- (void)dealloc { - [self stopFetching]; // releases connection_ - - [request_ release]; - [downloadedData_ release]; - [credential_ release]; - [proxyCredential_ release]; - [postData_ release]; - [postStream_ release]; - [loggedStreamData_ release]; - [response_ release]; - [userData_ release]; - [properties_ release]; - [runLoopModes_ release]; - [fetchHistory_ release]; - [self destroyRetryTimer]; - - [super dealloc]; -} - -#pragma mark - - -// Begin fetching the URL. |delegate| is not retained -// The delegate must provide and implement the finished and failed selectors. -// -// finishedSEL has a signature like: -// - (void)fetcher:(GTMHTTPFetcher *)fetcher finishedWithData:(NSData *)data -// failedSEL has a signature like: -// - (void)fetcher:(GTMHTTPFetcher *)fetcher failedWithError:(NSError *)error - -- (BOOL)beginFetchWithDelegate:(id)delegate - didFinishSelector:(SEL)finishedSEL - didFailSelector:(SEL)failedSEL { - - GTMAssertSelectorNilOrImplementedWithArguments(delegate, finishedSEL, - @encode(GTMHTTPFetcher *), - @encode(NSData *), NULL); - GTMAssertSelectorNilOrImplementedWithArguments(delegate, failedSEL, - @encode(GTMHTTPFetcher *), - @encode(NSError *), NULL); - GTMAssertSelectorNilOrImplementedWithArguments(delegate, receivedDataSEL_, - @encode(GTMHTTPFetcher *), - @encode(NSData *), NULL); - GTMAssertSelectorNilOrImplementedWithReturnTypeAndArguments(delegate, retrySEL_, - @encode(BOOL), - @encode(GTMHTTPFetcher *), - @encode(BOOL), - @encode(NSError *), - NULL); - - if (connection_ != nil) { - // COV_NF_START - since we want the assert, we can't really test this - _GTMDevAssert(connection_ != nil, - @"fetch object %@ being reused; this should never happen", - self); - goto CannotBeginFetch; - // COV_NF_END - } - - if (request_ == nil) { - _GTMDevLog(@"beginFetchWithDelegate requires a request"); - goto CannotBeginFetch; - } - - [downloadedData_ release]; - downloadedData_ = nil; - - [self setDelegate:delegate]; - finishedSEL_ = finishedSEL; - failedSEL_ = failedSEL; - - if (postData_ || postStream_) { - if ([request_ HTTPMethod] == nil || - [[request_ HTTPMethod] isEqual:@"GET"]) { - [request_ setHTTPMethod:@"POST"]; - } - - if (postData_) { - [request_ setHTTPBody:postData_]; - } else { - - // if logging is enabled, it needs a buffer to accumulate data from any - // NSInputStream used for uploading. Logging will wrap the input - // stream with a stream that lets us keep a copy the data being read. - if ([GTMHTTPFetcher isLoggingEnabled] && postStream_ != nil) { - loggedStreamData_ = [[NSMutableData alloc] init]; - [self logCapturePostStream]; - } - - [request_ setHTTPBodyStream:postStream_]; - } - } - - if (fetchHistory_) { - - // If this URL is in the history, set the Last-Modified header field - - // if we have a history, we're tracking across fetches, so we don't - // want to pull results from a cache - [request_ setCachePolicy:NSURLRequestReloadIgnoringCacheData]; - - NSDictionary* lastModifiedDict - = [fetchHistory_ objectForKey:kGTMHTTPFetcherHistoryLastModifiedKey]; - NSString* urlString = [[request_ URL] absoluteString]; - NSString* lastModifiedStr = [lastModifiedDict objectForKey:urlString]; - - // servers don't want last-modified-ifs on POSTs, so check for a body - if (lastModifiedStr - && [request_ HTTPBody] == nil - && [request_ HTTPBodyStream] == nil) { - - [request_ addValue:lastModifiedStr - forHTTPHeaderField:kGTMIfModifiedSinceHeader]; - } - } - - // get cookies for this URL from our storage array, if - // we have a storage array - if (cookieStorageMethod_ != kGTMHTTPFetcherCookieStorageMethodSystemDefault) { - - NSArray *cookies = [self cookiesForURL:[request_ URL]]; - - if ([cookies count]) { - - NSDictionary *headerFields - = [NSHTTPCookie requestHeaderFieldsWithCookies:cookies]; - NSString *cookieHeader - = [headerFields objectForKey:@"Cookie"]; // key used in header dict - if (cookieHeader) { - [request_ addValue:cookieHeader - forHTTPHeaderField:@"Cookie"]; // header name - } - } - } - - // finally, start the connection - - Class connectionClass = [[self class] connectionClass]; - - NSArray *runLoopModes = nil; - - if ([[self class] doesSupportRunLoopModes]) { - - // use the connection-specific run loop modes, if they were provided, - // or else use the GTMHTTPFetcher default run loop modes, if any - if (runLoopModes_) { - runLoopModes = runLoopModes_; - } else { - runLoopModes = gGTMFetcherDefaultRunLoopModes; - } - } - - if ([runLoopModes count] == 0) { - - // if no run loop modes were specified, then we'll start the connection - // on the current run loop in the current mode - connection_ = [[connectionClass connectionWithRequest:request_ - delegate:self] retain]; - } else { - - // schedule on current run loop in the specified modes - connection_ = [[connectionClass alloc] initWithRequest:request_ - delegate:self - startImmediately:NO]; - - for (NSUInteger idx = 0; idx < [runLoopModes count]; idx++) { - NSString *mode = [runLoopModes objectAtIndex:idx]; - [connection_ scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:mode]; - } - [connection_ start]; - } - - if (!connection_) { - // COV_NF_START - can't really create this case - _GTMDevLog(@"beginFetchWithDelegate could not create a connection"); - goto CannotBeginFetch; - // COV_NF_END - } - - // we'll retain the delegate only during the outstanding connection (similar - // to what Cocoa does with performSelectorOnMainThread:) since we'd crash - // if the delegate was released in the interim. We don't retain the selector - // at other times, to avoid vicious retain loops. This retain is balanced in - // the -stopFetch method. - [delegate_ retain]; - - downloadedData_ = [[NSMutableData alloc] init]; - return YES; - -CannotBeginFetch: - - if (failedSEL) { - - NSError *error = [NSError errorWithDomain:kGTMHTTPFetcherErrorDomain - code:kGTMHTTPFetcherErrorDownloadFailed - userInfo:nil]; - - [[self retain] autorelease]; // in case the callback releases us - - [delegate performSelector:failedSEL - withObject:self - withObject:error]; - } - - return NO; -} - -// Returns YES if this is in the process of fetching a URL, or waiting to -// retry -- (BOOL)isFetching { - return (connection_ != nil || retryTimer_ != nil); -} - -// 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; -} - -// Cancel the fetch of the URL that's currently in progress. -- (void)stopFetching { - [self destroyRetryTimer]; - - 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; - - // this may be called in a callback from the connection, so use autorelease - [oldConnection cancel]; - [oldConnection autorelease]; - - // balance the retain done when the connection was opened - [delegate_ release]; - } -} - -- (void)retryFetch { - - id holdDelegate = [[delegate_ retain] autorelease]; - - [self stopFetching]; - - [self beginFetchWithDelegate:holdDelegate - didFinishSelector:finishedSEL_ - didFailSelector:failedSEL_]; -} - -#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 { - - if (redirectRequest && 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]; - if (redirectHeaders) { - NSString *key; - GTM_FOREACH_KEY(key, redirectHeaders) { - NSString *value = [redirectHeaders objectForKey:key]; - [newRequest setValue:value forHTTPHeaderField:key]; - } - } - redirectRequest = newRequest; - - // save cookies from the response - [self handleCookiesForResponse:redirectResponse]; - - // log the response we just received - [self setResponse:redirectResponse]; - [self logFetchWithError:nil]; - - // update the request for future logging - [self setRequest:redirectRequest]; - } - return redirectRequest; -} - -- (void)connection:(NSURLConnection *)connection - didReceiveResponse:(NSURLResponse *)response { - - // 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]; - - [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) { - - // do nothing special for NSURLConnection's default storage mechanism - - } 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) { - - NSMutableArray *cookieArray = nil; - - // static cookies are stored in gGTMFetcherStaticCookies; fetchHistory - // cookies are stored in fetchHistory_'s kGTMHTTPFetcherHistoryCookiesKey - - if (cookieStorageMethod_ == kGTMHTTPFetcherCookieStorageMethodStatic) { - - cookieArray = gGTMFetcherStaticCookies; - - } else if (cookieStorageMethod_ == kGTMHTTPFetcherCookieStorageMethodFetchHistory - && fetchHistory_ != nil) { - - cookieArray - = [fetchHistory_ objectForKey:kGTMHTTPFetcherHistoryCookiesKey]; - if (cookieArray == nil) { - cookieArray = [NSMutableArray array]; - [fetchHistory_ setObject:cookieArray - forKey:kGTMHTTPFetcherHistoryCookiesKey]; - } - } - - if (cookieArray) { - @synchronized(cookieArray) { - [self setCookies:cookies inArray:cookieArray]; - } - } - } - } - } -} - --(void)connection:(NSURLConnection *)connection - didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge { - - 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; - } - } - - // If we don't have credentials, or we've already failed auth 3x, give up and - // report the error, putting the challenge as a value in the userInfo - // dictionary - // Store the challenge first to ensure that it lives past being cancelled. - NSDictionary *userInfo - = [NSDictionary dictionaryWithObject:challenge - forKey:kGTMHTTPFetcherErrorChallengeKey]; - [[challenge sender] cancelAuthenticationChallenge:challenge]; - - NSError *error - = [NSError errorWithDomain:kGTMHTTPFetcherErrorDomain - code:kGTMHTTPFetcherErrorAuthenticationChallengeFailed - userInfo:userInfo]; - - [self connection:connection didFailWithError:error]; -} - - - -- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data { - [downloadedData_ appendData:data]; - - if (receivedDataSEL_) { - [delegate_ performSelector:receivedDataSEL_ - withObject:self - withObject:downloadedData_]; - } -} - -- (void)updateFetchHistory { - - if (fetchHistory_) { - - NSString* urlString = [[request_ URL] absoluteString]; - if ([response_ respondsToSelector:@selector(allHeaderFields)]) { - NSDictionary *headers = [(NSHTTPURLResponse *)response_ allHeaderFields]; - NSString* lastModifiedStr = [headers objectForKey:kGTMLastModifiedHeader]; - - // get the dictionary mapping URLs to last-modified dates - NSMutableDictionary* lastModifiedDict - = [fetchHistory_ objectForKey:kGTMHTTPFetcherHistoryLastModifiedKey]; - if (!lastModifiedDict) { - lastModifiedDict = [NSMutableDictionary dictionary]; - [fetchHistory_ setObject:lastModifiedDict - forKey:kGTMHTTPFetcherHistoryLastModifiedKey]; - } - - NSMutableDictionary* datedDataCache = nil; - if (shouldCacheDatedData_) { - // get the dictionary mapping URLs to cached, dated data - datedDataCache - = [fetchHistory_ objectForKey:kGTMHTTPFetcherHistoryDatedDataKey]; - if (!datedDataCache) { - datedDataCache = [NSMutableDictionary dictionary]; - [fetchHistory_ setObject:datedDataCache - forKey:kGTMHTTPFetcherHistoryDatedDataKey]; - } - } - - NSInteger statusCode = [self statusCode]; - if (statusCode != kGTMHTTPFetcherStatusNotModified) { - - // save this last modified date 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 datedDataCache is non-nil.) - if (lastModifiedStr && statusCode < 300) { - [lastModifiedDict setObject:lastModifiedStr forKey:urlString]; - if (downloadedData_) { - [datedDataCache setObject:downloadedData_ forKey:urlString]; - } else { - [datedDataCache removeObjectForKey:urlString]; - } - } else { - [lastModifiedDict removeObjectForKey:urlString]; - [datedDataCache removeObjectForKey:urlString]; - } - } - } - } -} - -// 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 to downloadedData_. -// For other errors or if there's no cached data, just return the actual status. -- (NSInteger)statusAfterHandlingNotModifiedError { - - NSInteger status = [self statusCode]; - if (status == kGTMHTTPFetcherStatusNotModified && shouldCacheDatedData_) { - - // get the dictionary of URLs and data - NSString* urlString = [[request_ URL] absoluteString]; - - NSDictionary* datedDataCache - = [fetchHistory_ objectForKey:kGTMHTTPFetcherHistoryDatedDataKey]; - NSData* cachedData = [datedDataCache objectForKey:urlString]; - - if (cachedData) { - // copy our stored data, and forge the status to pass on to the delegate - [downloadedData_ setData:cachedData]; - status = 200; - } - } - return status; -} - -- (void)connectionDidFinishLoading:(NSURLConnection *)connection { - - [self updateFetchHistory]; - - [[self retain] autorelease]; // in case the callback releases us - - [self logFetchWithError:nil]; - - NSInteger status = [self statusAfterHandlingNotModifiedError]; - - if (status >= 300) { - - if ([self shouldRetryNowForStatus:status error:nil]) { - - [self beginRetryTimer]; - - } else { - // not retrying - - // did they want failure notifications? - if (failedSEL_) { - - NSDictionary *userInfo = - [NSDictionary dictionaryWithObject:downloadedData_ - forKey:kGTMHTTPFetcherStatusDataKey]; - NSError *error = [NSError errorWithDomain:kGTMHTTPFetcherStatusDomain - code:status - userInfo:userInfo]; - - [delegate_ performSelector:failedSEL_ - withObject:self - withObject:error]; - } - // we're done fetching - [self stopFetching]; - } - - } else if (finishedSEL_) { - - // successful http status (under 300) - [delegate_ performSelector:finishedSEL_ - withObject:self - withObject:downloadedData_]; - [self stopFetching]; - } - -} - -- (void)connection:(NSURLConnection *)connection - didFailWithError:(NSError *)error { - - [self logFetchWithError:error]; - - if ([self shouldRetryNowForStatus:0 error:error]) { - - [self beginRetryTimer]; - - } else { - - if (failedSEL_) { - [[self retain] autorelease]; // in case the callback releases us - - [delegate_ performSelector:failedSEL_ - withObject:self - withObject:error]; - } - - [self stopFetching]; - } -} - -#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 } - }; - - BOOL isGood = NO; - // 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) { - - isGood = YES; - break; - } - } - return isGood; -} - - -// 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. -- (BOOL)shouldRetryNowForStatus:(NSInteger)status - error:(NSError *)error { - - if ([self isRetryEnabled]) { - - if ([self nextRetryInterval] < [self maxRetryInterval]) { - - if (error == nil) { - // make an error for the status - error = [NSError errorWithDomain:kGTMHTTPFetcherStatusDomain - code:status - userInfo:nil]; - } - - BOOL willRetry = [self isRetryError:error]; - - if (retrySEL_) { - NSMethodSignature *signature = [delegate_ methodSignatureForSelector:retrySEL_]; - NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature]; - [invocation setSelector:retrySEL_]; - [invocation setTarget:delegate_]; - [invocation setArgument:&self atIndex:2]; - [invocation setArgument:&willRetry atIndex:3]; - [invocation setArgument:&error atIndex:4]; - [invocation invoke]; - - [invocation getReturnValue:&willRetry]; - } - - return willRetry; - } - } - - return NO; -} - -- (void)beginRetryTimer { - - NSTimeInterval nextInterval = [self nextRetryInterval]; - NSTimeInterval maxInterval = [self maxRetryInterval]; - - NSTimeInterval newInterval = MIN(nextInterval, maxInterval); - - [self primeTimerWithNewTimeInterval:newInterval]; -} - -- (void)primeTimerWithNewTimeInterval:(NSTimeInterval)secs { - - [self destroyRetryTimer]; - - lastRetryInterval_ = secs; - - retryTimer_ = [NSTimer scheduledTimerWithTimeInterval:secs - target:self - selector:@selector(retryTimerFired:) - userInfo:nil - repeats:NO]; - [retryTimer_ retain]; -} - -- (void)retryTimerFired:(NSTimer *)timer { - - [self destroyRetryTimer]; - - retryCount_++; - - [self retryFetch]; -} - -- (void)destroyRetryTimer { - - [retryTimer_ invalidate]; - [retryTimer_ autorelease]; - retryTimer_ = nil; -} - -- (unsigned int)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)setIsRetryEnabled:(BOOL)flag { - - if (flag && !isRetryEnabled_) { - // We defer initializing these until the user calls setIsRetryEnabled - // to avoid seeding the random number generator if it's not needed. - // However, it means min and max intervals for this fetcher are reset - // as a side effect of calling setIsRetryEnabled. - // - // seed the random value, and make an initial retry interval - // random between 1.0 and 2.0 seconds - srandomdev(); - [self setMinRetryInterval:0.0]; - [self setMaxRetryInterval:kDefaultMaxRetryInterval]; - [self setRetryFactor:2.0]; - lastRetryInterval_ = 0.0; - } - isRetryEnabled_ = flag; -}; - -- (SEL)retrySelector { - return retrySEL_; -} - -- (void)setRetrySelector:(SEL)theSelector { - retrySEL_ = theSelector; -} - -- (NSTimeInterval)maxRetryInterval { - return maxRetryInterval_; -} - -- (void)setMaxRetryInterval:(NSTimeInterval)secs { - if (secs > 0) { - maxRetryInterval_ = secs; - } else { - maxRetryInterval_ = kDefaultMaxRetryInterval; - } -} - -- (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)(random() & 0x0FFFF) / (double) 0x0FFFF); - } -} - -- (double)retryFactor { - return retryFactor_; -} - -- (void)setRetryFactor:(double)multiplier { - retryFactor_ = multiplier; -} - -#pragma mark Getters and Setters - -- (NSMutableURLRequest *)request { - return request_; -} - -- (void)setRequest:(NSURLRequest *)theRequest { - [request_ autorelease]; - request_ = [theRequest mutableCopy]; -} - -- (NSURLCredential *)credential { - return credential_; -} - -- (void)setCredential:(NSURLCredential *)theCredential { - [credential_ autorelease]; - credential_ = [theCredential retain]; -} - -- (NSURLCredential *)proxyCredential { - return proxyCredential_; -} - -- (void)setProxyCredential:(NSURLCredential *)theCredential { - [proxyCredential_ autorelease]; - proxyCredential_ = [theCredential retain]; -} - -- (NSData *)postData { - return postData_; -} - -- (void)setPostData:(NSData *)theData { - [postData_ autorelease]; - postData_ = [theData retain]; -} - -- (NSInputStream *)postStream { - return postStream_; -} - -- (void)setPostStream:(NSInputStream *)theStream { - [postStream_ autorelease]; - postStream_ = [theStream retain]; -} - -- (GTMHTTPFetcherCookieStorageMethod)cookieStorageMethod { - return cookieStorageMethod_; -} - -- (void)setCookieStorageMethod:(GTMHTTPFetcherCookieStorageMethod)method { - - cookieStorageMethod_ = method; - - BOOL handleCookies - = (method == kGTMHTTPFetcherCookieStorageMethodSystemDefault) ? YES : NO; - [request_ setHTTPShouldHandleCookies:handleCookies]; -} - -- (id)delegate { - return delegate_; -} - -- (void)setDelegate:(id)theDelegate { - - // we retain delegate_ only during the life of the connection - if (connection_) { - [delegate_ autorelease]; - delegate_ = [theDelegate retain]; - } else { - delegate_ = theDelegate; - } -} - -- (SEL)receivedDataSelector { - return receivedDataSEL_; -} - -- (void)setReceivedDataSelector:(SEL)theSelector { - receivedDataSEL_ = theSelector; -} - -- (NSURLResponse *)response { - return response_; -} - -- (void)setResponse:(NSURLResponse *)response { - [response_ autorelease]; - response_ = [response retain]; -} - -- (NSMutableDictionary *)fetchHistory { - return fetchHistory_; -} - -- (void)setFetchHistory:(NSMutableDictionary *)fetchHistory { - [fetchHistory_ autorelease]; - fetchHistory_ = [fetchHistory retain]; - GTMHTTPFetcherCookieStorageMethod method - = fetchHistory_ ? kGTMHTTPFetcherCookieStorageMethodFetchHistory - : kGTMHTTPFetcherCookieStorageMethodStatic; - [self setCookieStorageMethod:method]; -} - -- (void)setShouldCacheDatedData:(BOOL)flag { - shouldCacheDatedData_ = flag; - if (!flag) { - [self clearDatedDataHistory]; - } -} - -- (BOOL)shouldCacheDatedData { - return shouldCacheDatedData_; -} - -// delete last-modified dates and cached data from the fetch history -- (void)clearDatedDataHistory { - [fetchHistory_ removeObjectForKey:kGTMHTTPFetcherHistoryLastModifiedKey]; - [fetchHistory_ removeObjectForKey:kGTMHTTPFetcherHistoryDatedDataKey]; -} - -- (id)userData { - return userData_; -} - -- (void)setUserData:(id)theObj { - [userData_ autorelease]; - userData_ = [theObj retain]; -} - -- (void)setProperties:(NSDictionary *)dict { - [properties_ autorelease]; - properties_ = [dict mutableCopy]; -} - -- (NSDictionary *)properties { - return properties_; -} - -- (void)setProperty:(id)obj forKey:(NSString *)key { - - if (properties_ == nil && obj != nil) { - properties_ = [[NSMutableDictionary alloc] init]; - } - - [properties_ setValue:obj forKey:key]; -} - -- (id)propertyForKey:(NSString *)key { - return [properties_ objectForKey:key]; -} - -- (NSArray *)runLoopModes { - return runLoopModes_; -} - -- (void)setRunLoopModes:(NSArray *)modes { - [runLoopModes_ autorelease]; - runLoopModes_ = [modes retain]; -} - -+ (BOOL)doesSupportRunLoopModes { - SEL sel = @selector(initWithRequest:delegate:startImmediately:); - return [NSURLConnection instancesRespondToSelector:sel]; -} - -+ (NSArray *)defaultRunLoopModes { - return gGTMFetcherDefaultRunLoopModes; -} - -+ (void)setDefaultRunLoopModes:(NSArray *)modes { - [gGTMFetcherDefaultRunLoopModes autorelease]; - gGTMFetcherDefaultRunLoopModes = [modes retain]; - GTMNSMakeUncollectable(gGTMFetcherDefaultRunLoopModes); -} - -+ (Class)connectionClass { - if (gGTMFetcherConnectionClass == nil) { - gGTMFetcherConnectionClass = [NSURLConnection class]; - } - return gGTMFetcherConnectionClass; -} - -+ (void)setConnectionClass:(Class)theClass { - gGTMFetcherConnectionClass = theClass; -} - -#pragma mark Cookies - -// 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 cookieStorageArray should -// be valid (non-nil name, domains, paths) -- (NSHTTPCookie *)cookieMatchingCookie:(NSHTTPCookie *)cookie - inArray:(NSArray *)cookieStorageArray { - - NSUInteger numberOfCookies = [cookieStorageArray count]; - NSString *name = [cookie name]; - NSString *domain = [cookie domain]; - NSString *path = [cookie path]; - - _GTMDevAssert(name && domain && path, - @"Invalid cookie (name:%@ domain:%@ path:%@)", - name, domain, path); - - for (NSUInteger idx = 0; idx < numberOfCookies; idx++) { - - NSHTTPCookie *storedCookie = [cookieStorageArray objectAtIndex:idx]; - - if ([[storedCookie name] isEqual:name] - && [[storedCookie domain] isEqual:domain] - && [[storedCookie path] isEqual:path]) { - - return storedCookie; - } - } - return nil; -} - -// remove any expired cookies from the array, excluding cookies with nil -// expirations -- (void)removeExpiredCookiesInArray:(NSMutableArray *)cookieStorageArray { - - // count backwards since we're deleting items from the array - for (NSInteger idx = [cookieStorageArray count] - 1; idx >= 0; idx--) { - - NSHTTPCookie *storedCookie = [cookieStorageArray objectAtIndex:idx]; - - NSDate *expiresDate = [storedCookie expiresDate]; - if (expiresDate && [expiresDate timeIntervalSinceNow] < 0) { - [cookieStorageArray removeObjectAtIndex:idx]; - } - } -} - - -// 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 - inArray:(NSMutableArray *)cookieStorageArray { - - [self removeExpiredCookiesInArray:cookieStorageArray]; - - NSMutableArray *foundCookies = [NSMutableArray array]; - - // 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]; - NSString *path = [theURL path]; - NSString *scheme = [theURL scheme]; - - NSString *domain = nil; - - if (host) { - domain = [[@"." stringByAppendingString:host] lowercaseString]; - } - - NSUInteger numberOfCookies = [cookieStorageArray count]; - for (NSUInteger idx = 0; idx < numberOfCookies; idx++) { - - NSHTTPCookie *storedCookie = [cookieStorageArray objectAtIndex:idx]; - - NSString *cookieDomain = [[storedCookie domain] lowercaseString]; - NSString *cookiePath = [storedCookie path]; - BOOL cookieIsSecure = [storedCookie isSecure]; - - BOOL domainIsOK = [domain hasSuffix:cookieDomain]; - if (!domainIsOK && [domain hasSuffix:@"localhost"]) { - // On Leopard and below, localhost Cookies always come back - // with a domain of localhost.local. On SnowLeopard they come - // back as just localhost. - domainIsOK = [@".localhost.local" hasSuffix:cookieDomain]; - } - BOOL pathIsOK = [cookiePath isEqual:@"/"] || [path hasPrefix:cookiePath]; - BOOL secureIsOK = (!cookieIsSecure) || [scheme isEqual:@"https"]; - - if (domainIsOK && pathIsOK && secureIsOK) { - [foundCookies addObject:storedCookie]; - } - } - return foundCookies; -} - -// return cookies for the given URL using the current cookie storage method -- (NSArray *)cookiesForURL:(NSURL *)theURL { - - NSArray *cookies = nil; - NSMutableArray *cookieStorageArray = nil; - - if (cookieStorageMethod_ == kGTMHTTPFetcherCookieStorageMethodStatic) { - cookieStorageArray = gGTMFetcherStaticCookies; - } else if (cookieStorageMethod_ == kGTMHTTPFetcherCookieStorageMethodFetchHistory) { - cookieStorageArray - = [fetchHistory_ objectForKey:kGTMHTTPFetcherHistoryCookiesKey]; - } else { - // kGTMHTTPFetcherCookieStorageMethodSystemDefault - NSHTTPCookieStorage *storage - = [NSHTTPCookieStorage sharedHTTPCookieStorage]; - cookies = [storage cookiesForURL:theURL]; - } - - if (cookieStorageArray) { - - @synchronized(cookieStorageArray) { - - // cookiesForURL returns a new array of immutable NSCookie objects - // from cookieStorageArray - cookies = [self cookiesForURL:theURL - inArray:cookieStorageArray]; - } - } - return cookies; -} - - -// add all cookies in the array |newCookies| 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 - inArray:(NSMutableArray *)cookieStorageArray { - - [self removeExpiredCookiesInArray:cookieStorageArray]; - - NSHTTPCookie *newCookie; - - GTM_FOREACH_OBJECT(newCookie, 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 - inArray:cookieStorageArray]; - if (oldCookie) { - [cookieStorageArray removeObject:oldCookie]; - } - - // make sure the cookie hasn't already expired - NSDate *expiresDate = [newCookie expiresDate]; - if ((!expiresDate) || [expiresDate timeIntervalSinceNow] > 0) { - [cookieStorageArray addObject:newCookie]; - } - - } else { - _GTMDevAssert(NO, @"Cookie incomplete: %@", newCookie); // COV_NF_LINE - } - } -} -@end - -#pragma mark Logging - -// NOTE: Threads and Logging -// -// All the NSURLConnection callbacks happen on one thread, so we don't have -// to put any synchronization into the logging code. Yes, the state around -// logging (it's directory, etc.) could use it, but for now, that's punted. - - -// We don't invoke Leopard methods on 10.4, because we check if the methods are -// implemented before invoking it, but we need to be able to compile without -// warnings. -// This declaration means if you target <=10.4, this method will compile -// without complaint in this source, so you must test with -// -respondsToSelector:, too. -#if MAC_OS_X_VERSION_MAX_ALLOWED <= MAC_OS_X_VERSION_10_4 -@interface NSFileManager (LeopardMethodsOnTigerBuilds) -- (BOOL)removeItemAtPath:(NSString *)path error:(NSError **)error; -@end -#endif -// The iPhone Foundation removes the deprecated removeFileAtPath:handler: -#if GTM_IPHONE_SDK -@interface NSFileManager (TigerMethodsOniPhoneBuilds) -- (BOOL)removeFileAtPath:(NSString *)path handler:(id)handler; -@end -#endif - -@implementation GTMHTTPFetcher (GTMHTTPFetcherLogging) - -// if GTM_HTTPFETCHER_ENABLE_LOGGING is defined by the user's project then -// logging code will be compiled into the framework - -#if !GTM_HTTPFETCHER_ENABLE_LOGGING -- (void)logFetchWithError:(NSError *)error {} - -+ (void)setLoggingDirectory:(NSString *)path {} -+ (NSString *)loggingDirectory {return nil;} - -+ (void)setIsLoggingEnabled:(BOOL)flag {} -+ (BOOL)isLoggingEnabled {return NO;} - -+ (void)setLoggingProcessName:(NSString *)str {} -+ (NSString *)loggingProcessName {return nil;} - -+ (void)setLoggingDateStamp:(NSString *)str {} -+ (NSString *)loggingDateStamp {return nil;} - -- (void)appendLoggedStreamData:(NSData *)newData {} -- (void)logCapturePostStream {} -#else // GTM_HTTPFETCHER_ENABLE_LOGGING - -// fetchers come and fetchers go, but statics are forever -static BOOL gIsLoggingEnabled = NO; -static NSString *gLoggingDirectoryPath = nil; -static NSString *gLoggingDateStamp = nil; -static NSString* gLoggingProcessName = nil; - -+ (void)setLoggingDirectory:(NSString *)path { - [gLoggingDirectoryPath autorelease]; - gLoggingDirectoryPath = [path copy]; -} - -+ (NSString *)loggingDirectory { - - if (!gLoggingDirectoryPath) { - -#if GTM_IPHONE_SDK - // default to a directory called GTMHTTPDebugLogs into a sandbox-safe - // directory that a devloper can find easily, the application home - NSArray *arr = [NSArray arrayWithObject:NSHomeDirectory()]; -#else - // default to a directory called GTMHTTPDebugLogs in the desktop folder - NSArray *arr = NSSearchPathForDirectoriesInDomains(NSDesktopDirectory, - NSUserDomainMask, YES); -#endif - - if ([arr count] > 0) { - NSString *const kGTMLogFolderName = @"GTMHTTPDebugLogs"; - - NSString *desktopPath = [arr objectAtIndex:0]; - NSString *logsFolderPath - = [desktopPath stringByAppendingPathComponent:kGTMLogFolderName]; - - BOOL doesFolderExist; - BOOL isDir = NO; - NSFileManager *fileManager = [NSFileManager defaultManager]; - doesFolderExist = [fileManager fileExistsAtPath:logsFolderPath - isDirectory:&isDir]; - - if (!doesFolderExist) { - // make the directory - doesFolderExist = [fileManager createDirectoryAtPath:logsFolderPath - attributes:nil]; - } - - if (doesFolderExist) { - // it's there; store it in the global - gLoggingDirectoryPath = [logsFolderPath copy]; - } - } - } - return gLoggingDirectoryPath; -} - -+ (void)setIsLoggingEnabled:(BOOL)flag { - gIsLoggingEnabled = flag; -} - -+ (BOOL)isLoggingEnabled { - return gIsLoggingEnabled; -} - -+ (void)setLoggingProcessName:(NSString *)str { - [gLoggingProcessName release]; - gLoggingProcessName = [str copy]; -} - -+ (NSString *)loggingProcessName { - - // get the process name (once per run) replacing spaces with underscores - if (!gLoggingProcessName) { - - NSString *procName = [[NSProcessInfo processInfo] processName]; - NSMutableString *loggingProcessName; - loggingProcessName = [[NSMutableString alloc] initWithString:procName]; - NSRange nameRange = NSMakeRange(0, [gLoggingProcessName length]); - [loggingProcessName replaceOccurrencesOfString:@" " - withString:@"_" - options:0 - range:nameRange]; - gLoggingProcessName = loggingProcessName; - } - return gLoggingProcessName; -} - -+ (void)setLoggingDateStamp:(NSString *)str { - [gLoggingDateStamp release]; - gLoggingDateStamp = [str copy]; -} - -+ (NSString *)loggingDateStamp { - // we'll pick one date stamp per run, so a run that starts at a later second - // will get a unique results html file - if (!gLoggingDateStamp) { - // produce a string like 08-21_01-41-23PM - - NSDateFormatter *formatter = [[[NSDateFormatter alloc] init] autorelease]; - [formatter setFormatterBehavior:NSDateFormatterBehavior10_4]; - [formatter setDateFormat:@"M-dd_hh-mm-ssa"]; - - gLoggingDateStamp = [[formatter stringFromDate:[NSDate date]] retain] ; - } - return gLoggingDateStamp; -} - -- (NSString *)cleanParameterFollowing:(NSString *)paramName - fromString:(NSString *)originalStr { - // We don't want the password written to disk - // - // find "&Passwd=" in the string, and replace it and the stuff that - // follows it with "Passwd=_snip_" - - NSRange passwdRange = [originalStr rangeOfString:@"&Passwd="]; - if (passwdRange.location != NSNotFound) { - - // we found Passwd=; find the & that follows the parameter - NSUInteger origLength = [originalStr length]; - NSRange restOfString = NSMakeRange(passwdRange.location+1, - origLength - passwdRange.location - 1); - NSRange rangeOfFollowingAmp = [originalStr rangeOfString:@"&" - options:0 - range:restOfString]; - NSRange replaceRange; - if (rangeOfFollowingAmp.location == NSNotFound) { - // found no other & so replace to end of string - replaceRange = NSMakeRange(passwdRange.location, - rangeOfFollowingAmp.location - passwdRange.location); - } else { - // another parameter after &Passwd=foo - replaceRange = NSMakeRange(passwdRange.location, - rangeOfFollowingAmp.location - passwdRange.location); - } - - NSMutableString *result = [NSMutableString stringWithString:originalStr]; - NSString *replacement = [NSString stringWithFormat:@"%@_snip_", paramName]; - - [result replaceCharactersInRange:replaceRange withString:replacement]; - return result; - } - return originalStr; -} - -// stringFromStreamData creates a string given the supplied data -// -// If NSString can create a UTF-8 string from the data, then that is returned. -// -// Otherwise, this routine tries to find a MIME boundary at the beginning of -// the data block, and uses that to break up the data into parts. Each part -// will be used to try to make a UTF-8 string. For parts that fail, a -// replacement string showing the part header and <<n bytes>> is supplied -// in place of the binary data. - -- (NSString *)stringFromStreamData:(NSData *)data { - - if (data == nil) return nil; - - // optimistically, see if the whole data block is UTF-8 - NSString *streamDataStr - = [[[NSString alloc] initWithData:data - encoding:NSUTF8StringEncoding] autorelease]; - if (streamDataStr) return streamDataStr; - - // Munge a buffer by replacing non-ASCII bytes with underscores, - // and turn that munged buffer an NSString. That gives us a string - // we can use with NSScanner. - NSMutableData *mutableData = [NSMutableData dataWithData:data]; - unsigned char *bytes = [mutableData mutableBytes]; - - for (NSUInteger idx = 0; idx < [mutableData length]; idx++) { - if (bytes[idx] > 0x7F || bytes[idx] == 0) { - bytes[idx] = '_'; - } - } - - NSString *mungedStr - = [[[NSString alloc] initWithData:mutableData - encoding:NSUTF8StringEncoding] autorelease]; - if (mungedStr != nil) { - - // scan for the boundary string - NSString *boundary = nil; - NSScanner *scanner = [NSScanner scannerWithString:mungedStr]; - - if ([scanner scanUpToString:@"\r\n" intoString:&boundary] - && [boundary hasPrefix:@"--"]) { - - // we found a boundary string; use it to divide the string into parts - NSArray *mungedParts = [mungedStr componentsSeparatedByString:boundary]; - - // look at each of the munged parts in the original string, and try to - // convert those into UTF-8 - NSMutableArray *origParts = [NSMutableArray array]; - NSUInteger offset = 0; - for (NSUInteger partIdx = 0; partIdx < [mungedParts count]; partIdx++) { - - NSString *mungedPart = [mungedParts objectAtIndex:partIdx]; - NSUInteger partSize = [mungedPart length]; - - NSRange range = NSMakeRange(offset, partSize); - NSData *origPartData = [data subdataWithRange:range]; - - NSString *origPartStr - = [[[NSString alloc] initWithData:origPartData - encoding:NSUTF8StringEncoding] autorelease]; - if (origPartStr) { - // we could make this original part into UTF-8; use the string - [origParts addObject:origPartStr]; - } else { - // this part can't be made into UTF-8; scan the header, if we can - NSString *header = nil; - NSScanner *headerScanner = [NSScanner scannerWithString:mungedPart]; - if (![headerScanner scanUpToString:@"\r\n\r\n" intoString:&header]) { - // we couldn't find a header - header = @""; - } - - // make a part string with the header and <<n bytes>> - NSString *binStr = [NSString stringWithFormat:@"\r%@\r<<%u bytes>>\r", - header, partSize - [header length]]; - [origParts addObject:binStr]; - } - offset += partSize + [boundary length]; - } - - // rejoin the original parts - streamDataStr = [origParts componentsJoinedByString:boundary]; - } - } - - if (!streamDataStr) { - // give up; just make a string showing the uploaded bytes - streamDataStr = [NSString stringWithFormat:@"<<%u bytes>>", [data length]]; - } - return streamDataStr; -} - -// logFetchWithError is called following a successful or failed fetch attempt -// -// This method does all the work for appending to and creating log files - -- (void)logFetchWithError:(NSError *)error { - - if (![[self class] isLoggingEnabled]) return; - - NSFileManager *fileManager = [NSFileManager defaultManager]; - - // TODO: add Javascript to display response data formatted in hex - - NSString *logDirectory = [[self class] loggingDirectory]; - NSString *processName = [[self class] loggingProcessName]; - NSString *dateStamp = [[self class] loggingDateStamp]; - - // each response's NSData goes into its own xml or txt file, though all - // responses for this run of the app share a main html file. This - // counter tracks all fetch responses for this run of the app. - static int zResponseCounter = 0; - zResponseCounter++; - - // file name for the html file containing plain text in a <textarea> - NSString *responseDataUnformattedFileName = nil; - - // file name for the "formatted" (raw) data file - NSString *responseDataFormattedFileName = nil; - NSUInteger responseDataLength = [downloadedData_ length]; - - NSURLResponse *response = [self response]; - NSString *responseBaseName = nil; - - // if there's response data, decide what kind of file to put it in based - // on the first bytes of the file or on the mime type supplied by the server - if (responseDataLength) { - NSString *responseDataExtn = nil; - - // generate a response file base name like - // SyncProto_http_response_10-16_01-56-58PM_3 - responseBaseName = [NSString stringWithFormat:@"%@_http_response_%@_%d", - processName, dateStamp, zResponseCounter]; - - NSString *dataStr - = [[[NSString alloc] initWithData:downloadedData_ - encoding:NSUTF8StringEncoding] autorelease]; - if (dataStr) { - // we were able to make a UTF-8 string from the response data - - NSCharacterSet *whitespaceSet = [NSCharacterSet whitespaceCharacterSet]; - dataStr = [dataStr stringByTrimmingCharactersInSet:whitespaceSet]; - - // save a plain-text version of the response data in an html cile - // containing a wrapped, scrollable <textarea> - // - // we'll use <textarea rows="33" cols="108" readonly=true wrap=soft> - // </textarea> to fit inside our iframe - responseDataUnformattedFileName - = [responseBaseName stringByAppendingPathExtension:@"html"]; - NSString *textFilePath - = [logDirectory stringByAppendingPathComponent:responseDataUnformattedFileName]; - - NSString* wrapFmt = @"<textarea rows=\"33\" cols=\"108\" readonly=true" - " wrap=soft>\n%@\n</textarea>"; - NSString* wrappedStr = [NSString stringWithFormat:wrapFmt, dataStr]; - [wrappedStr writeToFile:textFilePath - atomically:NO - encoding:NSUTF8StringEncoding - error:nil]; - - // now determine the extension for the "formatted" file, which is really - // the raw data written with an appropriate extension - - // for known file types, we'll write the data to a file with the - // appropriate extension - if ([dataStr hasPrefix:@"<?xml"]) { - responseDataExtn = @"xml"; - } else if ([dataStr hasPrefix:@"<html"]) { - responseDataExtn = @"html"; - } else { - // add more types of identifiable text here - } - - } else if ([[response MIMEType] isEqual:@"image/jpeg"]) { - responseDataExtn = @"jpg"; - } else if ([[response MIMEType] isEqual:@"image/gif"]) { - responseDataExtn = @"gif"; - } else if ([[response MIMEType] isEqual:@"image/png"]) { - responseDataExtn = @"png"; - } else { - // add more non-text types here - } - - // if we have an extension, save the raw data in a file with that - // extension to be our "formatted" display file - if (responseDataExtn) { - responseDataFormattedFileName - = [responseBaseName stringByAppendingPathExtension:responseDataExtn]; - NSString *formattedFilePath - = [logDirectory stringByAppendingPathComponent:responseDataFormattedFileName]; - - [downloadedData_ writeToFile:formattedFilePath atomically:NO]; - } - } - - // we'll have one main html file per run of the app - NSString *htmlName = [NSString stringWithFormat:@"%@_http_log_%@.html", - processName, dateStamp]; - NSString *htmlPath =[logDirectory stringByAppendingPathComponent:htmlName]; - - // if the html file exists (from logging previous fetches) we don't need - // to re-write the header or the scripts - BOOL didFileExist = [fileManager fileExistsAtPath:htmlPath]; - - NSMutableString* outputHTML = [NSMutableString string]; - NSURLRequest *request = [self request]; - - // we need file names for the various div's that we're going to show and hide, - // names unique to this response's bundle of data, so we format our div - // names with the counter that we incremented earlier - NSString *requestHeadersName - = [NSString stringWithFormat:@"RequestHeaders%d", zResponseCounter]; - NSString *postDataName - = [NSString stringWithFormat:@"PostData%d", zResponseCounter]; - - NSString *responseHeadersName - = [NSString stringWithFormat:@"ResponseHeaders%d", zResponseCounter]; - NSString *responseDataDivName - = [NSString stringWithFormat:@"ResponseData%d", zResponseCounter]; - NSString *dataIFrameID - = [NSString stringWithFormat:@"DataIFrame%d", zResponseCounter]; - - // we need a header to say we'll have UTF-8 text - if (!didFileExist) { - [outputHTML appendFormat:@"<html><head><meta http-equiv=\"content-type\" " - @"content=\"text/html; charset=UTF-8\"><title>%@ HTTP fetch log %@</title>", - processName, dateStamp]; - } - - // write style sheets for each hideable element; each style sheet is - // customized with our current response number, since they'll share - // the html page with other responses - NSString *styleFormat = @"<style type=\"text/css\">div#%@ " - @"{ margin: 0px 20px 0px 20px; display: none; }</style>\n"; - - [outputHTML appendFormat:styleFormat, requestHeadersName]; - [outputHTML appendFormat:styleFormat, postDataName]; - [outputHTML appendFormat:styleFormat, responseHeadersName]; - [outputHTML appendFormat:styleFormat, responseDataDivName]; - - if (!didFileExist) { - // write javascript functions. The first one shows/hides the layer - // containing the iframe. - NSString *scriptFormat = @"<script type=\"text/javascript\"> " - @"function toggleLayer(whichLayer){ " - @"var style2 = document.getElementById(whichLayer).style; " - @"style2.display = style2.display ? \"\":\"block\";}</script>\n"; - [outputHTML appendFormat:scriptFormat]; - - // the second function is passed the src file; if it's what's shown, it - // toggles the iframe's visibility. If some other src is shown, it shows - // the iframe and loads the new source. Note we want to load the source - // whenever we show the iframe too since Firefox seems to format it wrong - // when showing it if we don't reload it. - NSString *toggleIFScriptFormat = @"<script type=\"text/javascript\"> " - @"function toggleIFrame(whichLayer,iFrameID,newsrc)" - @"{ \n var iFrameElem=document.getElementById(iFrameID); " - @"if (iFrameElem.src.indexOf(newsrc) != -1) { toggleLayer(whichLayer); } " - @"else { document.getElementById(whichLayer).style.display=\"block\"; } " - @"iFrameElem.src=newsrc; }</script>\n</head>\n<body>\n"; - [outputHTML appendFormat:toggleIFScriptFormat]; - } - - // now write the visible html elements - - // write the date & time - [outputHTML appendFormat:@"<b>%@</b><br>", [[NSDate date] description]]; - - // write the request URL - [outputHTML appendFormat:@"<b>request:</b> %@ <i>URL:</i> <code>%@</code><br>\n", - [request HTTPMethod], [request URL]]; - - // write the request headers, toggleable - NSDictionary *requestHeaders = [request allHTTPHeaderFields]; - if ([requestHeaders count]) { - NSString *requestHeadersFormat = @"<a href=\"javascript:toggleLayer('%@');\">" - @"request headers (%d)</a><div id=\"%@\"><pre>%@</pre></div><br>\n"; - [outputHTML appendFormat:requestHeadersFormat, - requestHeadersName, // layer name - [requestHeaders count], - requestHeadersName, - [requestHeaders description]]; // description gives a human-readable dump - } else { - [outputHTML appendString:@"<i>Request headers: none</i><br>"]; - } - - // write the request post data, toggleable - NSData *postData = postData_; - if (loggedStreamData_) { - postData = loggedStreamData_; - } - - if ([postData length]) { - NSString *postDataFormat = @"<a href=\"javascript:toggleLayer('%@');\">" - "posted data (%d bytes)</a><div id=\"%@\">%@</div><br>\n"; - NSString *postDataStr = [self stringFromStreamData:postData]; - if (postDataStr) { - NSString *postDataTextAreaFmt = @"<pre>%@</pre>"; - if ([postDataStr rangeOfString:@"<"].location != NSNotFound) { - postDataTextAreaFmt = @"<textarea rows=\"15\" cols=\"100\"" - " readonly=true wrap=soft>\n%@\n</textarea>"; - } - NSString *cleanedPostData = [self cleanParameterFollowing:@"&Passwd=" - fromString:postDataStr]; - NSString *postDataTextArea = [NSString stringWithFormat: - postDataTextAreaFmt, cleanedPostData]; - - [outputHTML appendFormat:postDataFormat, - postDataName, // layer name - [postData length], - postDataName, - postDataTextArea]; - } - } else { - // no post data - } - - // write the response status, MIME type, URL - if (response) { - NSString *statusString = @""; - if ([response respondsToSelector:@selector(statusCode)]) { - NSInteger status = [(NSHTTPURLResponse *)response statusCode]; - statusString = @"200"; - if (status != 200) { - // purple for errors - statusString - = [NSString stringWithFormat:@"<FONT COLOR=\"#FF00FF\">%d</FONT>", - status]; - } - } - - // show the response URL only if it's different from the request URL - NSString *responseURLStr = @""; - NSURL *responseURL = [response URL]; - - if (responseURL && ![responseURL isEqual:[request URL]]) { - NSString *responseURLFormat = @"<br><FONT COLOR=\"#FF00FF\">response URL:" - @"</FONT> <code>%@</code>"; - responseURLStr = [NSString stringWithFormat:responseURLFormat, - [responseURL absoluteString]]; - } - - NSDictionary *responseHeaders = nil; - if ([response respondsToSelector:@selector(allHeaderFields)]) { - responseHeaders = [(NSHTTPURLResponse *)response allHeaderFields]; - } - [outputHTML appendFormat:@"<b>response:</b> <i>status:</i> %@ <i> " - @" MIMEType:</i><code> %@</code>%@<br>\n", - statusString, - [response MIMEType], - responseURLStr, - responseHeaders ? [responseHeaders description] : @""]; - - // write the response headers, toggleable - if ([responseHeaders count]) { - - NSString *cookiesSet = [responseHeaders objectForKey:@"Set-Cookie"]; - - NSString *responseHeadersFormat = @"<a href=\"javascript:toggleLayer(" - @"'%@');\">response headers (%d) %@</a><div id=\"%@\"><pre>%@</pre>" - @"</div><br>\n"; - [outputHTML appendFormat:responseHeadersFormat, - responseHeadersName, - [responseHeaders count], - (cookiesSet ? @"<i>sets cookies</i>" : @""), - responseHeadersName, - [responseHeaders description]]; - - } else { - [outputHTML appendString:@"<i>Response headers: none</i><br>\n"]; - } - } - - // error - if (error) { - [outputHTML appendFormat:@"<b>error:</b> %@ <br>\n", [error description]]; - } - - // write the response data. We have links to show formatted and text - // versions, but they both show it in the same iframe, and both - // links also toggle visible/hidden - if (responseDataFormattedFileName || responseDataUnformattedFileName) { - - // response data, toggleable links -- formatted and text versions - if (responseDataFormattedFileName) { - [outputHTML appendFormat:@"response data (%d bytes) formatted <b>%@</b> ", - responseDataLength, - [responseDataFormattedFileName pathExtension]]; - - // inline (iframe) link - NSString *responseInlineFormattedDataNameFormat = @" <a " - @"href=\"javascript:toggleIFrame('%@','%@','%@');\">inline</a>\n"; - [outputHTML appendFormat:responseInlineFormattedDataNameFormat, - responseDataDivName, // div ID - dataIFrameID, // iframe ID (for reloading) - responseDataFormattedFileName]; // src to reload - - // plain link (so the user can command-click it into another tab) - [outputHTML appendFormat:@" <a href=\"%@\">stand-alone</a><br>\n", - [responseDataFormattedFileName - stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]]; - } - if (responseDataUnformattedFileName) { - [outputHTML appendFormat:@"response data (%d bytes) plain text ", - responseDataLength]; - - // inline (iframe) link - NSString *responseInlineDataNameFormat = @" <a href=\"" - @"javascript:toggleIFrame('%@','%@','%@');\">inline</a> \n"; - [outputHTML appendFormat:responseInlineDataNameFormat, - responseDataDivName, // div ID - dataIFrameID, // iframe ID (for reloading) - responseDataUnformattedFileName]; // src to reload - - // plain link (so the user can command-click it into another tab) - [outputHTML appendFormat:@" <a href=\"%@\">stand-alone</a><br>\n", - [responseDataUnformattedFileName - stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]]; - } - - // make the iframe - NSString *divHTMLFormat = @"<div id=\"%@\">%@</div><br>\n"; - NSString *src = responseDataFormattedFileName ? - responseDataFormattedFileName : responseDataUnformattedFileName; - NSString *escapedSrc - = [src stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]; - NSString *iframeFmt = @" <iframe src=\"%@\" id=\"%@\" width=800 height=400>" - @"\n<a href=\"%@\">%@</a>\n </iframe>\n"; - NSString *dataIFrameHTML = [NSString stringWithFormat:iframeFmt, - escapedSrc, dataIFrameID, escapedSrc, src]; - [outputHTML appendFormat:divHTMLFormat, - responseDataDivName, dataIFrameHTML]; - } else { - // could not parse response data; just show the length of it - [outputHTML appendFormat:@"<i>Response data: %d bytes </i>\n", - responseDataLength]; - } - - [outputHTML appendString:@"<br><hr><p>"]; - - // append the HTML to the main output file - const char* htmlBytes = [outputHTML UTF8String]; - NSOutputStream *stream = [NSOutputStream outputStreamToFileAtPath:htmlPath - append:YES]; - [stream open]; - [stream write:(const uint8_t *) htmlBytes maxLength:strlen(htmlBytes)]; - [stream close]; - - // make a symlink to the latest html - NSString *symlinkName = [NSString stringWithFormat:@"%@_http_log_newest.html", - processName]; - NSString *symlinkPath - = [logDirectory stringByAppendingPathComponent:symlinkName]; - - // removeFileAtPath might be going away, but removeItemAtPath does not exist - // in 10.4 - if ([fileManager respondsToSelector:@selector(removeFileAtPath:handler:)]) { - [fileManager removeFileAtPath:symlinkPath handler:nil]; - } else if ([fileManager respondsToSelector:@selector(removeItemAtPath:error:)]) { - // To make the next line compile when targeting 10.4, we declare - // removeItemAtPath:error: in an @interface above - [fileManager removeItemAtPath:symlinkPath error:NULL]; - } - - [fileManager createSymbolicLinkAtPath:symlinkPath pathContent:htmlPath]; -} - -- (void)logCapturePostStream { - -#if GTM_HTTPFETCHER_ENABLE_INPUTSTREAM_LOGGING - // This is called when beginning a fetch. The caller should have already - // verified that logging is enabled, and should have allocated - // loggedStreamData_ as a mutable object. - - // If we're logging, we need to wrap the upload stream with our monitor - // stream subclass that will call us back with the bytes being read from the - // stream - - // our wrapper will retain the old post stream - [postStream_ autorelease]; - - // length can be - postStream_ = [GTMInputStreamLogger inputStreamWithStream:postStream_ - length:0]; - [postStream_ retain]; - - // we don't really want monitoring callbacks; our subclass will be - // calling our appendLoggedStreamData: method at every read instead - [(GTMInputStreamLogger *)postStream_ setMonitorDelegate:self - selector:nil]; -#endif // GTM_HTTPFETCHER_ENABLE_INPUTSTREAM_LOGGING -} - -- (void)appendLoggedStreamData:(NSData *)newData { - [loggedStreamData_ appendData:newData]; -} - -#endif // GTM_HTTPFETCHER_ENABLE_LOGGING -@end - -#if GTM_HTTPFETCHER_ENABLE_INPUTSTREAM_LOGGING -@implementation GTMInputStreamLogger -- (NSInteger)read:(uint8_t *)buffer maxLength:(NSUInteger)len { - - // capture the read stream data, and pass it to the delegate to append to - NSInteger result = [super read:buffer maxLength:len]; - if (result >= 0) { - NSData *data = [NSData dataWithBytes:buffer length:result]; - [monitorDelegate_ appendLoggedStreamData:data]; - } - return result; -} -@end -#endif // GTM_HTTPFETCHER_ENABLE_INPUTSTREAM_LOGGING diff --git a/Foundation/GTMHTTPFetcherTest.m b/Foundation/GTMHTTPFetcherTest.m deleted file mode 100644 index a541d1f..0000000 --- a/Foundation/GTMHTTPFetcherTest.m +++ /dev/null @@ -1,516 +0,0 @@ -// -// GTMHTTPFetcherTest.m -// -// Copyright 2007-2008 Google Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); you may not -// use this file except in compliance with the License. You may obtain a copy -// of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -// License for the specific language governing permissions and limitations under -// the License. -// - -#import "GTMSenTestCase.h" -#import "GTMHTTPFetcher.h" -#import "GTMTestHTTPServer.h" -#import "GTMUnitTestDevLog.h" - -@interface GTMHTTPFetcherTest : GTMTestCase { - // these ivars are checked after fetches, and are reset by resetFetchResponse - NSData *fetchedData_; - NSError *fetcherError_; - NSInteger fetchedStatus_; - NSURLResponse *fetchedResponse_; - NSMutableURLRequest *fetchedRequest_; - - // setup/teardown ivars - NSMutableDictionary *fetchHistory_; - GTMTestHTTPServer *testServer_; -} -@end - -@interface GTMHTTPFetcherTest (PrivateMethods) -- (GTMHTTPFetcher *)doFetchWithURLString:(NSString *)urlString - cachingDatedData:(BOOL)doCaching; - -- (GTMHTTPFetcher *)doFetchWithURLString:(NSString *)urlString - cachingDatedData:(BOOL)doCaching - retrySelector:(SEL)retrySel - maxRetryInterval:(NSTimeInterval)maxRetryInterval - userData:(id)userData; - -- (NSString *)fileURLStringToTestFileName:(NSString *)name; -- (BOOL)countRetriesFetcher:(GTMHTTPFetcher *)fetcher - willRetry:(BOOL)suggestedWillRetry - forError:(NSError *)error; -- (BOOL)fixRequestFetcher:(GTMHTTPFetcher *)fetcher - willRetry:(BOOL)suggestedWillRetry - forError:(NSError *)error; -- (void)fetcher:(GTMHTTPFetcher *)fetcher finishedWithData:(NSData *)data; -- (void)fetcher:(GTMHTTPFetcher *)fetcher failedWithError:(NSError *)error; -@end - -@implementation GTMHTTPFetcherTest - -static const NSTimeInterval kRunLoopInterval = 0.01; -// The bogus-fetch test can take >10s to pass. Pick something way higher -// to avoid failing. -static const NSTimeInterval kGiveUpInterval = 60.0; // bail on the test if 60 seconds elapse - -static NSString *const kValidFileName = @"GTMHTTPFetcherTestPage.html"; - -- (void)setUp { - fetchHistory_ = [[NSMutableDictionary alloc] init]; - - NSBundle *testBundle = [NSBundle bundleForClass:[self class]]; - STAssertNotNil(testBundle, nil); - NSString *docRoot = [testBundle pathForResource:@"GTMHTTPFetcherTestPage" - ofType:@"html"]; - docRoot = [docRoot stringByDeletingLastPathComponent]; - STAssertNotNil(docRoot, nil); - - testServer_ = [[GTMTestHTTPServer alloc] initWithDocRoot:docRoot]; - STAssertNotNil(testServer_, @"failed to create a testing server"); -} - -- (void)resetFetchResponse { - [fetchedData_ release]; - fetchedData_ = nil; - - [fetcherError_ release]; - fetcherError_ = nil; - - [fetchedRequest_ release]; - fetchedRequest_ = nil; - - [fetchedResponse_ release]; - fetchedResponse_ = nil; - - fetchedStatus_ = 0; -} - -- (void)tearDown { - [testServer_ release]; - testServer_ = nil; - - [self resetFetchResponse]; - - [fetchHistory_ release]; - fetchHistory_ = nil; -} - -- (void)testValidFetch { - NSString *urlString = [self fileURLStringToTestFileName:kValidFileName]; - - GTMHTTPFetcher *fetcher = - [self doFetchWithURLString:urlString cachingDatedData:YES]; - - STAssertNotNil(fetchedData_, - @"failed to fetch data, status:%ld error:%@, URL:%@", - (long)fetchedStatus_, fetcherError_, urlString); - STAssertNotNil(fetchedResponse_, - @"failed to get fetch response, status:%ld error:%@", - (long)fetchedStatus_, fetcherError_); - STAssertNotNil(fetchedRequest_, @"failed to get fetch request, URL %@", - urlString); - STAssertNil(fetcherError_, @"fetching data gave error: %@", fetcherError_); - STAssertEquals(fetchedStatus_, (NSInteger)200, - @"fetching data expected status 200, instead got %ld, for URL %@", - (long)fetchedStatus_, urlString); - - // no cookies should be sent with our first request - NSDictionary *headers = [fetchedRequest_ allHTTPHeaderFields]; - NSString *cookiesSent = [headers objectForKey:@"Cookie"]; - STAssertNil(cookiesSent, @"Cookies sent unexpectedly: %@", cookiesSent); - - - // cookies should have been set by the response; specifically, TestCookie - // should be set to the name of the file requested - NSDictionary *responseHeaders; - - responseHeaders = [(NSHTTPURLResponse *)fetchedResponse_ allHeaderFields]; - NSString *cookiesSetString = [responseHeaders objectForKey:@"Set-Cookie"]; - NSString *cookieExpected = [NSString stringWithFormat:@"TestCookie=%@", - kValidFileName]; - STAssertEqualObjects(cookiesSetString, cookieExpected, @"Unexpected cookie"); - - // test properties - NSDictionary *dict = [NSDictionary dictionaryWithObjectsAndKeys: - @"val1", @"key1", @"val2", @"key2", nil]; - [fetcher setProperties:dict]; - STAssertEqualObjects([fetcher properties], dict, @"properties as dictionary"); - STAssertEqualObjects([fetcher propertyForKey:@"key2"], @"val2", - @"single property"); - - NSDictionary *dict2 = [NSDictionary dictionaryWithObjectsAndKeys: - @"valx1", @"key1", @"val3", @"key3", nil]; - [fetcher setProperty:@"valx1" forKey:@"key1"]; - [fetcher setProperty:nil forKey:@"key2"]; - [fetcher setProperty:@"val3" forKey:@"key3"]; - STAssertEqualObjects([fetcher properties], dict2, @"property changes"); - - // make a copy of the fetched data to compare with our next fetch from the - // cache - NSData *originalFetchedData = [[fetchedData_ copy] autorelease]; - - // Now fetch again so the "If modified since" header will be set (because - // we're calling setFetchHistory: below) and caching ON, and verify that we - // got a good data from the cache, along with a "Not modified" status - - [self resetFetchResponse]; - - [self doFetchWithURLString:urlString cachingDatedData:YES]; - - STAssertEqualObjects(fetchedData_, originalFetchedData, - @"cache data mismatch"); - - STAssertNotNil(fetchedData_, - @"failed to fetch data, status:%ld error:%@, URL:%@", - (long)fetchedStatus_, fetcherError_, urlString); - STAssertNotNil(fetchedResponse_, - @"failed to get fetch response, status:%;d error:%@", - (long)fetchedStatus_, fetcherError_); - STAssertNotNil(fetchedRequest_, @"failed to get fetch request, URL %@", - urlString); - STAssertNil(fetcherError_, @"fetching data gave error: %@", fetcherError_); - - STAssertEquals(fetchedStatus_, (NSInteger)kGTMHTTPFetcherStatusNotModified, // 304 - @"fetching data expected status 304, instead got %ld, for URL %@", - (long)fetchedStatus_, urlString); - - // the TestCookie set previously should be sent with this request - cookiesSent = [[fetchedRequest_ allHTTPHeaderFields] objectForKey:@"Cookie"]; - STAssertEqualObjects(cookiesSent, cookieExpected, @"Cookie not sent"); - - // Now fetch twice without caching enabled, and verify that we got a - // "Not modified" status, along with a non-nil but empty NSData (which - // is normal for that status code) - - [self resetFetchResponse]; - - [fetchHistory_ removeAllObjects]; - - [self doFetchWithURLString:urlString cachingDatedData:NO]; - - STAssertEqualObjects(fetchedData_, originalFetchedData, - @"cache data mismatch"); - - [self resetFetchResponse]; - [self doFetchWithURLString:urlString cachingDatedData:NO]; - - STAssertNotNil(fetchedData_, @""); - STAssertEquals([fetchedData_ length], (NSUInteger)0, @"unexpected data"); - STAssertEquals(fetchedStatus_, (NSInteger)kGTMHTTPFetcherStatusNotModified, - @"fetching data expected status 304, instead got %d", fetchedStatus_); - STAssertNil(fetcherError_, @"unexpected error: %@", fetcherError_); - -} - -- (void)testBogusFetch { - // fetch a live, invalid URL - NSString *badURLString = @"http://localhost:86/"; - [self doFetchWithURLString:badURLString cachingDatedData:NO]; - - const int kServiceUnavailableStatus = 503; - - if (fetchedStatus_ == kServiceUnavailableStatus) { - // some proxies give a "service unavailable" error for bogus fetches - } else { - - if (fetchedData_) { - NSString *str = [[[NSString alloc] initWithData:fetchedData_ - encoding:NSUTF8StringEncoding] autorelease]; - STAssertNil(fetchedData_, @"fetched unexpected data: %@", str); - } - - STAssertNotNil(fetcherError_, @"failed to receive fetching error"); - STAssertEquals(fetchedStatus_, (NSInteger)0, - @"fetching data expected no status from no response, instead got %d", - fetchedStatus_); - } - - // fetch with a specific status code from our http server - [self resetFetchResponse]; - - NSString *invalidWebPageFile = - [kValidFileName stringByAppendingString:@"?status=400"]; - NSString *statusUrlString = - [self fileURLStringToTestFileName:invalidWebPageFile]; - - [self doFetchWithURLString:statusUrlString cachingDatedData:NO]; - - STAssertNotNil(fetchedData_, @"fetch lacked data with error info"); - STAssertNil(fetcherError_, @"expected bad status but got an error"); - STAssertEquals(fetchedStatus_, (NSInteger)400, - @"unexpected status, error=%@", fetcherError_); -} - -- (void)testRetryFetches { - GTMHTTPFetcher *fetcher; - - NSString *invalidFile = [kValidFileName stringByAppendingString:@"?status=503"]; - NSString *urlString = [self fileURLStringToTestFileName:invalidFile]; - - SEL countRetriesSel = @selector(countRetriesFetcher:willRetry:forError:); - SEL fixRequestSel = @selector(fixRequestFetcher:willRetry:forError:); - - // - // test: retry until timeout, then expect failure with status message - // - - NSNumber *lotsOfRetriesNumber = [NSNumber numberWithInt:1000]; - - fetcher= [self doFetchWithURLString:urlString - cachingDatedData:NO - retrySelector:countRetriesSel - maxRetryInterval:5.0 // retry intervals of 1, 2, 4 - userData:lotsOfRetriesNumber]; - - STAssertNotNil(fetchedData_, @"error data is expected"); - STAssertEquals(fetchedStatus_, (NSInteger)503, nil); - STAssertEquals([fetcher retryCount], 3U, @"retry count unexpected"); - - // - // test: retry twice, then give up - // - [self resetFetchResponse]; - - NSNumber *twoRetriesNumber = [NSNumber numberWithInt:2]; - - fetcher= [self doFetchWithURLString:urlString - cachingDatedData:NO - retrySelector:countRetriesSel - maxRetryInterval:10.0 // retry intervals of 1, 2, 4, 8 - userData:twoRetriesNumber]; - - STAssertNotNil(fetchedData_, @"error data is expected"); - STAssertEquals(fetchedStatus_, (NSInteger)503, nil); - STAssertEquals([fetcher retryCount], 2U, @"retry count unexpected"); - - - // - // test: retry, making the request succeed on the first retry - // by fixing the URL - // - [self resetFetchResponse]; - - fetcher= [self doFetchWithURLString:urlString - cachingDatedData:NO - retrySelector:fixRequestSel - maxRetryInterval:30.0 // should only retry once due to selector - userData:lotsOfRetriesNumber]; - - STAssertNotNil(fetchedData_, @"data is expected"); - STAssertEquals(fetchedStatus_, (NSInteger)200, nil); - STAssertEquals([fetcher retryCount], 1U, @"retry count unexpected"); -} - -- (void)testNilFetch { - GTMHTTPFetcher *fetcher = [[GTMHTTPFetcher alloc] init]; - [GTMUnitTestDevLog expectString:@"beginFetchWithDelegate requires a request"]; - BOOL wasGood = [fetcher beginFetchWithDelegate:nil - didFinishSelector:NULL - didFailSelector:NULL]; - STAssertFalse(wasGood, nil); -} - -- (void)testCookies { - // This is checking part one of - // rdar://6293862 NSHTTPCookie cookieWithProperties doesn't work with - // NSHTTPCookieOriginURL key - NSString *urlString = @"http://www.apple.com/index.html"; - NSURL *url = [NSURL URLWithString:@"http://www.apple.com/index.html"]; - - NSDictionary *properties = [NSDictionary dictionaryWithObjectsAndKeys: - url, NSHTTPCookieOriginURL, - @"testCookies", NSHTTPCookieName, - @"1", NSHTTPCookieValue, - nil]; - NSHTTPCookie *cookie = [NSHTTPCookie cookieWithProperties:properties]; - - STAssertNil(cookie, nil); - - // This is checking part two of - // rdar://6293862 NSHTTPCookie cookieWithProperties doesn't work with - // NSHTTPCookieOriginURL key - properties = [NSDictionary dictionaryWithObjectsAndKeys: - urlString, NSHTTPCookieOriginURL, - @"testCookies", NSHTTPCookieName, - @"1", NSHTTPCookieValue, - nil]; - cookie = [NSHTTPCookie cookieWithProperties:properties]; - - STAssertNil(cookie, nil); -} - -#pragma mark - - -- (GTMHTTPFetcher *)doFetchWithURLString:(NSString *)urlString - cachingDatedData:(BOOL)doCaching { - - return [self doFetchWithURLString:(NSString *)urlString - cachingDatedData:(BOOL)doCaching - retrySelector:nil - maxRetryInterval:0 - userData:nil]; -} - -- (GTMHTTPFetcher *)doFetchWithURLString:(NSString *)urlString - cachingDatedData:(BOOL)doCaching - retrySelector:(SEL)retrySel - maxRetryInterval:(NSTimeInterval)maxRetryInterval - userData:(id)userData { - - NSURL *url = [NSURL URLWithString:urlString]; - NSURLRequest *req = [NSURLRequest requestWithURL:url - cachePolicy:NSURLRequestReloadIgnoringCacheData - timeoutInterval:kGiveUpInterval]; - GTMHTTPFetcher *fetcher = [GTMHTTPFetcher httpFetcherWithRequest:req]; - - STAssertNotNil(fetcher, @"Failed to allocate fetcher"); - - // setting the fetch history will add the "If-modified-since" header - // to repeat requests - [fetcher setFetchHistory:fetchHistory_]; - if (doCaching != [fetcher shouldCacheDatedData]) { - // only set the value when it changes since setting it to nil clears out - // some of the state and our tests need the state between some non caching - // fetches. - [fetcher setShouldCacheDatedData:doCaching]; - } - - if (retrySel) { - [fetcher setIsRetryEnabled:YES]; - [fetcher setRetrySelector:retrySel]; - [fetcher setMaxRetryInterval:maxRetryInterval]; - [fetcher setUserData:userData]; - - // we force a minimum retry interval for unit testing; otherwise, - // we'd have no idea how many retries will occur before the max - // retry interval occurs, since the minimum would be random - [fetcher setMinRetryInterval:1.0]; - } - - BOOL isFetching = - [fetcher beginFetchWithDelegate:self - didFinishSelector:@selector(fetcher:finishedWithData:) - didFailSelector:@selector(fetcher:failedWithError:)]; - STAssertTrue(isFetching, @"Begin fetch failed"); - - if (isFetching) { - - // Give time for the fetch to happen, but give up if 10 seconds elapse with - // no response - NSDate* giveUpDate = [NSDate dateWithTimeIntervalSinceNow:kGiveUpInterval]; - while ((!fetchedData_ && !fetcherError_) && - [giveUpDate timeIntervalSinceNow] > 0) { - NSDate* loopIntervalDate = - [NSDate dateWithTimeIntervalSinceNow:kRunLoopInterval]; - [[NSRunLoop currentRunLoop] runUntilDate:loopIntervalDate]; - } - } - - return fetcher; -} - -- (NSString *)fileURLStringToTestFileName:(NSString *)name { - - // we need to create http URLs referring to the desired - // resource to be found by the python http server running locally - - // return a localhost:port URL for the test file - NSString *urlString = [NSString stringWithFormat:@"http://localhost:%d/%@", - [testServer_ port], name]; - - // we exclude the "?status=" that would indicate that the URL - // should cause a retryable error - NSRange range = [name rangeOfString:@"?status="]; - if (range.length > 0) { - name = [name substringToIndex:range.location]; - } - - // we exclude the ".auth" extension that would indicate that the URL - // should be tested with authentication - if ([[name pathExtension] isEqual:@"auth"]) { - name = [name stringByDeletingPathExtension]; - } - - // just for sanity, let's make sure we see the file locally, so - // we can expect the Python http server to find it too - NSBundle *testBundle = [NSBundle bundleForClass:[self class]]; - STAssertNotNil(testBundle, nil); - - NSString *filePath = - [testBundle pathForResource:[name stringByDeletingPathExtension] - ofType:[name pathExtension]]; - STAssertNotNil(filePath, nil); - - return urlString; -} - - - -- (void)fetcher:(GTMHTTPFetcher *)fetcher finishedWithData:(NSData *)data { - fetchedData_ = [data copy]; - fetchedStatus_ = [fetcher statusCode]; // this implicitly tests that the fetcher has kept the response - fetchedRequest_ = [[fetcher request] retain]; - fetchedResponse_ = [[fetcher response] retain]; -} - -- (void)fetcher:(GTMHTTPFetcher *)fetcher failedWithError:(NSError *)error { - // if it's a status error, don't hang onto the error, just the status/data - if ([[error domain] isEqual:kGTMHTTPFetcherStatusDomain]) { - fetchedData_ = [[[error userInfo] objectForKey:kGTMHTTPFetcherStatusDataKey] copy]; - fetchedStatus_ = [error code]; // this implicitly tests that the fetcher has kept the response - } else { - fetcherError_ = [error retain]; - fetchedStatus_ = [fetcher statusCode]; - } -} - - -// Selector for allowing up to N retries, where N is an NSNumber in the -// fetcher's userData -- (BOOL)countRetriesFetcher:(GTMHTTPFetcher *)fetcher - willRetry:(BOOL)suggestedWillRetry - forError:(NSError *)error { - - int count = [fetcher retryCount]; - int allowedRetryCount = [[fetcher userData] intValue]; - - BOOL shouldRetry = (count < allowedRetryCount); - - STAssertEquals([fetcher nextRetryInterval], pow(2.0, [fetcher retryCount]), - @"unexpected next retry interval (expected %f, was %f)", - pow(2.0, [fetcher retryCount]), - [fetcher nextRetryInterval]); - - return shouldRetry; -} - -// Selector for retrying and changing the request to one that will succeed -- (BOOL)fixRequestFetcher:(GTMHTTPFetcher *)fetcher - willRetry:(BOOL)suggestedWillRetry - forError:(NSError *)error { - - STAssertEquals([fetcher nextRetryInterval], pow(2.0, [fetcher retryCount]), - @"unexpected next retry interval (expected %f, was %f)", - pow(2.0, [fetcher retryCount]), - [fetcher nextRetryInterval]); - - // fix it - change the request to a URL which does not have a status value - NSString *urlString = [self fileURLStringToTestFileName:kValidFileName]; - - NSURL *url = [NSURL URLWithString:urlString]; - [fetcher setRequest:[NSURLRequest requestWithURL:url]]; - - return YES; // do the retry fetch; it should succeed now -} - -@end diff --git a/Foundation/GTMProgressMonitorInputStream.h b/Foundation/GTMProgressMonitorInputStream.h index d3da5cd..5f779bc 100644 --- a/Foundation/GTMProgressMonitorInputStream.h +++ b/Foundation/GTMProgressMonitorInputStream.h @@ -16,58 +16,7 @@ // the License. // -#import <Foundation/Foundation.h> - -// The monitored input stream calls back into the monitor delegate -// with the number of bytes and total size -// -// - (void)inputStream:(GTMProgressMonitorInputStream *)stream -// hasDeliveredByteCount:(unsigned long long)numberOfBytesRead -// ofTotalByteCount:(unsigned long long)dataLength; - -@interface GTMProgressMonitorInputStream : NSInputStream { - - NSInputStream *inputStream_; // encapsulated stream that does the work - - unsigned long long dataSize_; // size of data in the source - unsigned long long numBytesRead_; // bytes read from the input stream so far - - __weak id monitorDelegate_; // WEAK, not retained - SEL monitorSelector_; - - __weak id monitorSource_; // WEAK, not retained -} - -// Length is passed to the progress callback; it may be zero if the progress -// callback can handle that (mainly meant so the monitor delegate can update the -// bounds/position for a progress indicator. -+ (id)inputStreamWithStream:(NSInputStream *)input - length:(unsigned long long)length; - -- (id)initWithStream:(NSInputStream *)input - length:(unsigned long long)length; - -// The monitor is called when bytes have been read -// -// monitorDelegate should respond to a selector with a signature matching: -// -// - (void)inputStream:(GTMProgressMonitorInputStream *)stream -// hasDeliveredBytes:(unsigned long long)numReadSoFar -// ofTotalBytes:(unsigned long long)total -// -// |total| will be the length passed when this GTMProgressMonitorInputStream was -// created. - -- (void)setMonitorDelegate:(id)monitorDelegate // not retained - selector:(SEL)monitorSelector; -- (id)monitorDelegate; -- (SEL)monitorSelector; - -// The source argument lets the delegate know the source of this input stream. -// this class does nothing w/ this, it's just here to provide context to your -// monitorDelegate. -- (void)setMonitorSource:(id)source; // not retained -- (id)monitorSource; - -@end - +// This class is no more. If you want something like it's functionality, look +// at using the version in the Objective-C GData Client +// (http://code.google.com/p/gdata-objectivec-client/). It provides the same +// functionality and will continue to be maintained. diff --git a/Foundation/GTMProgressMonitorInputStream.m b/Foundation/GTMProgressMonitorInputStream.m deleted file mode 100644 index 94212dd..0000000 --- a/Foundation/GTMProgressMonitorInputStream.m +++ /dev/null @@ -1,217 +0,0 @@ -// -// GTMProgressMonitorInputStream.m -// -// Copyright 2007-2008 Google Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); you may not -// use this file except in compliance with the License. You may obtain a copy -// of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -// License for the specific language governing permissions and limitations under -// the License. -// - -#import "GTMProgressMonitorInputStream.h" -#import "GTMDefines.h" -#import "GTMDebugSelectorValidation.h" - -@implementation GTMProgressMonitorInputStream - -// we'll forward all unhandled messages to the NSInputStream class -// or to the encapsulated input stream. This is needed -// for all messages sent to NSInputStream which aren't -// handled by our superclass; that includes various private run -// loop calls. -+ (NSMethodSignature*)methodSignatureForSelector:(SEL)selector { - return [NSInputStream methodSignatureForSelector:selector]; -} - -+ (void)forwardInvocation:(NSInvocation*)invocation { - [invocation invokeWithTarget:[NSInputStream class]]; -} - -- (NSMethodSignature*)methodSignatureForSelector:(SEL)selector { - return [inputStream_ methodSignatureForSelector:selector]; -} - -- (void)forwardInvocation:(NSInvocation*)invocation { - [invocation invokeWithTarget:inputStream_]; -} - -#pragma mark - - -+ (id)inputStreamWithStream:(NSInputStream *)input - length:(unsigned long long)length { - - return [[[self alloc] initWithStream:input - length:length] autorelease]; -} - -- (id)initWithStream:(NSInputStream *)input - length:(unsigned long long)length { - - if ((self = [super init]) != nil) { - - inputStream_ = [input retain]; - dataSize_ = length; - - if (!inputStream_) { - [self release]; - self = nil; - } - } - return self; -} - -#pragma mark - - -- (id)init { - return [self initWithStream:nil length:0]; -} - -- (id)initWithData:(NSData *)data { - unsigned long long dataLength = [data length]; - NSInputStream *inputStream = nil; - if (data) { - inputStream = [NSInputStream inputStreamWithData:data]; - } - return [self initWithStream:inputStream length:dataLength]; -} - -- (id)initWithFileAtPath:(NSString *)path { - NSDictionary *fileAttrs = - [[NSFileManager defaultManager] fileAttributesAtPath:path - traverseLink:YES]; - unsigned long long fileSize = [fileAttrs fileSize]; - NSInputStream *inputStream = nil; - if (fileSize) { - inputStream = [NSInputStream inputStreamWithFileAtPath:path]; - } - return [self initWithStream:inputStream length:fileSize]; -} - -- (void)dealloc { - [inputStream_ release]; - [super dealloc]; -} - -#pragma mark - - -- (NSInteger)read:(uint8_t *)buffer maxLength:(NSUInteger)len { - - NSInteger numRead = [inputStream_ read:buffer maxLength:len]; - - if (numRead > 0) { - - numBytesRead_ += numRead; - - if (monitorDelegate_ && monitorSelector_) { - - // call the monitor delegate with the number of bytes read and the - // total bytes read - - NSMethodSignature *signature = [monitorDelegate_ methodSignatureForSelector:monitorSelector_]; - NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature]; - [invocation setSelector:monitorSelector_]; - [invocation setTarget:monitorDelegate_]; - [invocation setArgument:&self atIndex:2]; - [invocation setArgument:&numBytesRead_ atIndex:3]; - [invocation setArgument:&dataSize_ atIndex:4]; - [invocation invoke]; - } - } - return numRead; -} - -- (BOOL)getBuffer:(uint8_t **)buffer length:(NSUInteger *)len { - // TODO: doesn't this advance the stream so we should warn the progress - // callback? NSInputStream w/ a file and NSData both seem to return NO for - // this so I'm not sure how to test it. - return [inputStream_ getBuffer:buffer length:len]; -} - -- (BOOL)hasBytesAvailable { - return [inputStream_ hasBytesAvailable]; -} - -#pragma mark Standard messages - -// Pass expected messages to our encapsulated stream. -// -// We want our encapsulated NSInputStream to handle the standard messages; -// we don't want the superclass to handle them. -- (void)open { - [inputStream_ open]; -} - -- (void)close { - [inputStream_ close]; -} - -- (id)delegate { - return [inputStream_ delegate]; -} - -- (void)setDelegate:(id)delegate { - [inputStream_ setDelegate:delegate]; -} - -- (id)propertyForKey:(NSString *)key { - return [inputStream_ propertyForKey:key]; -} - -- (BOOL)setProperty:(id)property forKey:(NSString *)key { - return [inputStream_ setProperty:property forKey:key]; -} - -- (void)scheduleInRunLoop:(NSRunLoop *)aRunLoop forMode:(NSString *)mode { - [inputStream_ scheduleInRunLoop:aRunLoop forMode:mode]; -} -- (void)removeFromRunLoop:(NSRunLoop *)aRunLoop forMode:(NSString *)mode { - [inputStream_ removeFromRunLoop:aRunLoop forMode:mode]; -} - -- (NSStreamStatus)streamStatus { - return [inputStream_ streamStatus]; -} - -- (NSError *)streamError { - return [inputStream_ streamError]; -} - -#pragma mark Setters and getters - -- (void)setMonitorDelegate:(id)monitorDelegate - selector:(SEL)monitorSelector { - monitorDelegate_ = monitorDelegate; // non-retained - monitorSelector_ = monitorSelector; - GTMAssertSelectorNilOrImplementedWithArguments(monitorDelegate, - monitorSelector, - @encode(GTMProgressMonitorInputStream *), - @encode(unsigned long long), - @encode(unsigned long long), - NULL); -} - -- (id)monitorDelegate { - return monitorDelegate_; -} - -- (SEL)monitorSelector { - return monitorSelector_; -} - -- (void)setMonitorSource:(id)source { - monitorSource_ = source; // non-retained -} - -- (id)monitorSource { - return monitorSource_; -} - -@end diff --git a/Foundation/GTMProgressMonitorInputStreamTest.m b/Foundation/GTMProgressMonitorInputStreamTest.m deleted file mode 100644 index 69d331a..0000000 --- a/Foundation/GTMProgressMonitorInputStreamTest.m +++ /dev/null @@ -1,262 +0,0 @@ -// -// GTMProgressMonitorInputStreamTest.m -// -// Copyright 2008 Google Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); you may not -// use this file except in compliance with the License. You may obtain a copy -// of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -// License for the specific language governing permissions and limitations under -// the License. -// - -#import "GTMSenTestCase.h" -#import "GTMProgressMonitorInputStream.h" -#import "GTMUnitTestDevLog.h" -#import "GTMSystemVersion.h" - -@interface GTMProgressMonitorInputStreamTest : GTMTestCase -@end - -@interface TestStreamMonitor : NSObject { - @private - NSMutableArray *reportedDeliverySizesArray_; - NSMutableSet *reportedTotalSizesSet_; -} -- (NSArray *)reportedSizes; -- (NSSet *)reportedTotals; -- (void)inputStream:(GTMProgressMonitorInputStream *)stream - hasDeliveredBytes:(unsigned long long)numRead - ofTotalBytes:(unsigned long long)total; -@end - -@implementation GTMProgressMonitorInputStreamTest - -static const unsigned long long kSourceDataByteCount = (10000*10); - -- (void)testInit { - - // bad inputs - - // init - STAssertNil([[GTMProgressMonitorInputStream alloc] init], nil); - STAssertNil([[GTMProgressMonitorInputStream alloc] initWithStream:nil length:0], nil); - STAssertNil([[GTMProgressMonitorInputStream alloc] initWithData:nil], nil); - STAssertNil([[GTMProgressMonitorInputStream alloc] initWithFileAtPath:nil], nil); - - // class helpers - STAssertNil([GTMProgressMonitorInputStream inputStreamWithStream:nil length:0], nil); - STAssertNil([GTMProgressMonitorInputStream inputStreamWithData:nil], nil); - STAssertNil([GTMProgressMonitorInputStream inputStreamWithFileAtPath:nil], nil); - - // some data for next round - NSData *data = [@"some data" dataUsingEncoding:NSUTF8StringEncoding]; - STAssertNotNil(data, nil); - GTMProgressMonitorInputStream *monStream; - - // good inputs - - NSInputStream *inputStream = [NSInputStream inputStreamWithData:data]; - STAssertNotNil(inputStream, nil); - monStream = - [GTMProgressMonitorInputStream inputStreamWithStream:inputStream - length:[data length]]; - STAssertNotNil(monStream, nil); - - monStream = [GTMProgressMonitorInputStream inputStreamWithData:data]; - STAssertNotNil(monStream, nil); - - monStream = - [GTMProgressMonitorInputStream inputStreamWithFileAtPath:@"/etc/services"]; - STAssertNotNil(monStream, nil); - -} - -- (void)testMonitorAccessors { - - NSData *data = [@"some data" dataUsingEncoding:NSUTF8StringEncoding]; - STAssertNotNil(data, nil); - GTMProgressMonitorInputStream *monStream = - [GTMProgressMonitorInputStream inputStreamWithData:data]; - STAssertNotNil(monStream, nil); - - TestStreamMonitor *monitor = [[[TestStreamMonitor alloc] init] autorelease]; - STAssertNotNil(monitor, nil); - - SEL monSel = @selector(inputStream:hasDeliveredBytes:ofTotalBytes:); - [monStream setMonitorDelegate:monitor selector:monSel]; - STAssertEquals([monStream monitorDelegate], monitor, nil); - STAssertEquals([monStream monitorSelector], monSel, nil); - - [monStream setMonitorSource:data]; - STAssertEquals([monStream monitorSource], data, nil); -} - -- (void)testInputStreamAccessors { - - GTMProgressMonitorInputStream *monStream = - [GTMProgressMonitorInputStream inputStreamWithFileAtPath:@"/etc/services"]; - STAssertNotNil(monStream, nil); - - // delegate - - [monStream setDelegate:self]; - STAssertEquals([monStream delegate], self, nil); - [monStream setDelegate:nil]; - STAssertNil([monStream delegate], nil); - - if (![GTMSystemVersion isBuildEqualTo:kGTMSystemBuild10_6_0_WWDC]) { - // error (we get unknown error before we open things) - // This was changed on SnowLeopard. - // rdar://689714 Calling streamError on unopened stream no longer returns - // error - // was filed to check this behaviour. - NSError *err = [monStream streamError]; - STAssertEqualObjects([err domain], @"NSUnknownErrorDomain", nil); - } - - // status and properties - - // pre open - STAssertEquals([monStream streamStatus], - (NSStreamStatus)NSStreamStatusNotOpen, nil); - [monStream open]; - // post open - STAssertEquals([monStream streamStatus], - (NSStreamStatus)NSStreamStatusOpen, nil); - STAssertEqualObjects([monStream propertyForKey:NSStreamFileCurrentOffsetKey], - [NSNumber numberWithInt:0], nil); - // read some - uint8_t buf[8]; - long bytesRead = [monStream read:buf maxLength:sizeof(buf)]; - STAssertGreaterThanOrEqual(bytesRead, (long)sizeof(buf), nil); - // post read - STAssertEqualObjects([monStream propertyForKey:NSStreamFileCurrentOffsetKey], - [NSNumber numberWithLong:bytesRead], nil); - [monStream close]; - // post close - STAssertEquals([monStream streamStatus], - (NSStreamStatus)NSStreamStatusClosed, nil); - -} - -- (void)testProgressMessagesViaRead { - - // make a big data buffer (sourceData) - NSMutableData *sourceData = - [NSMutableData dataWithCapacity:kSourceDataByteCount]; - for (int idx = 0; idx < 10000; idx++) { - [sourceData appendBytes:"0123456789" length:10]; - } - STAssertEquals([sourceData length], (NSUInteger)kSourceDataByteCount, nil); - - // make a buffer to hold the data as read from the stream, and an array - // to hold the size of each read - NSMutableData *resultData = [NSMutableData data]; - NSMutableArray *deliverySizesArray = [NSMutableArray array]; - - TestStreamMonitor *monitor = [[[TestStreamMonitor alloc] init] autorelease]; - STAssertNotNil(monitor, nil); - - // create the stream; set self as the monitor - GTMProgressMonitorInputStream* monStream = - [GTMProgressMonitorInputStream inputStreamWithData:sourceData]; - [monStream setMonitorDelegate:monitor - selector:@selector(inputStream:hasDeliveredBytes:ofTotalBytes:)]; - [monStream open]; - - // we'll read random-sized chunks of data from our stream, adding the chunk - // size to deliverySizesArray and the data itself to resultData - srandomdev(); - - NSUInteger bytesReadSoFar = 0; - uint8_t readBuffer[2048]; - while (1) { - NSStreamStatus status = [monStream streamStatus]; - if (bytesReadSoFar < kSourceDataByteCount) { - STAssertTrue([monStream hasBytesAvailable], nil); - STAssertEquals(status, (NSStreamStatus)NSStreamStatusOpen, nil); - } else { - STAssertFalse([monStream hasBytesAvailable], nil); - STAssertEquals(status, (NSStreamStatus)NSStreamStatusAtEnd, nil); - } - - // read a random block size between 1 and 2048 bytes - NSUInteger bytesToRead = (random() % sizeof(readBuffer)) + 1; - NSInteger bytesRead = [monStream read:readBuffer maxLength:bytesToRead]; - - // done? - if (bytesRead <= 0) { - break; - } - - // save the data we just read, and the size of the read - [resultData appendBytes:readBuffer length:bytesRead]; - bytesReadSoFar += bytesRead; - NSNumber *bytesReadSoFarNumber = - [NSNumber numberWithUnsignedLongLong:(unsigned long long)bytesReadSoFar]; - [deliverySizesArray addObject:bytesReadSoFarNumber]; - } - - [monStream close]; - - // compare deliverySizesArray to the array built by our callback, and - // resultData to the sourceData - STAssertEqualObjects(deliverySizesArray, [monitor reportedSizes], - @"unexpected size deliveries"); - NSNumber *sourceNumber = - [NSNumber numberWithUnsignedLongLong:kSourceDataByteCount]; - STAssertEqualObjects([NSSet setWithObject:sourceNumber], - [monitor reportedTotals], - @"unexpected total sizes"); - - // STAssertEqualObjects on the actual NSDatas is hanging when they are unequal - // here so I'll just assert True - STAssertTrue([sourceData isEqualToData:resultData], - @"unexpected data read"); -} - -@end - -@implementation TestStreamMonitor - -- (id)init { - self = [super init]; - if (self) { - reportedDeliverySizesArray_ = [[NSMutableArray alloc] init]; - reportedTotalSizesSet_ = [[NSMutableSet alloc] init]; - } - return self; -} - -- (void) dealloc { - [reportedDeliverySizesArray_ release]; - [reportedTotalSizesSet_ release]; - [super dealloc]; -} - -- (NSArray *)reportedSizes { - return reportedDeliverySizesArray_; -} - -- (NSSet *)reportedTotals { - return reportedTotalSizesSet_; -} - -- (void)inputStream:(GTMProgressMonitorInputStream *)stream - hasDeliveredBytes:(unsigned long long)numRead - ofTotalBytes:(unsigned long long)total { - // add the number read so far to the array - [reportedDeliverySizesArray_ addObject: - [NSNumber numberWithUnsignedLongLong:numRead]]; - [reportedTotalSizesSet_ addObject: - [NSNumber numberWithUnsignedLongLong:total]]; -} - -@end diff --git a/Foundation/TestData/GTMHTTPFetcherTestPage.html b/Foundation/TestData/GTMHTTPFetcherTestPage.html deleted file mode 100644 index 1f44469..0000000 --- a/Foundation/TestData/GTMHTTPFetcherTestPage.html +++ /dev/null @@ -1,9 +0,0 @@ -<html xmlns="http://www.w3.org/1999/xhtml"> -<head> - <title>Test Page</title> - <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> -</head> -<body> -This is a simple test page -</body> -</html> |