Re: Using a SOAP Web Service from iPhone
Re: Using a SOAP Web Service from iPhone
- Subject: Re: Using a SOAP Web Service from iPhone
- From: Dru Satori <email@hidden>
- Date: Fri, 9 Apr 2010 11:52:55 -0400
This is an 'iffy' subject in general. I can tell you that my approach is not what it appears that most have done.
The first problem for me, most of the WebService is deal with are written in .NET (C#) and as such the WSMakeStubs doesn't actually work (if you have parameters).
The second, as you have discovered is that WSMakeStubs classes are not iPhoneOS usable.
In order to solve the problem, I went away from the generator tools (though I plan to create my own at some point) and do it myself. I created a base class that is the SOAP request.
//
// SASoapRequest.h
//
// Created by Andy Satori on 2/4/10.
// Copyright 2010 druware software designs. All rights reserved.
//
#import <Foundation/Foundation.h>
#import <libxml/xmlmemory.h>
@interface SASoapRequest : NSObject {
// service properties
NSString *methodName;
NSString *nameSpace;
NSString *serviceUrl;
NSMutableArray *parameters;
NSMutableString *soapRequestXml;
NSMutableString *soapResponseXml;
NSData *soapResponseData;
}
@property(nonatomic, retain) NSString *methodName;
@property(nonatomic, retain) NSString *nameSpace;
@property(nonatomic, retain) NSString *serviceUrl;
-(BOOL)createSoapRequest;
-(BOOL)postSoapRequest;
-(void)setParameters:(NSArray *)value;
-(NSArray *)parameters;
-(long)addParameter:(NSDictionary *)dictionary;
-(NSString *)soapRequestXml;
-(NSString *)soapResponseXml;
-(NSData *)soapResponseData;
@end
The implementation of this 'base' class is fairly straightforward:
//
// SASoapRequest.m
// TWHQMobile
//
// Created by Andy Satori on 2/4/10.
// Copyright 2010 druware software designs. All rights reserved.
//
#import "SASoapRequest.h"
@implementation SASoapRequest
@synthesize methodName;
@synthesize nameSpace;
@synthesize serviceUrl;
-(BOOL)createSoapRequest
{
soapRequestXml = [[NSMutableString alloc] init];
[[soapRequestXml retain] autorelease];
// document
[soapRequestXml appendString:@"<?xml version=\"1.0\" encoding=\"utf-16\"?>\n"];
// envelope
[soapRequestXml appendString:@"<soap:Envelope "];
[soapRequestXml appendString:@"xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" "];
[soapRequestXml appendString:@"xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" "];
[soapRequestXml appendString:@"xmlns:soap=\"http://schemas.xmlsoap.org/soap/envelope/\">\n"];
// body
[soapRequestXml appendString:@"\t<soap:Body>\n"];
// method
[soapRequestXml appendFormat:@"\t\t<%@ xmlns=\"%@\">\n", self.methodName, self.nameSpace];
// parameters
//int i;
//for (i = 0; i < [parameters count]; i++)
for (NSDictionary *parameter in parameters)
{
// parameter = [parameters objectAtIndex:i] ;
NSString *paramName = [parameter objectForKey:@"name"];
NSString *paramValue = [parameter objectForKey:@"value"];
[soapRequestXml appendFormat:@"\t\t\t<%@>%@</%@>\n", paramName, paramValue, paramName];
}
// close the tags
[soapRequestXml appendFormat:@"\t\t</%@>\n", self.methodName];
[soapRequestXml appendString:@"\t</soap:Body>\n"];
[soapRequestXml appendString:@"</soap:Envelope>\n"];
return ((soapRequestXml != nil) && ([soapRequestXml length] > 0));
}
-(BOOL)postSoapRequest
{
NSURL *soapRequestUrl = [NSURL URLWithString:self.serviceUrl];
// Use the private method setAllowsAnyHTTPSCertificate:forHost:
// to not validate the HTTPS certificate. This is used if you are using a
// testing environment and have a sample SSL certificate set up, this is not
// a published api and will need to be removed before submission to the app store.
[NSURLRequest setAllowsAnyHTTPSCertificate:YES forHost:[soapRequestUrl host]];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:soapRequestUrl];
NSString *soapAction = [NSString stringWithFormat:@"\"%@/%@\"", self.nameSpace, self.methodName];
[request addValue:@"text/xml; charset=utf-8" forHTTPHeaderField:@"Content-Type"];
[request addValue:soapAction forHTTPHeaderField:@"SOAPAction"];
[request setHTTPMethod:@"POST"];
[request setHTTPBody:[soapRequestXml dataUsingEncoding:NSUTF8StringEncoding]];
// Create the connection
NSURLConnection *conn = [[NSURLConnection alloc] initWithRequest:request delegate:self];
NSMutableData *myMutableData;
// Check the connection object
if(conn)
{
myMutableData=[[NSMutableData data] retain];
}
// Make this class the delegate so that the other connection events fire here.
[NSURLConnection connectionWithRequest:request delegate:self];
NSError *WSerror;
NSURLResponse *WSresponse;
// Call the xml service and return response into a MutableData object
myMutableData = [[NSMutableData alloc] initWithData:[NSURLConnection sendSynchronousRequest:request returningResponse:&WSresponse error:&WSerror]];
soapResponseData = [[NSData alloc] initWithData:myMutableData];
[soapResponseData retain];
[myMutableData release];
// parse the results and place them into an NSDictionary *
NSMutableArray *soapResponse;
soapResponse = [[NSMutableArray alloc] init];
[soapResponse retain];
soapResponseXml = [[NSString alloc] initWithData:soapResponseData encoding:NSUTF8StringEncoding];
[soapResponseXml retain];
xmlDocPtr doc = xmlParseMemory([soapResponseXml UTF8String],
[soapResponseXml lengthOfBytesUsingEncoding:NSUTF8StringEncoding]);
xmlNodePtr rootNode = xmlDocGetRootElement(doc);
NSLog(@"%s", rootNode->name); // Envelope
// loop all of the children ( there is only one, the Body Element )
xmlNodePtr bodyNode = rootNode->children;
NSLog(@"%s", bodyNode->name); // Body
xmlNodePtr responseNode = bodyNode->children;
NSLog(@"%s", responseNode->name); // Response
// confirm that this the expected node MethodNameReponse
NSString *expectedNodeName = [NSString stringWithFormat:@"%@Response", self.methodName];
NSString *responseNodeName = [NSString stringWithFormat:@"%s", responseNode->name];
if ([expectedNodeName compare:responseNodeName] == NSOrderedSame)
{
// iterate the result elements.
xmlNodePtr resultNode = responseNode->children;
while (resultNode != nil)
{
NSLog(@"%s", resultNode->name); // Result
NSString *nodeValue = [[NSString alloc] initWithFormat:@"%s", resultNode->content];
NSMutableDictionary *dict = [[NSMutableDictionary alloc] init];
[dict setValue:nodeValue forKey:responseNodeName];
[soapResponse addObject:dict];
resultNode = resultNode->next;
}
}
return ([soapResponseData length] > 0);
}
-(void)setParameters:(NSArray *)value
{
if (parameters != nil)
{
[parameters release];
parameters = nil;
}
parameters = [[NSMutableArray alloc] initWithArray:value];
[parameters retain];
}
-(NSArray *)parameters
{
return [[[NSArray alloc] initWithArray:parameters] autorelease];
}
-(long)addParameter:(NSDictionary *)dictionary
{
long currentCount = 0;
if (parameters == nil)
{
parameters = [[NSMutableArray alloc] init];
[parameters retain];
}
currentCount = [parameters count];
[parameters addObject:dictionary];
if (currentCount = [parameters count])
{
return -1;
}
return [parameters count];
}
-(NSString *)soapRequestXml
{
if (soapRequestXml == nil)
{
return nil;
}
return [NSString stringWithString:soapRequestXml];
}
-(NSString *)soapResponseXml
{
if (soapResponseXml == nil)
{
if (soapResponseData == nil)
{
return nil;
}
soapResponseXml = [[NSString alloc] initWithData:soapResponseData encoding:NSUTF8StringEncoding];
}
return [NSString stringWithString:soapResponseXml];
}
-(NSData *)soapResponseData
{
if (soapResponseData == nil)
{
return nil;
}
return [NSData dataWithData:soapResponseData];
}
@end
This is boilerplate code, that is then inherited by the actual class that represents the SOAP service:
The following is an example class that implements the above:
//
// TWHQLogin.h
// TWHQMobile
//
// Created by Andy Satori on 2/4/10.
// Copyright 2010 druware software designs. All rights reserved.
//
#import <Foundation/Foundation.h>
#import "SASoapRequest.h"
@interface TWHQLogin : SASoapRequest {
// parameters
NSString *userId;
NSString *password;
NSNumber *crmUserId;
NSString *sessionId;
BOOL isValid;
NSMutableString *inprocessString;
}
@property(nonatomic, retain) NSString *userId;
@property(nonatomic, retain) NSString *password;
@property(nonatomic, retain) NSNumber *crmUserId;
@property(nonatomic, retain) NSString *sessionId;
@property(readonly) BOOL isValid;
-(BOOL)validateUserId;
-(BOOL)validateSessionId;
@end
implemented as
//
// TWHQLogin.m
// TWHQMobile
//
// Created by Andy Satori on 2/4/10.
// Copyright 2010 druware software designs. All rights reserved.
//
#import "TWHQLogin.h"
#import <CommonCrypto/CommonDigest.h>
@implementation TWHQLogin
@synthesize userId;
@synthesize password;
@synthesize crmUserId;
@synthesize sessionId;
@synthesize isValid;
-(id)init
{
self = [super init];
if (self == nil) { return nil; }
inprocessString = nil;
isValid = NO;
self.nameSpace = @"http://my.host.url/services/login/user.asmx";
self.serviceUrl = @"http://my.host.url/services/login/user.asmx";
return self;
}
-(BOOL)validateUserId
{
isValid = NO;
self.methodName = @"ValidateLogin";
// set the parameters
NSMutableDictionary *param;
param = [[NSMutableDictionary alloc] init];
[param setObject:@"sUserName" forKey:@"name"];
[param setObject:self.userId forKey:@"value"];
[param setObject:@"NSString" forKey:@"type"];
[self addParameter:param];
param = [[NSMutableDictionary alloc] init];
[param setObject:@"sPassword" forKey:@"name"];
[param setObject:self.password forKey:@"value"];
[param setObject:@"NSString" forKey:@"type"];
[self addParameter:param];
param = [[NSMutableDictionary alloc] init];
[param setObject:@"iCRMUserId" forKey:@"name"];
[param setObject:@"-1" forKey:@"value"];
[param setObject:@"NSString" forKey:@"type"];
[self addParameter:param];
// create the request
[self createSoapRequest];
// submit the reuqest
if (![self postSoapRequest])
{
// the login failed.
}
// review the results
NSXMLParser *resultParser;
resultParser = [[NSXMLParser alloc] initWithData:soapResponseData];
[resultParser setDelegate:self];
[resultParser parse];
[resultParser release];
if (isValid)
{
// build the session id
NSString *sourceString = [[NSString alloc] initWithFormat:@"%@/%@", userId, password];
sessionId = [self generateMD5Hash:sourceString];
}
return self.isValid;
}
-(BOOL)validateSessionId
{
isValid = NO;
self.methodName = @"ValidateSessionId";
// set the parameters
NSMutableDictionary *param;
param = [[NSMutableDictionary alloc] init];
[param setObject:@"sSessionId" forKey:@"name"];
[param setObject:self.sessionId forKey:@"value"];
[param setObject:@"NSString" forKey:@"type"];
[self addParameter:param];
// create the request
[self createSoapRequest];
// submit the reuqest
if (![self postSoapRequest])
{
// the login failed.
}
// review the results
NSXMLParser *resultParser;
resultParser = [[NSXMLParser alloc] initWithData:soapResponseData];
[resultParser setDelegate:self];
[resultParser parse];
[resultParser release];
if (isValid)
{
// build the session id
NSString *sourceString = [[NSString alloc] initWithFormat:@"%@/%@", userId, password];
sessionId = [self generateMD5Hash:sourceString];
}
self.methodName = @"ValidateSessionId";
return NO;
}
- (NSString *)generateMD5Hash:(NSString *)inputString
{
const char rgbDigits[] = "0123456789abcdef";
const char *cStr = [inputString cStringUsingEncoding:NSWindowsCP1252StringEncoding];
unsigned char result[CC_MD5_DIGEST_LENGTH];
CC_MD5( cStr, strlen(cStr), result );
NSMutableString *sWork = [[NSMutableString alloc] init];
for (long i = 0; i < CC_MD5_DIGEST_LENGTH; i++)
{
[sWork appendFormat:@"%c%c", rgbDigits[result[i] >> 4],
rgbDigits[result[i] & 0xf]];
}
return [NSString stringWithString:sWork];
}
#pragma mark -
#pragma mark Delegate calls
- (void)parser:(NSXMLParser *)parser didStartElement:(NSString *)elementName
namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName
attributes:(NSDictionary *)attributeDict
{
BOOL isElementOfNote = NO;
// if this is not the response, go ahead and get out
if ([elementName rangeOfString:@"ValidateLoginResult"].location != NSNotFound)
{
isElementOfNote = YES;
}
if ([elementName rangeOfString:@"iCRMUserId"].location != NSNotFound)
{
isElementOfNote = YES;
}
if (isElementOfNote)
{
NSLog(@"starting Element: %@", elementName);
if (inprocessString != nil)
{
[inprocessString release];
inprocessString = nil;
}
inprocessString = [[NSMutableString alloc] init];
}
}
- (void)parser:(NSXMLParser *)parser
didEndElement:(NSString *)elementName
namespaceURI:(NSString *)namespaceURI
qualifiedName:(NSString *)qName
{
BOOL isElementOfNote = NO;
if ([elementName rangeOfString:@"ValidateSessionIdResult"].location != NSNotFound)
{
isValid = [inprocessString isEqualToString:@"true"];
isElementOfNote = YES;
}
if ([elementName rangeOfString:@"ValidateLoginResult"].location != NSNotFound)
{
isValid = [inprocessString isEqualToString:@"true"];
isElementOfNote = YES;
}
if ([elementName rangeOfString:@"iCRMUserId"].location != NSNotFound)
{
crmUserId = [[NSNumber alloc] initWithInteger:[inprocessString integerValue]];
isElementOfNote = YES;
}
if (isElementOfNote)
{
NSLog(@"ending Element: %@", elementName);
// Clear the text and key
[inprocessString release];
inprocessString = nil;
}
}
// This method can get called multiple times for the
// text in a single element
- (void)parser:(NSXMLParser *)parser
foundCharacters:(NSString *)string
{
NSLog(@"found characters: %@", string);
[inprocessString appendString:string];
}
@end
This is more or less code that is in use, but I know that it hasn't been profiled or really put under the microscope for performance or memory leaks, but hopefully it will give you something you can work with.
The short version is that XML is a little painful on the iPhoneOS. So we generate the Xml post in a string and then use the NSXMLParser on the back end to quickly break down the results. A lot of the ideas and concepts here came from looking at how other people tackled the problems, but in the end, this was the solution I used. It works, but I do not know if it is necessarily the best answer.
I apologize in advance for any formatting issues, this is composed on an iPad, and well, I am still getting used to it :-).
Andy 'Dru' Satori
On Apr 9, 2010, at 11:00 AM, Joanna Carter <email@hidden> wrote:
> Hi folks
>
> Well, I've just spent the last few days, getting to grips with consuming a web service.
>
> I though I would try things out in an OS X app first, then move to the iPhone, which is the end target.
>
> So, I decided to use WSMakeStubs on the WSDL - it did a reasonable job, apart from one or two minot bugs and niggles. Eventually, I got a test app working to prove that I could read the service.
>
> Then I thought I would move the app to iPhone, only to discover that the classes generated by WSStubMaker would not compile and that I would have to find another way to create the stub classes for an iPhone app.
>
> Can I ask what is considered the best route to go?
>
> Joanna
>
> --
> Joanna Carter
> Carter Consulting
>
> _______________________________________________
>
> Cocoa-dev mailing list (email@hidden)
>
> Please do not post admin requests or moderator comments to the list.
> Contact the moderators at cocoa-dev-admins(at)lists.apple.com
>
> Help/Unsubscribe/Update your Subscription:
>
> This email sent to email@hidden
>
_______________________________________________
Cocoa-dev mailing list (email@hidden)
Please do not post admin requests or moderator comments to the list.
Contact the moderators at cocoa-dev-admins(at)lists.apple.com
Help/Unsubscribe/Update your Subscription:
This email sent to email@hidden