Skip to content

Instantly share code, notes, and snippets.

@yelirekim
Created November 16, 2010 19:32
Show Gist options
  • Save yelirekim/702339 to your computer and use it in GitHub Desktop.
Save yelirekim/702339 to your computer and use it in GitHub Desktop.
//
// JSONRPC.m
// Sponduu
//
// Created by DO on 5/18/10.
// Copyright 2010 Digital Operative Inc.. All rights reserved.
//
#import "JSONRPC.h"
#import "SponduuStatic.h"
#import "SBJSON.h"
#import "doutil.h"
#import "UserConfiguration.h"
#import "crypto.h"
@implementation JSONRPC
#pragma mark -
#pragma mark Properties
@synthesize delegate, showActivityIndicator, timeoutTracker, connection, data;
- (BOOL)isActive
{
return requestIsActive;
}
#pragma mark -
#pragma mark Initializers
/*
* Bare initialization, makes the assumption that you're going to assign the delegate separately.
*
*/
- (id)init
{
self = [super init];
if(self)
{
self.delegate = self;
self.showActivityIndicator = YES;
self.timeoutTracker = nil;
self.connection = nil;
self.data = nil;
}
return self;
}
- (id)initWithDelegate:(id)reciever
{
self = [self init];
if(self)
{
self.delegate = reciever;
}
return self;
}
#pragma mark -
#pragma mark Invocation Methods
/*
* Puts the current instance into "operating mode" where the status bar activity indicator is working, and no additional requests can be
* made until this object is put back into "inactive mode".
*
*/
- (void)startActivity
{
l(@"starting activity");
requestIsActive = YES;
if(showActivityIndicator)
{
[[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:YES];
}
}
- (void)endActivity
{
if(self.timeoutTracker)
{
[timeoutTracker invalidate];
}
if(connection)
{
[connection cancel];
}
[doutil releaseIfExists:connection];
[doutil releaseIfExists:data];
requestIsActive = NO;
[[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:NO];
}
- (void)invokeError:(NSError *)error
{
l(@"failure invoked");
[delegate rpcFailure:self error:error];
[self endActivity];
}
/*
* Called from any context that would indicate this instance has successfully performed an RPC call, sets the object to "inactive mode" and
* passes the decoded response to the delegate.
*
*/
- (void)invokeSuccess:(NSDictionary *)response
{
l(@"success invoked");
[delegate rpcSuccess:self response:response];
[self endActivity];
}
/*
* Cancels the request and sends a timeout error to the delegate
*
*/
- (void)timeoutReached:(NSTimer *)invoker
{
[connection cancel];
[self invokeError:[NSError errorWithDomain:@"JSONRPC" code:JSONRPC_TIMEOUT_ERROR userInfo:nil]];
}
#pragma mark -
#pragma mark Usage Methods
- (BOOL)activateConnectionWithRequest:(NSMutableURLRequest *)request
{
l(@"activating");
if(self.isActive)
{
l(@"already active");
return NO;
}
[self startActivity];
self.data = [[NSMutableData alloc] init];
self.connection = [[NSURLConnection alloc] initWithRequest:request delegate:self];
if(connection)
{
//https://devforums.apple.com/message/108087 - this is ugly but actually their suggested solution to a bug in NSMutableURLRequest
self.timeoutTracker = [NSTimer scheduledTimerWithTimeInterval:[(NSNumber *)
[SponduuStatic objectForKey:@"RequestTimeoutLength"] doubleValue]
target:self
selector:@selector(timeoutReached:)
userInfo:nil
repeats:NO];
return YES;
}
[self endActivity];
return NO;
}
/*
* We have to preemptively set our credentials here, essentially nullifying any use we could get out of the authentication challenge
* and response chain, or NSURLProtectionSpace, or NSURLCredentialStorage. This is a limitation of the way that NSURLConnection works
* and you can't subclass it in order to override this because doing so would require you to message private APIs. It's unnaceptable
* to effectively double the length of a request in order to allow didRecieveAuthenticationChallenge: to work its magic, instead we
* are going to force authentication every single time that credentials are available. Oh yeah, and there is no ability to base64
* encode things either so we have to provide that ourselves.
*
* tldr; You can't automatically use the built in classes to preemptively authenticate a user.
*
*/
- (void)setCustomRequestHeaders:(NSMutableURLRequest *)request
{
// !!!: HTTP Basic Fiasco
if([UserConfiguration urlCredential] != nil)
{
[request setValue:[NSString stringWithFormat:@"Basic %@", [[UserConfiguration urlCredential] basicAuthenticationString]]
forHTTPHeaderField:@"Authorization"];
}
if([UserConfiguration currentLocation] != nil)
{
CLLocation * userLocation = [UserConfiguration currentLocation];
[request setValue:[NSString stringWithFormat:@"%f", userLocation.coordinate.latitude] forHTTPHeaderField:@"SDUlatitude"];
[request setValue:[NSString stringWithFormat:@"%f", userLocation.coordinate.longitude] forHTTPHeaderField:@"SDUlongitude"];
}
//custom headers to expose a bit more information to our web service about who we are
[request setValue:[crypto base64encode:[[UserConfiguration uuid] dataUsingEncoding:NSASCIIStringEncoding]]
forHTTPHeaderField:@"IOSUUID"];
[request setValue:[SponduuStatic stringForKey:@"ApiVersion"]
forHTTPHeaderField:@"Sponduu"];
}
- (BOOL)requestWithMethod:(NSString *)method parameters:(NSArray *)parameters
{
l(method);
l(@"requesting");
//create a dictionary of the correct format for a JSON-RPC 2.0 request
NSArray *jsonRpcRequestFormat = [NSDictionary dictionaryWithObjectsAndKeys:
@"2.0", @"jsonrpc",
method, @"method",
parameters, @"params",
@"0", @"id",
nil];
//Create a new request to the server using the URL defined in SponduuStatic
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[SponduuStatic rpcEndpoint]];
[request setHTTPShouldHandleCookies:NO];
//prepare the request dictionary to be submitted as NSData, which we can set as the server POST header
NSData *jsonDataRequest = [[jsonRpcRequestFormat JSONRepresentation] dataUsingEncoding:NSUTF8StringEncoding];
//set the headers for an HTTP request, in the case of JSON-RPC 2.0, the entire request consists of a POST header containing JSON
[request setValue:[[NSNumber numberWithInt:[jsonDataRequest length]] stringValue] forHTTPHeaderField:@"Content-Length"];
[request setValue:@"application/json" forHTTPHeaderField:@"Content-Type"];
[request setHTTPMethod:@"POST"];
[request setHTTPBody:jsonDataRequest];
[self setCustomRequestHeaders:request];
return [self activateConnectionWithRequest:request];
}
#pragma mark -
#pragma mark NSURLConnection delegate methods
/*
* Messaged when the URL connection currently being managed initially recieves a response, zeroes the data in it just in case.
*
*/
- (void)connection:(NSURLConnection *)aConnection didReceiveResponse:(NSURLResponse *)response
{
[data setLength:0];
}
/*
* Messaged when the URL connection currently being managed recieves data, this can happen several times over the lifetime of a connection
* and it's assured that sequential calls to the method recieve sequential portions of the data. We are just appending each piece to the
* NSData object we have so that when all is said and done, we'll have the complete response in one place.
*
*/
- (void)connection:(NSURLConnection *)aConnection didReceiveData:(NSData *)newData
{
[data appendData:newData];
}
/*
* Messaged when an error occurs (network or otherwise) while attempting to complete a URL connection request.
*
*/
- (void)connection:(NSURLConnection *)aConnection didFailWithError:(NSError *)error
{
[self invokeError:error];
}
/*
* This method can be subclassed but MUST result in an invocation of success or failure
*
*/
- (void)assertInvocationForResult:(id)responseObject
{
if(![responseObject isKindOfClass:[NSDictionary class]])
{
responseObject = nil;
}
NSDictionary * responseDictionary = (NSDictionary *)[NSDictionary dictionaryWithDictionary:responseObject];
//TODO: add stricter requirements for confirming that this is actually a service map
if([responseDictionary objectForKey:@"services"])
{
[self invokeSuccess:responseDictionary];
return;
}
NSDictionary * responseDictionaryError = [responseDictionary objectForKey:@"error"];
//if the error portion of the response is set, or the dictionary is null, generate an error
if([doutil isEmpty:responseDictionary] || ![doutil isEmpty:responseDictionaryError] || responseObject == nil)
{
NSMutableDictionary *errorInfo = [NSMutableDictionary dictionary];
int errorCode = [doutil isEmpty:responseDictionaryError] ?
JSONRPC_PARSE_ERROR : [[responseDictionaryError objectForKey:@"code"] intValue];
//we can provide a little better context for the error by including the full response if we did in fact parse the response
if(![doutil isEmpty:responseDictionary])
{
[errorInfo setValue:responseDictionary forKey:@"response"];
}
if(![doutil isEmpty:responseDictionaryError])
{
[errorInfo setValue:[responseDictionaryError objectForKey:@"message"] forKey:NSLocalizedDescriptionKey];
}
[self invokeError:[NSError errorWithDomain:@"JSONRPC" code:errorCode userInfo:errorInfo]];
return;
}
//invoke success with the parsed response
[self invokeSuccess:responseDictionary];
}
/*
* Messaged when the connection has finished loading, attempts to parse the response and verify it is a JSON RPC call
* //TODO: verify that the response is actually a valid JSON RPC call.. right now it just assumes it got a correct response back if it's
* valid JSON
*
*/
- (void)connectionDidFinishLoading:(NSURLConnection *)aConnection
{
//turn the response from data to a string (which hopefully contains JSON)
NSString * responseString = [[NSString alloc]initWithData:data encoding:NSUTF8StringEncoding];
l(@"finished loading");
l(responseString);
//This will result in null being put into the dictionary if the parse fails, which we check below
id responseObject = [responseString JSONValue];
[responseString release];
l(@"calling invocation");
[self assertInvocationForResult:responseObject];
}
- (BOOL)connection:(NSURLConnection *)connection canAuthenticateAgainstProtectionSpace:(NSURLProtectionSpace *)protectionSpace
{
return YES;
}
#pragma mark -
#pragma mark Implicit Delegate Support
/*
* This just allows us to quickly make RPC requests without the need to set up a delegate for the purpose of debugging, IE:
*
* [[[JSONRPC alloc] init]] discoverMethods]
*
* Will log the discovered methods (or error) to the console once the request completes.
*
*/
- (BOOL)discoverMethods
{
NSMutableURLRequest * urlRequest = [NSMutableURLRequest requestWithURL:[SponduuStatic rpcEndpoint]];
[self setCustomRequestHeaders:urlRequest];
return [self activateConnectionWithRequest:urlRequest];
}
- (void)rpcFailure:(JSONRPC *)jsonrpc error:(NSError *)error
{
l(error);
}
- (void)rpcSuccess:(JSONRPC *)jsonrpc response:(NSDictionary *)response
{
l(response);
}
#pragma mark -
#pragma mark Memory Management
/*
* Since this object could have a running NSTimer out there when it gets dealloced, we need to account for that and make sure it doesn't
* try to message this instance after its gone. We also might have the activity indicator spinning if we're in the middle of a request,
* luckily we already have a method that takes care of this for us, and also releases the data / connection if they are set.
*
*/
- (void)dealloc
{
[self endActivity];
[timeoutTracker release];
[super dealloc];
}
@end //JSONRPC implementation
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment