Saturday, November 20, 2010

 

HttpHelper for all, redux

Yeah, I know I said I wouldn't be posting much, but I've cleaned up and updated my HttpHelper class for one of my paid projects so that it supports cookies, form/multipart file uploads, non-ASCII characters, etc., so I thought I'd release it into the wild for y'all.

Apache License 2.0, as before.

The interface should be pretty self-evident from the .h file:


//
// HttpHelper.h
//
// Created by Jon Evans on 04/09/09.
// Copyright 2009 Jon Evans. Apache 2.0 license.
//

#import <Foundation/Foundation.h>

@interface HttpHelper : NSObject {
}

//Convenience methods
+(NSURLRequestCachePolicy)getCachePolicyFor:(NSString *)urlString;

//Cookie handling
+(void) useCookies:(BOOL)use;
+(NSArray*) latestCookies;
+(void) getCookiesFrom:(NSHTTPURLResponse*)response forURL:(NSURL*)url;

//HTTP Posts
+(NSURLRequest*) buildPostRequestForURL:(NSString *)urlString withData:(NSDictionary*) data;
+(NSURLRequest *) buildRequestWithPostKeys:(NSArray *) postKeys postValues:(NSArray *) postValues urlString:(NSString *)urlString;
+(BOOL) doPost:(NSURLRequest *)request forCaller:(id)caller onSuccess:(SEL)successSelector onFailure:(SEL)failSelector;

//HTTP Gets
+(NSURLRequest *)getURLRequestFor:(NSString *)urlString;
+(BOOL) doGet:(NSString *)urlString forCaller:(id)caller onSuccess:(SEL)successSelector onFailure:(SEL)failSelector;

//Synchronized accessors
+(NSString *) doSynchronizedGet:(NSString *)urlString;
+(NSData *) doSynchronizedDataGet:(NSString *)urlString;
+(NSString *) doSynchronizedPostTo:(NSString *)urlString withRequest:(NSURLRequest*)request;
+(NSString *) doSynchronizedPostTo:(NSString *)urlString withData:(NSDictionary*)data;
+(NSString *) doSynchronizedPostTo:(NSString *)urlString withKeys:(NSArray*)postKeys andValues:(NSArray*)postValues;

//Form/multipart (for file upload)
+(NSString *) doSynchronizedMultipartPostTo:(NSString*)urlString withData:(NSDictionary*)data fileKeys:(NSArray*)fileKeys;
+(NSString *) doSynchronizedMultipartPostTo:(NSString*)urlString withKeys:(NSArray*)keys andValues:(NSArray*)values fileKeys:(NSArray*)fileKeys;

@end


...and here's where the magic happens:


//
// HttpHelper.m
//
// Created by Jon Evans on 04/09/09.
// Copyright 2009 Jon Evans. Apache 2.0 license.
//

#import "HttpHelper.h"


@implementation HttpHelper

static HttpHelper *singleton=nil;
static NSArray *latestCookies=nil;
static BOOL useCookies=NO;


#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;
}

-(NSUInteger)retainCount {
return UINT_MAX;
}

-(void)release {
//pass
}

-(id)autorelease {
return self;
}

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


#pragma mark -
#pragma mark Convenience methods

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


#pragma mark -
#pragma mark Cookie handling

+(void) useCookies:(BOOL)use
{
useCookies=use;
}

+(void) getCookiesFrom:(NSHTTPURLResponse*)response forURL:(NSURL*)url {
if ([response class]==[NSHTTPURLResponse class])
latestCookies = [NSHTTPCookie cookiesWithResponseHeaderFields:[response allHeaderFields] forURL:url];
}

+(NSArray*) latestCookies {
return latestCookies;
}



#pragma mark -
#pragma mark HTTP Posts

+(NSURLRequest*) buildPostRequestForURL:(NSString *)urlString withData:(NSDictionary*) data
{
NSArray *keys = [data allKeys];
NSMutableArray *values = [NSMutableArray arrayWithCapacity:[keys count]];
for (NSString *key in keys) {
NSString *value = [data objectForKey:key];
[values addObject:(value ? [NSString stringWithFormat:@"%@",value] : @"")];
}
return [self buildRequestWithPostKeys:keys postValues:values urlString:urlString];
}

+(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];
[request setHTTPMethod:@"POST"];

if (useCookies && latestCookies!=nil && [latestCookies count]>0) {
NSDictionary * headers = [NSHTTPCookie requestHeaderFieldsWithCookies:latestCookies];
[request setAllHTTPHeaderFields:headers];
}

NSMutableData *paramData = [NSMutableData dataWithCapacity:8192];

