PageRenderTime 20ms CodeModel.GetById 8ms RepoModel.GetById 0ms app.codeStats 0ms

/Zinc/Private/ZincHTTPURLConnectionOperation.m

http://github.com/mindsnacks/Zinc-ObjC
Objective C | 323 lines | 244 code | 51 blank | 28 comment | 53 complexity | 41fb7d1df1f98396ab2c62bf4c7de085 MD5 | raw file
  1. // ZincHTTPURLConnectionOperation.m
  2. //
  3. // Copyright (c) 2011 Gowalla (http://gowalla.com/)
  4. //
  5. // Permission is hereby granted, free of charge, to any person obtaining a copy
  6. // of this software and associated documentation files (the "Software"), to deal
  7. // in the Software without restriction, including without limitation the rights
  8. // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  9. // copies of the Software, and to permit persons to whom the Software is
  10. // furnished to do so, subject to the following conditions:
  11. //
  12. // The above copyright notice and this permission notice shall be included in
  13. // all copies or substantial portions of the Software.
  14. //
  15. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  16. // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  17. // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  18. // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  19. // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  20. // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  21. // THE SOFTWARE.
  22. #import "ZincHTTPURLConnectionOperation.h"
  23. #import <objc/runtime.h>
  24. // Workaround for change in imp_implementationWithBlock() with Xcode 4.5
  25. #if defined(__IPHONE_6_0) || defined(__MAC_10_8)
  26. #define ZINC_CAST_TO_BLOCK id
  27. #else
  28. #define ZINC_CAST_TO_BLOCK __bridge void *
  29. #endif
  30. #pragma clang diagnostic push
  31. #pragma clang diagnostic ignored "-Wstrict-selector-match"
  32. NSSet * ZincContentTypesFromHTTPHeader(NSString *string) {
  33. if (!string) {
  34. return nil;
  35. }
  36. NSArray *mediaRanges = [string componentsSeparatedByString:@","];
  37. NSMutableSet *mutableContentTypes = [NSMutableSet setWithCapacity:mediaRanges.count];
  38. [mediaRanges enumerateObjectsUsingBlock:^(NSString *mediaRange, __unused NSUInteger idx, __unused BOOL *stop) {
  39. NSRange parametersRange = [mediaRange rangeOfString:@";"];
  40. if (parametersRange.location != NSNotFound) {
  41. mediaRange = [mediaRange substringToIndex:parametersRange.location];
  42. }
  43. mediaRange = [mediaRange stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
  44. if (mediaRange.length > 0) {
  45. [mutableContentTypes addObject:mediaRange];
  46. }
  47. }];
  48. return [NSSet setWithSet:mutableContentTypes];
  49. }
  50. static void GetMediaTypeAndSubtypeWithString(NSString *string, NSString **type, NSString **subtype) {
  51. if (!string) {
  52. return;
  53. }
  54. NSScanner *scanner = [NSScanner scannerWithString:string];
  55. [scanner setCharactersToBeSkipped:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
  56. [scanner scanUpToString:@"/" intoString:type];
  57. [scanner scanString:@"/" intoString:nil];
  58. [scanner scanUpToString:@";" intoString:subtype];
  59. }
  60. static NSString * StringFromIndexSet(NSIndexSet *indexSet) {
  61. NSMutableString *string = [NSMutableString string];
  62. NSRange range = NSMakeRange([indexSet firstIndex], 1);
  63. while (range.location != NSNotFound) {
  64. NSUInteger nextIndex = [indexSet indexGreaterThanIndex:range.location];
  65. while (nextIndex == range.location + range.length) {
  66. range.length++;
  67. nextIndex = [indexSet indexGreaterThanIndex:nextIndex];
  68. }
  69. if (string.length) {
  70. [string appendString:@","];
  71. }
  72. if (range.length == 1) {
  73. [string appendFormat:@"%lu", (long)range.location];
  74. } else {
  75. NSUInteger firstIndex = range.location;
  76. NSUInteger lastIndex = firstIndex + range.length - 1;
  77. [string appendFormat:@"%lu-%lu", (long)firstIndex, (long)lastIndex];
  78. }
  79. range.location = nextIndex;
  80. range.length = 1;
  81. }
  82. return string;
  83. }
  84. static void SwizzleClassMethodWithClassAndSelectorUsingBlock(Class klass, SEL selector, id block) {
  85. Method originalMethod = class_getClassMethod(klass, selector);
  86. IMP implementation = imp_implementationWithBlock((ZINC_CAST_TO_BLOCK)block);
  87. class_replaceMethod(objc_getMetaClass([NSStringFromClass(klass) UTF8String]), selector, implementation, method_getTypeEncoding(originalMethod));
  88. }
  89. #pragma mark -
  90. @interface ZincHTTPURLConnectionOperation ()
  91. @property (readwrite, nonatomic, strong) NSURLRequest *request;
  92. @property (readwrite, nonatomic, strong) NSHTTPURLResponse *response;
  93. @property (readwrite, nonatomic, strong) NSError *HTTPError;
  94. @end
  95. @implementation ZincHTTPURLConnectionOperation
  96. @synthesize HTTPError = _HTTPError;
  97. @synthesize successCallbackQueue = _successCallbackQueue;
  98. @synthesize failureCallbackQueue = _failureCallbackQueue;
  99. @dynamic request;
  100. @dynamic response;
  101. - (void)dealloc {
  102. if (_successCallbackQueue) {
  103. #if !OS_OBJECT_USE_OBJC
  104. dispatch_release(_successCallbackQueue);
  105. #endif
  106. _successCallbackQueue = NULL;
  107. }
  108. if (_failureCallbackQueue) {
  109. #if !OS_OBJECT_USE_OBJC
  110. dispatch_release(_failureCallbackQueue);
  111. #endif
  112. _failureCallbackQueue = NULL;
  113. }
  114. }
  115. - (NSError *)error {
  116. if (!self.HTTPError && self.response) {
  117. if (![self hasAcceptableStatusCode] || ![self hasAcceptableContentType]) {
  118. NSMutableDictionary *userInfo = [NSMutableDictionary dictionary];
  119. [userInfo setValue:self.responseString forKey:NSLocalizedRecoverySuggestionErrorKey];
  120. [userInfo setValue:[self.request URL] forKey:NSURLErrorFailingURLErrorKey];
  121. [userInfo setValue:self.request forKey:ZincNetworkingOperationFailingURLRequestErrorKey];
  122. [userInfo setValue:self.response forKey:ZincNetworkingOperationFailingURLResponseErrorKey];
  123. if (![self hasAcceptableStatusCode]) {
  124. NSUInteger statusCode = ([self.response isKindOfClass:[NSHTTPURLResponse class]]) ? (NSUInteger)[self.response statusCode] : 200;
  125. [userInfo setValue:[NSString stringWithFormat:NSLocalizedStringFromTable(@"Expected status code in (%@), got %d", @"ZincNetworking", nil), StringFromIndexSet([[self class] acceptableStatusCodes]), statusCode] forKey:NSLocalizedDescriptionKey];
  126. self.HTTPError = [[NSError alloc] initWithDomain:ZincNetworkingErrorDomain code:NSURLErrorBadServerResponse userInfo:userInfo];
  127. } else if (![self hasAcceptableContentType]) {
  128. // Don't invalidate content type if there is no content
  129. if ([self.responseData length] > 0) {
  130. [userInfo setValue:[NSString stringWithFormat:NSLocalizedStringFromTable(@"Expected content type %@, got %@", @"ZincNetworking", nil), [[self class] acceptableContentTypes], [self.response MIMEType]] forKey:NSLocalizedDescriptionKey];
  131. self.HTTPError = [[NSError alloc] initWithDomain:ZincNetworkingErrorDomain code:NSURLErrorCannotDecodeContentData userInfo:userInfo];
  132. }
  133. }
  134. }
  135. }
  136. if (self.HTTPError) {
  137. return self.HTTPError;
  138. } else {
  139. return [super error];
  140. }
  141. }
  142. - (NSStringEncoding)responseStringEncoding {
  143. // When no explicit charset parameter is provided by the sender, media subtypes of the "text" type are defined to have a default charset value of "ISO-8859-1" when received via HTTP. Data in character sets other than "ISO-8859-1" or its subsets MUST be labeled with an appropriate charset value.
  144. // See http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.4.1
  145. if (self.response && !self.response.textEncodingName && self.responseData && [self.response respondsToSelector:@selector(allHeaderFields)]) {
  146. NSString *type = nil;
  147. GetMediaTypeAndSubtypeWithString([[self.response allHeaderFields] valueForKey:@"Content-Type"], &type, nil);
  148. if ([type isEqualToString:@"text"]) {
  149. return NSISOLatin1StringEncoding;
  150. }
  151. }
  152. return [super responseStringEncoding];
  153. }
  154. - (void)pause {
  155. unsigned long long offset = 0;
  156. if ([self.outputStream propertyForKey:NSStreamFileCurrentOffsetKey]) {
  157. offset = [[self.outputStream propertyForKey:NSStreamFileCurrentOffsetKey] unsignedLongLongValue];
  158. } else {
  159. offset = [[self.outputStream propertyForKey:NSStreamDataWrittenToMemoryStreamKey] length];
  160. }
  161. NSMutableURLRequest *mutableURLRequest = [self.request mutableCopy];
  162. if ([self.response respondsToSelector:@selector(allHeaderFields)] && [[self.response allHeaderFields] valueForKey:@"ETag"]) {
  163. [mutableURLRequest setValue:[[self.response allHeaderFields] valueForKey:@"ETag"] forHTTPHeaderField:@"If-Range"];
  164. }
  165. [mutableURLRequest setValue:[NSString stringWithFormat:@"bytes=%llu-", offset] forHTTPHeaderField:@"Range"];
  166. self.request = mutableURLRequest;
  167. [super pause];
  168. }
  169. - (BOOL)hasAcceptableStatusCode {
  170. if (!self.response) {
  171. return NO;
  172. }
  173. NSUInteger statusCode = ([self.response isKindOfClass:[NSHTTPURLResponse class]]) ? (NSUInteger)[self.response statusCode] : 200;
  174. return ![[self class] acceptableStatusCodes] || [[[self class] acceptableStatusCodes] containsIndex:statusCode];
  175. }
  176. - (BOOL)hasAcceptableContentType {
  177. if (!self.response) {
  178. return NO;
  179. }
  180. // Any HTTP/1.1 message containing an entity-body SHOULD include a Content-Type header field defining the media type of that body. If and only if the media type is not given by a Content-Type field, the recipient MAY attempt to guess the media type via inspection of its content and/or the name extension(s) of the URI used to identify the resource. If the media type remains unknown, the recipient SHOULD treat it as type "application/octet-stream".
  181. // See http://www.w3.org/Protocols/rfc2616/rfc2616-sec7.html
  182. NSString *contentType = [self.response MIMEType];
  183. if (!contentType) {
  184. contentType = @"application/octet-stream";
  185. }
  186. return ![[self class] acceptableContentTypes] || [[[self class] acceptableContentTypes] containsObject:contentType];
  187. }
  188. - (void)setSuccessCallbackQueue:(dispatch_queue_t)successCallbackQueue {
  189. if (successCallbackQueue != _successCallbackQueue) {
  190. if (_successCallbackQueue) {
  191. #if !OS_OBJECT_USE_OBJC
  192. dispatch_release(_successCallbackQueue);
  193. #endif
  194. _successCallbackQueue = NULL;
  195. }
  196. if (successCallbackQueue) {
  197. #if !OS_OBJECT_USE_OBJC
  198. dispatch_retain(successCallbackQueue);
  199. #endif
  200. _successCallbackQueue = successCallbackQueue;
  201. }
  202. }
  203. }
  204. - (void)setFailureCallbackQueue:(dispatch_queue_t)failureCallbackQueue {
  205. if (failureCallbackQueue != _failureCallbackQueue) {
  206. if (_failureCallbackQueue) {
  207. #if !OS_OBJECT_USE_OBJC
  208. dispatch_release(_failureCallbackQueue);
  209. #endif
  210. _failureCallbackQueue = NULL;
  211. }
  212. if (failureCallbackQueue) {
  213. #if !OS_OBJECT_USE_OBJC
  214. dispatch_retain(failureCallbackQueue);
  215. #endif
  216. _failureCallbackQueue = failureCallbackQueue;
  217. }
  218. }
  219. }
  220. - (void)setCompletionBlockWithSuccess:(void (^)(ZincHTTPURLConnectionOperation *operation, id responseObject))success
  221. failure:(void (^)(ZincHTTPURLConnectionOperation *operation, NSError *error))failure
  222. {
  223. // completionBlock is manually nilled out in ZincURLConnectionOperation to break the retain cycle.
  224. #pragma clang diagnostic push
  225. #pragma clang diagnostic ignored "-Warc-retain-cycles"
  226. #pragma clang diagnostic ignored "-Wgnu"
  227. self.completionBlock = ^{
  228. if (self.error) {
  229. if (failure) {
  230. dispatch_async(self.failureCallbackQueue ?: dispatch_get_main_queue(), ^{
  231. failure(self, self.error);
  232. });
  233. }
  234. } else {
  235. if (success) {
  236. dispatch_async(self.successCallbackQueue ?: dispatch_get_main_queue(), ^{
  237. success(self, self.responseData);
  238. });
  239. }
  240. }
  241. };
  242. #pragma clang diagnostic pop
  243. }
  244. #pragma mark - ZincHTTPURLConnectionOperation
  245. + (NSIndexSet *)acceptableStatusCodes {
  246. return [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(200, 100)];
  247. }
  248. + (void)addAcceptableStatusCodes:(NSIndexSet *)statusCodes {
  249. NSMutableIndexSet *mutableStatusCodes = [[NSMutableIndexSet alloc] initWithIndexSet:[self acceptableStatusCodes]];
  250. [mutableStatusCodes addIndexes:statusCodes];
  251. SwizzleClassMethodWithClassAndSelectorUsingBlock([self class], @selector(acceptableStatusCodes), ^(__unused id _self) {
  252. return mutableStatusCodes;
  253. });
  254. }
  255. + (NSSet *)acceptableContentTypes {
  256. return nil;
  257. }
  258. + (void)addAcceptableContentTypes:(NSSet *)contentTypes {
  259. NSMutableSet *mutableContentTypes = [[NSMutableSet alloc] initWithSet:[self acceptableContentTypes] copyItems:YES];
  260. [mutableContentTypes unionSet:contentTypes];
  261. SwizzleClassMethodWithClassAndSelectorUsingBlock([self class], @selector(acceptableContentTypes), ^(__unused id _self) {
  262. return mutableContentTypes;
  263. });
  264. }
  265. + (BOOL)canProcessRequest:(NSURLRequest *)request {
  266. if ([[self class] isEqual:[ZincHTTPURLConnectionOperation class]]) {
  267. return YES;
  268. }
  269. return [[self acceptableContentTypes] intersectsSet:ZincContentTypesFromHTTPHeader([request valueForHTTPHeaderField:@"Accept"])];
  270. }
  271. @end
  272. #pragma clang diagnostic pop