diff --git a/MKNetworkKit/Categories/NSDictionary+RequestEncoding.h b/MKNetworkKit/Categories/NSDictionary+RequestEncoding.h index 7bb0535..d67e6ff 100644 --- a/MKNetworkKit/Categories/NSDictionary+RequestEncoding.h +++ b/MKNetworkKit/Categories/NSDictionary+RequestEncoding.h @@ -26,4 +26,6 @@ @interface NSDictionary (RequestEncoding) -(NSString*) urlEncodedKeyValueString; +-(NSString*) jsonEncodedKeyValueString; +-(NSString*) plistEncodedKeyValueString; @end diff --git a/MKNetworkKit/Categories/NSDictionary+RequestEncoding.m b/MKNetworkKit/Categories/NSDictionary+RequestEncoding.m index e3c276d..84c278e 100644 --- a/MKNetworkKit/Categories/NSDictionary+RequestEncoding.m +++ b/MKNetworkKit/Categories/NSDictionary+RequestEncoding.m @@ -32,7 +32,7 @@ -(NSString*) urlEncodedKeyValueString { NSMutableString *string = [NSMutableString string]; for (NSString *key in self) { - + NSObject *value = [self valueForKey:key]; if([value isKindOfClass:[NSString class]]) [string appendFormat:@"%@=%@&", [key urlEncodedString], [((NSString*)value) urlEncodedString]]; @@ -47,5 +47,34 @@ -(NSString*) urlEncodedKeyValueString { } +-(NSString*) jsonEncodedKeyValueString { + + if([NSJSONSerialization class]) { + NSError *error = nil; + NSData *data = [NSJSONSerialization dataWithJSONObject:self + options:0 // non-pretty printing + error:&error]; + if(error) + DLog(@"JSON Parsing Error: %@", error); + + return [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; + } else { + + DLog(@"JSON encoder missing, falling back to URL encoding"); + return [self urlEncodedKeyValueString]; + } +} + +-(NSString*) plistEncodedKeyValueString { + + NSError *error = nil; + NSData *data = [NSPropertyListSerialization dataWithPropertyList:self + format:NSPropertyListXMLFormat_v1_0 + options:0 error:&error]; + if(error) + DLog(@"JSON Parsing Error: %@", error); + + return [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; +} @end diff --git a/MKNetworkKit/MKNetworkOperation.h b/MKNetworkKit/MKNetworkOperation.h index 2382bff..c08a7b1 100644 --- a/MKNetworkKit/MKNetworkOperation.h +++ b/MKNetworkKit/MKNetworkOperation.h @@ -36,6 +36,14 @@ typedef void (^MKNKErrorBlock)(NSError* error); typedef void (^MKNKAuthBlock)(NSURLAuthenticationChallenge* challenge); +typedef NSString* (^MKNKEncodingBlock) (NSDictionary* postDataDict); + +typedef enum { + + MKNKPostDataEncodingTypeURL = 0, // default + MKNKPostDataEncodingTypeJSON, + MKNKPostDataEncodingTypePlist, +} MKNKPostDataEncodingType; /*! @header MKNetworkOperation.h @abstract Represents a single unique network operation. @@ -127,15 +135,47 @@ typedef void (^MKNKAuthBlock)(NSURLAuthenticationChallenge* challenge); * This property is readonly cannot be modified. */ @property (nonatomic, assign, readonly) NSInteger HTTPStatusCode; + +/*! + * @abstract Post Data Encoding Type Property + * @property postDataEncoding + * + * @discussion + * Specifies which type of encoding should be used to encode post data. + * MKNKPostDataEncodingTypeURL is the default which defaults to application/x-www-form-urlencoded + * MKNKPostDataEncodingTypeJSON uses JSON encoding. + * JSON Encoding is supported only in iOS 5 or Mac OS X 10.7 and above. + * On older operating systems, JSON Encoding reverts back to URL Encoding + * You can use the postDataEncodingHandler to provide a custom postDataEncoding + * For example, JSON encoding using a third party library. + * + * @seealso + * setCustomPostDataEncodingHandler:forType: + * + */ +@property (nonatomic, assign) MKNKPostDataEncodingType postDataEncoding; + +/*! + * @abstract Set a customized Post Data Encoding Handler for a given HTTP Content Type + * + * @discussion + * If you need customized post data encoding support, provide a block method here. + * This block method will be invoked only when your HTTP Method is POST or PUT + * For default URL encoding or JSON encoding, use the property postDataEncoding + * If you change the postData format, it's your responsiblity to provide a correct Content-Type. + * + * @seealso + * postDataEncoding + */ + +-(void) setCustomPostDataEncodingHandler:(MKNKEncodingBlock) postDataEncodingHandler forType:(NSString*) contentType; + /*! * @abstract String Encoding Property * @property stringEncoding * * @discussion - * Creates an operation with the given URL string. - * The default headers you specified in your MKNetworkEngine subclass gets added to the headers - * The params dictionary in this method gets attached to the URL as query parameters if the HTTP Method is GET/DELETE - * The params dictionary is attached to the body if the HTTP Method is POST/PUT + * Specifies which type of encoding should be used to encode URL strings */ @property (nonatomic, assign) NSStringEncoding stringEncoding; @@ -294,6 +334,15 @@ typedef void (^MKNKAuthBlock)(NSURLAuthenticationChallenge* challenge); */ -(void) onDownloadProgressChanged:(MKNKProgressBlock) downloadProgressBlock; +/*! + * @abstract Uploads a resource from a stream + * + * @discussion + * This method can be used to upload a resource for a post body directly from a stream. + * + */ +-(void) setUploadStream:(NSInputStream*) inputStream; + /*! * @abstract Downloads a resource directly to a file or any output stream * @@ -303,7 +352,7 @@ typedef void (^MKNKAuthBlock)(NSURLAuthenticationChallenge* challenge); * A stream cannot be removed after it is added. * */ --(void) setDownloadStream:(NSOutputStream*) outputStream; +-(void) addDownloadStream:(NSOutputStream*) outputStream; /*! * @abstract Helper method to check if the response is from cache @@ -323,7 +372,7 @@ typedef void (^MKNKAuthBlock)(NSURLAuthenticationChallenge* challenge); * This method is used for accessing the downloaded data. If the operation is still in progress, the method returns nil instead of partial data. To access partial data, use a downloadStream. * * @seealso - * setDownloadStream: + * addDownloadStream: */ -(NSData*) responseData; @@ -334,7 +383,7 @@ typedef void (^MKNKAuthBlock)(NSURLAuthenticationChallenge* challenge); * This method is used for accessing the downloaded data. If the operation is still in progress, the method returns nil instead of partial data. To access partial data, use a downloadStream. The method also converts the responseData to a NSString using the stringEncoding specified in the operation * * @seealso - * setDownloadStream: + * addDownloadStream: * stringEncoding */ -(NSString*)responseString; @@ -355,7 +404,7 @@ typedef void (^MKNKAuthBlock)(NSURLAuthenticationChallenge* challenge); * This method is used for accessing the downloaded data. If the operation is still in progress, the method returns nil instead of partial data. To access partial data, use a downloadStream. The method also converts the responseData to a NSString using the stringEncoding specified in the parameter * * @seealso - * setDownloadStream: + * addDownloadStream: * stringEncoding */ -(NSString*) responseStringWithEncoding:(NSStringEncoding) encoding; @@ -367,7 +416,7 @@ typedef void (^MKNKAuthBlock)(NSURLAuthenticationChallenge* challenge); * This method is used for accessing the downloaded data as a UIImage. If the operation is still in progress, the method returns nil instead of a partial image. To access partial data, use a downloadStream. If the response is not a valid image, this method returns nil. This method doesn't obey the response mime type property. If the server response with a proper image data but set the mime type incorrectly, this method will still be able access the response as an image. * * @seealso - * setDownloadStream: + * addDownloadStream: */ #if TARGET_OS_IPHONE -(UIImage*) responseImage; @@ -376,7 +425,6 @@ typedef void (^MKNKAuthBlock)(NSURLAuthenticationChallenge* challenge); -(NSXMLDocument*) responseXML; #endif -#ifdef __IPHONE_5_0 /*! * @abstract Helper method to retrieve the contents as a NSDictionary or NSArray depending on the JSON contents * @@ -384,10 +432,9 @@ typedef void (^MKNKAuthBlock)(NSURLAuthenticationChallenge* challenge); * This method is used for accessing the downloaded data as a NSDictionary or an NSArray. If the operation is still in progress, the method returns nil. If the response is not a valid JSON, this method returns nil. * * @availability - * iOS 5 and above + * iOS 5 and above or Mac OS 10.7 and above */ -(id) responseJSON; -#endif /*! * @abstract Overridable custom method where you can add your custom business logic error handling diff --git a/MKNetworkKit/MKNetworkOperation.m b/MKNetworkKit/MKNetworkOperation.m index bc27d31..f0c8490 100644 --- a/MKNetworkKit/MKNetworkOperation.m +++ b/MKNetworkKit/MKNetworkOperation.m @@ -48,18 +48,20 @@ @interface MKNetworkOperation (/*Private Methods*/) @property (strong, nonatomic) NSString *username; @property (strong, nonatomic) NSString *password; -@property (nonatomic, retain) NSMutableArray *responseBlocks; -@property (nonatomic, retain) NSMutableArray *errorBlocks; +@property (nonatomic, strong) NSMutableArray *responseBlocks; +@property (nonatomic, strong) NSMutableArray *errorBlocks; @property (nonatomic, assign) MKNetworkOperationState state; @property (nonatomic, assign) BOOL isCancelled; @property (strong, nonatomic) NSMutableData *mutableData; -@property (nonatomic, retain) NSMutableArray *uploadProgressChangedHandlers; -@property (nonatomic, retain) NSMutableArray *downloadProgressChangedHandlers; -@property (nonatomic, retain) NSMutableArray *downloadStreams; -@property (nonatomic, retain) NSData *cachedResponse; +@property (nonatomic, strong) NSMutableArray *uploadProgressChangedHandlers; +@property (nonatomic, strong) NSMutableArray *downloadProgressChangedHandlers; +@property (nonatomic, copy) MKNKEncodingBlock postDataEncodingHandler; + +@property (nonatomic, strong) NSMutableArray *downloadStreams; +@property (nonatomic, strong) NSData *cachedResponse; @property (nonatomic, copy) MKNKResponseBlock cacheHandlingBlock; #if TARGET_OS_IPHONE @property (nonatomic, assign) UIBackgroundTaskIdentifier backgroundTaskId; @@ -74,9 +76,13 @@ - (id)initWithURLString:(NSString *)aURLString -(NSData*) bodyData; -(BOOL) isCacheable; +-(NSString*) encodedPostDataString; + @end @implementation MKNetworkOperation +@synthesize postDataEncoding = _postDataEncoding; +@synthesize postDataEncodingHandler = _postDataEncodingHandler; @synthesize stringEncoding = _stringEncoding; @dynamic freezable; @@ -128,6 +134,29 @@ -(BOOL) isCacheable { return [self.request.HTTPMethod isEqualToString:@"GET"]; } +-(NSString*) encodedPostDataString { + + NSString *returnValue = @""; + if(self.postDataEncodingHandler) + returnValue = self.postDataEncodingHandler(self.fieldsToBePosted); + else if(self.postDataEncoding == MKNKPostDataEncodingTypeURL) + returnValue = [self.fieldsToBePosted urlEncodedKeyValueString]; + else if(self.postDataEncoding == MKNKPostDataEncodingTypeJSON) + returnValue = [self.fieldsToBePosted jsonEncodedKeyValueString]; + else if(self.postDataEncoding == MKNKPostDataEncodingTypePlist) + returnValue = [self.fieldsToBePosted plistEncodedKeyValueString]; + return returnValue; +} + +-(void) setCustomPostDataEncodingHandler:(MKNKEncodingBlock) postDataEncodingHandler forType:(NSString*) contentType { + + NSString *charset = (__bridge NSString *)CFStringConvertEncodingToIANACharSetName(CFStringConvertNSStringEncodingToEncoding(self.stringEncoding)); + + self.postDataEncodingHandler = postDataEncodingHandler; + [self.request addValue: + [NSString stringWithFormat:@"%@; charset=%@", contentType, charset] + forHTTPHeaderField:@"Content-Type"]; +} //=========================================================== // freezable //=========================================================== @@ -152,7 +181,7 @@ -(NSHTTPURLResponse*) readonlyResponse { } - (NSDictionary *) readonlyPostDictionary { - + return [self.fieldsToBePosted copy]; } @@ -412,7 +441,13 @@ -(void) onDownloadProgressChanged:(MKNKProgressBlock) downloadProgressBlock { [self.downloadProgressChangedHandlers addObject:[downloadProgressBlock copy]]; } --(void) setDownloadStream:(NSOutputStream*) outputStream { +-(void) setUploadStream:(NSInputStream*) inputStream { + +#warning Method not tested yet. + self.request.HTTPBodyStream = inputStream; +} + +-(void) addDownloadStream:(NSOutputStream*) outputStream { [self.downloadStreams addObject:outputStream]; } @@ -449,7 +484,7 @@ - (id)initWithURLString:(NSString *)aURLString [method isEqualToString:@"DELETE"]) && (params && [params count] > 0)) { finalURL = [NSURL URLWithString:[NSString stringWithFormat:@"%@?%@", aURLString, - [params urlEncodedKeyValueString]]]; + [self encodedPostDataString]]]; } else { finalURL = [NSURL URLWithString:aURLString]; } @@ -473,11 +508,37 @@ - (id)initWithURLString:(NSString *)aURLString if (([method isEqualToString:@"POST"] || [method isEqualToString:@"PUT"]) && (params && [params count] > 0)) { - [self.request addValue: - [NSString stringWithFormat:@"application/x-www-form-urlencoded; charset=%@", charset] - forHTTPHeaderField:@"Content-Type"]; + switch (self.postDataEncoding) { + + case MKNKPostDataEncodingTypeURL: { + [self.request addValue: + [NSString stringWithFormat:@"application/x-www-form-urlencoded; charset=%@", charset] + forHTTPHeaderField:@"Content-Type"]; + } + break; + case MKNKPostDataEncodingTypeJSON: { + if([NSJSONSerialization class]) { + [self.request addValue: + [NSString stringWithFormat:@"application/json; charset=%@", charset] + forHTTPHeaderField:@"Content-Type"]; + } else { + [self.request addValue: + [NSString stringWithFormat:@"application/x-www-form-urlencoded; charset=%@", charset] + forHTTPHeaderField:@"Content-Type"]; + } + } + break; + case MKNKPostDataEncodingTypePlist: { + [self.request addValue: + [NSString stringWithFormat:@"application/x-plist; charset=%@", charset] + forHTTPHeaderField:@"Content-Type"]; + } + + default: + break; + } } - + self.state = MKNetworkOperationStateReady; } @@ -590,7 +651,7 @@ -(NSData*) bodyData { if([self.filesToBePosted count] == 0 && [self.dataToBePosted count] == 0) { - return [[self.fieldsToBePosted urlEncodedKeyValueString] dataUsingEncoding:self.stringEncoding]; + return [[self encodedPostDataString] dataUsingEncoding:self.stringEncoding]; } NSString *boundary = @"0xKhTmLbOuNdArY"; @@ -787,7 +848,7 @@ - (void)connection:(NSURLConnection *)connection willSendRequestForAuthenticatio [challenge.sender useCredential:credential forAuthenticationChallenge:challenge]; } else if ((challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodClientCertificate) && self.clientCertificate) { - + NSData *certData = [[NSData alloc] initWithContentsOfFile:self.clientCertificate]; #warning method not implemented. Don't use client certicate authentication for now. @@ -808,32 +869,32 @@ - (void)connection:(NSURLConnection *)connection willSendRequestForAuthenticatio SecTrustRef serverTrust = [[challenge protectionSpace] serverTrust]; SecTrustResultType result; SecTrustEvaluate(serverTrust, &result); - + if(result == kSecTrustResultProceed) { - [challenge.sender useCredential:[NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust] forAuthenticationChallenge:challenge]; + [challenge.sender useCredential:[NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust] forAuthenticationChallenge:challenge]; } else if(result == kSecTrustResultConfirm) { // ask user BOOL userOkWithWrongCert = NO; // (ACTUALLY CHEAT., DON'T BE A F***ING BROWSER, USERS ALWAYS TAP YES WHICH IS RISKY) if(userOkWithWrongCert) { - + // Cert not trusted, but user is OK with that [challenge.sender useCredential:[NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust] forAuthenticationChallenge:challenge]; } else { - + // Cert not trusted, and user is not OK with that. Don't proceed [challenge.sender continueWithoutCredentialForAuthenticationChallenge:challenge]; } } else { - + // invalid or revoked certificate [challenge.sender continueWithoutCredentialForAuthenticationChallenge:challenge]; } } else if (self.authHandler) { - + // forward the authentication to the view controller that created this operation // If this happens for NSURLAuthenticationMethodHTMLForm, you have to // do some shit work like showing a modal webview controller and close it after authentication. @@ -1028,16 +1089,19 @@ -(NSXMLDocument*) responseXML { } #endif -#ifdef __IPHONE_5_0 -(id) responseJSON { - if([self responseData] == nil) return nil; - NSError *error = nil; - id returnValue = [NSJSONSerialization JSONObjectWithData:[self responseData] options:0 error:&error]; - DLog(@"JSON Parsing Error: %@", error); - return returnValue; + if([NSJSONSerialization class]) { + if([self responseData] == nil) return nil; + NSError *error = nil; + id returnValue = [NSJSONSerialization JSONObjectWithData:[self responseData] options:0 error:&error]; + DLog(@"JSON Parsing Error: %@", error); + return returnValue; + } else { + DLog("No valid JSON Serializers found"); + return [self responseString]; + } } -#endif #pragma mark - diff --git a/README.mdown b/README.mdown index 7351349..453e8ae 100644 --- a/README.mdown +++ b/README.mdown @@ -80,8 +80,6 @@ Gentle Bytes appledoc@gentlebytes.com --- ###TODO -* Stream post body 0.85 -* Multiple encoding types in URL * Fix and test Client certificate/Server trust auth * Fix AppleDoc warnings @@ -90,6 +88,7 @@ Gentle Bytes appledoc@gentlebytes.com * Scheduling in current runloop * Test multiple file attachments in a single request +* Test streaming I'll be working on this soon. But you can start forking it and getting it right diff --git a/iOS-Demo/MKNetworkKitDemo/AppDelegate.h b/iOS-Demo/MKNetworkKitDemo/AppDelegate.h index c9f0ffb..5128963 100644 --- a/iOS-Demo/MKNetworkKitDemo/AppDelegate.h +++ b/iOS-Demo/MKNetworkKitDemo/AppDelegate.h @@ -44,5 +44,5 @@ @end -#define kTwitterUserName @"" -#define kTwitterPassword @"" \ No newline at end of file +#define kTwitterUserName @"mksg" +#define kTwitterPassword @"fQJz74jGAs2hs2m" \ No newline at end of file diff --git a/iOS-Demo/MKNetworkKitDemo/ExampleDownloader.m b/iOS-Demo/MKNetworkKitDemo/ExampleDownloader.m index 45ae401..6d01488 100644 --- a/iOS-Demo/MKNetworkKitDemo/ExampleDownloader.m +++ b/iOS-Demo/MKNetworkKitDemo/ExampleDownloader.m @@ -34,7 +34,7 @@ -(MKNetworkOperation*) downloadFatAssFileFrom:(NSString*) remoteURL toFile:(NSSt params:nil httpMethod:@"GET"]; - [op setDownloadStream:[NSOutputStream outputStreamToFileAtPath:filePath + [op addDownloadStream:[NSOutputStream outputStreamToFileAtPath:filePath append:YES]]; [self enqueueOperation:op];