NSMutableString *params=[[NSMutableString alloc] initWithCapacity:1024];
for (int i=0; i<[postValues count]; i++) {
NSString *param = i>0 ? @"&" : @"";
param = [param stringByAppendingFormat:@"%@=",[postKeys objectAtIndex:i]];
[paramData appendData:[param dataUsingEncoding:NSUTF8StringEncoding]];

NSObject* valueToPost = [postValues objectAtIndex:i];
NSString *stringToPost = [valueToPost class]==[NSString class] ? (NSString*)valueToPost : [NSString stringWithFormat:@"%@",valueToPost];
stringToPost = [stringToPost stringByReplacingOccurrencesOfString:@"&" withString:@"%26"];
[paramData appendData:[stringToPost 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 detachNewThreadSelector:@selector(doHttp:) toTarget:[self getInstance] withObject:args];
return TRUE;
}


#pragma mark -
#pragma mark HTTP Gets

+(NSURLRequest *)getURLRequestFor:(NSString *)urlString {
NSURL *url = [NSURL URLWithString:urlString];
NSURLRequestCachePolicy policy = [self getCachePolicyFor:urlString];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url cachePolicy:policy timeoutInterval:30.0];
[request setHTTPMethod:@"GET"];

if (useCookies && latestCookies!=nil && [latestCookies count]>0) {
NSDictionary * headers = [NSHTTPCookie requestHeaderFieldsWithCookies:latestCookies];
[request setAllHTTPHeaderFields:headers];
}
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;
}


#pragma mark -
#pragma mark Internals

-(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:@"Error %@ '%@': %@",NSLocalizedString(@"loading page",nil), [request URL], [error localizedDescription]];
else if (response==nil || returnData==nil)
errorString = [NSString stringWithFormat:@"Error - %@ %@",NSLocalizedString(@"No response from URL",nil), [request URL]];
else if (response.statusCode!=200)
errorString = [NSString stringWithFormat:@"Error - %@ %i %@ %@",NSLocalizedString(@"HTTP Error",nil), response.statusCode, NSLocalizedString(@"from URL",nil), [request URL]];

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


#pragma mark -
#pragma mark Synchronized methods

+(NSString *) doSynchronizedGet:(NSString *)urlString {
NSData *returnData = [HttpHelper doSynchronizedDataGet:urlString];
if (!returnData)
return [NSString stringWithFormat:@"Error: %@ %@",NSLocalizedString(@"No data received from server",nil), 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 %@ '%@': %@",NSLocalizedString(@"loading page",nil), urlString, [error localizedDescription]];
else if (response==nil || returnData==nil)
errorString = [NSString stringWithFormat:@"Error - %@ %@",NSLocalizedString(@"No response from URL",nil), urlString];
else if (response.statusCode!=200)
errorString = [NSString stringWithFormat:@"Error - %@ %i %@ %@",NSLocalizedString(@"HTTP Error",nil), response.statusCode, NSLocalizedString(@"from URL",nil), urlString];

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

if (useCookies)
[HttpHelper getCookiesFrom:response forURL:[request URL]];

return returnData;
}

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

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

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

if (useCookies)
[HttpHelper getCookiesFrom:response forURL:[request URL]];

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

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

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

return responseString;
}

+(NSString*) doSynchronizedMultipartPostTo:(NSString*)urlString withData:(NSDictionary*)data fileKeys:(NSArray*)fileKeys
{
NSArray *keys = [data allKeys];
NSMutableArray *values = [NSMutableArray arrayWithCapacity:[keys count]];
for (NSString *key in keys) {
NSString *value = [data objectForKey:key];
[values addObject:(value ? value : @"")];
}
return [self doSynchronizedMultipartPostTo:urlString withKeys:keys andValues:values fileKeys:fileKeys];
}

+(NSString *) doSynchronizedMultipartPostTo:(NSString*)urlString withKeys:(NSArray*)keys andValues:(NSArray*)values fileKeys:(NSArray*)fileKeys
{
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];
[request setHTTPMethod:@"POST"];

NSString *boundary = [[NSProcessInfo processInfo] globallyUniqueString];
[request setValue: [NSString stringWithFormat:@"multipart/form-data; boundary=%@", boundary] forHTTPHeaderField:@"Content-Type"];

