JSONHTTPClient.m 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369
  1. //
  2. // JSONModelHTTPClient.m
  3. //
  4. // @version 1.2
  5. // @author Marin Todorov (http://www.underplot.com) and contributors
  6. //
  7. // Copyright (c) 2012-2015 Marin Todorov, Underplot ltd.
  8. // This code is distributed under the terms and conditions of the MIT license.
  9. //
  10. // Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
  11. // The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
  12. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
  13. //
  14. #import "JSONHTTPClient.h"
  15. typedef void (^RequestResultBlock)(NSData *data, JSONModelError *error);
  16. #pragma mark - constants
  17. NSString* const kHTTPMethodGET = @"GET";
  18. NSString* const kHTTPMethodPOST = @"POST";
  19. NSString* const kContentTypeAutomatic = @"jsonmodel/automatic";
  20. NSString* const kContentTypeJSON = @"application/json";
  21. NSString* const kContentTypeWWWEncoded = @"application/x-www-form-urlencoded";
  22. #pragma mark - static variables
  23. /**
  24. * Defaults for HTTP requests
  25. */
  26. static NSStringEncoding defaultTextEncoding = NSUTF8StringEncoding;
  27. static NSURLRequestCachePolicy defaultCachePolicy = NSURLRequestReloadIgnoringLocalCacheData;
  28. static int defaultTimeoutInSeconds = 60;
  29. /**
  30. * Custom HTTP headers to send over with *each* request
  31. */
  32. static NSMutableDictionary* requestHeaders = nil;
  33. /**
  34. * Default request content type
  35. */
  36. static NSString* requestContentType = nil;
  37. #pragma mark - implementation
  38. @implementation JSONHTTPClient
  39. #pragma mark - initialization
  40. +(void)initialize
  41. {
  42. static dispatch_once_t once;
  43. dispatch_once(&once, ^{
  44. requestHeaders = [NSMutableDictionary dictionary];
  45. requestContentType = kContentTypeAutomatic;
  46. });
  47. }
  48. #pragma mark - configuration methods
  49. +(NSMutableDictionary*)requestHeaders
  50. {
  51. return requestHeaders;
  52. }
  53. +(void)setDefaultTextEncoding:(NSStringEncoding)encoding
  54. {
  55. defaultTextEncoding = encoding;
  56. }
  57. +(void)setCachingPolicy:(NSURLRequestCachePolicy)policy
  58. {
  59. defaultCachePolicy = policy;
  60. }
  61. +(void)setTimeoutInSeconds:(int)seconds
  62. {
  63. defaultTimeoutInSeconds = seconds;
  64. }
  65. +(void)setRequestContentType:(NSString*)contentTypeString
  66. {
  67. requestContentType = contentTypeString;
  68. }
  69. #pragma mark - helper methods
  70. +(NSString*)contentTypeForRequestString:(NSString*)requestString
  71. {
  72. //fetch the charset name from the default string encoding
  73. NSString* contentType = requestContentType;
  74. if (requestString.length>0 && [contentType isEqualToString:kContentTypeAutomatic]) {
  75. //check for "eventual" JSON array or dictionary
  76. NSString* firstAndLastChar = [NSString stringWithFormat:@"%@%@",
  77. [requestString substringToIndex:1],
  78. [requestString substringFromIndex: requestString.length -1]
  79. ];
  80. if ([firstAndLastChar isEqualToString:@"{}"] || [firstAndLastChar isEqualToString:@"[]"]) {
  81. //guessing for a JSON request
  82. contentType = kContentTypeJSON;
  83. } else {
  84. //fallback to www form encoded params
  85. contentType = kContentTypeWWWEncoded;
  86. }
  87. }
  88. //type is set, just add charset
  89. NSString *charset = (NSString *)CFStringConvertEncodingToIANACharSetName(CFStringConvertNSStringEncodingToEncoding(NSUTF8StringEncoding));
  90. return [NSString stringWithFormat:@"%@; charset=%@", contentType, charset];
  91. }
  92. +(NSString*)urlEncode:(id<NSObject>)value
  93. {
  94. //make sure param is a string
  95. if ([value isKindOfClass:[NSNumber class]]) {
  96. value = [(NSNumber*)value stringValue];
  97. }
  98. NSAssert([value isKindOfClass:[NSString class]], @"request parameters can be only of NSString or NSNumber classes. '%@' is of class %@.", value, [value class]);
  99. NSString *str = (NSString *)value;
  100. #if __IPHONE_OS_VERSION_MIN_REQUIRED >= __IPHONE_7_0 || __MAC_OS_X_VERSION_MIN_REQUIRED >= __MAC_10_9
  101. return [str stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]];
  102. #else
  103. return (NSString *)CFBridgingRelease(CFURLCreateStringByAddingPercentEscapes(
  104. NULL,
  105. (__bridge CFStringRef)str,
  106. NULL,
  107. (CFStringRef)@"!*'();:@&=+$,/?%#[]",
  108. kCFStringEncodingUTF8));
  109. #endif
  110. }
  111. #pragma mark - networking worker methods
  112. +(void)requestDataFromURL:(NSURL*)url method:(NSString*)method requestBody:(NSData*)bodyData headers:(NSDictionary*)headers handler:(RequestResultBlock)handler
  113. {
  114. NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL: url
  115. cachePolicy: defaultCachePolicy
  116. timeoutInterval: defaultTimeoutInSeconds];
  117. [request setHTTPMethod:method];
  118. if ([requestContentType isEqualToString:kContentTypeAutomatic]) {
  119. //automatic content type
  120. if (bodyData) {
  121. NSString *bodyString = [[NSString alloc] initWithData:bodyData encoding:NSUTF8StringEncoding];
  122. [request setValue: [self contentTypeForRequestString: bodyString] forHTTPHeaderField:@"Content-type"];
  123. }
  124. } else {
  125. //user set content type
  126. [request setValue: requestContentType forHTTPHeaderField:@"Content-type"];
  127. }
  128. //add all the custom headers defined
  129. for (NSString* key in [requestHeaders allKeys]) {
  130. [request setValue:requestHeaders[key] forHTTPHeaderField:key];
  131. }
  132. //add the custom headers
  133. for (NSString* key in [headers allKeys]) {
  134. [request setValue:headers[key] forHTTPHeaderField:key];
  135. }
  136. if (bodyData) {
  137. [request setHTTPBody: bodyData];
  138. [request setValue:[NSString stringWithFormat:@"%lu", (unsigned long)bodyData.length] forHTTPHeaderField:@"Content-Length"];
  139. }
  140. void (^completionHandler)(NSData *, NSURLResponse *, NSError *) = ^(NSData *data, NSURLResponse *origResponse, NSError *origError) {
  141. NSHTTPURLResponse *response = (NSHTTPURLResponse *)origResponse;
  142. JSONModelError *error = nil;
  143. //convert an NSError to a JSONModelError
  144. if (origError) {
  145. error = [JSONModelError errorWithDomain:origError.domain code:origError.code userInfo:origError.userInfo];
  146. }
  147. //special case for http error code 401
  148. if (error.code == NSURLErrorUserCancelledAuthentication) {
  149. response = [[NSHTTPURLResponse alloc] initWithURL:url statusCode:401 HTTPVersion:@"HTTP/1.1" headerFields:@{}];
  150. }
  151. //if not OK status set the err to a JSONModelError instance
  152. if (!error && (response.statusCode >= 300 || response.statusCode < 200)) {
  153. error = [JSONModelError errorBadResponse];
  154. }
  155. //if there was an error, assign the response to the JSONModel instance
  156. if (error) {
  157. error.httpResponse = [response copy];
  158. }
  159. //empty respone, return nil instead
  160. if (!data.length) {
  161. data = nil;
  162. }
  163. handler(data, error);
  164. };
  165. //fire the request
  166. #if __IPHONE_OS_VERSION_MIN_REQUIRED >= __IPHONE_7_0 || __MAC_OS_X_VERSION_MIN_REQUIRED >= __MAC_10_10
  167. NSURLSessionTask *task = [[NSURLSession sharedSession] dataTaskWithRequest:request completionHandler:completionHandler];
  168. [task resume];
  169. #else
  170. NSOperationQueue *queue = [NSOperationQueue new];
  171. [NSURLConnection sendAsynchronousRequest:request queue:queue completionHandler:^(NSURLResponse *response, NSData *data, NSError *error) {
  172. completionHandler(data, response, error);
  173. }];
  174. #endif
  175. }
  176. +(void)requestDataFromURL:(NSURL*)url method:(NSString*)method params:(NSDictionary*)params headers:(NSDictionary*)headers handler:(RequestResultBlock)handler
  177. {
  178. //create the request body
  179. NSMutableString* paramsString = nil;
  180. if (params) {
  181. //build a simple url encoded param string
  182. paramsString = [NSMutableString stringWithString:@""];
  183. for (NSString* key in [[params allKeys] sortedArrayUsingSelector:@selector(compare:)]) {
  184. [paramsString appendFormat:@"%@=%@&", key, [self urlEncode:params[key]] ];
  185. }
  186. if ([paramsString hasSuffix:@"&"]) {
  187. paramsString = [[NSMutableString alloc] initWithString: [paramsString substringToIndex: paramsString.length-1]];
  188. }
  189. }
  190. //set the request params
  191. if ([method isEqualToString:kHTTPMethodGET] && params) {
  192. //add GET params to the query string
  193. url = [NSURL URLWithString:[NSString stringWithFormat: @"%@%@%@",
  194. [url absoluteString],
  195. [url query] ? @"&" : @"?",
  196. paramsString
  197. ]];
  198. }
  199. //call the more general synq request method
  200. [self requestDataFromURL: url
  201. method: method
  202. requestBody: [method isEqualToString:kHTTPMethodPOST]?[paramsString dataUsingEncoding:NSUTF8StringEncoding]:nil
  203. headers: headers
  204. handler:handler];
  205. }
  206. #pragma mark - Async network request
  207. +(void)JSONFromURLWithString:(NSString*)urlString method:(NSString*)method params:(NSDictionary*)params orBodyString:(NSString*)bodyString completion:(JSONObjectBlock)completeBlock
  208. {
  209. [self JSONFromURLWithString:urlString
  210. method:method
  211. params:params
  212. orBodyString:bodyString
  213. headers:nil
  214. completion:completeBlock];
  215. }
  216. +(void)JSONFromURLWithString:(NSString *)urlString method:(NSString *)method params:(NSDictionary *)params orBodyString:(NSString *)bodyString headers:(NSDictionary *)headers completion:(JSONObjectBlock)completeBlock
  217. {
  218. [self JSONFromURLWithString:urlString
  219. method:method
  220. params:params
  221. orBodyData:[bodyString dataUsingEncoding:NSUTF8StringEncoding]
  222. headers:headers
  223. completion:completeBlock];
  224. }
  225. +(void)JSONFromURLWithString:(NSString*)urlString method:(NSString*)method params:(NSDictionary *)params orBodyData:(NSData*)bodyData headers:(NSDictionary*)headers completion:(JSONObjectBlock)completeBlock
  226. {
  227. RequestResultBlock handler = ^(NSData *responseData, JSONModelError *error) {
  228. id jsonObject = nil;
  229. //step 3: if there's no response so far, return a basic error
  230. if (!responseData && !error) {
  231. //check for false response, but no network error
  232. error = [JSONModelError errorBadResponse];
  233. }
  234. //step 4: if there's a response at this and no errors, convert to object
  235. if (error==nil) {
  236. // Note: it is possible to have a valid response with empty response data (204 No Content).
  237. // So only create the JSON object if there is some response data.
  238. if(responseData.length > 0)
  239. {
  240. //convert to an object
  241. jsonObject = [NSJSONSerialization JSONObjectWithData:responseData options:kNilOptions error:&error];
  242. }
  243. }
  244. //step 4.5: cover an edge case in which meaningful content is return along an error HTTP status code
  245. else if (error && responseData && jsonObject==nil) {
  246. //try to get the JSON object, while preserving the original error object
  247. jsonObject = [NSJSONSerialization JSONObjectWithData:responseData options:kNilOptions error:nil];
  248. //keep responseData just in case it contains error information
  249. error.responseData = responseData;
  250. }
  251. //step 5: invoke the complete block
  252. dispatch_async(dispatch_get_main_queue(), ^{
  253. if (completeBlock) {
  254. completeBlock(jsonObject, error);
  255. }
  256. });
  257. };
  258. NSURL *url = [NSURL URLWithString:urlString];
  259. if (bodyData) {
  260. [self requestDataFromURL:url method:method requestBody:bodyData headers:headers handler:handler];
  261. } else {
  262. [self requestDataFromURL:url method:method params:params headers:headers handler:handler];
  263. }
  264. }
  265. #pragma mark - request aliases
  266. +(void)getJSONFromURLWithString:(NSString*)urlString completion:(JSONObjectBlock)completeBlock
  267. {
  268. [self JSONFromURLWithString:urlString method:kHTTPMethodGET
  269. params:nil
  270. orBodyString:nil completion:^(id json, JSONModelError* e) {
  271. if (completeBlock) completeBlock(json, e);
  272. }];
  273. }
  274. +(void)getJSONFromURLWithString:(NSString*)urlString params:(NSDictionary*)params completion:(JSONObjectBlock)completeBlock
  275. {
  276. [self JSONFromURLWithString:urlString method:kHTTPMethodGET
  277. params:params
  278. orBodyString:nil completion:^(id json, JSONModelError* e) {
  279. if (completeBlock) completeBlock(json, e);
  280. }];
  281. }
  282. +(void)postJSONFromURLWithString:(NSString*)urlString params:(NSDictionary*)params completion:(JSONObjectBlock)completeBlock
  283. {
  284. [self JSONFromURLWithString:urlString method:kHTTPMethodPOST
  285. params:params
  286. orBodyString:nil completion:^(id json, JSONModelError* e) {
  287. if (completeBlock) completeBlock(json, e);
  288. }];
  289. }
  290. +(void)postJSONFromURLWithString:(NSString*)urlString bodyString:(NSString*)bodyString completion:(JSONObjectBlock)completeBlock
  291. {
  292. [self JSONFromURLWithString:urlString method:kHTTPMethodPOST
  293. params:nil
  294. orBodyString:bodyString completion:^(id json, JSONModelError* e) {
  295. if (completeBlock) completeBlock(json, e);
  296. }];
  297. }
  298. +(void)postJSONFromURLWithString:(NSString*)urlString bodyData:(NSData*)bodyData completion:(JSONObjectBlock)completeBlock
  299. {
  300. [self JSONFromURLWithString:urlString method:kHTTPMethodPOST
  301. params:nil
  302. orBodyString:[[NSString alloc] initWithData:bodyData encoding:defaultTextEncoding]
  303. completion:^(id json, JSONModelError* e) {
  304. if (completeBlock) completeBlock(json, e);
  305. }];
  306. }
  307. @end