Interacting with SOAP based web services from Cocoa, part 2.

Author: Alexander Griekspoor
Web Site: www.mekentosj.com

SOAP based web services and Cocoa have never been good friends, and although REST based webservices are fortunately today's standard there are still tons of SOAP-based ones out there. What has always been missing in Cocoa is high level support for interacting with SOAP-based webservices. Here I describe one way in which you can build such frameworks yourself. Last time we started by analysing the webservice calls in detail, in this second part we generate the necessary Cocoa classes for use in a Mac or iPhone application.

Recall last time

In part 1 of this tutorial we introduced a SOAP-based acronym disambiguation webservice that we analysed using Ruby on Rails and locomotive. Using these tools we could see exactly what XML messages were sent back and forth over the wire (after all, that's all SOAP really is). What we'll do next is take these messages and use them as templates in our sample Cocoa app. You can download the full XCode project of the example application from here.

Cocoa Soap

The simple Cocoa application we are going to build will allow the user to enter some acronym containing text in a textview, and has a single button that invokes a call to the NaCTeM Acromine Disambiguation Web Service we introduced earlier. This webservice will try to disambiguate any acronyms present in the text and return them marked up inside the original text. For the sake of simplicity we will just replace the text in the textview with the results returned by the webservice.

Screenshot Sample App

A screenshot of the Cocoa SOAP sample application.

No magic required

We leave setting up a new XCode project and the simple UI with a textview and a button as an excercise to the reader, the sample project will show you how to do that. We'll focus first on the first task of creating a template for the SOAP request. Instead of relying on a dynamic framework (like the soap4r Ruby framework we used in part 1 ) we take a much simpler approach here. Once we have figured out what a SOAP call looks like as a plain XML message, it's easy to identify where the parameters that you wish to send along fit in. Our approach is to simply create an plain text template and fill in the parameters just before sending the actual SOAP message.

Obviously this approach is a lot less dynamic than using a fancy SOAP framework like the foundation based Web Services Core framework, and if the SOAP service is ever going to change it's likely it will invalidate our templates. In practise however changing webservices usually means some rewriting of code anyway, and at least our approach is very transparent, something that cannot always be said of the "magic" that goes on behind the scenes of automatic SOAP frameworks.

A second downside of our approach is that we need to create a template for each SOAP call we wish to invoke. With some of the more complex web services the number of calls can count up, and it is therefore advised to carefully consider first which exact functions we wish to invoke or require.

Creating a SOAP call template

Back to our sample. Last time we captured the outgoing XML message in our debug output in the terminal:


<?xml version="1.0" encoding="utf-8" ?><env:Envelope xmlns:xsd="http://www.w3.org/2001/XMLSchema"\n xmlns:env="http://schemas.xmlsoap.org/soap/envelope/" \n xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <env:Body>  <n1:analyze xmlns:n1="urn:acromine_disambiguation" \n env:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"> <text xsi:type="xsd:string">Resistance to tamoxifen is observed in half of the recurrences in breast cancer, where the anti-estrogen tamoxifen acquires agonistic properties for transactivating estrogen receptor alpha (ERalpha). In a previous study, we showed that protein kinase A (PKA)-mediated phosphorylation of serine 305 (S305) of ERalpha results in resistance to tamoxifen. </text> </n1:analyze> \n </env:Body> \n </env:Envelope>

The first thing we'll do is copy this message to a text editor, any will do. To make things a bit easier to see we'll start by cleaning things up a bit. First we convert all \" into proper quotes. Next we turn all linebreaks in the form of \n into proper returns, and we apply some proper indentation (Many applications, like TextMate, have some helpful tools to do this for you). There, that looks much better already:

Soap template

The raw SOAP XML message call after we cleaned it up in TextEdit. The text parameter input is highlighted.

Now it's also easy to see where the input text ends up. The trick is to replace this input with a simple %@ placeholder, the standard Cocoa placeholder for an NSString object in a printf like statement (you'll see later why we choose this one). If the SOAP call was more complex and had multiple input parameters replace all parameters with %@ placeholders.

Soap template 2

The raw SOAP XML message call after we replaced the input with a %@ placeholder.

