Passing arrays via WSMethodInvocationInvoke() [WebServicesCore]
Passing arrays via WSMethodInvocationInvoke() [WebServicesCore]
- Subject: Passing arrays via WSMethodInvocationInvoke() [WebServicesCore]
- From: Dan Korn <email@hidden>
- Date: Fri, 23 Oct 2009 16:28:32 -0500
I'm writing some Objective-C (actually Objective-C++) code to create a
Cocoa framework to call into a Web Service (hosted in ASP.NET) using
WSMethodInvocationInvoke(), based on the example from here:
http://developer.apple.com/internet/webservices/webservicescoreandcfnetwork.html
This works great for Web Service calls which take simple parameters
(basically key-value pairs of strings), for example:
NSDictionary *params = [NSDictionary dictionaryWithObject:@"test"
forKey:@"GroupName"];
To call a method like so (leaving off some of the outer
<soap:Envelope> tags and such):
<soap:Body>
<ListSubGroups xmlns="foo">
<GroupName>test</GroupName>
</ListSubGroups>
</soap:Body>
And I can get responses with simple parameters (such as a returned
string) as well, like so:
NSDictionary *result = (NSDictionary *)WSMethodInvocationInvoke
(soapReq);
NSString *resultField = [result objectForKey:@"ListSubGroupsResult"];
To parse a response like:
<soap:Body>
<ListSubGroupsResponse xmlns="foo">
<ListSubGroupsResult>Return Value Here</ListSubGroupsResult>
</ListSubGroupsResponse>
</soap:Body>
Things get more complicated, though, when dealing with array
parameters (string[] in the C# source code of the Web Service). I've
figured out how to process a return value (output parameter) which is
an array (of strings), like so:
<soap:Body>
<ListSubGroupsResponse xmlns="foo">
<ListSubGroupsResult>
<string>Group1</string>
<string>Group2</string>
</ListSubGroupsResult>
</ListSubGroupsResponse>
</soap:Body>
With code like this:
NSDictionary* resultField = [result
objectForKey:@"ListSubGroupsResult"];
id names = [resultField valueForKey:@"string"];
NSString* Items = nil;
int count = 0;
if ([names isKindOfClass:[NSString class]])
{
count = 1;
Items = (NSString*)names;
}
else if ([names isKindOfClass:[NSArray class]])
{
count = [(NSArray*)names count];
Items = [(NSArray*)names componentsJoinedByString:@"\t"];
}
However, I'm stymied trying to generate a call to a function that
takes an array as an input parameter, like this:
<soap:Body>
<MethodWithArrayParam xmlns="foo">
<Data>
<string>Value1</string>
<string>Value2</string>
</Data>
</MethodWithArrayParam >
</soap:Body>
If I try to duplicate what I get back in the return parameter case
(above), by creating an NSArray and adding it to a NSDictionary with
the key "string", like so:
NSArray* values; // init not shown
// ...
NSDictionary* valuesDict = [NSDictionary
dictionaryWithObject:values forKey:@"string"];
NSDictionary *params = [NSDictionary dictionaryWithObject:
valuesDict forKey:@"Data"];
Then the calling markup ends up like this (leaving off some of the
extraneous xsi:type attributes and such):
<soap:Body>
<MethodWithArrayParam xmlns="foo">
<Data>
<string>
<item_0>Value1</item_0>
<item_1>Value2</item_1>
</string>
</Data>
</MethodWithArrayParam >
</soap:Body>
And the Web Service doesn't recognize those tags, so I get a fault
response. I can get rid of the intermediate NSDictionary, but the
NSArray is still serialized as <item_0>, <item_1>, etc. And AFAIK an
NSDictionary can't have more than one key named "string" to generate
the multiple <string> tags.
I can't find any combination of NSArray, NSDictionary, or any other
Cocoa objects that will serialize properly. Again, what the Web
Service wants is something like:
<Data>
<string>Value1</string>
<string>Value2</string>
<string>Value3</string>
</Data>
Which ends up as a string[] (array of string) parameter in the C# code
of my ASP.NET Web Service.
Here's a live, public example of a similar Web Service method with the
same kind of input array parameter (from someone else's site, found
via Google):
http://www.chemspider.com/Search.asmx?op=CSID2ExtRefs
I found this old post which seems to describe the same problem, but
there were no responses:
http://lists.apple.com/archives/cocoa-dev/2004/Mar/msg01729.html
http://www.cocoabuilder.com/archive/message/cocoa/2004/3/26/102571
Now, as that post suggests, I've tried using a workaround using
WSMethodInvocationAddSerializationOverride(), similar to the one in
this post:
http://lists.apple.com/archives/Macnetworkprog/2009/Apr/msg00063.html
The serialization function is:
static CFStringRef _arrayToXML(WSMethodInvocationRef invocation,
CFTypeRef obj, void *info)
{
if (obj != nil && (CFGetTypeID(obj) == CFArrayGetTypeID()))
{
NSArray* array = (NSArray*)obj;
NSMutableString* result = [[NSMutableString alloc]
initWithFormat:@"<%s>", "%@"];
for (int i = 0; i < [array count]; i++)
[result appendFormat:@"<string>%@</string>", [array
objectAtIndex:i]];
[result appendFormat:@"</%s>", "%@"];
return (CFStringRef)result;
}
return NULL;
}
Which allows the entry in the parameter dictionary to be an NSArray,
and it generates the right markup.
This basically works, but it doesn't properly handle certain things,
such as XML control characters (less-than signs and ampersands) in the
strings in the array. Those obviously need to be replaced with
entities. There are also other subtle encoding issues with UTF-16
characters.
Now, I can do the entity replacements myself, and otherwise try to
roll my own code to handle all the vagaries of properly encoding the
data into the XML markup, in the serialization override function.
However, it seems like there must be an easier way to do this.
So, I have two questions:
1. Is there any Cocoa object, or combination of objects, that I can
use to generate the required XML without having to call
WSMethodInvocationAddSerializationOverride()?
2. Is there something built-in I can call in my serialization override
function to properly encode the data, escaping XML control characters
with entities and such, in the same way that the default serialization
works?
For the latter, I've poked around in here:
http://developer.apple.com/mac/library/documentation/Cocoa/Conceptual/Archiving/Concepts/serializations.html
And looked at NSArchiver, NSEncoder, et al, but I can't seem to figure
out how to hook those into my custom serialization.
I suppose I could just change the Web Service method itself (or add an
alternate method) to take a single string parameter, and make it tab-
delimited or something, but the Web Service is already published and
in use and I don't really want to have to change it. I could also
just roll my own XML generation for the entire SOAP message, but that
seems like overkill just to get this one last part working. Anyway, I
really want to figure out how to call the function as-is with
WSMethodInvocationInvoke().
I'm using Xcode 3.2 on Snow Leopard on a MacBook Pro. (I'm pretty new
to Cocoa and Objective-C, so my code is leaking like a sieve, but I
just want to get it functional first, then I'll try to clean up the
memory.)
Apologies if there's a more appropriate forum for this question;
please redirect me.
Thanks,
Dan
_______________________________________________
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