blob: f60f53b50cf8052b770d637d428cf46a38d52d48 [file] [log] [blame]
/* Copyright (c) 2010 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
//
// GTMHTTPFetcherService.m
//
#import "GTMHTTPFetcherService.h"
@interface GTMHTTPFetcher (ServiceMethods)
- (BOOL)beginFetchMayDelay:(BOOL)mayDelay
mayAuthorize:(BOOL)mayAuthorize;
@end
@interface GTMHTTPFetcherService ()
@property (retain, readwrite) NSDictionary *delayedHosts;
@property (retain, readwrite) NSDictionary *runningHosts;
- (void)detachAuthorizer;
@end
@implementation GTMHTTPFetcherService
@synthesize maxRunningFetchersPerHost = maxRunningFetchersPerHost_,
userAgent = userAgent_,
timeout = timeout_,
delegateQueue = delegateQueue_,
runLoopModes = runLoopModes_,
credential = credential_,
proxyCredential = proxyCredential_,
cookieStorageMethod = cookieStorageMethod_,
shouldFetchInBackground = shouldFetchInBackground_,
allowedInsecureSchemes = allowedInsecureSchemes_,
allowLocalhostRequest = allowLocalhostRequest_,
fetchHistory = fetchHistory_;
- (id)init {
self = [super init];
if (self) {
fetchHistory_ = [[GTMHTTPFetchHistory alloc] init];
delayedHosts_ = [[NSMutableDictionary alloc] init];
runningHosts_ = [[NSMutableDictionary alloc] init];
cookieStorageMethod_ = kGTMHTTPFetcherCookieStorageMethodFetchHistory;
maxRunningFetchersPerHost_ = 10;
}
return self;
}
- (void)dealloc {
[self detachAuthorizer];
[delayedHosts_ release];
[runningHosts_ release];
[fetchHistory_ release];
[userAgent_ release];
[delegateQueue_ release];
[runLoopModes_ release];
[credential_ release];
[proxyCredential_ release];
[authorizer_ release];
[super dealloc];
}
#pragma mark Generate a new fetcher
- (id)fetcherWithRequest:(NSURLRequest *)request
fetcherClass:(Class)fetcherClass {
GTMHTTPFetcher *fetcher = [fetcherClass fetcherWithRequest:request];
fetcher.fetchHistory = self.fetchHistory;
fetcher.delegateQueue = self.delegateQueue;
fetcher.runLoopModes = self.runLoopModes;
fetcher.cookieStorageMethod = self.cookieStorageMethod;
fetcher.credential = self.credential;
fetcher.proxyCredential = self.proxyCredential;
fetcher.shouldFetchInBackground = self.shouldFetchInBackground;
fetcher.allowedInsecureSchemes = self.allowedInsecureSchemes;
fetcher.allowLocalhostRequest = self.allowLocalhostRequest;
fetcher.authorizer = self.authorizer;
fetcher.service = self;
NSString *userAgent = self.userAgent;
if ([userAgent length] > 0
&& [request valueForHTTPHeaderField:@"User-Agent"] == nil) {
[fetcher.mutableRequest setValue:userAgent
forHTTPHeaderField:@"User-Agent"];
}
NSTimeInterval timeout = self.timeout;
if (timeout > 0.0) {
[fetcher.mutableRequest setTimeoutInterval:timeout];
}
return fetcher;
}
- (GTMHTTPFetcher *)fetcherWithRequest:(NSURLRequest *)request {
return [self fetcherWithRequest:request
fetcherClass:[GTMHTTPFetcher class]];
}
- (GTMHTTPFetcher *)fetcherWithURL:(NSURL *)requestURL {
return [self fetcherWithRequest:[NSURLRequest requestWithURL:requestURL]];
}
- (GTMHTTPFetcher *)fetcherWithURLString:(NSString *)requestURLString {
return [self fetcherWithURL:[NSURL URLWithString:requestURLString]];
}
#pragma mark Queue Management
- (void)addRunningFetcher:(GTMHTTPFetcher *)fetcher
forHost:(NSString *)host {
// Add to the array of running fetchers for this host, creating the array
// if needed
NSMutableArray *runningForHost = [runningHosts_ objectForKey:host];
if (runningForHost == nil) {
runningForHost = [NSMutableArray arrayWithObject:fetcher];
[runningHosts_ setObject:runningForHost forKey:host];
} else {
[runningForHost addObject:fetcher];
}
}
- (void)addDelayedFetcher:(GTMHTTPFetcher *)fetcher
forHost:(NSString *)host {
// Add to the array of delayed fetchers for this host, creating the array
// if needed
NSMutableArray *delayedForHost = [delayedHosts_ objectForKey:host];
if (delayedForHost == nil) {
delayedForHost = [NSMutableArray arrayWithObject:fetcher];
[delayedHosts_ setObject:delayedForHost forKey:host];
} else {
[delayedForHost addObject:fetcher];
}
}
- (BOOL)isDelayingFetcher:(GTMHTTPFetcher *)fetcher {
@synchronized(self) {
NSString *host = [[[fetcher mutableRequest] URL] host];
NSArray *delayedForHost = [delayedHosts_ objectForKey:host];
NSUInteger idx = [delayedForHost indexOfObjectIdenticalTo:fetcher];
BOOL isDelayed = (delayedForHost != nil) && (idx != NSNotFound);
return isDelayed;
}
}
- (BOOL)fetcherShouldBeginFetching:(GTMHTTPFetcher *)fetcher {
// Entry point from the fetcher
@synchronized(self) {
NSURL *requestURL = [[fetcher mutableRequest] URL];
NSString *host = [requestURL host];
// Addresses "file:///path" case where localhost is the implicit host.
if ([host length] == 0 && [requestURL isFileURL]) {
host = @"localhost";
}
if ([host length] == 0) {
#if DEBUG
// Data URIs legitimately have no host, reject other hostless URLs.
NSAssert1([[requestURL scheme] isEqual:@"data"], @"%@ lacks host", fetcher);
#endif
return YES;
}
NSMutableArray *runningForHost = [runningHosts_ objectForKey:host];
if (runningForHost != nil
&& [runningForHost indexOfObjectIdenticalTo:fetcher] != NSNotFound) {
#if DEBUG
NSAssert1(0, @"%@ was already running", fetcher);
#endif
return YES;
}
// We'll save the host that serves as the key for this fetcher's array
// to avoid any chance of the underlying request changing, stranding
// the fetcher in the wrong array
fetcher.serviceHost = host;
fetcher.thread = [NSThread currentThread];
if (maxRunningFetchersPerHost_ == 0
|| maxRunningFetchersPerHost_ > [runningForHost count]) {
[self addRunningFetcher:fetcher forHost:host];
return YES;
} else {
[self addDelayedFetcher:fetcher forHost:host];
return NO;
}
}
return YES;
}
// Fetcher start and stop methods, invoked on the appropriate thread for
// the fetcher
- (void)performSelector:(SEL)sel onStartThreadForFetcher:(GTMHTTPFetcher *)fetcher {
NSOperationQueue *delegateQueue = fetcher.delegateQueue;
NSThread *thread = fetcher.thread;
if (delegateQueue != nil || [thread isEqual:[NSThread currentThread]]) {
// The fetcher should run on the thread we're on now, or there's a delegate
// queue specified so it doesn't matter what thread the fetcher is started
// on, since it will call back on the queue.
[self performSelector:sel withObject:fetcher];
} else {
// Fetcher must run on a specified thread (and that thread must have a
// run loop.)
[self performSelector:sel
onThread:thread
withObject:fetcher
waitUntilDone:NO];
}
}
- (void)startFetcherOnCurrentThread:(GTMHTTPFetcher *)fetcher {
[fetcher beginFetchMayDelay:NO
mayAuthorize:YES];
}
- (void)startFetcher:(GTMHTTPFetcher *)fetcher {
[self performSelector:@selector(startFetcherOnCurrentThread:)
onStartThreadForFetcher:fetcher];
}
- (void)stopFetcherOnCurrentThread:(GTMHTTPFetcher *)fetcher {
[fetcher stopFetching];
}
- (void)stopFetcher:(GTMHTTPFetcher *)fetcher {
[self performSelector:@selector(stopFetcherOnCurrentThread:)
onStartThreadForFetcher:fetcher];
}
- (void)fetcherDidStop:(GTMHTTPFetcher *)fetcher {
// Entry point from the fetcher
@synchronized(self) {
NSString *host = fetcher.serviceHost;
if (!host) {
// fetcher has been stopped previously
return;
}
NSMutableArray *runningForHost = [runningHosts_ objectForKey:host];
[runningForHost removeObject:fetcher];
NSMutableArray *delayedForHost = [delayedHosts_ objectForKey:host];
[delayedForHost removeObject:fetcher];
while ([delayedForHost count] > 0
&& [runningForHost count] < maxRunningFetchersPerHost_) {
// Start another delayed fetcher running, scanning for the minimum
// priority value, defaulting to FIFO for equal priorities
GTMHTTPFetcher *nextFetcher = nil;
for (GTMHTTPFetcher *delayedFetcher in delayedForHost) {
if (nextFetcher == nil
|| delayedFetcher.servicePriority < nextFetcher.servicePriority) {
nextFetcher = delayedFetcher;
}
}
if (nextFetcher) {
[self addRunningFetcher:nextFetcher forHost:host];
runningForHost = [runningHosts_ objectForKey:host];
[delayedForHost removeObjectIdenticalTo:nextFetcher];
[self startFetcher:nextFetcher];
}
}
if ([runningForHost count] == 0) {
// None left; remove the empty array
[runningHosts_ removeObjectForKey:host];
}
if ([delayedForHost count] == 0) {
[delayedHosts_ removeObjectForKey:host];
}
// The fetcher is no longer in the running or the delayed array,
// so remove its host and thread properties
fetcher.serviceHost = nil;
fetcher.thread = nil;
}
}
- (NSUInteger)numberOfFetchers {
@synchronized(self) {
NSUInteger running = [self numberOfRunningFetchers];
NSUInteger delayed = [self numberOfDelayedFetchers];
return running + delayed;
}
}
- (NSUInteger)numberOfRunningFetchers {
@synchronized(self) {
NSUInteger sum = 0;
for (NSString *host in runningHosts_) {
NSArray *fetchers = [runningHosts_ objectForKey:host];
sum += [fetchers count];
}
return sum;
}
}
- (NSUInteger)numberOfDelayedFetchers {
@synchronized(self) {
NSUInteger sum = 0;
for (NSString *host in delayedHosts_) {
NSArray *fetchers = [delayedHosts_ objectForKey:host];
sum += [fetchers count];
}
return sum;
}
}
- (NSArray *)issuedFetchersWithRequestURL:(NSURL *)requestURL {
@synchronized(self) {
NSMutableArray *array = nil;
NSString *host = [requestURL host];
if ([host length] == 0) return nil;
NSURL *absRequestURL = [requestURL absoluteURL];
NSArray *runningForHost = [runningHosts_ objectForKey:host];
for (GTMHTTPFetcher *fetcher in runningForHost) {
NSURL *fetcherURL = [[[fetcher mutableRequest] URL] absoluteURL];
if ([fetcherURL isEqual:absRequestURL]) {
if (array == nil) {
array = [NSMutableArray array];
}
[array addObject:fetcher];
}
}
NSArray *delayedForHost = [delayedHosts_ objectForKey:host];
for (GTMHTTPFetcher *fetcher in delayedForHost) {
NSURL *fetcherURL = [[[fetcher mutableRequest] URL] absoluteURL];
if ([fetcherURL isEqual:absRequestURL]) {
if (array == nil) {
array = [NSMutableArray array];
}
[array addObject:fetcher];
}
}
return array;
}
}
- (void)stopAllFetchers {
@synchronized(self) {
// Remove fetchers from the delayed list to avoid fetcherDidStop: from
// starting more fetchers running as a side effect of stopping one
NSArray *delayedForHosts = [delayedHosts_ allValues];
[delayedHosts_ removeAllObjects];
for (NSArray *delayedForHost in delayedForHosts) {
for (GTMHTTPFetcher *fetcher in delayedForHost) {
[self stopFetcher:fetcher];
}
}
NSArray *runningForHosts = [runningHosts_ allValues];
[runningHosts_ removeAllObjects];
for (NSArray *runningForHost in runningForHosts) {
for (GTMHTTPFetcher *fetcher in runningForHost) {
[self stopFetcher:fetcher];
}
}
}
}
#pragma mark Fetch History Settings
// Turn on data caching to receive a copy of previously-retrieved objects.
// Otherwise, fetches may return status 304 (No Change) rather than actual data
- (void)setShouldCacheETaggedData:(BOOL)flag {
self.fetchHistory.shouldCacheETaggedData = flag;
}
- (BOOL)shouldCacheETaggedData {
return self.fetchHistory.shouldCacheETaggedData;
}
- (void)setETaggedDataCacheCapacity:(NSUInteger)totalBytes {
self.fetchHistory.memoryCapacity = totalBytes;
}
- (NSUInteger)ETaggedDataCacheCapacity {
return self.fetchHistory.memoryCapacity;
}
- (void)setShouldRememberETags:(BOOL)flag {
self.fetchHistory.shouldRememberETags = flag;
}
- (BOOL)shouldRememberETags {
return self.fetchHistory.shouldRememberETags;
}
// reset the ETag cache to avoid getting a Not Modified status
// based on prior queries
- (void)clearETaggedDataCache {
[self.fetchHistory clearETaggedDataCache];
}
- (void)clearHistory {
[self clearETaggedDataCache];
[self.fetchHistory removeAllCookies];
}
#pragma mark Synchronous Wait for Unit Testing
- (void)waitForCompletionOfAllFetchersWithTimeout:(NSTimeInterval)timeoutInSeconds {
NSDate* giveUpDate = [NSDate dateWithTimeIntervalSinceNow:timeoutInSeconds];
BOOL isMainThread = [NSThread isMainThread];
while ([self numberOfFetchers] > 0
&& [giveUpDate timeIntervalSinceNow] > 0) {
// Run the current run loop 1/1000 of a second to give the networking
// code a chance to work
if (isMainThread || delegateQueue_ == nil) {
NSDate *stopDate = [NSDate dateWithTimeIntervalSinceNow:0.001];
[[NSRunLoop currentRunLoop] runUntilDate:stopDate];
} else {
// Sleep on the delegate queue's background thread.
[NSThread sleepForTimeInterval:0.001];
}
}
}
#pragma mark Accessors
- (NSDictionary *)runningHosts {
return runningHosts_;
}
- (void)setRunningHosts:(NSDictionary *)dict {
[runningHosts_ autorelease];
runningHosts_ = [dict mutableCopy];
}
- (NSDictionary *)delayedHosts {
return delayedHosts_;
}
- (void)setDelayedHosts:(NSDictionary *)dict {
[delayedHosts_ autorelease];
delayedHosts_ = [dict mutableCopy];
}
- (id <GTMFetcherAuthorizationProtocol>)authorizer {
return authorizer_;
}
- (void)setAuthorizer:(id <GTMFetcherAuthorizationProtocol>)obj {
if (obj != authorizer_) {
[self detachAuthorizer];
}
[authorizer_ autorelease];
authorizer_ = [obj retain];
// Use the fetcher service for the authorization fetches if the auth
// object supports fetcher services
if ([authorizer_ respondsToSelector:@selector(setFetcherService:)]) {
[authorizer_ setFetcherService:self];
}
}
- (void)detachAuthorizer {
// This method is called by the fetcher service's dealloc and setAuthorizer:
// methods; do not override.
//
// The fetcher service retains the authorizer, and the authorizer has a
// weak pointer to the fetcher service (a non-zeroing pointer for
// compatibility with iOS 4 and Mac OS X 10.5/10.6.)
//
// When this fetcher service no longer uses the authorizer, we want to remove
// the authorizer's dependence on the fetcher service. Authorizers can still
// function without a fetcher service.
if ([authorizer_ respondsToSelector:@selector(fetcherService)]) {
id authFetcherService = [authorizer_ fetcherService];
if (authFetcherService == self) {
[authorizer_ setFetcherService:nil];
}
}
}
@end