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;
}
}
Related
Right now I'm doing a huge refactory to my code and I want to unify the way I'm using Restkit. I had separated ways of making RPC calls to my API server and REST calls.
REST calls where made using objectMapping and RPC calls where made with the RKClient. Besides that, I'm using blocks instead of delegates, which is great but I have some doubts about how it works.
Here is the code I had before, which worked nicely to post the object and do the mapping manually using delegates and after that is the new code using blocks that does not send the params.
//This was the old way...
- (void) upload: (KFMedia *) pic {
RKParams* imageParams = [RKParams params];
NSData* imageData = UIImageJPEGRepresentation(pic.image, 0.7f);
[imageParams setData:imageData MIMEType:#"image/jpg" forParam:#"FileUpload"];
[[RKClient sharedClient] post:#"/api/upload/" params:imageParams delegate:self];
}
//This is the new way I'm trying...
- (void) upload: (KFMedia *) pic onLoad:(RKObjectLoaderDidLoadObjectBlock) loadBlock onError:(RKRequestDidFailLoadWithErrorBlock)failBlock{
RKParams* imageParams = [RKParams params];
NSData* imageData = UIImageJPEGRepresentation(pic.image, 0.7f);
[imageParams setData:imageData MIMEType:#"image/jpg" forParam:#"FileUpload"];
[[RKObjectManager sharedManager] loadObjectsAtResourcePath:#"/api/upload/" usingBlock:^(RKObjectLoader *loader) {
//Trying to set params here, but it seems that I'm not sending anything :(
loader.params = imageParams;
loader.objectMapping = [[RKObjectManager sharedManager].mappingProvider objectMappingForClass:[KFMedia class]];
loader.delegate = self;
loader.onDidLoadObject = loadBlock;
loader.onDidFailWithError = failBlock;
loader.onDidFailLoadWithError = failBlock;
loader.onDidLoadResponse = ^(RKResponse *response) {
[self fireErrorBlock:failBlock onErrorInResponse:response];
};
}];
}
The request I'm sending has its body empty, that means that the params are not being sendend or setted properly. Any ideas on how to get this to work?
I just solved the problem. To send the extra params with loadObjectsAtResourcePath you must force a post using
loader.method = RKRequestMethodPOST;
the code is as follows:
- (void) upload: (KFMedia *) pic onLoad:(RKObjectLoaderDidLoadObjectBlock) loadBlock onError:(RKRequestDidFailLoadWithErrorBlock)failBlock{
RKParams* imageParams = [RKParams params];
NSData* imageData = UIImageJPEGRepresentation(pic.image, 0.7f);
[imageParams setData:imageData MIMEType:#"image/jpg" forParam:#"FileUpload"];
[[RKObjectManager sharedManager] loadObjectsAtResourcePath:#"/api/upload/" usingBlock:^(RKObjectLoader *loader) {
loader.method = RKRequestMethodPOST; //This line solved the problem
loader.params = imageParams;
loader.objectMapping = [[RKObjectManager sharedManager].mappingProvider objectMappingForClass:[KFMedia class]];
loader.delegate = self;
loader.onDidLoadObject = loadBlock;
loader.onDidFailWithError = failBlock;
loader.onDidFailLoadWithError = failBlock;
loader.onDidLoadResponse = ^(RKResponse *response) {
[self fireErrorBlock:failBlock onErrorInResponse:response];
};
}];
}
Why doesn't Restkit parse correctly?
Here my delegate call:
- (void)objectLoader:(RKObjectLoader *)objectLoader didLoadObject:(id)object {
NSString *s = [[NSString alloc] initWithData:objectLoader.response.body encoding:NSUTF8StringEncoding];
RKObjectMapping *rm = objectLoader.objectMapping;
NSLog(#"%#",s);
NSLog(#"%#",rm);
NSLog(#"%#", object);
}
and here the output
{"device_id": "4f75c887e45b583e4f000004"}
RKObjectMapping class => ETDevice: keyPath mappings => (
"RKObjectKeyPathMapping: device_token => secret",
"RKObjectKeyPathMapping: deivce_id => identifier"
)
(null)
any reason why it shouldn't work?
I don't think is a problem of the way I make the http request but anyway here is the code:
RKObjectManager *objectManager = [RKObjectManager sharedManager];
[objectManager sendObject:device delegate:self block:^(RKObjectLoader *loader) {
loader.serializationMapping = [[ETDevice objectMapper] inverseMapping];
loader.method = RKRequestMethodPOST;
loader.resourcePath = #"/devices/init";
loader.serializationMIMEType = #"application/json";
loader.objectMapping = [ETDevice objectMapper];
}];
btw I've also tried with:
{"device_id": "4f75c887e45b583e4f000004"}
{devices: {"device_id": "4f75c887e45b583e4f000004"}}
{devices: [{"device_id": "4f75c887e45b583e4f000004"}]}
please help, I'm timeboxing using restkit ;P
Ok I got completely crazy for this. I've debugged almost all the stack in Restkit.
The bug was just:
"deivce_id" instead of "device_id".
I've learned a lot about restKit at least
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);
}
};
}];
I am new to restKit and I have a few questions for you. I
cant understand how to send Post request using json/xml to my web
services and map the incoming reply with my classes. Can any one give
me a help on that. The code I am using is this:
in my applicationDelegate I am instantiating the RKObjectManager
providing the base URL:
RKObjectManager* manager = [RKObjectManager objectManagerWithBaseURL:#"https://example.com/services/"];
// Request params in Dictionary
NSArray *objects = [NSArray arrayWithObjects: email, password,
nil];
NSArray *keys = [NSArray arrayWithObjects:#"username",
#"password", nil];
NSDictionary *params = [NSDictionary dictionaryWithObjects:objects
forKeys:keys];
NSLog(#"Manager: %#", [RKObjectManager
sharedManager].description);
// User object Mapping
RKObjectMapping* userMapping = [RKObjectMapping mappingForClass:
[User class]];
[userMapping mapKeyPath:#"userName" toAttribute:#"userName"];
[userMapping mapKeyPath:#"lastName" toAttribute:#"lastName"];
[userMapping mapKeyPath:#"active" toAttribute:#"active"];
[[RKObjectManager sharedManager].mappingProvider
setMapping:userMapping forKeyPath:#"user"];
[[RKObjectManager sharedManager] loadObjectsAtResourcePath:#"/
login" delegate:self];
When a post is send to /login the server should send back a valid json
and then map that json to my User class.
- (void)objectLoader:(RKObjectLoader*)objectLoader didLoadObjects:
(NSArray*)objects {
RKLogInfo(#"Load collection of Articles: %#", objects);
}
- (void)objectLoader:(RKObjectLoader*)objectLoader didFailWithError:
(NSError *)error
{
NSLog(#"Objecy Loader failed: %#", error);
}
- (void)request:(RKRequest *)request didFailLoadWithError:(NSError
*)error
{
NSLog(#"Request failed");
}
- (void)request:(RKRequest*)request didLoadResponse:
(RKResponse*)response {
if ([request isGET]) {
// Handling GET /foo.xml
if ([response isOK]) {
// Success! Let's take a look at the data
NSLog(#"Retrieved XML: %#", [response bodyAsString]);
}
} else if ([request isPOST]) {
// Handling POST /other.json
if ([response isXML]) {
///NSLog(#"Seng a JSON request:! \n %#", [request
HTTPBodyString]);
NSLog(#"Got a responce! \n %#", [response bodyAsString]);
}
} else if ([request isDELETE]) {
// Handling DELETE /missing_resource.txt
if ([response isNotFound]) {
NSLog(#"The resource path '%#' was not found.", [request
resourcePath]);
}
}
}
When I execute it the objectLoader method are not triggered, my
understanding of restkit is that they should get called when
[[RKObjectManager sharedManager] loadObjectsAtResourcePath:#"/login"
delegate:self];
is executed ? Any help is appreciated :)
Well, you do not even send your data to the server. A couple of days ago, I wrote a snippet explaining how to post a NSDictionary (as JSON) to a server using RestKit.
did you remember to set the delegate for restkit to that user class (or where ever you have the delegates being caught) with
#interface User : NSObject<RKObjectLoaderDelegate>{
}
you probably did but its worth mentioning incase :P
and the self that gets passed in this line is that class?
[[RKObjectManager sharedManager] loadObjectsAtResourcePath:#"/
login" delegate:self];
also i think the newer way to add the mapping is and set up for posts is
[sharedManager.mappingProvider addObjectMapping:userMapping];
[sharedManager.mappingProvider setMapping:userMapping forKeyPath:#"/somepath"];
[sharedManager.mappingProvider setSerializationMapping:[userMapping inverseMapping] forClass:[User class]];
// Must set the router up to handle posts
[sharedManager.router routeClass:[User class] toResourcePath:#"/api/users" forMethod:RKRequestMethodPOST];
edit again : and create a post using the loader like this. probably all over kill for what your doing but cant hurt to have a look
RKObjectLoader* loader = [RKObjectLoader loaderWithResourcePath:url objectManager:self.objectManager delegate:self.currentDelegate];
loader.method = RKRequestMethodPOST;
loader.sourceObject = self;
loader.targetObject = self;
loader.serializationMIMEType = self.objectManager.serializationMIMEType;
loader.serializationMapping = [self.objectManager.mappingProvider serializationMappingForClass:self.mappingClass];
loader.objectMapping = [self.objectManager.mappingProvider objectMappingForClass:self.mappingClass];
[loader send]; //<<the actual post
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]"];