iOS - Multipart POST

The multipart POST required me to enhance the base class which communicates with the server: ContentBroker. Specifically, I had to change the way I constructed the content and how it was sent to the server.

Note: The original class is described in detail here. The comments below will focus on new functionality only.

ContentBroker.h

  1. #import <Foundation/Foundation.h>
  2. #import "ContentResponse.h"
  3. #import "ContentBrokerDelegate.h"
  4.  
  5. #define CB_CANCEL -1
  6. #define CB_NONE 0
  7. #define CB_LIST 1
  8. #define CB_READ 2
  9. #define CB_CREATE_JSON 3
  10. #define CB_CREATE_MULTIPART 4
  11. #define CB_DELETE 5
  12. #define CB_UPDATE 6
  13.  
  14. #define TRUSTED_HOST @"{insert your server name here}"
  15. #define BASE_URL @"https://" TRUSTED_HOST @":8443/fishingserver/"
  16. #define LOCATION_BASE_URL ( BASE_URL @"locations.json?action=" )
  17. #define FISH_BASE_URL ( BASE_URL @"fish.json?action=" )
  18. #define CATCH_BASE_URL ( BASE_URL @"catches.json?action=" )
  19.  
  20. @interface ContentBroker : NSObject<NSURLConnectionDelegate>
  21.  
  22. @property (nonatomic, assign) id<ContentBrokerDelegate> delegate;
  23. @property BOOL allowSSLBypass;
  24. @property NSString *userId;
  25. @property NSString *token;
  26.  
  27. @property int action;
  28.  
  29. -(void)loadContent;
  30. -(void) postContent;
  31. -(NSString *) getEncodedString: (NSString *)rawString;
  32. -(NSMutableData *) getBody: (NSString *) curBoundary;
  33. -(NSString *) getContentURL;
  34. -(ContentResponse *) parseContents: (NSString *)rawJSON;
  35. @end

Comments:

  • Lines 9, 10: It became necessary to differentiate between sending JSON data to the server, and multipart data. So, I split CB_CREATE into two separate actions.
  • Lines 14 - 18: A convenient place to define the various endpoints of the fishing server. I could have placed them in a separate config file, but since the changes are few, I think it makes sense to keep them in code.

The remaining methods will be described in the implementation file.

