Serialize nested image in RestKit (Rails Backend) - objective-c

Here's what I'm getting:
food_name_token Pizza
id 1
category_id 1
review {"note"=>"tasty!", "image"=>"<RKParamsAttachment: 0x7834500>"}
And this is what I'm expecting:
food_name_token Pizza
id 1
category_id 1
review {"note"=>"tasty!", "image"=>{"filename"=>"image", "type"=>"image/png", "name"=>"image", "tempfile"=>"#", "head"=>"Content-Disposition: form-data; name=\"image\"; filename=\"image\"\r\nContent-Type: image/png\r\n"}}
I'm using Restkit and I'm trying to serialize my data on the Iphone and send it to my Rails backend. For the most part, it works, but I just can't seem to get my multipart image to serialize.
The image belongs to a 'Review' class, which in turn is nested in a 'Dish' class. Here is what I have so far:
// Set up routes
RKObjectManager* manager = [RKObjectManager objectManagerWithBaseURL:#"http://postbin.org"];
[manager.router routeClass:[Dish class] toResourcePath:#"/5a850fe1" forMethod:RKRequestMethodPOST];
[manager.router routeClass:[Review class] toResourcePath:#"/5a850fe1" forMethod:RKRequestMethodPOST];
// Map Dish Object
RKObjectMapping *dishMapping = [RKObjectMapping mappingForClass:[NSMutableDictionary class]];
[dishMapping mapKeyPath:#"id" toAttribute:#"identifier"];
[dishMapping mapKeyPath:#"category_id" toAttribute:#"categoryID"];
[dishMapping mapKeyPath:#"food_name_token" toAttribute:#"name"];
// Map Review Object
RKObjectMapping *reviewMapping = [RKObjectMapping mappingForClass:[Review class]];
[reviewMapping mapKeyPath:#"note" toAttribute:#"note"];
[reviewMapping mapKeyPath:#"image" toAttribute:#"image"];
// Define the relationship mapping
[dishMapping mapKeyPath:#"review" toRelationship:#"review" withMapping:reviewMapping];
// Set serialization for Dish Class
RKObjectMapping* dishSerializationMapping = [dishMapping inverseMapping];
[[RKObjectManager sharedManager].mappingProvider setSerializationMapping:dishSerializationMapping forClass:[Dish class] ];
// Set serialization for Review Class
RKObjectMapping* reviewSerializationMapping = [reviewMapping inverseMapping];
[[RKObjectManager sharedManager].mappingProvider setSerializationMapping:reviewSerializationMapping forClass:[Review class] ];
Then I assign the values:
[Dish sharedDish].name = #"Pizza";
[Dish sharedDish].identifier = [NSNumber numberWithInt:1];
[Dish sharedDish].categoryID = [NSNumber numberWithInt:1];
[Review sharedReview].note = #"tasty!";
// Turn an image into an RKParamsAttachment object and assign it to the image attribute of the Review class
RKParams* params = [RKParams params];
UIImage* imageOrig = [UIImage imageNamed:#"pizza.jpg"];
NSData* imageData = UIImagePNGRepresentation(imageOrig);
RKParamsAttachment* image = [params setData:imageData MIMEType:#"image/png" forParam:#"image"];
[params release];
[Review sharedReview].image = image;
And then I assign my Review object to my Dish object's review attribute and send it off:
[Dish sharedDish].review = [Review sharedReview];
[[RKObjectManager sharedManager] postObject:[Dish sharedDish] delegate:self];
It seems like I'm attaching the image to the object the wrong way.
What's the right way to do it?

Did you try manually adding the nesting to the param name:
RKParamsAttachment* image = [params setData:imageData
MIMEType:#"image/png" forParam:#"review[image]"];

Related

How to post without post data using RestKit

I was looking for a way to map an empty object to be posted to an endpoint. The call needs to be a POST, but there shouldn't be any data posted to the endpoint (empty body), it's only about calling the endpoint directly without data.
I've tried doing the same trick as in RestKit: How to handle empty response.body? but using RKRequestDescriptor instead.
Doing so leads to following error when using postData:nil in RKObjectMapping's postObject method:
Uncaught exception: RKRequestDescriptor objects must be initialized with a mapping whose target class is NSMutableDictionary, got 'NSNull' (see [RKObjectMapping requestMapping]);
Using NSNull for the RKRequestDescriptor's mapping seems to work, but nil seems to fail the mapping action.
As the error mentions, it's looking for an NSMutableDictionary for the mapping action. so using an empty NSMutableDictionary like #{} instead of nil for postObject does the trick.
AFRKHTTPClient *client = [self getClient];
RKObjectManager *objectManager = [[RKObjectManager alloc] initWithHTTPClient:client];
RKObjectMapping *requestMapping = [RKObjectMapping mappingForClass:[NSNull class]];
[objectManager addRequestDescriptor:
[RKRequestDescriptor requestDescriptorWithMapping:requestMapping
objectClass:[NSNull class]
rootKeyPath:nil
method:RKRequestMethodAny]];
RKObjectMapping *responseMapping = [RKObjectMapping mappingForClass:[NSNull class]];
[objectManager addResponseDescriptor:
[RKResponseDescriptor responseDescriptorWithMapping:responseMapping
method:RKRequestMethodPOST
pathPattern:nil
keyPath:nil
statusCodes:RKStatusCodeIndexSetForClass(RKStatusCodeClassSuccessful)]];
[objectManager postObject:#{} // <-- this works, but nil doesn't
path:#"/api/some/endpoint"
parameters:nil
success:^(RKObjectRequestOperation *operation, RKMappingResult *mappingResult) {
// succes code here
} failure:^(RKObjectRequestOperation *operation, NSError *error) {
// failure code here
}];

RestKit using cached or old response descriptor

On my main controller, the RESTKIT is working fine:
My code and response descriptor looks like this:
// register mappings with the provider using a response descriptor
RKResponseDescriptor *responseDescriptor =
[RKResponseDescriptor responseDescriptorWithMapping:workOrderMapping
method:RKRequestMethodGET
pathPattern:#"/api/workorder/GetWorkOrderListSimple"
keyPath:nil
statusCodes:nil];
[objectManager addResponseDescriptor:responseDescriptor];
[[RKObjectManager sharedManager] getObjectsAtPath:#"/api/workorder/GetWorkOrderListSimple"
parameters:nil
success:^(RKObjectRequestOperation *operation, RKMappingResult *mappingResult) {
NSLog(#"It Worked");
_workOrders = mappingResult.array;
[self.tableView reloadData];
}
failure:^(RKObjectRequestOperation *operation, NSError *error) {
NSLog(#"error': %#", error);
}];
So this first call works fine, however, on my 2nd controller, it seems to be somehow reusing this old response descriptor, I created a new one, but in the error message it's still referencing GetWorkOrderListSimple, when I clearly told it to use GetWorkOrderDetail.
RKResponseDescriptor *responseDescriptor =
[RKResponseDescriptor responseDescriptorWithMapping:workOrderBigMapping
method:RKRequestMethodGET
pathPattern:#"/api/workorder/GetWorkOrderDetail"
keyPath:nil
statusCodes:nil];
However for some reason, here is my error message, can anyone point me in the right direction for debugging? Thanks!!!
A 200 response was loaded from the URL 'http://xxxxxxx.ws/api/workorder/GetWorkOrderDetail?workOrderId=116194', which failed to match all (1) response descriptors:
http://xxxxxxx.ws pathPattern=/api/workorder/GetWorkOrderListSimple statusCodes=(null)> failed to match: response path '/api/workorder/GetWorkOrderDetail?workOrderId=116194' did not match the path pattern '/api/workorder/GetWorkOrderListSimple'.
I have the same "loading" or "setup" code in the Viewdidload of each view controller, there are two view controllers
I call configureRestKit in every Viewdidload, should I not? Should this be in the app delegate or somewhere else?
I thought since I was configuring the kit in each view controller viewdidload it would be a fresh one every time
- (void)configureRestKit
{
// initialize AFNetworking HTTPClient
NSURL *baseURL = [NSURL URLWithString:#"http://xxxxxxxx.ws"];
AFHTTPClient *client = [[AFHTTPClient alloc] initWithBaseURL:baseURL];
// initialize RestKit
RKObjectManager *objectManager = [[RKObjectManager alloc] initWithHTTPClient:client];
// setup object mappings
RKObjectMapping *workOrderBigMapping = [RKObjectMapping mappingForClass:[WorkOrderBig class]];
[workOrderBigMapping addAttributeMappingsFromArray:#[#"WorkOrderId", #"Job", #"Address", #"Supervisor", #"PO", #"Priority", #"Status", #"ReceivedDate"]];
RKObjectMapping *workOrderDetailMapping = [RKObjectMapping mappingForClass:[WorkOrderDetail class]];
[workOrderDetailMapping addAttributeMappingsFromArray:#[#"WorkOrderDetailId", #"WorkOrderId", #"WorkOrderProblemId", #"DetailDescription", #"ProductId", #"Qty", #"PONumber", #"Code", #"ProductDescription", #"UOM", #"Price", #"OriginalPrice", #"PctMarkup", #"LineItem", #"OriginalTotal", #"TotalPrice"]];
RKObjectMapping *workOrderProblemMapping = [RKObjectMapping mappingForClass:[WorkOrderProblem class]];
[workOrderProblemMapping addAttributeMappingsFromArray:#[#"WorkOrderId", #"WorkOrderProblemId", #"Description", #"SpanishDescription", #"Action", #"LineItem"]];
//Define Relationships
[workOrderBigMapping addPropertyMapping:[RKRelationshipMapping relationshipMappingFromKeyPath:#"Details"
toKeyPath:#"Details"
withMapping:workOrderBigMapping]];
[workOrderBigMapping addPropertyMapping:[RKRelationshipMapping relationshipMappingFromKeyPath:#"Problems"
toKeyPath:#"Problems"
withMapping:workOrderProblemMapping]];
// register mappings with the provider using a response descriptor
RKResponseDescriptor *responseDescriptor =
[RKResponseDescriptor responseDescriptorWithMapping:workOrderBigMapping
method:RKRequestMethodGET
pathPattern:#"/api/workorder/GetWorkOrderDetail"
keyPath:#"/api/workorder/GetWorkOrderDetail"
statusCodes:nil];
[objectManager addResponseDescriptor:responseDescriptor];
- (void)loadWorkOrders
{
NSString *WorkOrderId = [NSString stringWithFormat:#"%i", _workOrderId];
NSMutableDictionary *params =[[NSMutableDictionary alloc] init];
[params setValue:WorkOrderId forKey:#"workOrderId"];
[[RKObjectManager sharedManager] getObjectsAtPath:#"/api/workorder/GetWorkOrderDetail"
parameters:params
success:^(RKObjectRequestOperation *operation, RKMappingResult *mappingResult) {
NSLog(#"It Worked");
_workOrders = mappingResult.array;
//paint screen
WorkOrderBig *mainWorkOrder = [_workOrders objectAtIndex:0];
self.lblWorkOrderId.text = mainWorkOrder.WorkOrderId;
}
failure:^(RKObjectRequestOperation *operation, NSError *error) {
NSLog(#"What do you mean by 'there is no coffee?': %#", error);
}];
}
I call configureRestKit in every Viewdidload, should I not?
Based on your configuration, yes, you should
Should this be in the app delegate or somewhere else?
No, the app delegate is for application level event management, not data configuration
So, your configuration is almost fine, the problem is your usage: [RKObjectManager sharedManager]. When you call sharedManager you will always get back the first object manager that was created - not the one that was 'locally' created.
You should be using an #property on each of your view controllers to store the objectManager that they create and then using self.objectManager instead of [RKObjectManager sharedManager].

Mapping to Multiple Classes in Restkit

I am using RestKit to map data to properties in two classes, Region and Category, for use in two table views, Regions and Categories. However, it would appear that it is only correctly mapping data for the first view that I go to when testing the application. For instance, when I view Categories first, then switch the Regions, my regions are getting stored as objects of type Category. If I view Regions first, then Categories, my categories get mapped as objects of type Region. I can tell that the right information is being retrieved, due the number of objects, but it is not being stored as the correct type. How can I make sure that each mapping request is done correctly? The code for each mapping is below:
Categories:
RKURL *baseURL = [RKURL URLWithBaseURLString:#"MyUrl"];
RKObjectManager *objectManager = [RKObjectManager objectManagerWithBaseURL:baseURL];
objectManager.client.baseURL = baseURL;
RKObjectMapping *categoryMapping = [RKObjectMapping mappingForClass:[Category class]];
[categoryMapping mapKeyPathsToAttributes:#"categoryID", #"categoryID", #"parentID", #"parentID", #"categoryName", #"categoryName", #"childrenCount", #"childrenCount", #"parentCount", #"parentCount", #"catCount", #"catCount", nil];
[objectManager.mappingProvider setMapping:categoryMapping forKeyPath:#""];
Regions:
RKURL *baseURL = [RKURL URLWithBaseURLString:#"MyUrl"];
RKObjectManager *objectManager = [RKObjectManager objectManagerWithBaseURL:baseURL];
objectManager.client.baseURL = baseURL;
NSLog(#"URL Created");
RKObjectMapping *regionMapping = [RKObjectMapping mappingForClass:[Region class]];
[regionMapping mapKeyPathsToAttributes:#"regionHome", #"regionHome", #"regionID", #"regionID", #"regionName", #"regionName", #"parentCount", #"parentCount", #"parentID", #"parentID", #"childrenCount", #"childrenCount", #"parentName", #"parentName", nil];
[objectManager.mappingProvider setMapping:regionMapping forKeyPath:#""];
In my judgment you don't want to leave the key path as #"" in [objectManager.mappingProvider setMapping:regionMapping forKeyPath:#""];. You will probably replace those with #"region" and #"category" so RestKit can tell based on the key path what objects it should expect.
Also, are you using the isKindOfClass: method to distinguish the objects in the objectLoader didLoadObjects method?
Perhaps this will help you:
- (void)objectLoader:(RKObjectLoader*)objectLoader didLoadObjects:(NSArray*)objects {
if ([[objects objectAtIndex:0] isKindOfClass:[Region class]]) {
Region *region = [objects objectAtIndex:0];
}
else if ([[objects objectAtIndex:0] isKindOfClass:[Category class]]) {
Category *category = [objects objectAtIndex:0];
}
}

How to use Restkit to POST JSON and map response

I'm using RestKit for the first time, and its feature-set looks great. I've read the document multiple times now and I'm struggling to find a way to POST JSON params to a feed and map the JSON response. From searching on stackoverflow I found a way to send the JSON params via a GET, but my server only takes POST.
Here is the code I have so far:
RKObjectMapping *issueMapping = [RKObjectMapping mappingForClass:[CDIssue class]];
[objectMapping mapKeyPath:#"issue_id" toAttribute:#"issueId"];
[objectMapping mapKeyPath:#"title" toAttribute:#"issueTitle"];
[objectMapping mapKeyPath:#"description" toAttribute:#"issueDescription"];
RKObjectManager* manager = [RKObjectManager objectManagerWithBaseURL:#"http://restkit.org"];
RKManagedObjectStore* objectStore = [RKManagedObjectStore objectStoreWithStoreFilename:#"News.sqlite"];
objectManager.objectStore = objectStore;
NSDictionary params = [NSDictionary dictionaryWithObjectsAndKeys: #"myUsername", #"username", #"myPassword", #"password", nil];
NSURL *someURL = [objectManager.client URLForResourcePath:#"/feed/getIssues.json" queryParams:params];
[manager loadObjectsAtResourcePath:[someURL absoluteString] objectMapping:objectMapping delegate:self]
From the another stackoverflow thread (http://stackoverflow.com/questions/9102262/do-a-simple-json-post-using-restkit), I know how to do a simple POST request with the following code:
RKClient *myClient = [RKClient sharedClient];
NSMutableDictionary *rpcData = [[NSMutableDictionary alloc] init ];
NSMutableDictionary *params = [[NSMutableDictionary alloc] init];
//User and password params
[params setObject:password forKey:#"password"];
[params setObject:username forKey:#"email"];
//The server ask me for this format, so I set it here:
[rpcData setObject:#"2.0" forKey:#"jsonrpc"];
[rpcData setObject:#"authenticate" forKey:#"method"];
[rpcData setObject:#"" forKey:#"id"];
[rpcData setObject:params forKey:#"params"];
//Parsing rpcData to JSON!
id<RKParser> parser = [[RKParserRegistry sharedRegistry] parserForMIMEType:RKMIMETypeJSON];
NSError *error = nil;
NSString *json = [parser stringFromObject:rpcData error:&error];
//If no error we send the post, voila!
if (!error){
[[myClient post:#"/" params:[RKRequestSerialization serializationWithData:[json dataUsingEncoding:NSUTF8StringEncoding] MIMEType:RKMIMETypeJSON] delegate:self] send];
}
I was hoping someone would help me marry these two code snippets into a workable solution.
To post an object what I do is associate a path to an object. Then use the method postObject from RKObjectManager.
I asume that you have already configured RestKit so you have the base path set and defined the object mapping for your CDIssue as you have in the code that you already have. With that in mind try this code:
//We tell RestKit to asociate a path with our CDIssue class
RKObjectRouter *router = [[RKObjectRouter alloc] init];
[router routeClass:[CDIssue class] toResourcePath:#"/path/to/my/cdissue/" forMethod:RKRequestMethodPOST];
[RKObjectManager sharedManager].router = router;
//We get the mapping for the object that you want, in this case CDIssue assuming you already set that in another place
RKObjectMapping *mapping = [[RKObjectManager sharedManager].mappingProvider objectMappingForClass:[CDIssue class]];
//Post the object using the ObjectMapping with blocks
[[RKObjectManager sharedManager] postObject:myEntity usingBlock:^(RKObjectLoader *loader) {
loader.objectMapping = mapping;
loader.delegate = self;
loader.onDidLoadObject = ^(id object) {
NSLog(#"Got the object mapped");
//Be Happy and do some stuff here
};
loader.onDidFailWithError = ^(NSError * error){
NSLog(#"Error on request");
};
loader.onDidFailLoadWithError = ^(NSError * error){
NSLog(#"Error on load");
};
loader.onDidLoadResponse = ^(RKResponse *response) {
NSLog(#"Response did arrive");
if([response statusCode]>299){
//This is useful when you get an error. You can check what did the server returned
id parsedResponse = [KFHelper JSONObjectWithData:[response body]];
NSLog(#"%#",parsedResponse);
}
};
}];

RestKit Image Upload

I am using RestKit to drive interactions with my web server. I am trying to use routing to POST an Event object to the server with an image attached to it. The code is as follows:
RKObjectManager *manager = [RKObjectManager sharedManager];
RKObjectMapping *map = [self eventMapping];
manager.serializationMIMEType = RKMIMETypeFormURLEncoded;
map.rootKeyPath = #"event";
[manager.mappingProvider setSerializationMapping:map forClass:[Event class]];
[manager.router routeClass:[Event class] toResourcePath:#"/v1/events.json" forMethod:RKRequestMethodPOST];
[manager postObject:event delegate:self block:^(RKObjectLoader *loader){
RKObjectMapping *serMap = [[[RKObjectManager sharedManager] mappingProvider] serializationMappingForClass:[Event class]];
NSError *error = nil;
NSDictionary *d = [[RKObjectSerializer serializerWithObject:event mapping:serMap] serializedObject:&error];
RKParams *p = [RKParams paramsWithDictionary:d];
[p setData:[event imageData] MIMEType:#"image/jpeg" forParam:#"image"];
loader.params = p;
}];
If I create an instance of RKParams using the serialized Event object, then add the image data and assign it as the RKObjectLoader's params property, all the properties become one massive serialized string. There must be a way to upload an image without the massive string serialization.
I have also tried having an NSData property that is mapped to some attribute, converting a UIImage into NSData along the way, but RestKit complains that it can't be mapped. Has anyone done this before?
I did something very similar and it worked out just fine. I realize your question is about why RKObjectSerializer isn't working the way you expect, but maybe it is something else with your setup. I'm posting my code to give a clean example of something that does work. That said, after reading the RKObjectSerializer documentation, I don't see why you couldn't initialize your RKParams that way instead of setting them directly as I do in my example.
Router setup:
RKObjectManager *objectManager = [RKObjectManager objectManagerWithBaseURL:kApiUrlBase];
[objectManager.router routeClass:[PAPetPhoto class] toResourcePath:#"/pet/uploadPhoto" forMethod:RKRequestMethodPOST];
Mapping setup:
RKObjectMapping *papetPhotoMapping = [RKObjectMapping mappingForClass:[PAPetPhoto class]];
[papetPhotoMapping mapKeyPath:#"id" toAttribute:#"identifier"];
[papetPhotoMapping mapAttributes:#"accountId", #"petId", #"photoId", #"filename", #"contentType", nil];
[objectManager.mappingProvider addObjectMapping:papetPhotoMapping];
[objectManager.mappingProvider setSerializationMapping:[papetPhotoMapping inverseMapping] forClass:[PAPetPhoto class]];
[objectManager.mappingProvider setMapping:papetPhotoMapping forKeyPath:#"petPhoto"];
The post: (notice since I built up all my params in the block my object is just a dummy instance to trigger the proper routing and mapper).
PAPetPhoto *photo = [[PAPetPhoto alloc] init];
[[RKObjectManager sharedManager] postObject:photo delegate:self block:^(RKObjectLoader *loader){
RKParams* params = [RKParams params];
[params setValue:pet.accountId forParam:#"accountId"];
[params setValue:pet.identifier forParam:#"petId"];
[params setValue:_photoId forParam:#"photoId"];
[params setValue:_isThumb ? #"THUMB" : #"FULL" forParam:#"photoSize"];
[params setData:data MIMEType:#"image/png" forParam:#"image"];
loader.params = params;
}];
Server endpoint (Java, Spring MVC)
#RequestMapping(value = "/uploadPhoto", method = RequestMethod.POST)
#ResponseBody
public Map<String, Object> handleFormUpload(#RequestParam("accountId") String accountId,
#RequestParam("petId") String petId,
#RequestParam("photoId") String photoId,
#RequestParam("photoSize") PhotoSizeEnum photoSize,
#RequestParam("image") Part image) throws IOException {
if (log.isTraceEnabled())
log.trace("uploadPhoto. accountId=" + accountId + " petId=" + petId + " photoId=" + photoId + " photoSize=" + photoSize);
PetPhoto petPhoto = petDao.savePetPhoto(accountId, petId, photoId, photoSize, image);
Map<String, Object> map = GsonUtils.wrapWithKeypath(petPhoto, "petPhoto");
return map;
}
Server response JSON (note the keyPath of "petPhoto" that corresponds to the mapping setup):
{
petPhoto = {
accountId = 4ebee3469ae2d8adf983c561;
contentType = "image/png";
filename = "4ebee3469ae2d8adf983c561_4ec0983d036463d900841f09_3FED4959-1042-4D8B-91A8-76AA873851A3";
id = 4ee2e80203646ecd096d5201;
petId = 4ec0983d036463d900841f09;
photoId = "3FED4959-1042-4D8B-91A8-76AA873851A3";
};
}
Delegate:
- (void) objectLoader:(RKObjectLoader*)objectLoader didLoadObject:(id)object {
if ([objectLoader wasSentToResourcePath:#"/pet/uploadPhoto"]) {
PAPetPhoto *photo = (PAPetPhoto*)object;
}
}