// // JSONModelHTTPClient.m // JSONModel // #import "JSONHTTPClient.h" #pragma GCC diagnostic ignored "-Wdeprecated-declarations" #pragma GCC diagnostic ignored "-Wdeprecated-implementations" typedef void (^RequestResultBlock)(NSData *data, JSONModelError *error); #pragma mark - constants NSString* const kHTTPMethodGET = @"GET"; NSString* const kHTTPMethodPOST = @"POST"; NSString* const kContentTypeAutomatic = @"jsonmodel/automatic"; NSString* const kContentTypeJSON = @"application/json"; NSString* const kContentTypeWWWEncoded = @"application/x-www-form-urlencoded"; #pragma mark - static variables /** * Defaults for HTTP requests */ static NSStringEncoding defaultTextEncoding = NSUTF8StringEncoding; static NSURLRequestCachePolicy defaultCachePolicy = NSURLRequestReloadIgnoringLocalCacheData; static int defaultTimeoutInSeconds = 60; /** * Custom HTTP headers to send over with *each* request */ static NSMutableDictionary* requestHeaders = nil; /** * Default request content type */ static NSString* requestContentType = nil; #pragma mark - implementation @implementation JSONHTTPClient #pragma mark - initialization +(void)initialize { static dispatch_once_t once; dispatch_once(&once, ^{ requestHeaders = [NSMutableDictionary dictionary]; requestContentType = kContentTypeAutomatic; }); } #pragma mark - configuration methods +(NSMutableDictionary*)requestHeaders { return requestHeaders; } +(void)setDefaultTextEncoding:(NSStringEncoding)encoding { defaultTextEncoding = encoding; } +(void)setCachingPolicy:(NSURLRequestCachePolicy)policy { defaultCachePolicy = policy; } +(void)setTimeoutInSeconds:(int)seconds { defaultTimeoutInSeconds = seconds; } +(void)setRequestContentType:(NSString*)contentTypeString { requestContentType = contentTypeString; } #pragma mark - helper methods +(NSString*)contentTypeForRequestString:(NSString*)requestString { //fetch the charset name from the default string encoding NSString* contentType = requestContentType; if (requestString.length>0 && [contentType isEqualToString:kContentTypeAutomatic]) { //check for "eventual" JSON array or dictionary NSString* firstAndLastChar = [NSString stringWithFormat:@"%@%@", [requestString substringToIndex:1], [requestString substringFromIndex: requestString.length -1] ]; if ([firstAndLastChar isEqualToString:@"{}"] || [firstAndLastChar isEqualToString:@"[]"]) { //guessing for a JSON request contentType = kContentTypeJSON; } else { //fallback to www form encoded params contentType = kContentTypeWWWEncoded; } } //type is set, just add charset NSString *charset = (NSString *)CFStringConvertEncodingToIANACharSetName(CFStringConvertNSStringEncodingToEncoding(NSUTF8StringEncoding)); return [NSString stringWithFormat:@"%@; charset=%@", contentType, charset]; } +(NSString*)urlEncode:(id)value { //make sure param is a string if ([value isKindOfClass:[NSNumber class]]) { value = [(NSNumber*)value stringValue]; } NSAssert([value isKindOfClass:[NSString class]], @"request parameters can be only of NSString or NSNumber classes. '%@' is of class %@.", value, [value class]); NSString *str = (NSString *)value; #if __IPHONE_OS_VERSION_MIN_REQUIRED >= __IPHONE_7_0 || __MAC_OS_X_VERSION_MIN_REQUIRED >= __MAC_10_9 return [str stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]]; #else return (NSString *)CFBridgingRelease(CFURLCreateStringByAddingPercentEscapes( NULL, (__bridge CFStringRef)str, NULL, (CFStringRef)@"!*'();:@&=+$,/?%#[]", kCFStringEncodingUTF8)); #endif } #pragma mark - networking worker methods +(void)requestDataFromURL:(NSURL*)url method:(NSString*)method requestBody:(NSData*)bodyData headers:(NSDictionary*)headers handler:(RequestResultBlock)handler { NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL: url cachePolicy: defaultCachePolicy timeoutInterval: defaultTimeoutInSeconds]; [request setHTTPMethod:method]; if ([requestContentType isEqualToString:kContentTypeAutomatic]) { //automatic content type if (bodyData) { NSString *bodyString = [[NSString alloc] initWithData:bodyData encoding:NSUTF8StringEncoding]; [request setValue: [self contentTypeForRequestString: bodyString] forHTTPHeaderField:@"Content-type"]; } } else { //user set content type [request setValue: requestContentType forHTTPHeaderField:@"Content-type"]; } //add all the custom headers defined for (NSString* key in [requestHeaders allKeys]) { [request setValue:requestHeaders[key] forHTTPHeaderField:key]; } //add the custom headers for (NSString* key in [headers allKeys]) { [request setValue:headers[key] forHTTPHeaderField:key]; } if (bodyData) { [request setHTTPBody: bodyData]; [request setValue:[NSString stringWithFormat:@"%lu", (unsigned long)bodyData.length] forHTTPHeaderField:@"Content-Length"]; } void (^completionHandler)(NSData *, NSURLResponse *, NSError *) = ^(NSData *data, NSURLResponse *origResponse, NSError *origError) { NSHTTPURLResponse *response = (NSHTTPURLResponse *)origResponse; JSONModelError *error = nil; //convert an NSError to a JSONModelError if (origError) { error = [JSONModelError errorWithDomain:origError.domain code:origError.code userInfo:origError.userInfo]; } //special case for http error code 401 if (error.code == NSURLErrorUserCancelledAuthentication) { response = [[NSHTTPURLResponse alloc] initWithURL:url statusCode:401 HTTPVersion:@"HTTP/1.1" headerFields:@{}]; } //if not OK status set the err to a JSONModelError instance if (!error && (response.statusCode >= 300 || response.statusCode < 200)) { error = [JSONModelError errorBadResponse]; } //if there was an error, assign the response to the JSONModel instance if (error) { error.httpResponse = [response copy]; } //empty respone, return nil instead if (!data.length) { data = nil; } handler(data, error); }; //fire the request #if __IPHONE_OS_VERSION_MIN_REQUIRED >= __IPHONE_7_0 || __MAC_OS_X_VERSION_MIN_REQUIRED >= __MAC_10_10 NSURLSessionTask *task = [[NSURLSession sharedSession] dataTaskWithRequest:request completionHandler:completionHandler]; [task resume]; #else NSOperationQueue *queue = [NSOperationQueue new]; [NSURLConnection sendAsynchronousRequest:request queue:queue completionHandler:^(NSURLResponse *response, NSData *data, NSError *error) { completionHandler(data, response, error); }]; #endif } +(void)requestDataFromURL:(NSURL*)url method:(NSString*)method params:(NSDictionary*)params headers:(NSDictionary*)headers handler:(RequestResultBlock)handler { //create the request body NSMutableString* paramsString = nil; if (params) { //build a simple url encoded param string paramsString = [NSMutableString stringWithString:@""]; for (NSString* key in [[params allKeys] sortedArrayUsingSelector:@selector(compare:)]) { [paramsString appendFormat:@"%@=%@&", key, [self urlEncode:params[key]] ]; } if ([paramsString hasSuffix:@"&"]) { paramsString = [[NSMutableString alloc] initWithString: [paramsString substringToIndex: paramsString.length-1]]; } } //set the request params if ([method isEqualToString:kHTTPMethodGET] && params) { //add GET params to the query string url = [NSURL URLWithString:[NSString stringWithFormat: @"%@%@%@", [url absoluteString], [url query] ? @"&" : @"?", paramsString ]]; } //call the more general synq request method [self requestDataFromURL: url method: method requestBody: [method isEqualToString:kHTTPMethodPOST]?[paramsString dataUsingEncoding:NSUTF8StringEncoding]:nil headers: headers handler:handler]; } #pragma mark - Async network request +(void)JSONFromURLWithString:(NSString*)urlString method:(NSString*)method params:(NSDictionary*)params orBodyString:(NSString*)bodyString completion:(JSONObjectBlock)completeBlock { [self JSONFromURLWithString:urlString method:method params:params orBodyString:bodyString headers:nil completion:completeBlock]; } +(void)JSONFromURLWithString:(NSString *)urlString method:(NSString *)method params:(NSDictionary *)params orBodyString:(NSString *)bodyString headers:(NSDictionary *)headers completion:(JSONObjectBlock)completeBlock { [self JSONFromURLWithString:urlString method:method params:params orBodyData:[bodyString dataUsingEncoding:NSUTF8StringEncoding] headers:headers completion:completeBlock]; } +(void)JSONFromURLWithString:(NSString*)urlString method:(NSString*)method params:(NSDictionary *)params orBodyData:(NSData*)bodyData headers:(NSDictionary*)headers completion:(JSONObjectBlock)completeBlock { RequestResultBlock handler = ^(NSData *responseData, JSONModelError *error) { id jsonObject = nil; //step 3: if there's no response so far, return a basic error if (!responseData && !error) { //check for false response, but no network error error = [JSONModelError errorBadResponse]; } //step 4: if there's a response at this and no errors, convert to object if (error==nil) { // Note: it is possible to have a valid response with empty response data (204 No Content). // So only create the JSON object if there is some response data. if(responseData.length > 0) { //convert to an object jsonObject = [NSJSONSerialization JSONObjectWithData:responseData options:kNilOptions error:&error]; } } //step 4.5: cover an edge case in which meaningful content is return along an error HTTP status code else if (error && responseData && jsonObject==nil) { //try to get the JSON object, while preserving the original error object jsonObject = [NSJSONSerialization JSONObjectWithData:responseData options:kNilOptions error:nil]; //keep responseData just in case it contains error information error.responseData = responseData; } //step 5: invoke the complete block dispatch_async(dispatch_get_main_queue(), ^{ if (completeBlock) { completeBlock(jsonObject, error); } }); }; NSURL *url = [NSURL URLWithString:urlString]; if (bodyData) { [self requestDataFromURL:url method:method requestBody:bodyData headers:headers handler:handler]; } else { [self requestDataFromURL:url method:method params:params headers:headers handler:handler]; } } #pragma mark - request aliases +(void)getJSONFromURLWithString:(NSString*)urlString completion:(JSONObjectBlock)completeBlock { [self JSONFromURLWithString:urlString method:kHTTPMethodGET params:nil orBodyString:nil completion:^(id json, JSONModelError* e) { if (completeBlock) completeBlock(json, e); }]; } +(void)getJSONFromURLWithString:(NSString*)urlString params:(NSDictionary*)params completion:(JSONObjectBlock)completeBlock { [self JSONFromURLWithString:urlString method:kHTTPMethodGET params:params orBodyString:nil completion:^(id json, JSONModelError* e) { if (completeBlock) completeBlock(json, e); }]; } +(void)postJSONFromURLWithString:(NSString*)urlString params:(NSDictionary*)params completion:(JSONObjectBlock)completeBlock { [self JSONFromURLWithString:urlString method:kHTTPMethodPOST params:params orBodyString:nil completion:^(id json, JSONModelError* e) { if (completeBlock) completeBlock(json, e); }]; } +(void)postJSONFromURLWithString:(NSString*)urlString bodyString:(NSString*)bodyString completion:(JSONObjectBlock)completeBlock { [self JSONFromURLWithString:urlString method:kHTTPMethodPOST params:nil orBodyString:bodyString completion:^(id json, JSONModelError* e) { if (completeBlock) completeBlock(json, e); }]; } +(void)postJSONFromURLWithString:(NSString*)urlString bodyData:(NSData*)bodyData completion:(JSONObjectBlock)completeBlock { [self JSONFromURLWithString:urlString method:kHTTPMethodPOST params:nil orBodyString:[[NSString alloc] initWithData:bodyData encoding:defaultTextEncoding] completion:^(id json, JSONModelError* e) { if (completeBlock) completeBlock(json, e); }]; } @end