diff --git a/AssetManagerTest/AssetManagerTest.xcodeproj/project.pbxproj b/AssetManagerTest/AssetManagerTest.xcodeproj/project.pbxproj index 4229f09..869a2e8 100644 --- a/AssetManagerTest/AssetManagerTest.xcodeproj/project.pbxproj +++ b/AssetManagerTest/AssetManagerTest.xcodeproj/project.pbxproj @@ -164,6 +164,7 @@ B67896F613328E390022CA67 /* Project object */ = { isa = PBXProject; attributes = { + LastUpgradeCheck = 0420; ORGANIZATIONNAME = "Zarra Studios LLC"; }; buildConfigurationList = B67896F913328E390022CA67 /* Build configuration list for PBXProject "AssetManagerTest" */; @@ -253,6 +254,7 @@ GCC_DYNAMIC_NO_PIC = NO; GCC_PRECOMPILE_PREFIX_HEADER = YES; GCC_PREFIX_HEADER = Prefix.pch; + GCC_VERSION = com.apple.compilers.llvm.clang.1_0; HEADER_SEARCH_PATHS = /usr/include/libxml2; INFOPLIST_FILE = Info.plist; OTHER_LDFLAGS = "-lxml2"; @@ -268,6 +270,7 @@ COPY_PHASE_STRIP = YES; GCC_PRECOMPILE_PREFIX_HEADER = YES; GCC_PREFIX_HEADER = Prefix.pch; + GCC_VERSION = com.apple.compilers.llvm.clang.1_0; HEADER_SEARCH_PATHS = /usr/include/libxml2; INFOPLIST_FILE = Info.plist; OTHER_LDFLAGS = "-lxml2"; diff --git a/AssetManagerTest/Classes/AppDelegate.m b/AssetManagerTest/Classes/AppDelegate.m index 45cba37..6bdb2c9 100644 --- a/AssetManagerTest/Classes/AppDelegate.m +++ b/AssetManagerTest/Classes/AppDelegate.m @@ -31,11 +31,17 @@ #define kFeedHREF @"http://api.flickr.com/services/feeds/groups_pool.gne?id=1621520@N24&lang=en-us&format=rss_200" +@interface AppDelegate () +@property (readwrite, retain) GDataXMLDocument *document; +@end + @implementation AppDelegate @synthesize assetManager; @synthesize window; +@synthesize document; + - (BOOL)application:(UIApplication*)application didFinishLaunchingWithOptions:(NSDictionary*)launchOptions { window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] applicationFrame]]; @@ -60,6 +66,7 @@ - (BOOL)application:(UIApplication*)application didFinishLaunchingWithOptions:(N MCRelease(delegate); assetManager = [[ZSAssetManager alloc] init]; + [root setAssetManager:assetManager]; return YES; } @@ -69,7 +76,7 @@ - (void)feedDownloadSuccessful:(ZSURLConnectionDelegate*)delegate DLog(@"success!"); NSError *error = nil; - GDataXMLDocument *document = [[GDataXMLDocument alloc] initWithData:[delegate data] options:0 error:&error]; + document = [[GDataXMLDocument alloc] initWithData:[delegate data] options:0 error:&error]; ZAssert(!error || document, @"Error parsing xml: %@", error); DLog(@"document %@", document); @@ -86,6 +93,14 @@ - (void)feedDownloadSuccessful:(ZSURLConnectionDelegate*)delegate NSURL *url = [NSURL URLWithString:urlString]; ZAssert(url, @"Bad url: %@", urlString); [cacheRequest addObject:url]; + + GDataXMLElement *mediaThumbnail = [[item elementsForName:@"media:thumbnail"] lastObject]; + ZAssert(mediaThumbnail, @"Failed to find media thumbnail: %@", item); + + NSString *thumbnailURLString = [[mediaThumbnail attributeForName:@"url"] stringValue]; + NSURL *thumbnailURL = [NSURL URLWithString:thumbnailURLString]; + ZAssert(thumbnailURL, @"Bad URL: %@", thumbnailURLString); + [cacheRequest addObject:thumbnailURL]; } [assetManager queueAssetsForRetrievalFromURLSet:cacheRequest]; @@ -95,7 +110,6 @@ - (void)feedDownloadSuccessful:(ZSURLConnectionDelegate*)delegate id root = [[navController viewControllers] objectAtIndex:0]; [root populateWithXMLItems:items]; - MCRelease(document); } - (void)feedDownloadFailure:(ZSURLConnectionDelegate*)error @@ -103,4 +117,9 @@ - (void)feedDownloadFailure:(ZSURLConnectionDelegate*)error ALog(@"Failure: %@", error); } +- (void)dealloc +{ + MCRelease(document); + [super dealloc]; +} @end diff --git a/AssetManagerTest/Classes/RootViewController.h b/AssetManagerTest/Classes/RootViewController.h index 0e6a8a8..209c365 100644 --- a/AssetManagerTest/Classes/RootViewController.h +++ b/AssetManagerTest/Classes/RootViewController.h @@ -26,7 +26,11 @@ // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR // OTHER DEALINGS IN THE SOFTWARE. -@interface RootViewController : UITableViewController +#import "ZSAssetManager.h" + +@interface RootViewController : UITableViewController + +@property (readwrite, retain) ZSAssetManager *assetManager; - (void)populateWithXMLItems:(NSArray*)items; diff --git a/AssetManagerTest/Classes/RootViewController.m b/AssetManagerTest/Classes/RootViewController.m index c59c400..234e563 100644 --- a/AssetManagerTest/Classes/RootViewController.m +++ b/AssetManagerTest/Classes/RootViewController.m @@ -37,10 +37,13 @@ @interface RootViewController() @implementation RootViewController @synthesize xmlItems; +@synthesize assetManager; - (void)viewDidLoad { [super viewDidLoad]; + + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(imageDownloadComplete:) name:kImageDownloadComplete object:[self assetManager]]; } - (void)populateWithXMLItems:(NSArray*)items @@ -49,6 +52,11 @@ - (void)populateWithXMLItems:(NSArray*)items [[self tableView] reloadData]; } +- (void)imageDownloadComplete:(NSNotification *)notification +{ + [[self tableView] reloadData]; +} + #pragma mark - #pragma mark UITableViewDatasource @@ -65,15 +73,35 @@ - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(N [cell setAccessoryType:UITableViewCellAccessoryDisclosureIndicator]; } -// GDataXMLElement *item = [[self xmlItems] objectAtIndex:[indexPath row]]; -// GDataXMLElement *title = [[item elementsForName:@"title"] lastObject]; -// if (!title) { -// [[cell textLabel] setText:@"Untitled"]; -// } else { -// [[cell textLabel] setText:[title stringValue]]; -// } + GDataXMLElement *item = [[self xmlItems] objectAtIndex:[indexPath row]]; + GDataXMLElement *title = [[item elementsForName:@"title"] lastObject]; + if (!title) { + [[cell textLabel] setText:@"Untitled"]; + } else { + [[cell textLabel] setText:[title stringValue]]; + } + + GDataXMLElement *mediaThumbnail = [[item elementsForName:@"media:thumbnail"] lastObject]; + ZAssert(mediaThumbnail, @"Failed to find media thumbnail: %@", item); + + NSString *thumbnailURLString = [[mediaThumbnail attributeForName:@"url"] stringValue]; + NSURL *thumbnailURL = [NSURL URLWithString:thumbnailURLString]; + [[cell imageView] setImage:[assetManager imageForURL:thumbnailURL]]; return cell; } +- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath +{ + [tableView deselectRowAtIndexPath:indexPath animated:NO]; +} + +#pragma mark - +#pragma mark Memory management +- (void)dealloc +{ + [xmlItems release]; + [assetManager release]; + [super dealloc]; +} @end diff --git a/README.markdown b/README.markdown index c1af3fe..797679e 100644 --- a/README.markdown +++ b/README.markdown @@ -2,6 +2,15 @@ In this repository is code that we share between many projects that we work on. It is licensed under the BSD license and is free to use as you wish. +## About this fork ## + +This fork makes the following changes to ZSURLConnectionDelegate: + +* Incoming data is saved to a file rather than kept in memory, which is useful if you're expecting a large download. +* It's possible to initialize ZSURLConnectionDelegate with an NSURLRequest instead of an NSURL, which is useful if you need to set custom HTTP headers on the request or want to provide a POST body. +* API has been added to optionally support self-signed HTTPS certificates for all hosts or for specific hosts. +* A userInfo parameter and some convenience constructors have been added. + ## Prefix.pch ## You will probably find macros in this code that does not compile. The reason is that we have several macros that we add to our Prefix.pch file upon project creation. Those macros are as follows: diff --git a/ZSReachability.h b/ZSReachability.h index 4e66ee9..930febf 100644 --- a/ZSReachability.h +++ b/ZSReachability.h @@ -48,6 +48,7 @@ #import #import +#import typedef enum { NotReachable = 0, diff --git a/ZSURLConnectionDelegate.h b/ZSURLConnectionDelegate.h index 13e0990..55317cc 100755 --- a/ZSURLConnectionDelegate.h +++ b/ZSURLConnectionDelegate.h @@ -41,12 +41,13 @@ void decrementNetworkActivity(id sender); @property (nonatomic, assign, getter=isVerbose) BOOL verbose; @property (nonatomic, assign, getter=isDone) BOOL done; -@property (nonatomic, readonly) NSMutableData *data; +@property (nonatomic, readonly) NSData *data; @property (nonatomic, retain) id object; @property (nonatomic, retain) NSString *filePath; @property (nonatomic, retain) NSURL *myURL; @property (nonatomic, retain) NSHTTPURLResponse *response; +@property (readonly) NSInteger HTTPStatus; @property (nonatomic, retain) id delegate; @property (nonatomic, assign) SEL successSelector; @@ -56,6 +57,15 @@ void decrementNetworkActivity(id sender); @property (nonatomic, assign) NSTimeInterval startTime; @property (nonatomic, assign) NSTimeInterval duration; +@property (nonatomic, assign) BOOL acceptSelfSignedCertificates; +@property (nonatomic, copy) NSArray *acceptSelfSignedCertificatesFromHosts; + +@property (readwrite, retain) id userInfo; + +- (id)initWithRequest:(NSURLRequest *)newRequest delegate:(id)delegate; - (id)initWithURL:(NSURL*)aURL delegate:(id)delegate; ++ (id)operationWithRequest:(NSURLRequest *)newRequest delegate:(id)aDelegate; ++ (id)operationWithURL:(NSURL *)aURL delegate:(id)aDelegate; + @end diff --git a/ZSURLConnectionDelegate.m b/ZSURLConnectionDelegate.m index 02086a2..7931a6f 100755 --- a/ZSURLConnectionDelegate.m +++ b/ZSURLConnectionDelegate.m @@ -61,6 +61,14 @@ void decrementNetworkActivity(id sender) } } +@interface ZSURLConnectionDelegate () +@property (readwrite, retain) NSString *inProgressFilePath; +@property (readwrite, retain) NSFileHandle *inProgressFileHandle; +@property (readwrite) NSInteger HTTPStatus; +@property (readwrite, retain) NSURLRequest *request; +@property (readwrite, assign) dispatch_group_t dispatchFileWriteGroup; +@end + @implementation ZSURLConnectionDelegate @synthesize verbose; @@ -72,6 +80,9 @@ @implementation ZSURLConnectionDelegate @synthesize filePath; @synthesize myURL; @synthesize response; +@synthesize HTTPStatus; +@synthesize request; +@synthesize dispatchFileWriteGroup; @synthesize successSelector; @synthesize failureSelector; @@ -81,17 +92,27 @@ @implementation ZSURLConnectionDelegate @synthesize startTime; @synthesize duration; +@synthesize inProgressFilePath; +@synthesize inProgressFileHandle; + +@synthesize acceptSelfSignedCertificates; +@synthesize acceptSelfSignedCertificatesFromHosts; + +@synthesize userInfo; + static dispatch_queue_t writeQueue; static dispatch_queue_t pngQueue; -- (id)initWithURL:(NSURL*)aURL delegate:(id)aDelegate; +#pragma mark - +#pragma mark Initializers +- (id)initWithRequest:(NSURLRequest *)newRequest delegate:(id)aDelegate; { - ZAssert(aURL, @"incoming url is nil"); if (!(self = [super init])) return nil; + request = [newRequest retain]; delegate = [aDelegate retain]; - [self setMyURL:aURL]; - + [self setMyURL:[newRequest URL]]; + if (writeQueue == NULL) { writeQueue = dispatch_queue_create("cache write queue", NULL); } @@ -100,32 +121,88 @@ - (id)initWithURL:(NSURL*)aURL delegate:(id)aDelegate; pngQueue = dispatch_queue_create("png generation queue", NULL); } + if (dispatchFileWriteGroup == NULL) { + dispatchFileWriteGroup = dispatch_group_create(); + } + return self; } +- (id)initWithURL:(NSURL*)aURL delegate:(id)aDelegate; +{ + ZAssert(aURL, @"incoming url is nil"); + return [self initWithRequest:[NSURLRequest requestWithURL:aURL] delegate:aDelegate]; +} + +#pragma mark - +#pragma mark Convenience factory methods ++ (id)operationWithRequest:(NSURLRequest *)newRequest delegate:(id)aDelegate +{ + return [[[ZSURLConnectionDelegate alloc] initWithRequest:newRequest delegate:aDelegate] autorelease]; +} + ++ (id)operationWithURL:(NSURL *)aURL delegate:(id)aDelegate +{ + return [[[ZSURLConnectionDelegate alloc] initWithURL:aURL delegate:aDelegate] autorelease]; +} + +#pragma mark - +#pragma mark Memory management - (void)dealloc { if ([self isVerbose]) DLog(@"fired"); connection = nil; object = nil; + if (![self filePath]) { + // If no filePath was set, don't litter the temp dir with orphaned downloaded files. + [[NSFileManager defaultManager] removeItemAtPath:[self inProgressFilePath] error:nil]; + } MCRelease(delegate); MCRelease(filePath); MCRelease(myURL); MCRelease(data); + MCRelease(request); MCRelease(response); + MCRelease(userInfo); + dispatch_release([self dispatchFileWriteGroup]); + + MCRelease(inProgressFilePath); + MCRelease(inProgressFileHandle); + [super dealloc]; } +#pragma mark - +#pragma mark Accessors +- (void)setAcceptSelfSignedCertificates:(BOOL)flag +{ + acceptSelfSignedCertificates = flag; + if (flag) { + // Setting the flag to YES implies trusting self-signed certs from everyone. + [acceptSelfSignedCertificatesFromHosts release]; + acceptSelfSignedCertificatesFromHosts = nil; + } +} + +- (void)setAcceptSelfSignedCertificatesFromHosts:(NSArray *)hosts +{ + [acceptSelfSignedCertificatesFromHosts release]; + acceptSelfSignedCertificatesFromHosts = [hosts copy]; + // Set the flag to NO to turn off accept-from-all behavior, limiting access to only the host list. + [self setAcceptSelfSignedCertificates:NO]; +} + +#pragma mark - +#pragma mark Entry point - (void)main { if ([self isCancelled]) return; incrementNetworkActivity(self); - NSURLRequest *request = [NSURLRequest requestWithURL:[self myURL]]; - [self setConnection:[NSURLConnection connectionWithRequest:request delegate:self]]; + [self setConnection:[NSURLConnection connectionWithRequest:[self request] delegate:self]]; CFRunLoopRun(); @@ -137,40 +214,30 @@ - (void)finish CFRunLoopStop(CFRunLoopGetCurrent()); } +#pragma mark - +#pragma mark NSURLConnection delegate methods - (void)connectionDidFinishLoading:(NSURLConnection*)connection { - DLog(@"finished for %@", [self myURL]); - if ([self isCancelled]) { - [[self connection] cancel]; - [self finish]; - return; - } - - [self setDuration:([NSDate timeIntervalSinceReferenceDate] - [self startTime])]; - - if (![self filePath]) { + // Hold the completion block until all outstanding file writes have finished, as indicated by dispatchFileWriteGroup. + dispatch_group_notify([self dispatchFileWriteGroup], writeQueue, ^{ + [[self inProgressFileHandle] closeFile]; + + DLog(@"finished for %@", [self myURL]); + if ([self isCancelled]) { + [[self connection] cancel]; + [self finish]; + return; + } + + [self setDuration:([NSDate timeIntervalSinceReferenceDate] - [self startTime])]; + + // Even if filePath was set, the delegate might try to look at the data blob. + data = [[NSData alloc] initWithContentsOfMappedFile:[self inProgressFilePath]]; if ([[self delegate] respondsToSelector:[self successSelector]]) { [[self delegate] performSelectorOnMainThread:[self successSelector] withObject:self waitUntilDone:YES]; } [self finish]; - return; - } - - NSData *localizedData = [self data]; - NSString *localizedFilepath = [self filePath]; - - dispatch_sync(writeQueue, ^{ - NSError *error = nil; - ZAssert([localizedData writeToFile:localizedFilepath atomically:NO], @"Failed to write to %@\n%@\n%@", localizedFilepath, [error localizedDescription], [error userInfo]); - - if (![[self delegate] respondsToSelector:[self successSelector]]) return; - - dispatch_sync(dispatch_get_main_queue(), ^{ - [[self delegate] performSelector:[self successSelector] withObject:self]; - }); }); - - [self finish]; } - (void)connection:(NSURLConnection*)connection didReceiveResponse:(NSHTTPURLResponse*)resp @@ -182,8 +249,16 @@ - (void)connection:(NSURLConnection*)connection didReceiveResponse:(NSHTTPURLRes } if ([self isVerbose]) DLog(@"fired"); [self setResponse:resp]; - MCRelease(data); - data = [[NSMutableData alloc] init]; + [self setHTTPStatus:[resp statusCode]]; + + if ([self filePath]) { + [self setInProgressFilePath:[self filePath]]; + } else { + [self setInProgressFilePath:[[NSTemporaryDirectory() stringByAppendingPathComponent:[NSString stringWithFormat:@"%d", abs([[[self myURL] absoluteString] hash])]] retain]]; + } + [[NSFileManager defaultManager] removeItemAtPath:[self inProgressFilePath] error:nil]; + [[NSFileManager defaultManager] createFileAtPath:[self inProgressFilePath] contents:nil attributes:nil]; + [self setInProgressFileHandle:[NSFileHandle fileHandleForWritingAtPath:[self inProgressFilePath]]]; [self setStartTime:[NSDate timeIntervalSinceReferenceDate]]; } @@ -195,11 +270,15 @@ - (void)connection:(NSURLConnection*)connection didReceiveData:(NSData*)newData return; } if ([self isVerbose]) DLog(@"fired"); - [data appendData:newData]; + dispatch_group_async([self dispatchFileWriteGroup], writeQueue, ^{ + [[self inProgressFileHandle] writeData:newData]; + }); } - (void)connection:(NSURLConnection*)connection didFailWithError:(NSError*)error { + [[self inProgressFileHandle] closeFile]; + if ([self isCancelled]) { [[self connection] cancel]; [self finish]; @@ -213,4 +292,22 @@ - (void)connection:(NSURLConnection*)connection didFailWithError:(NSError*)error [self finish]; } +- (BOOL)connection:(NSURLConnection *)connection canAuthenticateAgainstProtectionSpace:(NSURLProtectionSpace *)protectionSpace { + if (([self acceptSelfSignedCertificates]) || ([[self acceptSelfSignedCertificatesFromHosts] count] > 0)) { + return [[protectionSpace authenticationMethod] isEqualToString:NSURLAuthenticationMethodServerTrust]; + } else { + return NO; + } +} + +- (void)connection:(NSURLConnection *)connection didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge { + if (([self acceptSelfSignedCertificates]) || ([[self acceptSelfSignedCertificatesFromHosts] containsObject:[[challenge protectionSpace] host]])) { + if ([[[challenge protectionSpace] authenticationMethod] isEqualToString:NSURLAuthenticationMethodServerTrust]) { + [[challenge sender] useCredential:[NSURLCredential credentialForTrust:[[challenge protectionSpace] serverTrust]] forAuthenticationChallenge:challenge]; + } else { + [[challenge sender] continueWithoutCredentialForAuthenticationChallenge:challenge]; + } + } +} + @end