Next we save the message as a plain text file and add it to our XCode project under the name request.txt.

Creating a SOAP URL request

Now all we need to do the invoke the SOAP service is to create a URL request that takes the template, fills in the input parameters and sends it off to the SOAP service. But what should this request look like? Again, the ruby debug output gives us a hint:


POST /acrodisambiguation HTTP/1.1
Accept: */*
Content-Type: text/xml; 
charset=utf-8
User-Agent: SOAP4R/1.5.5
Soapaction: ""
Content-Length: 1327
Host: www.nactem.ac.uk

We can see our request should be a POST request, and we can see the http headers that we need to send along. The only other thing we need is the actual URL of the services, which we find in the WSDL file of the service:


<service name="acromine_disambiguation">
 <port name="acromine_disambiguation" binding="tns:acromine_disambiguation">
  <SOAP:address location="http://www.nactem.ac.uk/acrodisambiguation"/>
 </port>
</service>

So now that we have all parameters of the request we need, we can build the URL request we need to send to the disambiguation SOAP service. The first thing we do is build a general purpose SOAP request, a subclass of NSMutableURLRequest:


@implementation mtSOAPRequest

// This is to generate a post request for SOAP calls
+(id)requestWithURL: (NSURL *)url soapAction: (NSString *)action body: (NSString *)body{
	mtSOAPRequest *req = [self requestWithURL: url];
	[req setHTTPMethod: @"POST"];
	[req setHTTPBody: [body dataUsingEncoding: NSUTF8StringEncoding]];
	[req addValue: [NSString stringWithFormat: @"\"%@\"", action] forHTTPHeaderField: @"SOAPAction"];
	[req addValue: @"text/xml; charset=utf-8" forHTTPHeaderField: @"Content-Type"];	
	[req setCachePolicy: NSURLRequestReloadIgnoringCacheData];
	return req;
}


//	the request object is deep-copied as part of the initialization process. 
- (id)copyWithZone:(NSZone *)zone{
	[self setHTTPBody:[self HTTPBody]];
	[self setAllHTTPHeaderFields: [self allHTTPHeaderFields]];
	[self setCachePolicy: NSURLRequestReloadIgnoringCacheData];
	return [super copyWithZone:zone];
}

@end

Note the copyWithZone: method which we need to preserve the contents of our URL request while the system throws our request around during the different parts of fetching the contents of the request. Other than that all we do is to set the proper http method to Post and we mimic the http header fields to those we saw earlier.

The final thing we do is create a subclass of mtSOAPRequest specific for this SOAP service:


@implementation mtNactemRequest

+(id)disambiguateRequestWithParameters:(NSDictionary *)p{

	if(![p valueForKey: @"text"]) return nil;

	NSString *action = @"";
	NSURL *url = [NSURL URLWithString: @"http://www.nactem.ac.uk/acrodisambiguation"];

	NSString *path = [[NSBundle mainBundle] pathForResource: @"request" ofType: @"txt"];   
	NSString *message = [NSString stringWithContentsOfFile: path encoding: NSUTF8StringEncoding error: NULL];

	NSString *text = [p valueForKey: @"text"];
	NSString *body = [NSString stringWithFormat: message, text];

	return [self requestWithURL: url soapAction: action body: body];
}

@end

First we check whether the input parameter dictionary contains the required key for this method. Next we generate the NSURL we will send the request to. Next we load the template from the request.txt file we added to our project earlier, and in the following step we use the stringWithFormat: method to replace the placeholder in our template with the proper parameters. Finally we create the complete NSURLRequest with the proper body and url.

If the SOAP service would have been more complex we could have added one convenience method for each SOAP service method that we wish to be able to invoke, in each one loading a different template.

Invoking the SOAP service call

We have everything we need now to send the request to the SOAP service, the only thing that remains is doing the call and fetching the results:


// generate a dictionary with the Soap parameters
// we only have one here, the string in the textView
NSMutableDictionary *p = [NSMutableDictionary dictionaryWithObjectsAndKeys: [textView string], @"text", nil];

// generate the request
mtNactemRequest *req = [mtNactemRequest disambiguateRequestWithParameters: p];
if(!req){
	NSLog(@"Could not create request from input");
	return;	
}

// Send of the request and get the data for this query
NSURLResponse *response = nil;
NSError *err = nil;
NSData *resultsData = [NSURLConnection sendSynchronousRequest: req returningResponse: &response error: &err];

if(err){
	NSLog(@"Error sending SOAP request: %@", err);
	return;	
}	

// transform data into an xml document
err = nil;
NSXMLDocument *xmlDoc = [[[NSXMLDocument alloc] initWithData: resultsData options: NSXMLDocumentTidyXML error: &err]autorelease]; 

if(err || !xmlDoc){
	NSLog(@"Error creating XML doc: %@", err);
	return;	
}	

// parse data from document
NSArray *nodes = [[xmlDoc rootDocument] nodesForXPath: @".//result" error: &err];
if([nodes count] > 0){
	NSString *result = [[nodes lastObject]stringValue];
	// filter out weird characters and escape sequences that Nactem sends along
	[textView setString: [result stringByReplacingEscapes]];
}

First we create the dictionary of input parameters, in this case we simply take the contents of the textview. Next we create the URL request and send it of using NSURLConnection's sendSynchronousRequest:returningResponse:error: method. If the method does not return an error we use the data and transform it into an XML document (after all the response of a SOAP service is also just an XML document). Finally in this case we simply replace the contents of the textview with the results using an XPath query from the XML document. The complete XCode project can be downloaded from here.

How did we know what the structure of the resulting XML response would be? This is again something we previously already found out using the ruby debug output. An excellent tool that helps you to construct the right XPath queries is the XMLBrowser sample application that you can find in /Developer/Examples/Foundation/XMLBrowser and that comes with the XCode documentation set.

XML Browser

A screenshot of the XML browser sample application.

Conclusions

Is this the most elegant solutions for dealing with SOAP based webservices? Definitely not, but it works just fine. I've used it to deal with a variety of SOAP based webservices, also in cases where the automatic frameworks got stuck translating the WSDL in something useful. In this two-part tutorial we have seen how we can first analyze the SOAP messages of a webservice using Ruby on Rails and the terminal, and use this info to create a simple template for invoking a SOAP based method call. Invoking the SOAP service from a Cocoa based application then becomes trivial.

Comments

Comment viewing options

Select your preferred way to display the comments and click "Save settings" to activate your changes.

Copy object

Thanks for the tutorial, Alex. I really like how you make it very pragmatic and practical. In fact, XML is meant to be human-readable, and it is after all just plain text. Which means in many situations, it can be dealt with as plain text, and there is no need to get fancy about it and encapsulate it behind opaque magic classes...

One comment and question I had is about your copy method. I think the correct code should be:


- (id)copyWithZone:(NSZone *)zone{
	mtSOAPRequest *theCopy = [super copyWithZone:zone];
	[theCopy setHTTPBody:[self HTTPBody]];
	[theCopy setAllHTTPHeaderFields: [self allHTTPHeaderFields]];
	[theCopy setCachePolicy: NSURLRequestReloadIgnoringCacheData];
	return theCopy;
}

In your code, you only manipulate self, but not the copied object. Obviously, it still works, so I am not sure what is going on here?

100% correct

Not sure what went wrong there, but I think you're correct. Thanks Charles.

XML Entities

Wow, great post.

Have you run into any problems when passing weird XML characters as the SOAP parameter? I can imagine that sending the string @"is 5 < 7" as the text parameter on this line:

NSString *body = [NSString stringWithFormat: message, text];

...would cause problems because the "<" would be interpreted as the beginning of a new tag. XML would want an "&lt;" instead.

I've looked all over the place for an elegant way to escape an NSString for XML to no avail. Any ideas?

Thanks

UPDATE: Found something: http://code.google.com/p/google-toolbox-for-mac/source/browse/trunk/Foundation/GTMNSString%2BXML.h

Fancy

I would like to know if the WithZone code would work at the FancyDiamonds yellow diamonds section? how the diamonds will look @ SOAP?

The IgnoringCacheData? what kind of ignoring done?