AFNetworking: How to make AFHTTPClient handle both json and xml responses? - ios

can one AFTHTTPClient handle both json and xml?
I have one single domain in which some services only return json, while others only return xml. How would I make a GET request and instruct AFHTTPClient to use AFJSONRequestOperation for some services and an AFXMLRequestOperation for other GET requests?
So what I would like is:
chairs.com GET customerprofile ---> returns XML (no option for json)
charis.com GET inventory ---> returns JSON (no option for xml)
Is this a job for multiple AFHTTPClients?
Thanks

Your use of AFHTTPClient indicates you're using AFNetworking 1, but I'll answer this question for both versions, for future readers.
AFNetworking 1.x
You just need to register the appropriate AFHTTPOperation subclass. This is typically done in your subclass of initWithBaseURL::
- (instancetype) initWithBaseURL:(NSURL *)url {
self = [super initWithBaseURL:url];
if (self) {
[self registerHTTPOperationClass:[AFJSONRequestOperation class]];
[self registerHTTPOperationClass:[AFXMLRequestOperation class]];
}
return self;
}
When your app makes an outgoing request, you'll need to make sure your accept headers are set appropriately (for example, to text/json or text/xml, depending on what you expect from which endpoint you hit). Otherwise, AFNetworking won't know which operation to use for which request.
There are a few ways to easily solve this Accept header requirement. If one of your endpoints is an exception to a general rule, I might do this by overriding requestWithMethod:path:parameters::
- (NSMutableURLRequest *)requestWithMethod:(NSString *)method
path:(NSString *)path
parameters:(NSDictionary *)parameters {
request = [super requestWithMethod:method path:path parameters:parameters];
if (/* the endpoint specified in path returns XML */) {
[request setValue:#"text/xml" forHTTPHeaderField:#"Accept"];
} else {
[request setValue:#"text/json" forHTTPHeaderField:#"Accept"];
}
}
This is a small violation of tell, don't ask; feel free to refactor as necessary.
If you don't plan on upgrading to AFNetworking 2, then you can stop reading here.
AFNetworking 2.x
Version 2.0 of AFNetworking makes this simpler and more intuitive. In 2.0, the serialization responsibility is broken out into a separate class. Instances of this class are called response serializers. When you upgrade, you'll want an AFCompoundResponseSerializer. The documentation describes it best:
AFCompoundSerializer is a subclass of AFHTTPSerializer that delegates the response serialization to the first AFHTTPSerializer object that returns YES to validateResponse:data:error:, falling back on the default behavior of AFHTTPSerializer. This is useful for supporting multiple potential types and structures of server responses with a single serializer.
For example:
AFJSONResponseSerializer *jsonSerializer = [AFJSONResponseSerializer serializerWithReadingOptions:0];
AFXMLDocumentSerializer *xmlSerializer = [AFXMLDocumentSerializer serializerWithXMLDocumentOptions:0];
AFCompoundResponseSerializer *compoundSerializer = [AFCompoundResponseSerializer compoundSerializerWithResponseSerializers:#[jsonSerializer, xmlSerializer]];
[AFHTTPSessionManager manager].responseSerializer = compoundSerializer;

Related

Multiple NSURLConnection sendAsynchronous requests with a singleton class

I am using a Master Detail Controller. In the Master list there are 5 items. On selecting each item, there are Asynchronous calls made.
There is one SingleTon class, which handles all network calls.
[[MyNetwokCommunication connectionInstance]
makeRequest:"url1" delegate:self];
[[MyNetwokCommunication connectionInstance] makeRequest:"url2" delegate:self];
Actual implementation in makeRequest:delegate: is [[NSURLConnection alloc] initWithRequest:request delegate:self startImmediatley:YES].
So, is this a correct way to handle network connection with a singleton class, as it might over-ride data part of one request with another.
There are a lot of ways to handle it. Unfortunately, the fact that NSURLConnection can't be used as a key in a dictionary makes life particularly miserable.
The best thing to do, assuming you don't still need to support iOS 6 and earlier (or OS X v10.8 or earlier) is to ditch NSURLConnection for NSURLSession. Then use block-based calls to make the request and handle the response. Unlike NSURLConnection objects, you can use NSURLSessionTask objects as dictionary keys, which makes it easy to keep track of what data came from which request, and to store additional objects or other data associated with that request (e.g. storing the UI cell where the data should go).
If you have to use NSURLConnection to support older operating systems, you might consider subclassing NSURLRequest and adding extra data. Note that this alternative approach does not work with NSURLSession, and you'll need to provide your own redirect handler, because otherwise you'll end up getting back generic NSURLRequest objects whenever a redirect happens, IIRC. Or you can add Objective-C associated objects on the connection object. (At least I'm 99% sure that this works.)
That's not how I would do it.
The best paradigm on iOS to serialize things is an NSOperationQueue. You can create a queue with concurrency of 1, then queue your NSURLConnection or NSURLSession children as NSOperations.
This allows you to do neat things like create operations that are dependent on the success of other operations.
Here's the creation of the queue:
#property (strong, nonatomic) NSOperationQueue *queue;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_queue = [[NSOperationQueue alloc] init];
[_queue setMaxConcurrentOperationCount:1];
});
Then to create a network operation:
// HTTPOperation.h
#import <Foundation/Foundation.h>
#interface HTTPOperation : NSOperation
#end
//
// HTTPOperation.m
//
#import "HTTPOperation.h"
#implementation HTTPOperation
- (void) main
{
NSURLRequest * urlRequest = [NSURLRequest requestWithURL:[NSURL URLWithString:#"http://google.com"]];
NSURLResponse * response = nil;
NSError * error = nil;
NSData * data = [NSURLConnection sendSynchronousRequest:urlRequest
returningResponse:&response
error:&error];
if (error == nil)
{
// Parse data here
NSLog(#"Completed successfully");
}
}
#end
To execute the operations:
- (IBAction)queueHTTP {
HTTPOperation *op = [HTTPOperation new];
[self.queue addOperation:op];
}
You can queue as many as you want from anywhere and they will execute serially. I recommend watching the WWDC video on Advanced NSOperations:
https://developer.apple.com/videos/wwdc/2015/?id=226
It was very informative.

Redirect URL from NSURLConnection

I am trying to solve the following problem:
I am sending a POST request with some additional header values to a specific URL. For that purpose I use NSMutabeURLRequest. It works nice when i NSLog the response, but I also need the URL of the redirect. If I use something like request.URL in the competitionHandler it returns the URL i sent my POST request to, it's not what I need.
Any tips on how to get the URL of the redirect? (It would be nice if it won't change my code significantly.
Below is what I have so far:
url = [NSURL URLWithString:#"https://***"];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
[request setHTTPMethod:#"POST"];
[request setValue:#"application/json; charset=utf-8" forHTTPHeaderField:#"Content-Type"];
//Some additional values are set here
[request setHTTPBody:data];
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
[NSURLConnection sendAsynchronousRequest:request queue:queue completionHandler:^(NSURLResponse *response, NSData *data, NSError *error) {
NSLog(#"%#", response);
if (error)
NSLog(#"%s: NSURLConnection error: %#", __FUNCTION__, error);
NSString *responseString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
NSLog(#"responseString: %#",responseString);
}];
From Apple's Developer Docs:
A redirect occurs when a server responds to a request by indicating that the client should make a new request to a different URL. The NSURLSession, NSURLConnection, and NSURLDownload classes notify their delegates when this occurs.
To handle a redirect, your URL loading class delegate must implement one of the following delegate methods:
For NSURLSession, implement the URLSession:task:willPerformHTTPRedirection:newRequest:completionHandler: delegate method.
For NSURLConnection, implement the connection:willSendRequest:redirectResponse: delegate method.
For NSURLDownload, implement the download:willSendRequest:redirectResponse: delegate method.
In these methods, the delegate can examine the new request and the response that caused the redirect, and can return a new request object through the completion handler for NSURLSession or through the return value for NSURLConnection and NSURLDownload.
The delegate can do any of the following:
Allow the redirect by simply returning the provided request.
Create a new request, pointing to a different URL, and return that request.
Reject the redirect and receive any existing data from the connection by returning nil.
In addition, the delegate can cancel both the redirect and the connection. With NSURLSession, the delegate does this by sending the cancel message to the task object. With the NSURLConnection or NSURLDownload APIs, the delegate does this by sending the cancel message to the NSURLConnection or NSURLDownload object.
The delegate also receives the connection:willSendRequest:redirectResponse: message if the NSURLProtocol subclass that handles the request has changed the NSURLRequest in order to standardize its format, for example, changing a request for http://www.apple.com to http://www.apple.com/. This occurs because the standardized, or canonical, version of the request is used for cache management. In this special case, the response passed to the delegate is nil and the delegate should simply return the provided request.
So basically, you need to make use of the connection:willSendRequest:redirectResponse: delegate method to grab the new url and perform whatever operation you need to on it
The problem is that you're using the sendAsynchronousRequest:queue:completionHandler: method, which uses a completion handler and doesn't take a delegate.
After you implement the delegate method, you'll need to use NSURLConnection's initWithRequest:delegate: or initWithRequest:delegate:startImmediately: method to create the NSURLConnection object instead. You'll also have to implement a delegate method to obtain the data returned by the request.
Alternatively, if you don't have to support anything older than iOS 7 or OS X v10.9, NSURLSession provides the ability to use delegate methods for handling redirection even in combination with methods that take a completion callback.

AFNetworking 2.0: Passing header information

I am new to AFNetworking and I know how to pass URL parameters. But how would I pass headers into the same call.
I am also subclassing my AFHTTPSessionManager
See my code below:
- (void)getExpenses:(NSString *)page
success:(void (^) (NSArray *myExpenses))success
failure:(RequestFailureBlock)failure
{
NSString *resourceURL = [NSString stringWithFormat:#"%#/expenses/", APIBaseURLString];
NSDictionary *parameters = #{#"page":page, #"Authorization": APIAuthorization};
[self getExpenses:resourceURL parameters:parameters success:success failure:failure];
}
setAuthorizationHeaderFieldWithToken is deprecated due to servers having different requirements about how the access token is sent (token, bearer, etc)
michaels answer otherwise is correct, use
[self.requestSerializer setValue:#"Some-Value" forHTTPHeaderField:#"Header-Field"];
or
[self.requestSerializer setAuthorizationHeaderFieldWithUsername:#"" password:#""];
for basic auth
You set header values on the requestSerializer property of AFHTTPSessionManager:
[self.requestSerializer setValue:#"Some-Value" forHTTPHeaderField:#"Header-Field"];
EDIT:
It looks like you're trying to set authorization; there is a method for that too:
[self.requestSerializer setAuthorizationHeaderFieldWithUsername:#"" password:#""];
// OR
[self.requestSerializer setAuthorizationHeaderFieldWithToken:#""];
If you need to set the Content-Type header, see this SO answer on how to do that

Having trouble with multiple NSURLConnection

I've looked around a lot and cant seem to find a proper answer for my problem. As of now I have a network engine and I delegate into that from each of the view controllers to perform my network activity.
For example, to get user details I have a method like this:
- (void) getUserDetailsWithUserId:(NSString*) userId
{
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:[NSString stringWithFormat:#"%#Details", kServerAddress]]];
request.HTTPMethod = #"POST";
NSString *stringData = [NSString stringWithFormat:#"%#%#", kUserId, userId];
NSData *requestBodyData = [stringData dataUsingEncoding:NSUTF8StringEncoding];
request.HTTPBody = requestBodyData;
NSURLConnection *conn = [[NSURLConnection alloc] init];
[conn setTag:kGetUserInfoConnection];
(void)[conn initWithRequest:request delegate:self];
}
And when I get the data in connectionDidFinishLoading, I receive the data in a NSDictionary and based on the tag I've set for the connection, I transfer the data to the required NSDictionary.
This is working fine. But now I require two requests going from the same view controller. So when I do this, the data is getting mixed up. Say I have a connection for search being implemented, the data from the user details may come in when I do a search. The data is not being assigned to the right NSDictionary based on the switch I'm doing inside connectionDidFinishLoading. I'm using a single delegate for the entire network engine.
I'm new to NSURLConnection, should I setup a queue or something? Please help.
EDIT
Here's the part where I receive data in the connectionDidFinishLoading:
- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
if ([connection.tag integerValue] == kGetUserDetails)
networkDataSource.userData = self.jsonDetails;
if ([connection.tag integerValue] == kSearchConnection)
networkDataSource.searchData = self.jsonDetails;
}
and after this I have a switch case that calls the required delegate for the required view controller.
Anil here you need to identify for which request you got the data,
simplest way to check it is as below,
- (void)connectionDidFinishLoading:(NSURLConnection *)conn
{
// Check URL value for request, url for search and user details will be different, put if condition as per your need.
conn.currentRequest.URL
}
Try using conn.originalRequest.URL it will give original request.
You can do in many ways to accomplish your task as mentioned by others and it will solve your problem . But if you have many more connections , you need to change your approach.
You can cretae a subclass of NSOperation class. Provide all the required data, like url or any other any informmation you want to get back when task get accomplish , by passing a dictionary or data model to that class.
In Nsoperation class ovewrite 'main' method and start connection in that method ie put your all NSURRequest statements in that method. send a call back when download finish along with that info dict.
Points to be keep in mind: Create separte instance of thet operation class for evey download, and call its 'start method'.
It will look something like :
[self setDownloadOperationObj:[[DownloadFileOperation alloc] initWithData:metadataDict]];
[_downloadOperationObj setDelegate:self];
[_downloadOperationObj setSelectorForUpdateComplete:#selector(callBackForDownloadComplete)];
[_downloadOperationObj setQueuePriority:NSOperationQueuePriorityVeryHigh];
[_downloadOperationObj start];
metaDict will contain your user info.
In DownloadFileOperation class you will overwrite 'main' method like :
- (void)main {
// a lengthy operation
#autoreleasepool
{
if(self.isCancelled)
return;
// //You url connection code
}
}
You can add that operation to a NSOperationQueue if you want. You just need to add the operation to NSOperationQueue and it will call its start method.
Declare two NSURLConnection variables in the .h file.
NSURLConnection *conn1;
NSURLConnection *conn2;
- (void)connectionDidFinishLoading:(NSURLConnection *)connection{
if(connection == conn1){
}
else if(connection == conn2){
}
}

RestKit/RestEASY -- ClientAbortException when enqueuing request operations

I am working on an iOS application that will use RestKit 0.20 to make REST-based calls to a service that is running on JBoss AS 7.1.1 and using restEASY as its REST-based web service framework.
The REST service that the client app will be calling is used to retrieve objects based on their unique identifier. Since these objects can be small or large (> 1MB in size) and great in number (20? 50? 100 or more at a time) I don't want to make one large call to retrieve them all at once. Rather, I was planning on using RestKit's queued operation support to create a GET request for each object based on the object identifier, and execute the calls asynchronously. Once the GET has completed, each object will be processed through the use of Objective-C blocks so as to avoid any unnecessary blocking.
My RestKit client code looks like this...
NSArray *identifiers = ...
RKObjectManager *objectManager = [RKObjectManager sharedManager];
RKResponseDescriptor *getObjResp = [RKResponseDescriptor responseDescriptorWithMapping:[MyObject mapping] pathPattern:[WebServiceHelper pathForServiceOperation:#"/objects/:uniqueIdentifier"] keyPath:nil statusCodes:RKStatusCodeIndexSetForClass(RKStatusCodeClassSuccessful)];
for (int i=0; i < identifiers.count; i++) {
NSString *identifier = [identifiers objectAtIndex:i];
NSURL *serviceURL = [WebServiceHelper urlForServiceOperation:[NSString stringWithFormat:#"/objects/%#", identifier]];
NSURLRequest *request = [NSURLRequest requestWithURL:serviceURL];
RKObjectRequestOperation *requestOp = [[RKObjectRequestOperation alloc] initWithRequest:request responseDescriptors:#[getObjResp]];
[requestOp setCompletionBlockWithSuccess:^(RKObjectRequestOperation *operation, RKMappingResult *mappingResult) {
MyObject *obj = [mappingResult firstObject];
if (self.delegate != nil) {
[self.delegate didLoadObjectWithIdentifier:identifier myObj:obj];
}
} failure:^(RKObjectRequestOperation *operation, NSError *error){
if (self.delegate != nil) {
[self.delegate didFinishWithError:error];
}
}];
[objectManager enqueueObjectRequestOperation:requestOp];
}
From there, the delegate method that gets called when an object has been retrieved looks like this:
-(void)didLoadObjectWithIdentifier:(NSString *)identifier myObj:(MyObject *)myObj {
if(secureMessage != nil) {
NSLog(#"Message %# retrieved successfully : %#:%#", identifier, myObj);
} else {
NSLog(#"NO OBJ");
}
}
The calls appear to be functioning as expected, as I am able to print out information about the retrieve objects. However, I am seeing some weird/unexepcted behavior on the service side.
First, I see a number of Exceptions being thrown by restEASY:
13:22:02,903 WARN [org.jboss.resteasy.core.SynchronousDispatcher] (http--0.0.0.0-8080-10) Failed executing GET /objects/BBFE39EA126F610C: org.jboss.resteasy.spi.WriterException: ClientAbortException: java.net.SocketException: Broken pipe
at org.jboss.resteasy.core.ServerResponse.writeTo(ServerResponse.java:262) [resteasy-jaxrs-2.3.2.Final.jar:]
at org.jboss.resteasy.core.SynchronousDispatcher.writeJaxrsResponse(SynchronousDispatcher.java:585) [resteasy-jaxrs-2.3.2.Final.jar:]
at org.jboss.resteasy.core.SynchronousDispatcher.invoke(SynchronousDispatcher.java:506) [resteasy-jaxrs-2.3.2.Final.jar:]
at org.jboss.resteasy.core.SynchronousDispatcher.invoke(SynchronousDispatcher.java:119) [resteasy-jaxrs-2.3.2.Final.jar:]
at org.jboss.seam.resteasy.ResteasyResourceAdapter$1.process(ResteasyResourceAdapter.java:145) [jboss-seam-resteasy.jar:2.3.0.Final]
at org.jboss.seam.servlet.ContextualHttpServletRequest.run(ContextualHttpServletRequest.java:65) [jboss-seam.jar:2.3.0.Final]
at org.jboss.seam.resteasy.ResteasyResourceAdapter.getResource(ResteasyResourceAdapter.java:120) [jboss-seam-resteasy.jar:2.3.0.Final]
...
It would appear as though RestKit is closing the socket somehow (or some other error is preventing the object from being read from the server). I am unable to find anything in the documentation that could explain what is going on here.
Secondly, though, I also see another call for the very same object when a request fails with this error. Why is the GET being called more than once? Is RestKit redoing the failed GET request?
I'm mostly concerned about why the Exception is occurring within restEASY, as it will make it difficult to diagnose calls that really do fail. Has anyone seen this behavior before? Any tips as to how I can correct these issues? Thanks for any help you can give!!
Those exception are resulted from disconnected Clients i.e. some of the users might quit the app while waiting for the process to complete OR has a network failure (at the client end).
Hence, Broken Pipe.

Resources