Wednesday, June 16, 2010

 

HttpHelper for all

Well, that's flattering: the lead developer of WordPress for iOS (WordPress is the world's most popular blogging software) just emailed me, saying nice things about my previously-posted HttpHelper class, and asking for permission to use it in their project.

And why stop with them? So I hereby formally release HttpHelper under the Apache License 2.0, for anyone to use. I've made a few tweaks to the class since I last posted it - one bug fix, and some synchronous accessors for use when you're already in a background thread - so here it is in its entirety:

HttpHelper.h


// HttpHelper.h
// Created by Jonathan Evans on 04/09/09.
// Copyright 2009 Jonathan Evans.

#import


@interface HttpHelper : NSObject {
}
+(NSURLRequest *) buildRequestWithPostKeys:(NSArray *) postKeys postValues:(NSArray *) postValues urlString:(NSString *)urlString;
+(BOOL) doPost:(NSURLRequest *)request forCaller:(id)caller onSuccess:(SEL)successSelector onFailure:(SEL)failSelector;
-(BOOL) doHttp:(NSDictionary *)args;

+(NSURLRequestCachePolicy)getCachePolicyFor:(NSString *)urlString;
+(NSURLRequest *)getURLRequestFor:(NSString *)urlString;
+(BOOL) doGet:(NSString *)urlString forCaller:(id)caller onSuccess:(SEL)successSelector onFailure:(SEL)failSelector;
+(NSString *) doSynchronizedGet:(NSString *)urlString;
+(NSData *) doSynchronizedDataGet:(NSString *)urlString;
+(NSString *) doSynchronizedPostTo:(NSString *)urlString withKeys:(NSArray*)postKeys andValues:(NSArray*)postValues;

@end


HttpHelper.m


// HttpHelper.m
// Created by Jonathan Evans on 04/09/09.
// Copyright 2009 Jonathan Evans.
//

#import "HttpHelper.h"


@implementation HttpHelper

static HttpHelper *singleton=nil;

#pragma mark -
#pragma mark Singleton methods

+(HttpHelper *) getInstance {
if (singleton==nil)
singleton = [[[HttpHelper alloc] init] autorelease];
return singleton;
}

+(id)allocWithZone:(NSZone *)zone {
if (singleton == nil) {
singleton = [super allocWithZone:zone];
return singleton;
}
return nil;
}

-(id)copyWithZone:(NSZone *)zone {
return self;
}

-(id)retain {
return self;
}

-(unsigned)retainCount {
return UINT_MAX;
}

-(void)release {
//pass
}

-(id)autorelease {
return self;
}

- (void)dealloc {
[super dealloc];
}

#pragma mark -
#pragma mark Business logic

+(NSURLRequest*) buildRequestWithPostKeys:(NSArray *) postKeys postValues:(NSArray *) postValues urlString:(NSString *)urlString {
NSURL * url = [NSURL URLWithString:urlString];
NSURLRequestCachePolicy policy = NSURLRequestReloadIgnoringCacheData; // we never want a cache of a post response
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url cachePolicy:policy timeoutInterval:30.0]; //TODO: extend timeout
[request setHTTPMethod:@"POST"];

NSMutableData *paramData = [NSMutableData dataWithCapacity:8192];

NSMutableString *params=[[NSMutableString alloc] initWithCapacity:1024];
for (int i=0; i<[postValues count]; i++) {
NSObject* valueToPost = [postValues objectAtIndex:i];
if (i>0)
[params appendString:@"&"];
[params appendString:[postKeys objectAtIndex:i]];
[params appendString:@"="];
[params appendString:[NSString stringWithFormat:@"%@",valueToPost]];
}

[paramData appendData:[params dataUsingEncoding:NSUTF8StringEncoding]];

NSString *msgLength = [NSString stringWithFormat:@"%d", [paramData length]];
[request addValue: msgLength forHTTPHeaderField:@"Content-Length"];
[request setHTTPBody: paramData];
[params release];

return request;
}

+(BOOL) doPost:(NSURLRequest *)request forCaller:(id)caller onSuccess:(SEL)onSuccess onFailure:(SEL)onFailure {

NSArray *keys = [NSArray arrayWithObjects:@"request", @"caller", @"onSuccess", @"onFailure", nil];
NSArray *values = [NSArray arrayWithObjects:request, caller,
[NSValue valueWithBytes:&onSuccess objCType:@encode(SEL)],
[NSValue valueWithBytes:&onFailure objCType:@encode(SEL)],
nil];
NSDictionary *args = [NSDictionary dictionaryWithObjects:values forKeys: keys];
NSThread* uploadThread = [[NSThread alloc] initWithTarget:[self getInstance] selector:@selector(doHttp:) object:args];
[uploadThread start];
[uploadThread release];
return TRUE;
}

+(NSURLRequestCachePolicy)getCachePolicyFor:(NSString *)urlString {
//Hived out to a separate method because we might fine-tune this later.
return NSURLRequestUseProtocolCachePolicy;
// return NSURLRequestReloadIgnoringCacheData;
}

+(NSURLRequest *)getURLRequestFor:(NSString *)urlString {
NSURL *url = [NSURL URLWithString:urlString];
NSURLRequestCachePolicy policy = [self getCachePolicyFor:urlString];
NSURLRequest *request = [NSURLRequest requestWithURL:url cachePolicy:policy timeoutInterval:30.0];
return request;
}

+(BOOL) doGet:(NSString *)urlString forCaller:(id)caller onSuccess:(SEL)onSuccess onFailure:(SEL)onFailure {
NSURLRequest *request = [self getURLRequestFor:urlString];
NSArray *keys = [NSArray arrayWithObjects:@"request", @"caller", @"onSuccess", @"onFailure", nil];
NSArray *values = [NSArray arrayWithObjects:request, caller,
[NSValue valueWithBytes:&onSuccess objCType:@encode(SEL)],
[NSValue valueWithBytes:&onFailure objCType:@encode(SEL)],
nil];
NSDictionary *args = [NSDictionary dictionaryWithObjects:values forKeys: keys];
NSThread* uploadThread = [[NSThread alloc] initWithTarget:[self getInstance] selector:@selector(doHttp:) object:args];
[uploadThread start];
[uploadThread release];
return TRUE;
}

-(BOOL) doHttp:(NSDictionary *)args
{
@synchronized (self) {
//autorelease pool
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];

NSURLRequest *request = [args objectForKey:@"request"];
NSObject *caller = [args objectForKey:@"caller"];

SEL onSuccess;
[[args objectForKey:@"onSuccess"] getValue:&onSuccess];
SEL onFailure;
[[args objectForKey:@"onFailure"] getValue:&onFailure];

NSError *error=nil;
NSHTTPURLResponse *response=nil;
NSData *returnData = [NSURLConnection sendSynchronousRequest: request returningResponse: &response error: &error ];
NSString * responseString = [[[NSString alloc] initWithData: returnData encoding: NSUTF8StringEncoding] autorelease];

NSString *errorString=nil;

if (error)
errorString = [NSString stringWithFormat:@"HTTP error from %@: %@", [request URL], [error localizedDescription]];
else if (response==nil || returnData==nil)
errorString = [NSString stringWithFormat:@"No response from URL %@",[request URL]];
else if (response.statusCode!=200)
errorString = [NSString stringWithFormat:@"Server error from URL %@",[request URL]];

if (errorString && [errorString length]>0) {
NSLog(@"iTravel Error ",errorString);
[caller performSelectorOnMainThread: onFailure withObject:errorString waitUntilDone:NO];
}
else
[caller performSelectorOnMainThread: onSuccess withObject:responseString waitUntilDone:YES];
[pool release];
}
return TRUE;
}

+(NSString *) doSynchronizedGet:(NSString *)urlString {
NSData *returnData = [HttpHelper doSynchronizedDataGet:urlString];
if (!returnData)
return [NSString stringWithFormat: @"Error: No data received from server",urlString];

NSString * responseString = [[[NSString alloc] initWithData: returnData encoding: NSUTF8StringEncoding] autorelease];
return responseString;
}

+(NSData *) doSynchronizedDataGet:(NSString *)urlString {
NSURLRequest *request = [self getURLRequestFor:urlString];
NSHTTPURLResponse *response=nil;
NSError *error=nil;
NSData *returnData = [NSURLConnection sendSynchronousRequest:request returningResponse:&response error:&error];

NSString *errorString=@"";
if (error)
errorString = [NSString stringWithFormat:@"Error loading page: %@", urlString, [error localizedDescription]];
else if (response==nil || returnData==nil)
errorString = [NSString stringWithFormat:@"Error - No response from URL %@", urlString];
else if (response.statusCode!=200)
errorString = [NSString stringWithFormat:@"Error - HTTP Error %i from URL %@", response.statusCode, urlString];

if ([errorString length]>0)
return [errorString dataUsingEncoding:NSUTF8StringEncoding];

return returnData;
}