NSMutableData *postBody = [NSMutableData dataWithCapacity:65536];
for (int i=0; i<[keys count]; i++) {
NSString *key = [keys objectAtIndex:i];
NSObject *value = [values objectAtIndex:i];

if (value && fileKeys && [fileKeys containsObject:key]) {
[postBody appendData: [[NSString stringWithFormat:@"--%@\r\n", boundary] dataUsingEncoding:NSUTF8StringEncoding]];
if ([value isKindOfClass:[NSString class]]) {
NSString *valueString = (NSString*)value;
if ([valueString length]>0) {
NSRange slashRange = [valueString rangeOfString:@"/" options:NSBackwardsSearch];
NSString *fileName = slashRange.location!=NSNotFound && ![valueString hasSuffix:@"/"]
? [valueString substringFromIndex:slashRange.location+1]
: valueString;
[postBody appendData: [[NSString stringWithFormat: @"Content-Disposition: form-data; name=\"%@\"; filename=\"%@\"\r\n", key, fileName] dataUsingEncoding:NSUTF8StringEncoding]];
[postBody appendData: [[NSString stringWithFormat: @"Content-Type: application/octet-stream\r\n\r\n"] dataUsingEncoding:NSUTF8StringEncoding]];
[postBody appendData: [NSData dataWithContentsOfMappedFile:valueString]];
}
}
else if ([value isKindOfClass:[NSURL class]]) {
NSURL *fileURL = (NSURL*)value;
[postBody appendData: [[NSString stringWithFormat: @"Content-Disposition: form-data; name=\"%@\"; filename=\"%@\"\r\n", key, [fileURL lastPathComponent]] dataUsingEncoding:NSUTF8StringEncoding]];
[postBody appendData: [[NSString stringWithFormat: @"Content-Type: application/octet-stream\r\n\r\n"] dataUsingEncoding:NSUTF8StringEncoding]];
[postBody appendData: [NSData dataWithContentsOfURL:fileURL]];
}
else if ([value isKindOfClass:[NSData class]]) {
[postBody appendData: [[NSString stringWithFormat: @"Content-Disposition: form-data; name=\"%@\"; filename=\"%@\"\r\n", key, key] dataUsingEncoding:NSUTF8StringEncoding]];
[postBody appendData: [[NSString stringWithFormat: @"Content-Type: application/octet-stream\r\n\r\n"] dataUsingEncoding:NSUTF8StringEncoding]];
[postBody appendData: (NSData*)value];
}
}
else {
[postBody appendData:[[NSString stringWithFormat: @"--%@\r\n", boundary] dataUsingEncoding:NSUTF8StringEncoding]];
[postBody appendData:[[NSString stringWithFormat: @"Content-Disposition: form-data; name=\"%@\"\r\n",key] dataUsingEncoding:NSUTF8StringEncoding]];
[postBody appendData: [[NSString stringWithFormat: @"Content-Type: text/plain;charset=utf-8\r\n\r\n"] dataUsingEncoding:NSUTF8StringEncoding]];
NSString *valueString = [NSString stringWithFormat: @"%@", value];
[postBody appendData:[valueString dataUsingEncoding:NSUTF8StringEncoding]];
}
[postBody appendData:[@"\r\n" dataUsingEncoding:NSUTF8StringEncoding]];
}
[postBody appendData:[[NSString stringWithFormat:@"--%@--\r\n", boundary] dataUsingEncoding:NSUTF8StringEncoding]];

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

NSHTTPURLResponse *response=nil;
NSError *error=nil;
NSData *returnData = [NSURLConnection sendSynchronousRequest:request returningResponse:&response error:&error];

NSString *errorString=@"";
if (error)
errorString = [NSString stringWithFormat:@"Error - %@ '%@': %@",NSLocalizedString(@"HTTP Error posting to URL",nil), urlString, [error localizedDescription]];
else if (response==nil || returnData==nil)
errorString = [NSString stringWithFormat:@"Error - %@ %@",NSLocalizedString(@"No response from URL",nil), urlString];
else if (response.statusCode!=200)
errorString = [NSString stringWithFormat:@"Error - %@ %i %@ %@",NSLocalizedString(@"HTTP Error",nil), response.statusCode, NSLocalizedString(@"from URL",nil), 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 %@ %@ %@ %@",NSLocalizedString(@"message",nil), responseString, NSLocalizedString(@"from URL",nil), urlString];
else if ([[responseString lowercaseString] hasPrefix:@"fail"])
errorString = [NSString stringWithFormat:@"Error - %@ '%@' %@ %@",NSLocalizedString(@"Failure message",nil), responseString, NSLocalizedString(@"when posting to URL",nil), urlString];

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

if (useCookies)
[HttpHelper getCookiesFrom:response forURL:[request URL]];
return responseString;
}


@end

Friday, November 5, 2010

 

Pronoid Android sleeps

As you may have noticed, if your eyes for Nothing are as keen as Alice's, there hasn't been a whole lot of activity 'round here of late; that's because I've been up to my nose in paying app work, most of which I can't write about in public for obvious non-disclosure-agreement reasons. But I hope to find some pet-project time come the new year, so check back in then -

Jon

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

Subscribe to Posts [Atom]