/Zinc/Private/ZincHTTPURLConnectionOperation.m
Objective C | 323 lines | 244 code | 51 blank | 28 comment | 53 complexity | 41fb7d1df1f98396ab2c62bf4c7de085 MD5 | raw file
- // ZincHTTPURLConnectionOperation.m
- //
- // Copyright (c) 2011 Gowalla (http://gowalla.com/)
- //
- // 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:
- //
- // The above copyright notice and this permission notice shall be included in
- // all copies or substantial portions of the Software.
- //
- // 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.
- #import "ZincHTTPURLConnectionOperation.h"
- #import <objc/runtime.h>
- // Workaround for change in imp_implementationWithBlock() with Xcode 4.5
- #if defined(__IPHONE_6_0) || defined(__MAC_10_8)
- #define ZINC_CAST_TO_BLOCK id
- #else
- #define ZINC_CAST_TO_BLOCK __bridge void *
- #endif
- #pragma clang diagnostic push
- #pragma clang diagnostic ignored "-Wstrict-selector-match"
- NSSet * ZincContentTypesFromHTTPHeader(NSString *string) {
- if (!string) {
- return nil;
- }
- NSArray *mediaRanges = [string componentsSeparatedByString:@","];
- NSMutableSet *mutableContentTypes = [NSMutableSet setWithCapacity:mediaRanges.count];
- [mediaRanges enumerateObjectsUsingBlock:^(NSString *mediaRange, __unused NSUInteger idx, __unused BOOL *stop) {
- NSRange parametersRange = [mediaRange rangeOfString:@";"];
- if (parametersRange.location != NSNotFound) {
- mediaRange = [mediaRange substringToIndex:parametersRange.location];
- }
- mediaRange = [mediaRange stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
- if (mediaRange.length > 0) {
- [mutableContentTypes addObject:mediaRange];
- }
- }];
- return [NSSet setWithSet:mutableContentTypes];
- }
- static void GetMediaTypeAndSubtypeWithString(NSString *string, NSString **type, NSString **subtype) {
- if (!string) {
- return;
- }
- NSScanner *scanner = [NSScanner scannerWithString:string];
- [scanner setCharactersToBeSkipped:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
- [scanner scanUpToString:@"/" intoString:type];
- [scanner scanString:@"/" intoString:nil];
- [scanner scanUpToString:@";" intoString:subtype];
- }
- static NSString * StringFromIndexSet(NSIndexSet *indexSet) {
- NSMutableString *string = [NSMutableString string];
- NSRange range = NSMakeRange([indexSet firstIndex], 1);
- while (range.location != NSNotFound) {
- NSUInteger nextIndex = [indexSet indexGreaterThanIndex:range.location];
- while (nextIndex == range.location + range.length) {
- range.length++;
- nextIndex = [indexSet indexGreaterThanIndex:nextIndex];
- }
- if (string.length) {
- [string appendString:@","];
- }
- if (range.length == 1) {
- [string appendFormat:@"%lu", (long)range.location];
- } else {
- NSUInteger firstIndex = range.location;
- NSUInteger lastIndex = firstIndex + range.length - 1;
- [string appendFormat:@"%lu-%lu", (long)firstIndex, (long)lastIndex];
- }
- range.location = nextIndex;
- range.length = 1;
- }
- return string;
- }
- static void SwizzleClassMethodWithClassAndSelectorUsingBlock(Class klass, SEL selector, id block) {
- Method originalMethod = class_getClassMethod(klass, selector);
- IMP implementation = imp_implementationWithBlock((ZINC_CAST_TO_BLOCK)block);
- class_replaceMethod(objc_getMetaClass([NSStringFromClass(klass) UTF8String]), selector, implementation, method_getTypeEncoding(originalMethod));
- }
- #pragma mark -
- @interface ZincHTTPURLConnectionOperation ()
- @property (readwrite, nonatomic, strong) NSURLRequest *request;
- @property (readwrite, nonatomic, strong) NSHTTPURLResponse *response;
- @property (readwrite, nonatomic, strong) NSError *HTTPError;
- @end
- @implementation ZincHTTPURLConnectionOperation
- @synthesize HTTPError = _HTTPError;
- @synthesize successCallbackQueue = _successCallbackQueue;
- @synthesize failureCallbackQueue = _failureCallbackQueue;
- @dynamic request;
- @dynamic response;
- - (void)dealloc {
- if (_successCallbackQueue) {
- #if !OS_OBJECT_USE_OBJC
- dispatch_release(_successCallbackQueue);
- #endif
- _successCallbackQueue = NULL;
- }
- if (_failureCallbackQueue) {
- #if !OS_OBJECT_USE_OBJC
- dispatch_release(_failureCallbackQueue);
- #endif
- _failureCallbackQueue = NULL;
- }
- }
- - (NSError *)error {
- if (!self.HTTPError && self.response) {
- if (![self hasAcceptableStatusCode] || ![self hasAcceptableContentType]) {
- NSMutableDictionary *userInfo = [NSMutableDictionary dictionary];
- [userInfo setValue:self.responseString forKey:NSLocalizedRecoverySuggestionErrorKey];
- [userInfo setValue:[self.request URL] forKey:NSURLErrorFailingURLErrorKey];
- [userInfo setValue:self.request forKey:ZincNetworkingOperationFailingURLRequestErrorKey];
- [userInfo setValue:self.response forKey:ZincNetworkingOperationFailingURLResponseErrorKey];
- if (![self hasAcceptableStatusCode]) {
- NSUInteger statusCode = ([self.response isKindOfClass:[NSHTTPURLResponse class]]) ? (NSUInteger)[self.response statusCode] : 200;
- [userInfo setValue:[NSString stringWithFormat:NSLocalizedStringFromTable(@"Expected status code in (%@), got %d", @"ZincNetworking", nil), StringFromIndexSet([[self class] acceptableStatusCodes]), statusCode] forKey:NSLocalizedDescriptionKey];
- self.HTTPError = [[NSError alloc] initWithDomain:ZincNetworkingErrorDomain code:NSURLErrorBadServerResponse userInfo:userInfo];
- } else if (![self hasAcceptableContentType]) {
- // Don't invalidate content type if there is no content
- if ([self.responseData length] > 0) {
- [userInfo setValue:[NSString stringWithFormat:NSLocalizedStringFromTable(@"Expected content type %@, got %@", @"ZincNetworking", nil), [[self class] acceptableContentTypes], [self.response MIMEType]] forKey:NSLocalizedDescriptionKey];
- self.HTTPError = [[NSError alloc] initWithDomain:ZincNetworkingErrorDomain code:NSURLErrorCannotDecodeContentData userInfo:userInfo];
- }
- }
- }
- }
- if (self.HTTPError) {
- return self.HTTPError;
- } else {
- return [super error];
- }
- }
- - (NSStringEncoding)responseStringEncoding {
- // 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.
- // See http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.4.1
- if (self.response && !self.response.textEncodingName && self.responseData && [self.response respondsToSelector:@selector(allHeaderFields)]) {
- NSString *type = nil;
- GetMediaTypeAndSubtypeWithString([[self.response allHeaderFields] valueForKey:@"Content-Type"], &type, nil);
- if ([type isEqualToString:@"text"]) {
- return NSISOLatin1StringEncoding;
- }
- }
- return [super responseStringEncoding];
- }
- - (void)pause {
- unsigned long long offset = 0;
- if ([self.outputStream propertyForKey:NSStreamFileCurrentOffsetKey]) {
- offset = [[self.outputStream propertyForKey:NSStreamFileCurrentOffsetKey] unsignedLongLongValue];
- } else {
- offset = [[self.outputStream propertyForKey:NSStreamDataWrittenToMemoryStreamKey] length];
- }
- NSMutableURLRequest *mutableURLRequest = [self.request mutableCopy];
- if ([self.response respondsToSelector:@selector(allHeaderFields)] && [[self.response allHeaderFields] valueForKey:@"ETag"]) {
- [mutableURLRequest setValue:[[self.response allHeaderFields] valueForKey:@"ETag"] forHTTPHeaderField:@"If-Range"];
- }
- [mutableURLRequest setValue:[NSString stringWithFormat:@"bytes=%llu-", offset] forHTTPHeaderField:@"Range"];
- self.request = mutableURLRequest;
- [super pause];
- }
- - (BOOL)hasAcceptableStatusCode {
- if (!self.response) {
- return NO;
- }
- NSUInteger statusCode = ([self.response isKindOfClass:[NSHTTPURLResponse class]]) ? (NSUInteger)[self.response statusCode] : 200;
- return ![[self class] acceptableStatusCodes] || [[[self class] acceptableStatusCodes] containsIndex:statusCode];
- }
- - (BOOL)hasAcceptableContentType {
- if (!self.response) {
- return NO;
- }
- // 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".
- // See http://www.w3.org/Protocols/rfc2616/rfc2616-sec7.html
- NSString *contentType = [self.response MIMEType];
- if (!contentType) {
- contentType = @"application/octet-stream";
- }
- return ![[self class] acceptableContentTypes] || [[[self class] acceptableContentTypes] containsObject:contentType];
- }
- - (void)setSuccessCallbackQueue:(dispatch_queue_t)successCallbackQueue {
- if (successCallbackQueue != _successCallbackQueue) {
- if (_successCallbackQueue) {
- #if !OS_OBJECT_USE_OBJC
- dispatch_release(_successCallbackQueue);
- #endif
- _successCallbackQueue = NULL;
- }
- if (successCallbackQueue) {
- #if !OS_OBJECT_USE_OBJC
- dispatch_retain(successCallbackQueue);
- #endif
- _successCallbackQueue = successCallbackQueue;
- }
- }
- }
- - (void)setFailureCallbackQueue:(dispatch_queue_t)failureCallbackQueue {
- if (failureCallbackQueue != _failureCallbackQueue) {
- if (_failureCallbackQueue) {
- #if !OS_OBJECT_USE_OBJC
- dispatch_release(_failureCallbackQueue);
- #endif
- _failureCallbackQueue = NULL;
- }
- if (failureCallbackQueue) {
- #if !OS_OBJECT_USE_OBJC
- dispatch_retain(failureCallbackQueue);
- #endif
- _failureCallbackQueue = failureCallbackQueue;
- }
- }
- }
- - (void)setCompletionBlockWithSuccess:(void (^)(ZincHTTPURLConnectionOperation *operation, id responseObject))success
- failure:(void (^)(ZincHTTPURLConnectionOperation *operation, NSError *error))failure
- {
- // completionBlock is manually nilled out in ZincURLConnectionOperation to break the retain cycle.
- #pragma clang diagnostic push
- #pragma clang diagnostic ignored "-Warc-retain-cycles"
- #pragma clang diagnostic ignored "-Wgnu"
- self.completionBlock = ^{
- if (self.error) {
- if (failure) {
- dispatch_async(self.failureCallbackQueue ?: dispatch_get_main_queue(), ^{
- failure(self, self.error);
- });
- }
- } else {
- if (success) {
- dispatch_async(self.successCallbackQueue ?: dispatch_get_main_queue(), ^{
- success(self, self.responseData);
- });
- }
- }
- };
- #pragma clang diagnostic pop
- }
- #pragma mark - ZincHTTPURLConnectionOperation
- + (NSIndexSet *)acceptableStatusCodes {
- return [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(200, 100)];
- }
- + (void)addAcceptableStatusCodes:(NSIndexSet *)statusCodes {
- NSMutableIndexSet *mutableStatusCodes = [[NSMutableIndexSet alloc] initWithIndexSet:[self acceptableStatusCodes]];
- [mutableStatusCodes addIndexes:statusCodes];
- SwizzleClassMethodWithClassAndSelectorUsingBlock([self class], @selector(acceptableStatusCodes), ^(__unused id _self) {
- return mutableStatusCodes;
- });
- }
- + (NSSet *)acceptableContentTypes {
- return nil;
- }
- + (void)addAcceptableContentTypes:(NSSet *)contentTypes {
- NSMutableSet *mutableContentTypes = [[NSMutableSet alloc] initWithSet:[self acceptableContentTypes] copyItems:YES];
- [mutableContentTypes unionSet:contentTypes];
- SwizzleClassMethodWithClassAndSelectorUsingBlock([self class], @selector(acceptableContentTypes), ^(__unused id _self) {
- return mutableContentTypes;
- });
- }
- + (BOOL)canProcessRequest:(NSURLRequest *)request {
- if ([[self class] isEqual:[ZincHTTPURLConnectionOperation class]]) {
- return YES;
- }
- return [[self acceptableContentTypes] intersectsSet:ZincContentTypesFromHTTPHeader([request valueForHTTPHeaderField:@"Accept"])];
- }
- @end
- #pragma clang diagnostic pop