Created
November 16, 2010 19:32
-
-
Save yelirekim/702339 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// 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