Initial commit
This commit is contained in:
@@ -0,0 +1,361 @@
|
||||
//
|
||||
// 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<NSObject>)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
|
||||
Reference in New Issue
Block a user