ContentBroker.m

  1. #import "ContentBroker.h"
  2.  
  3. @implementation ContentBroker {
  4. NSMutableData *contentData;
  5. NSURLConnection *conn;
  6. }
  7.  
  8. @synthesize delegate, allowSSLBypass, userId, token, action;
  9.  
  10. -(void) loadContent {
  11. contentData = [NSMutableData data];
  12. NSString *contentURL = [self getContentURL];
  13. NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:contentURL]];
  14. conn = [[NSURLConnection alloc] initWithRequest: request delegate:self];
  15.  
  16. }
  17.  
  18. // If this is a post instead of a get, use this method
  19. -(void) postContent {
  20. contentData = [NSMutableData data];
  21. NSString *contentURL = [self getContentURL];
  22. NSMutableURLRequest *request = [[NSMutableURLRequest alloc] init];
  23. [request setURL:[NSURL URLWithString:contentURL]];
  24. [request setHTTPMethod:@"POST"];
  25.  
  26. NSString *boundary = @"0xToDaYiSaFuNdAyOhSaYcAnUcEe";
  27. NSString *contentType = [NSString stringWithFormat:@"multipart/form-data; boundary=%@", boundary];
  28. [request addValue:contentType forHTTPHeaderField:@"Content-Type"];
  29.  
  30. NSMutableData *body = [self getBody: boundary];
  31. [request setHTTPBody:body];
  32.  
  33. conn = [[NSURLConnection alloc] initWithRequest:request delegate:self];
  34. }
  35.  
  36. -(NSMutableData *) getBody: (NSString *) curBoundary {
  37. return nil;
  38. }
  39.  
  40. -(NSString *) getEncodedString: (NSString *)rawString {
  41. NSString *encodedString = (NSString *)
  42. CFBridgingRelease(CFURLCreateStringByAddingPercentEscapes(
  43. NULL, (CFStringRef)rawString, NULL, (CFStringRef)@"!*'();:@&=+$,/?%#[]",
  44. kCFStringEncodingUTF8 ));
  45. return encodedString;
  46. }
  47.  
  48.  
  49. - (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
  50. [contentData appendData:data];
  51. }
  52.  
  53. - (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
  54. NSLog(@"Bad: %@", [error description]);
  55. ContentResponse *response = [[ContentResponse alloc]initWithRc:-999 andDescr:@"error" andAction:0];
  56. if (delegate != nil) {
  57. [delegate contentLoaded: response];
  58. }
  59.  
  60. conn = nil;
  61. }
  62.  
  63. // ------------ If specified, TEMPORARILY allow all SSL connections ----------
  64. -(BOOL)connection:(NSURLConnection *)connection canAuthenticateAgainstProtectionSpace:
  65. (NSURLProtectionSpace *)protectionSpace {
  66. return [protectionSpace.authenticationMethod
  67. isEqualToString:NSURLAuthenticationMethodServerTrust];
  68. }
  69.  
  70. -(void)connection:(NSURLConnection *)connection didReceiveAuthenticationChallenge:
  71. (NSURLAuthenticationChallenge *)challenge {
  72. if (([challenge.protectionSpace.authenticationMethod
  73. isEqualToString:NSURLAuthenticationMethodServerTrust]) &&
  74. ([self allowSSLBypass])) {
  75. if ([challenge.protectionSpace.host isEqualToString:TRUSTED_HOST]) {
  76. NSLog(@"Allowing bypass...");
  77. NSURLCredential *credential = [NSURLCredential credentialForTrust:
  78. challenge.protectionSpace.serverTrust];
  79. [challenge.sender useCredential:credential
  80. forAuthenticationChallenge:challenge];
  81. }
  82. }
  83. [challenge.sender continueWithoutCredentialForAuthenticationChallenge:challenge];
  84. }
  85. // --------------------------------------------------------------------------------
  86.  
  87.  
  88. - (void)connectionDidFinishLoading:(NSURLConnection *)connection {
  89. NSString *loadedContent = [[NSString alloc] initWithData:
  90. contentData encoding:NSUTF8StringEncoding];
  91. NSLog(@"Loaded content: %@",loadedContent);
  92. ContentResponse *response = [self parseContents:loadedContent];
  93. if (delegate != nil) {
  94. [delegate contentLoaded: response];
  95. }
  96. }
  97.  
  98. // --------- ***** Override these in the subclass ******* ----------------------------
  99.  
  100. -(NSString *) getContentURL {
  101. NSString *contentURL = [NSString stringWithFormat:@"<call get url here %@ %@>", userId, token];
  102. return contentURL;
  103. }
  104.  
  105. -(ContentResponse *) parseContents: (NSString *)rawJSON {
  106. return nil;
  107. }
  108. @end

Comments:

  • Lines 18 - 34: Construct a multipart HTTP POST.
  • Line 21: The method getContentURL: should be overridden in each subclass so that the proper endpoint can be obtained. (See the CatchBroker class for details.)
  • Line 26: Each field within a multipart POST must have a boundary. The trick is to make this unique enough that there's no chance the data will occur within one of the fields.
  • Line 30: The method getBody: should be overridden in each subclass so that the contents can be formatted as needed for each type of request. (See the CatchBroker class for details.)
  • Lines 36 - 38: The default implementation of getBody:

Now that the base class has been updated, it's necessary to enhance CatchBroker to properly format a multipart POST.

CatchBroker.h

  1. #import "ContentBroker.h"
  2. #import "Catch.h"
  3.  
  4. @interface CatchBroker : ContentBroker
  5. -(void)setContent: (Catch *)_item;
  6. @end