+(NSString *) doSynchronizedPostTo:(NSString *)urlString withKeys:(NSArray*)postKeys andValues:(NSArray*)postValues {
NSURLRequest *request = [self buildRequestWithPostKeys:postKeys postValues:postValues urlString:urlString];
NSHTTPURLResponse *response=nil;
NSError *error=nil;
NSData *returnData = [NSURLConnection sendSynchronousRequest:request returningResponse:&response error:&error];

NSString *errorString=@"";
if (error)
errorString = [NSString stringWithFormat:@"Error - HTTP Error posting to URL %@: %@", urlString, [error localizedDescription]];
else if (response==nil || returnData==nil)
errorString = [NSString stringWithFormat:@"Error - No response from URL %@", urlString];
else if (response.statusCode!=200)
errorString = [NSString stringWithFormat:@"Error - HTTP Error %i from URL %@", response.statusCode, urlString];

if ([errorString length]>0)
return errorString;

NSString * responseString = [[[NSString alloc] initWithData: returnData encoding: NSUTF8StringEncoding] autorelease];

if ([[responseString lowercaseString] hasPrefix:@"error"])
errorString = [NSString stringWithFormat:@"Error message %@ from URL %@",responseString, urlString];
else if ([[responseString lowercaseString] hasPrefix:@"fail"])
errorString = [NSString stringWithFormat:@"Error - Failure message '%@' when posting to URL %@", responseString, urlString];

if ([errorString length]>0)
return errorString;

return nil; //indicates success
}

@end


How To Use It

The three chief asynchronous interface methods are, for a GET -


+(BOOL) doGet:(NSString *)urlString forCaller:(id)caller onSuccess:(SEL)onSuccess onFailure:(SEL)onFailure


and for a POST -


+(NSURLRequest*) buildRequestWithPostKeys:(NSArray *) postKeys postValues:(NSArray *) postValues urlString:(NSString *)urlString

+(BOOL) doPost:(NSURLRequest *)request forCaller:(id)caller onSuccess:(SEL)onSuccess onFailure:(SEL)onFailure {


I'll give you an example of the former first, as it's easier. The call itself is perfectly straightforward:


NSMutableString *urlString = [NSMutableString stringWithCapacity:128];
[urlString appendString:[Util getSearchPageURL]];
[urlString appendString:@"?locale="];
[urlString appendString:[UserSettings getLanguage]];
[urlString appendString:@"&searchTerms="];
[urlString appendString:[searchBar.text stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]];
[HttpHelper doGet:urlString forCaller:self onSuccess:@selector(parseSearchResults:) onFailure:@selector(searchFailed:)];


Not, however, the two "@selector" arguments. These must be methods on the caller, and they better expect an NSString* and an NSError*, respectively, as arguments. (Or they can take an NSObject* and cast from there, I suppose, but why bother, right?)

Inside HttpHelper we have to wrap the selectors in NSValues to pass them from method to method, which is a bit annoying, but hey, we only have to do it once.

To call a POST, by comparison:

NSArray *postKeys = [NSArray arrayWithObjects:@"title", @"location", @"comments", @"pageUri", @"sectionName", @"sectionNumber", @"listingName", nil];
NSArray *postValues = [NSArray arrayWithObjects:note.title, [note locationString], note.body, note.pageUri, note.sectionName, note.sectionNumber, note.listingName, nil];
NSURLRequest *request = [HttpHelper buildRequestWithPostKeys:postKeys postValues:postValues urlString:[Util getUploadNoteURL]];
[HttpHelper doPost:request forCaller:self onSuccess:@selector(onUploadSuccess) onFailure:@selector(onUploadError:)];


Note that "onUploadSuccess" here doesn't take an argument - I don't care about the web site's response, the fact of success is all that matters. OK, dubious wisdom that, but the example is relevant to show that the selector methods don't actually have to accept arguments.

We launch a new thread every time we call these methods, so we have to be careful lest we run into concurrency problems. So we synchronize the actual HTTP calls in the "doHttp:" method, which is the one place in the app where we actually go out and connect to the big bad scary Internet.

The synchronous methods are simple enough that I don't think they require examples:


+(NSString *) doSynchronizedGet:(NSString *)urlString;
+(NSData *) doSynchronizedDataGet:(NSString *)urlString;
+(NSString *) doSynchronizedPostTo:(NSString *)urlString withKeys:(NSArray*)postKeys andValues:(NSArray*)postValues;


The DataGet is in case you want NSData instead of an NSString - if you're loading an image, for instance.

This page is powered by Blogger. Isn't yours?

Subscribe to Posts [Atom]