JSONHTTPClient.m 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374
  1. //
  2. // JSONModelHTTPClient.m
  3. //
  4. // @version 1.0.2
  5. // @author Marin Todorov, http://www.touch-code-magazine.com
  6. //
  7. // Copyright (c) 2012-2014 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. // The MIT License in plain English: http://www.touch-code-magazine.com/JSONModel/MITLicense
  15. #import "JSONHTTPClient.h"
  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. return (NSString *)CFBridgingRelease(CFURLCreateStringByAddingPercentEscapes(
  100. NULL,
  101. (__bridge CFStringRef) value,
  102. NULL,
  103. (CFStringRef)@"!*'();:@&=+$,/?%#[]",
  104. kCFStringEncodingUTF8));
  105. }
  106. #pragma mark - networking worker methods
  107. +(NSData*)syncRequestDataFromURL:(NSURL*)url method:(NSString*)method requestBody:(NSData*)bodyData headers:(NSDictionary*)headers etag:(NSString**)etag error:(JSONModelError**)err
  108. {
  109. NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL: url
  110. cachePolicy: defaultCachePolicy
  111. timeoutInterval: defaultTimeoutInSeconds];
  112. [request setHTTPMethod:method];
  113. if ([requestContentType isEqualToString:kContentTypeAutomatic]) {
  114. //automatic content type
  115. if (bodyData) {
  116. NSString *bodyString = [[NSString alloc] initWithData:bodyData encoding:NSUTF8StringEncoding];
  117. [request setValue: [self contentTypeForRequestString: bodyString] forHTTPHeaderField:@"Content-type"];
  118. }
  119. } else {
  120. //user set content type
  121. [request setValue: requestContentType forHTTPHeaderField:@"Content-type"];
  122. }
  123. //add all the custom headers defined
  124. for (NSString* key in [requestHeaders allKeys]) {
  125. [request setValue:requestHeaders[key] forHTTPHeaderField:key];
  126. }
  127. //add the custom headers
  128. for (NSString* key in [headers allKeys]) {
  129. [request setValue:headers[key] forHTTPHeaderField:key];
  130. }
  131. if (bodyData) {
  132. [request setHTTPBody: bodyData];
  133. [request setValue:[NSString stringWithFormat:@"%lu", (unsigned long)bodyData.length] forHTTPHeaderField:@"Content-Length"];
  134. }
  135. //prepare output
  136. NSHTTPURLResponse* response = nil;
  137. //fire the request
  138. NSData *responseData = [NSURLConnection sendSynchronousRequest: request
  139. returningResponse: &response
  140. error: err];
  141. //convert an NSError to a JSONModelError
  142. if (*err != nil) {
  143. NSError* errObj = *err;
  144. *err = [JSONModelError errorWithDomain:errObj.domain code:errObj.code userInfo:errObj.userInfo];
  145. }
  146. //special case for http error code 401
  147. if ([*err code] == kCFURLErrorUserCancelledAuthentication) {
  148. response = [[NSHTTPURLResponse alloc] initWithURL:url
  149. statusCode:401
  150. HTTPVersion:@"HTTP/1.1"
  151. headerFields:@{}];
  152. }
  153. //if not OK status set the err to a JSONModelError instance
  154. if (response.statusCode >= 300 || response.statusCode < 200) {
  155. //create a new error
  156. if (*err==nil) *err = [JSONModelError errorBadResponse];
  157. }
  158. //if there was an error, include the HTTP response and return
  159. if (*err) {
  160. //assign the response to the JSONModel instance
  161. [*err setHttpResponse: [response copy]];
  162. //empty respone, return nil instead
  163. if ([responseData length]<1) {
  164. return nil;
  165. }
  166. }
  167. //return the data fetched from web
  168. return responseData;
  169. }
  170. +(NSData*)syncRequestDataFromURL:(NSURL*)url method:(NSString*)method params:(NSDictionary*)params headers:(NSDictionary*)headers etag:(NSString**)etag error:(JSONModelError**)err
  171. {
  172. //create the request body
  173. NSMutableString* paramsString = nil;
  174. if (params) {
  175. //build a simple url encoded param string
  176. paramsString = [NSMutableString stringWithString:@""];
  177. for (NSString* key in [[params allKeys] sortedArrayUsingSelector:@selector(compare:)]) {
  178. [paramsString appendFormat:@"%@=%@&", key, [self urlEncode:params[key]] ];
  179. }
  180. if ([paramsString hasSuffix:@"&"]) {
  181. paramsString = [[NSMutableString alloc] initWithString: [paramsString substringToIndex: paramsString.length-1]];
  182. }
  183. }
  184. //set the request params
  185. if ([method isEqualToString:kHTTPMethodGET] && params) {
  186. //add GET params to the query string
  187. url = [NSURL URLWithString:[NSString stringWithFormat: @"%@%@%@",
  188. [url absoluteString],
  189. [url query] ? @"&" : @"?",
  190. paramsString
  191. ]];
  192. }
  193. //call the more general synq request method
  194. return [self syncRequestDataFromURL: url
  195. method: method
  196. requestBody: [method isEqualToString:kHTTPMethodPOST]?[paramsString dataUsingEncoding:NSUTF8StringEncoding]:nil
  197. headers: headers
  198. etag: etag
  199. error: err];
  200. }
  201. #pragma mark - Async network request
  202. +(void)JSONFromURLWithString:(NSString*)urlString method:(NSString*)method params:(NSDictionary*)params orBodyString:(NSString*)bodyString completion:(JSONObjectBlock)completeBlock
  203. {
  204. [self JSONFromURLWithString:urlString
  205. method:method
  206. params:params
  207. orBodyString:bodyString
  208. headers:nil
  209. completion:completeBlock];
  210. }
  211. +(void)JSONFromURLWithString:(NSString *)urlString method:(NSString *)method params:(NSDictionary *)params orBodyString:(NSString *)bodyString headers:(NSDictionary *)headers completion:(JSONObjectBlock)completeBlock
  212. {
  213. [self JSONFromURLWithString:urlString
  214. method:method
  215. params:params
  216. orBodyData:[bodyString dataUsingEncoding:NSUTF8StringEncoding]
  217. headers:headers
  218. completion:completeBlock];
  219. }
  220. +(void)JSONFromURLWithString:(NSString*)urlString method:(NSString*)method params:(NSDictionary *)params orBodyData:(NSData*)bodyData headers:(NSDictionary*)headers completion:(JSONObjectBlock)completeBlock
  221. {
  222. NSDictionary* customHeaders = headers;
  223. dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
  224. id jsonObject = nil;
  225. JSONModelError* error = nil;
  226. NSData* responseData = nil;
  227. NSString* etag = nil;
  228. @try {
  229. if (bodyData) {
  230. responseData = [self syncRequestDataFromURL: [NSURL URLWithString:urlString]
  231. method: method
  232. requestBody: bodyData
  233. headers: customHeaders
  234. etag: &etag
  235. error: &error];
  236. } else {
  237. responseData = [self syncRequestDataFromURL: [NSURL URLWithString:urlString]
  238. method: method
  239. params: params
  240. headers: customHeaders
  241. etag: &etag
  242. error: &error];
  243. }
  244. }
  245. @catch (NSException *exception) {
  246. error = [JSONModelError errorBadResponse];
  247. }
  248. //step 3: if there's no response so far, return a basic error
  249. if (!responseData && !error) {
  250. //check for false response, but no network error
  251. error = [JSONModelError errorBadResponse];
  252. }
  253. //step 4: if there's a response at this and no errors, convert to object
  254. if (error==nil) {
  255. // Note: it is possible to have a valid response with empty response data (204 No Content).
  256. // So only create the JSON object if there is some response data.
  257. if(responseData.length > 0)
  258. {
  259. //convert to an object
  260. jsonObject = [NSJSONSerialization JSONObjectWithData:responseData options:kNilOptions error:&error];
  261. }
  262. }
  263. //step 4.5: cover an edge case in which meaningful content is return along an error HTTP status code
  264. else if (error && responseData && jsonObject==nil) {
  265. //try to get the JSON object, while preserving the origianl error object
  266. jsonObject = [NSJSONSerialization JSONObjectWithData:responseData options:kNilOptions error:nil];
  267. }
  268. //step 5: invoke the complete block
  269. dispatch_async(dispatch_get_main_queue(), ^{
  270. if (completeBlock) {
  271. completeBlock(jsonObject, error);
  272. }
  273. });
  274. });
  275. }
  276. #pragma mark - request aliases
  277. +(void)getJSONFromURLWithString:(NSString*)urlString completion:(JSONObjectBlock)completeBlock
  278. {
  279. [self JSONFromURLWithString:urlString method:kHTTPMethodGET
  280. params:nil
  281. orBodyString:nil completion:^(id json, JSONModelError* e) {
  282. if (completeBlock) completeBlock(json, e);
  283. }];
  284. }
  285. +(void)getJSONFromURLWithString:(NSString*)urlString params:(NSDictionary*)params completion:(JSONObjectBlock)completeBlock
  286. {
  287. [self JSONFromURLWithString:urlString method:kHTTPMethodGET
  288. params:params
  289. orBodyString:nil completion:^(id json, JSONModelError* e) {
  290. if (completeBlock) completeBlock(json, e);
  291. }];
  292. }
  293. +(void)postJSONFromURLWithString:(NSString*)urlString params:(NSDictionary*)params completion:(JSONObjectBlock)completeBlock
  294. {
  295. [self JSONFromURLWithString:urlString method:kHTTPMethodPOST
  296. params:params
  297. orBodyString:nil completion:^(id json, JSONModelError* e) {
  298. if (completeBlock) completeBlock(json, e);
  299. }];
  300. }
  301. +(void)postJSONFromURLWithString:(NSString*)urlString bodyString:(NSString*)bodyString completion:(JSONObjectBlock)completeBlock
  302. {
  303. [self JSONFromURLWithString:urlString method:kHTTPMethodPOST
  304. params:nil
  305. orBodyString:bodyString completion:^(id json, JSONModelError* e) {
  306. if (completeBlock) completeBlock(json, e);
  307. }];
  308. }
  309. +(void)postJSONFromURLWithString:(NSString*)urlString bodyData:(NSData*)bodyData completion:(JSONObjectBlock)completeBlock
  310. {
  311. [self JSONFromURLWithString:urlString method:kHTTPMethodPOST
  312. params:nil
  313. orBodyString:[[NSString alloc] initWithData:bodyData encoding:defaultTextEncoding]
  314. completion:^(id json, JSONModelError* e) {
  315. if (completeBlock) completeBlock(json, e);
  316. }];
  317. }
  318. @end