CatchBroker.m

  1. #import "CatchBroker.h"
  2. #import "User.h"
  3.  
  4. @implementation CatchBroker {
  5. Catch *item;
  6. }
  7.  
  8. -(NSString *) getContentURL {
  9. switch ([self action]) {
  10. case CB_CREATE_MULTIPART: {
  11. return [NSString stringWithFormat: @"%@catches/add.htm", CATCH_BASE_URL];
  12. }
  13. case CB_LIST:
  14. return [NSString stringWithFormat:
  15. @"%@list&userid=%@&token=%@", CATCH_BASE_URL, [self userId], [self token]];
  16. default:
  17. return nil;
  18. }
  19. }
  20.  
  21. -(void)setContent: (Catch *)_item {
  22. item = _item;
  23. }
  24.  
  25. -(NSMutableData *) getBody: (NSString *) curBoundary {
  26. NSMutableString *sBody = [[NSMutableString alloc] init];
  27. [sBody appendString:[NSString stringWithFormat:@"--%@\r\n", curBoundary]];
  28. long niceTime = item.catchDate.timeIntervalSince1970;
  29. [sBody appendString:[NSString stringWithFormat:@"Content-Disposition: form-data; name=\"catchDate\"\r\n\r\n"]];
  30. [sBody appendString:[NSString stringWithFormat:@"%ld", niceTime]];
  31. [sBody appendString:[NSString stringWithFormat:@"\r\n--%@\r\n", curBoundary]];
  32.  
  33. [sBody appendString:[NSString stringWithFormat:@"Content-Disposition: form-data; name=\"locationId\"\r\n\r\n"]];
  34. [sBody appendString:[NSString stringWithFormat:@"%ld", item.location.ID]];
  35. [sBody appendString:[NSString stringWithFormat:@"\r\n--%@\r\n", curBoundary]];
  36.  
  37. [sBody appendString:[NSString stringWithFormat:@"Content-Disposition: form-data; name=\"customLat\"\r\n\r\n"]];
  38. [sBody appendString:[NSString stringWithFormat:@"%f", item.customLat]];
  39. [sBody appendString:[NSString stringWithFormat:@"\r\n--%@\r\n", curBoundary]];
  40.  
  41. [sBody appendString:[NSString stringWithFormat:@"Content-Disposition: form-data; name=\"customLng\"\r\n\r\n"]];
  42. [sBody appendString:[NSString stringWithFormat:@"%f", item.customLng]];
  43. [sBody appendString:[NSString stringWithFormat:@"\r\n--%@\r\n", curBoundary]];
  44.  
  45. [sBody appendString:[NSString stringWithFormat:@"Content-Disposition: form-data; name=\"userId\"\r\n\r\n"]];
  46. [sBody appendString:[NSString stringWithFormat:@"%@", [self userId]]];
  47. [sBody appendString:[NSString stringWithFormat:@"\r\n--%@\r\n", curBoundary]];
  48.  
  49. [sBody appendString:[NSString stringWithFormat:@"Content-Disposition: form-data; name=\"token\"\r\n\r\n"]];
  50. [sBody appendString:[NSString stringWithFormat:@"%@", [self token]]];
  51. [sBody appendString:[NSString stringWithFormat:@"\r\n--%@\r\n", curBoundary]];
  52.  
  53. [sBody appendString:[NSString stringWithFormat:@"Content-Disposition: form-data; name=\"fishId\"\r\n\r\n"]];
  54. [sBody appendString:[NSString stringWithFormat:@"%ld", item.fish.ID]];
  55. [sBody appendString:[NSString stringWithFormat:@"\r\n--%@\r\n", curBoundary]];
  56.  
  57. [sBody appendString:[NSString stringWithFormat:@"Content-Disposition: form-data; name=\"fishLength\"\r\n\r\n"]];
  58. [sBody appendString:[NSString stringWithFormat:@"%f", item.length]];
  59. [sBody appendString:[NSString stringWithFormat:@"\r\n--%@\r\n", curBoundary]];
  60.  
  61. [sBody appendString:[NSString stringWithFormat:@"Content-Disposition: form-data; name=\"fishWeight\"\r\n\r\n"]];
  62. [sBody appendString:[NSString stringWithFormat:@"%f", item.weight]];
  63. [sBody appendString:[NSString stringWithFormat:@"\r\n--%@\r\n", curBoundary]];
  64.  
  65. [sBody appendString:[NSString stringWithFormat:@"Content-Disposition: form-data; name=\"comments\"\r\n\r\n"]];
  66. [sBody appendString:[NSString stringWithFormat:@"%@", item.comments]];
  67. [sBody appendString:[NSString stringWithFormat:@"\r\n--%@\r\n", curBoundary]];
  68.  
  69. [sBody appendString:[NSString stringWithFormat:@"Content-Disposition: form-data; name=\"id\"\r\n\r\n"]];
  70. [sBody appendString:[NSString stringWithFormat:@"%i", 0]];
  71. [sBody appendString:[NSString stringWithFormat:@"\r\n--%@\r\n", curBoundary]];
  72.  
  73. [sBody appendString:[NSString stringWithFormat:@"Content-Disposition: form-data; name=\"submitButton\"\r\n\r\n"]];
  74. [sBody appendString:[NSString stringWithFormat:@"\r\n--%@\r\n", curBoundary]];
  75.  
  76. [sBody appendString:[NSString stringWithFormat:
  77. @"Content-Disposition: form-data; name=\"catchImage\"; filename=\"test.jpg\"\r\n"]];
  78. [sBody appendString:@"Content-Type: image/jpeg\r\n\r\n"];
  79.  
  80. NSMutableData *body = [NSMutableData data];
  81. [body appendData:[sBody dataUsingEncoding:NSUTF8StringEncoding]];
  82.  
  83. NSData *binData = [[NSFileManager defaultManager] contentsAtPath:item.imagePath];
  84.  
  85. [body appendData:binData];
  86. [body appendData:[[NSString stringWithFormat:@"\r\n--%@--\r\n", curBoundary] dataUsingEncoding:NSUTF8StringEncoding]];
  87. return body;
  88. }
  89.  
  90. -(ContentResponse *) parseContents: (NSString *)rawJSON {
  91.  
  92. // A special form-based post and response
  93. if (self.action == CB_CREATE_MULTIPART) {
  94. ContentResponse *response = [[ContentResponse alloc] initWithRc:0 andDescr:rawJSON andAction:[self action]];
  95. return response;
  96. }
  97.  
  98. NSMutableArray *items = [[NSMutableArray alloc] init];
  99. NSError *jsonError = nil;
  100.  
  101. NSDictionary *topJson = (NSDictionary *)
  102. [NSJSONSerialization JSONObjectWithData:[rawJSON dataUsingEncoding:NSUTF8StringEncoding]
  103. options:kNilOptions error:&jsonError];
  104. if (jsonError != nil) {
  105. NSLog(@"JSON Error: %@", jsonError.localizedDescription);
  106. return nil;
  107. }
  108.  
  109. NSDictionary *topLevel = [topJson objectForKey:@"fishingResponse"];
  110. NSNumber *rc = [topLevel objectForKey:@"rc"];
  111. NSString *descr = [topLevel objectForKey:@"descr"];
  112. ContentResponse *response = [[ContentResponse alloc] initWithRc:[rc longValue] andDescr:descr andAction:[self action]];
  113. if ([rc intValue] != 0) {
  114. NSLog(@"Error (%i): %@", [rc intValue], descr);
  115. return response;
  116. }
  117.  
  118. // Delete is a special case. If successful, set the return code to the id that was deleted
  119. if (self.action == CB_DELETE) {
  120. [response setRc:item.ID];
  121. return response;
  122. }
  123.  
  124. NSArray *allItems = [topLevel objectForKey:@"items"];
  125.  
  126. for(NSDictionary *curItem in allItems) {
  127.  
  128. NSNumber *ID = [curItem objectForKey:@"id"];
  129. NSDictionary *owner = [curItem objectForKey:@"owner"];
  130. NSNumber *dateNum = [curItem objectForKey:@"catchDate"];
  131. NSNumber *blurredNum = [curItem objectForKey:@"locationBlurred"];
  132.  
  133. NSNumber *customLat = [curItem objectForKey:@"customLat"];
  134. NSNumber *customLng = [curItem objectForKey:@"customLng"];
  135.  
  136. NSNumber *fishLength = [curItem objectForKey:@"fishLength"];
  137. NSNumber *fishWeight = [curItem objectForKey:@"fishWeight"];
  138. NSString *comments = [curItem objectForKey:@"comments"];
  139.  
  140. // I hate NSNull
  141. NSObject *obj = [curItem objectForKey:@"location"];
  142. NSDictionary *curLoc = nil;
  143. if ((obj != nil) && (obj != [NSNull null])) {
  144. curLoc = (NSDictionary *)obj;
  145. }
  146. obj = [curItem objectForKey:@"fish"];
  147. NSDictionary *curFish = nil;
  148. if ((obj != nil) && (obj != [NSNull null])) {
  149. curFish = (NSDictionary *)obj;
  150. }
  151.  
  152. Catch *x = [[Catch alloc] init];
  153. x.ID = ID.longValue;
  154. NSTimeInterval tt = dateNum.longValue;
  155. x.catchDate = [[NSDate alloc] initWithTimeIntervalSince1970:tt];
  156. x.locationBlurred = blurredNum.boolValue;
  157. x.customLat = customLat.doubleValue;
  158. x.customLng = customLng.doubleValue;
  159. x.weight = fishWeight.floatValue;
  160. x.length = fishLength.floatValue;
  161. x.comments = comments;
  162.  
  163. // Parse out the location
  164. FishingSpot *fs = [[FishingSpot alloc] init];
  165. x.location = fs;
  166. if (curLoc == nil) {
  167. x.location = nil;
  168. } else {
  169. NSNumber *ID = [curLoc objectForKey:@"id"];
  170. fs.ID = ID.longValue;
  171. fs.name = [curLoc objectForKey:@"name"];
  172. fs.owner = [curLoc objectForKey:@"owner"];
  173.  
  174. NSNumber *lat = [curLoc objectForKey:@"lat"];
  175. NSNumber *lng = [curLoc objectForKey:@"lng"];
  176. fs.latitude = lat.doubleValue;
  177. fs.longitude = lng.doubleValue;
  178.  
  179. NSNumber *isPublic = [curLoc objectForKey:@"public"];
  180. fs.isPublic = ([isPublic intValue] == 1 ? YES : NO);
  181. fs.comments = [curLoc objectForKey:@"comments"];
  182.  
  183. }
  184.  
  185. // Parse out the fish
  186. Fish *f = [[Fish alloc] init];
  187. x.fish = f;
  188. if (curFish == nil) {
  189. f.ID = 0;
  190. f.name = @"unspecified";
  191. } else {
  192. NSNumber *ID = [curFish objectForKey:@"id"];
  193. f.ID = ID.longValue;
  194. f.name = [curFish objectForKey:@"name"];
  195. f.owner = [curFish objectForKey:@"owner"];
  196. f.descr = [curFish objectForKey:@"descr"];
  197. f.restrictions = [curFish objectForKey:@"restrictions"];
  198. }
  199.  
  200. // Get the owner
  201. User *u = [[User alloc] init];
  202. u.userId = [owner objectForKey:@"userId"];
  203. u.userName = [owner objectForKey:@"userName"];
  204. u.pictureUrl = [owner objectForKey:@"pictureUrl"];
  205. x.owner = u;
  206.  
  207. [items addObject:x];
  208.  
  209. }
  210. [response setItems:items];
  211. return response;
  212. }
  213.  
  214. @end

