JSONHTTPClient.m 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391
  1. //
  2. // JSONModelHTTPClient.m
  3. //
  4. // @version 1.0.0
  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. * Whether the iPhone net indicator automatically shows when making requests
  31. */
  32. static BOOL doesControlIndicator = YES;
  33. /**
  34. * Custom HTTP headers to send over with *each* request
  35. */
  36. static NSMutableDictionary* requestHeaders = nil;
  37. /**
  38. * Default request content type
  39. */
  40. static NSString* requestContentType = nil;
  41. #pragma mark - implementation
  42. @implementation JSONHTTPClient
  43. #pragma mark - initialization
  44. +(void)initialize
  45. {
  46. static dispatch_once_t once;
  47. dispatch_once(&once, ^{
  48. requestHeaders = [NSMutableDictionary dictionary];
  49. requestContentType = kContentTypeAutomatic;
  50. });
  51. }
  52. #pragma mark - configuration methods
  53. +(NSMutableDictionary*)requestHeaders
  54. {
  55. return requestHeaders;
  56. }
  57. +(void)setDefaultTextEncoding:(NSStringEncoding)encoding
  58. {
  59. defaultTextEncoding = encoding;
  60. }
  61. +(void)setCachingPolicy:(NSURLRequestCachePolicy)policy
  62. {
  63. defaultCachePolicy = policy;
  64. }
  65. +(void)setTimeoutInSeconds:(int)seconds
  66. {
  67. defaultTimeoutInSeconds = seconds;
  68. }
  69. +(void)setControlsNetworkIndicator:(BOOL)does
  70. {
  71. doesControlIndicator = does;
  72. }
  73. +(void)setRequestContentType:(NSString*)contentTypeString
  74. {
  75. requestContentType = contentTypeString;
  76. }
  77. #pragma mark - helper methods
  78. +(NSString*)contentTypeForRequestString:(NSString*)requestString
  79. {
  80. //fetch the charset name from the default string encoding
  81. NSString* contentType = requestContentType;
  82. if (requestString.length>0 && [contentType isEqualToString:kContentTypeAutomatic]) {
  83. //check for "eventual" JSON array or dictionary
  84. NSString* firstAndLastChar = [NSString stringWithFormat:@"%@%@",
  85. [requestString substringToIndex:1],
  86. [requestString substringFromIndex: requestString.length -1]
  87. ];
  88. if ([firstAndLastChar isEqualToString:@"{}"] || [firstAndLastChar isEqualToString:@"[]"]) {
  89. //guessing for a JSON request
  90. contentType = kContentTypeJSON;
  91. } else {
  92. //fallback to www form encoded params
  93. contentType = kContentTypeWWWEncoded;
  94. }
  95. }
  96. //type is set, just add charset
  97. NSString *charset = (NSString *)CFStringConvertEncodingToIANACharSetName(CFStringConvertNSStringEncodingToEncoding(NSUTF8StringEncoding));
  98. return [NSString stringWithFormat:@"%@; charset=%@", contentType, charset];
  99. }
  100. +(NSString*)urlEncode:(id<NSObject>)value
  101. {
  102. //make sure param is a string
  103. if ([value isKindOfClass:[NSNumber class]]) {
  104. value = [(NSNumber*)value stringValue];
  105. }
  106. NSAssert([value isKindOfClass:[NSString class]], @"request parameters can be only of NSString or NSNumber classes. '%@' is of class %@.", value, [value class]);
  107. return (NSString *)CFBridgingRelease(CFURLCreateStringByAddingPercentEscapes(
  108. NULL,
  109. (__bridge CFStringRef) value,
  110. NULL,
  111. (CFStringRef)@"!*'();:@&=+$,/?%#[]",
  112. kCFStringEncodingUTF8));
  113. }
  114. #pragma mark - networking worker methods
  115. +(NSData*)syncRequestDataFromURL:(NSURL*)url method:(NSString*)method requestBody:(NSData*)bodyData headers:(NSDictionary*)headers etag:(NSString**)etag error:(JSONModelError**)err
  116. {
  117. //turn on network indicator
  118. if (doesControlIndicator) dispatch_async(dispatch_get_main_queue(), ^{[self setNetworkIndicatorVisible:YES];});
  119. NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL: url
  120. cachePolicy: defaultCachePolicy
  121. timeoutInterval: defaultTimeoutInSeconds];
  122. [request setHTTPMethod:method];
  123. if ([requestContentType isEqualToString:kContentTypeAutomatic]) {
  124. //automatic content type
  125. if (bodyData) {
  126. NSString *bodyString = [[NSString alloc] initWithData:bodyData encoding:NSUTF8StringEncoding];
  127. [request setValue: [self contentTypeForRequestString: bodyString] forHTTPHeaderField:@"Content-type"];
  128. }
  129. } else {
  130. //user set content type
  131. [request setValue: requestContentType forHTTPHeaderField:@"Content-type"];
  132. }
  133. //add all the custom headers defined
  134. for (NSString* key in [requestHeaders allKeys]) {
  135. [request setValue:requestHeaders[key] forHTTPHeaderField:key];
  136. }
  137. //add the custom headers
  138. for (NSString* key in [headers allKeys]) {
  139. [request setValue:headers[key] forHTTPHeaderField:key];
  140. }
  141. if (bodyData) {
  142. [request setHTTPBody: bodyData];
  143. [request setValue:[NSString stringWithFormat:@"%lu", (unsigned long)bodyData.length] forHTTPHeaderField:@"Content-Length"];
  144. }
  145. //prepare output
  146. NSHTTPURLResponse* response = nil;
  147. //fire the request
  148. NSData *responseData = [NSURLConnection sendSynchronousRequest: request
  149. returningResponse: &response
  150. error: err];
  151. //convert an NSError to a JSONModelError
  152. if (*err != nil) {
  153. NSError* errObj = *err;
  154. *err = [JSONModelError errorWithDomain:errObj.domain code:errObj.code userInfo:errObj.userInfo];
  155. }
  156. //turn off network indicator
  157. if (doesControlIndicator) dispatch_async(dispatch_get_main_queue(), ^{[self setNetworkIndicatorVisible:NO];});
  158. //if not OK status set the err to a JSONModelError instance
  159. if (response.statusCode >= 300 || response.statusCode < 200) {
  160. //create a new error
  161. if (*err==nil) *err = [JSONModelError errorBadResponse];
  162. }
  163. //if there was an error, include the HTTP response and return
  164. if (*err) {
  165. //assign the response to the JSONModel instance
  166. [*err setHttpResponse: [response copy]];
  167. //empty respone, return nil instead
  168. if ([responseData length]<1) {
  169. return nil;
  170. }
  171. }
  172. //return the data fetched from web
  173. return responseData;
  174. }
  175. +(NSData*)syncRequestDataFromURL:(NSURL*)url method:(NSString*)method params:(NSDictionary*)params headers:(NSDictionary*)headers etag:(NSString**)etag error:(JSONModelError**)err
  176. {
  177. //create the request body
  178. NSMutableString* paramsString = nil;
  179. if (params) {
  180. //build a simple url encoded param string
  181. paramsString = [NSMutableString stringWithString:@""];
  182. for (NSString* key in [[params allKeys] sortedArrayUsingSelector:@selector(compare:)]) {
  183. [paramsString appendFormat:@"%@=%@&", key, [self urlEncode:params[key]] ];
  184. }
  185. if ([paramsString hasSuffix:@"&"]) {
  186. paramsString = [[NSMutableString alloc] initWithString: [paramsString substringToIndex: paramsString.length-1]];
  187. }
  188. }
  189. //set the request params
  190. if ([method isEqualToString:kHTTPMethodGET] && params) {
  191. //add GET params to the query string
  192. url = [NSURL URLWithString:[NSString stringWithFormat: @"%@%@%@",
  193. [url absoluteString],
  194. [url query] ? @"&" : @"?",
  195. paramsString
  196. ]];
  197. }
  198. //call the more general synq request method
  199. return [self syncRequestDataFromURL: url
  200. method: method
  201. requestBody: [method isEqualToString:kHTTPMethodPOST]?[paramsString dataUsingEncoding:NSUTF8StringEncoding]:nil
  202. headers: headers
  203. etag: etag
  204. error: err];
  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. NSDictionary* customHeaders = headers;
  228. dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
  229. id jsonObject = nil;
  230. JSONModelError* error = nil;
  231. NSData* responseData = nil;
  232. NSString* etag = nil;
  233. @try {
  234. if (bodyData) {
  235. responseData = [self syncRequestDataFromURL: [NSURL URLWithString:urlString]
  236. method: method
  237. requestBody: bodyData
  238. headers: customHeaders
  239. etag: &etag
  240. error: &error];
  241. } else {
  242. responseData = [self syncRequestDataFromURL: [NSURL URLWithString:urlString]
  243. method: method
  244. params: params
  245. headers: customHeaders
  246. etag: &etag
  247. error: &error];
  248. }
  249. }
  250. @catch (NSException *exception) {
  251. error = [JSONModelError errorBadResponse];
  252. }
  253. //step 3: if there's no response so far, return a basic error
  254. if (!responseData && !jsonObject) {
  255. //check for false response, but no network error
  256. error = [JSONModelError errorBadResponse];
  257. }
  258. //step 4: if there's a response at this and no errors, convert to object
  259. if (error==nil && jsonObject==nil) {
  260. // Note: it is possible to have a valid response with empty response data (204 No Content).
  261. // So only create the JSON object if there is some response data.
  262. if(responseData.length > 0)
  263. {
  264. //convert to an object
  265. jsonObject = [NSJSONSerialization JSONObjectWithData:responseData options:kNilOptions error:&error];
  266. }
  267. }
  268. //step 4.5: cover an edge case in which meaningful content is return along an error HTTP status code
  269. else if (error && responseData && jsonObject==nil) {
  270. //try to get the JSON object, while preserving the origianl error object
  271. jsonObject = [NSJSONSerialization JSONObjectWithData:responseData options:kNilOptions error:nil];
  272. }
  273. //step 5: invoke the complete block
  274. dispatch_async(dispatch_get_main_queue(), ^{
  275. if (completeBlock) {
  276. completeBlock(jsonObject, error);
  277. }
  278. });
  279. });
  280. }
  281. #pragma mark - request aliases
  282. +(void)getJSONFromURLWithString:(NSString*)urlString completion:(JSONObjectBlock)completeBlock
  283. {
  284. [self JSONFromURLWithString:urlString method:kHTTPMethodGET
  285. params:nil
  286. orBodyString:nil completion:^(id json, JSONModelError* e) {
  287. if (completeBlock) completeBlock(json, e);
  288. }];
  289. }
  290. +(void)getJSONFromURLWithString:(NSString*)urlString params:(NSDictionary*)params completion:(JSONObjectBlock)completeBlock
  291. {
  292. [self JSONFromURLWithString:urlString method:kHTTPMethodGET
  293. params:params
  294. orBodyString:nil completion:^(id json, JSONModelError* e) {
  295. if (completeBlock) completeBlock(json, e);
  296. }];
  297. }
  298. +(void)postJSONFromURLWithString:(NSString*)urlString params:(NSDictionary*)params completion:(JSONObjectBlock)completeBlock
  299. {
  300. [self JSONFromURLWithString:urlString method:kHTTPMethodPOST
  301. params:params
  302. orBodyString:nil completion:^(id json, JSONModelError* e) {
  303. if (completeBlock) completeBlock(json, e);
  304. }];
  305. }
  306. +(void)postJSONFromURLWithString:(NSString*)urlString bodyString:(NSString*)bodyString completion:(JSONObjectBlock)completeBlock
  307. {
  308. [self JSONFromURLWithString:urlString method:kHTTPMethodPOST
  309. params:nil
  310. orBodyString:bodyString completion:^(id json, JSONModelError* e) {
  311. if (completeBlock) completeBlock(json, e);
  312. }];
  313. }
  314. +(void)postJSONFromURLWithString:(NSString*)urlString bodyData:(NSData*)bodyData completion:(JSONObjectBlock)completeBlock
  315. {
  316. [self JSONFromURLWithString:urlString method:kHTTPMethodPOST
  317. params:nil
  318. orBodyString:[[NSString alloc] initWithData:bodyData encoding:defaultTextEncoding]
  319. completion:^(id json, JSONModelError* e) {
  320. if (completeBlock) completeBlock(json, e);
  321. }];
  322. }
  323. #pragma mark - iOS UI helper
  324. +(void)setNetworkIndicatorVisible:(BOOL)isVisible
  325. {
  326. #ifdef __IPHONE_OS_VERSION_MAX_ALLOWED
  327. [[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:isVisible];
  328. #endif
  329. }
  330. @end