Comments:

  • Lines 8 - 19: This method is overridden, and - based upon the action - a properly formatted URL is returned. (This is used by the superclass.)
  • Lines 25 - 88: Format the contents of the multipart POST. (This is where the magic happens.) The basic pattern is to: 1) Output a line that starts with 'Content-Disposition' and the name of the associated field. 2) Output the contents of the field. 3) Output a line boundary.
  • Lines 80 - 81: Convert the string to NSMutableData.
  • Lines 83 - 86: Load the image that was captured as part of this catch, and append it to the contents of the body. Provide the closing boundary string, and return the results to the superclass.
  • Lines 92 - 96: When the content has been sent, and the response is being read, bypass any attempt to parse a JSON response.

When the catch details have been collected, and the user indicates that they want to save the catch to the server, the following method is invoked:

  1. -(IBAction)btnDoneClicked:(id)sender {
  2. [self populateBO];
  3.  
  4. CatchBroker *broker = [[CatchBroker alloc] init];
  5. [broker setDelegate:self];
  6. [broker setContent:curCatch];
  7. [broker setAllowSSLBypass:YES];
  8. [broker setUserId: [eventManager getUserId]];
  9. [broker setToken:[eventManager getToken]];
  10. [broker setAction:CB_CREATE_MULTIPART];
  11. [broker postContent];
  12.  
  13. }

This completes the implementation of a multipart POST on the iOS platform.