Last modified by Pascal Robert on 2007/09/03 19:49

Hide last authors
Pascal Robert 4.1 1 WebObjects supports Web Services both as a producer and a consumer, and it actually works quite well once you figure out how to get things properly configured. Hopefully this walkthrough can jumpstart that process for you.
smmccraw 1.1 2
Pascal Robert 3.1 3 = Setting up a WO Web Services Project =
smmccraw 1.1 4
5 Here are the basic steps for setting up a Web Services producer with WebObjects and Eclipse/WOLips:
6
7 1. Create a new WOApplication project
8 1. Edit the project's Build Path, and go to the Libraries tab
9 11. Add the following external jars from /Library/WebObjects/Extensions.
10 11*. axis.jar
11 11*. commons-logging.jar
12 11*. commons-discovery.jar
13 11*. wsdl4j.jar
14 11*. saaj.jar
15 11*. jaxrpc.jar
16 11. Edit the WO Frameworks collection and add the JavaWebServicesSupport framework from the System frameworks
Pascal Robert 4.1 17 1. Create a class to hold your web service methods. The methods do not need to be static and can both take complex types as parameters and return complex types as return values. For now, just return primitive types and/or String.
smmccraw 1.1 18 1. Edit your Application class and add WOWebServiceRegistrar.registerWebService("PublishedNameOfYourWebService", NameOfTheClassYouJustMade.class, true);
19
Pascal Robert 4.1 20 That's it. Now when you start your app, you can request [[http:~~/~~/yourserver.com/cgi-bin/WebObjects/YourApp.woa/ws/PublishedNameOfYourWebService?wsdl>>url:http://yourserver.com/cgi-bin/WebObjects/YourApp.woa/ws/PublishedNameOfYourWebService?wsdl||shape="rect"]] and it will return the autogenerated WSDL document that you can use with any number of web service clients to interact with your server.
smmccraw 1.1 21
Pascal Robert 3.1 22 = Complex Types with WO Web Services =
smmccraw 1.1 23
Pascal Robert 4.1 24 So now the issue of complex types. Returning complex types is fine, but you have to register the serializer and deserializer classes for each complex type you reference. If you do not, the server will attempt to serialize your object using the ArraySerializer (you'll see this exception on the server), and the client will complain about a nonsensical error with SYSTEMID (gotta love terrible error handling!). The fix for this is for each of your complex types, call the following method in your Application constructor:
smmccraw 1.1 25
26 {{panel}}
Pascal Robert 4.1 27 WOWebServiceRegistrar.registerFactoriesForClassWithQName(new BeanSerializerFactory(_class, _qName), new BeanDeserializerFactory(_class, _qName), _class, _qName);
smmccraw 1.1 28 {{/panel}}
29
Pascal Robert 4.1 30 where _class is the Class object that represents your complex type, and _qName is the QName (fully qualified name) of the class as it appears in your WSDL document. For instance, if you created a complex return type named Person and it is in the com.yourserver.service package, _class would be com.yourserver.service.Person.class and _qName would be new QName("http:~/~/service.yourserver.com", "Person"). Notice that the namespace is the inverse of your package name. You will need to call this method for each of the parameters and return types your reference.
smmccraw 1.1 31
Pascal Robert 4.1 32 For the record, I have no idea why you have to do this step manually - The WSDL was autogenerated, and thus it KNOWS the classes and their QName WSDL mappings, but I was not able to get things to work properly without this step. If anyone knows why this is, or a way around it, please update this article.
smmccraw 1.1 33
34 With these registrations, you should now be able to communicate with WO using any standard Web Service client (Axis, .NET, etc).
35
Pascal Robert 3.1 36 = Sessions and WO Web Services =
smmccraw 1.1 37
Pascal Robert 4.1 38 You may have noticed in your Web Service methods that you have no WOContext, WORequest, WOSession, and friends passed in. Do not fret. The WebServiceRequestHandler takes care to hook you up in this department using Axis's MessageContext class. You can use the following code to get to your WOSession:
smmccraw 1.1 39
40 {{panel}}
Pascal Robert 3.1 41 WOContext context = (WOContext)MessageContext.getCurrentContext().getProperty("com.webobjects.appserver.WOContext");
Pascal Robert 4.1 42 WOSession session = context.session();
smmccraw 1.1 43 {{/panel}}
44
45 or the shortcut
46
47 {{panel}}
Pascal Robert 3.1 48 WOSession session = WOWebServiceUtilities.currentWOContext().session();
smmccraw 1.1 49 {{/panel}}
50
51 The following additional keys are accessible through the MessageContext:
52
53 * "com.webobjects.appserver.WOContext" = the WOContext for this request
54 * "transport.url" = I /believe/ this contains the full request URL up to the query string
Pascal Robert 4.1 55 * org.apache.axis.transport.http.HTTPConstants.MC_HTTP_SERVLETPATHINFO = contains the request's request handler path
smmccraw 1.1 56 * "Authorization" = contains the Authorization header, in the event that you need to process things like Kerberos/SPNEGO, etc.
57 * "remoteaddr" = contains the request's remote address
58
Pascal Robert 3.1 59 = Consuming with Axis in Java =
smmccraw 1.1 60
Pascal Robert 4.1 61 If you are using Axis to consume a WO Web Service, be advised that there is an outstanding bug (open since circa 2003, no less) that axis by default does not support passing more than one cookie to the server. WO sends both woinst AND wosid, so you lose your session ID from the client on the return trip to the server. This can be fixed by applying the patch from [[http:~~/~~/issues.apache.org/jira/browse/AXIS-1059>>url:http://issues.apache.org/jira/browse/AXIS-1059||shape="rect"]] to your client's axis.jar. Axis 1.1 has been archived at Apache, but you can download the source from [[http:~~/~~/archive.apache.org/dist/ws/axis/1_1/>>url:http://archive.apache.org/dist/ws/axis/1_1/||shape="rect"]] . The patch does not perfectly apply. There are two rejected hunks, but it should be very obvious how to fix the rejects (the patch has two System.out.printlns that it claims were in the original source that were not). After fixing that, you can setStoreSessionIdInCookies(true) on your server's WOSession and setMaintainSessions(true) on your client's ServiceLocator and you'll be good to go.
smmccraw 1.1 62
63 This Axis bug appears to be fixed in recent versions of Axis, including version 1.4. Trying to upgrade the version of Axis in your WO Web Services server is not likely to be a happy experience (and likely neither will be upgrading Axis in a Direct To Web Services client - though I haven't tried this). However, it does seem to be possible to use a later version of the Axis jars on the classpath of a WebObjects application that intends to use classes generated by WSDL2Java to connect to a remote Web Services server - assuming that there are no WebObjects classes included in the WSDL. It is important in this case that you use matching version of WSDL2Java.
64
Pascal Robert 3.1 65 = Consuming with WebServicesCore.framework =
smmccraw 1.1 66
Pascal Robert 4.1 67 There are several complications when it comes to using WebServicesCore with WebObjects, all of which stem from the WSMakeStubs generated code. Upon using the code generated by WSMakeStubs, you will run into the following issues that need to be fixed in its code:
smmccraw 1.1 68
Pascal Robert 3.1 69 = WSMakeStubs =
smmccraw 1.1 70
Pascal Robert 4.1 71 Apple provides a program called WSMakeStubs that is similar to WSDL2Java in Axis, except that it sucks. It will, however, at least give you a starting point for building your web service client code, and with the changes outlined below, you can end up with decent client APIs.
smmccraw 1.1 72
73 Running WSMakeStubs is very simple:
74
Pascal Robert 4.1 75 /Developer/Tools/WSMakeStubs -x ObjC -name NameOfServiceClass -url [[http:~~/~~/yourserver.com/cgi-bin/WebObjects/YourWOA.woa/ws/YourService?wsdl>>url:http://yourserver.com/cgi-bin/WebObjects/YourWOA.woa/ws/YourService?wsdl||shape="rect"]]
smmccraw 1.1 76
Pascal Robert 4.1 77 This will produce Objective-C code that you can use to call your web service. As opposed to Axis, WSMakeStubs produces stateless code for your service (i.e. no session tracking or cookie support - only static methods for each method of your web service). All of the methods appear at the end of NameOfServiceClass.m that you will need to call. WSMakeStubs also produces WSGeneratedObj.m, which contains the lower level web service core calls.
smmccraw 1.1 78
Pascal Robert 3.1 79 = Service Methods Without Return Values =
smmccraw 1.1 80
Pascal Robert 4.1 81 Another bug in WSMakeStubs is related to methods that don't have return values. For void methods, the methods are never actually CALLED by WSMakeStubs. If you look at the code for the returnValue method, you will see that it never calls [[doc:WO.super getResultDictionary]]. The problem with this is that [[doc:WO.super getResultDictionary]] is the code that actually executes the web service method. Simply change the definition for your void method to be:
smmccraw 1.1 82
Pascal Robert 3.1 83 {{code}}
smmccraw 1.1 84
Pascal Robert 3.1 85 - (id) resultValue {
86 return [self getResultDictionary];
87 }
smmccraw 1.1 88
89
Pascal Robert 3.1 90 {{/code}}
91
smmccraw 1.1 92 And everything will work as planned.
93
Pascal Robert 3.1 94 = Bugs and Changes to WSGeneratedObj =
smmccraw 1.1 95
Pascal Robert 4.1 96 WSGeneratedObj is MOSTLY bug free. However, there there are a couple changes required to fix a memory leak it generates (from cocoadev.com):
smmccraw 1.1 97
98 At the end of getResultDictionary, add:
99
Pascal Robert 3.1 100 {{code}}
smmccraw 1.1 101
Pascal Robert 3.1 102 if (fRef) { // new code
103 WSMethodInvocationSetCallBack(fRef, NULL, NULL); // new code
104 } // new code
105 return fResult; // original code
smmccraw 1.1 106
Pascal Robert 3.1 107 {{/code}}
smmccraw 1.1 108
109 which now reveals that the NSURL that is used is double-freed, fixable by removing one line from createInvocationRef:
110
Pascal Robert 3.1 111 {{code}}
smmccraw 1.1 112
Pascal Robert 3.1 113 NSURL* url = [NSURL URLWithString: endpoint];
114 if (url == NULL) {
115 [self handleError: @"NSURL URLWithString failed in createInvocationRef" errorString:NULL errorDomain:kCFStreamErrorDomainMacOSStatus errorNumber:paramErr];
116 } else {
117 ref = WSMethodInvocationCreate((CFURLRef) url, (CFStringRef)methodName, (CFStringRef) protocol);
118 // [url release]; remove this line
119 ....
smmccraw 1.1 120
Pascal Robert 3.1 121 {{/code}}
smmccraw 1.1 122
Pascal Robert 4.1 123 Another change I like to make in the generated is to remove the hard-coded service URLs and pass them in from the code that calls the service (much like Axis does). This should be a fairly straightforward change, but I wanted to make a note about doing it. It will be fairly common that you want to talk to a development server and a production server using the same code, and as a result, you will want that variable to be parameterized.
smmccraw 1.1 124
Pascal Robert 3.1 125 = Passing a Complex Type to WO =
smmccraw 1.1 126
Pascal Robert 3.1 127 WSMakeStubs provides no direct support for passing complex types around - All you get is an NSDictionary, and all you can send back is an NSDictionary, with no instructions as to what exactly is IN these dictionaries.
smmccraw 1.1 128
129 To send a complex type back to WO, you have to set the following keys in your dictionary:
130
Pascal Robert 3.1 131 {{code}}
smmccraw 1.1 132
Pascal Robert 3.1 133 [dictionary setObject:@"http://extranet.mdtask.mdimension.com" forKey:(NSString *)kWSRecordNamespaceURI];
134 [dictionary setObject:@"WSCompany" forKey:(NSString *)kWSRecordType];
smmccraw 1.1 135
Pascal Robert 3.1 136 {{/code}}
smmccraw 1.1 137
Pascal Robert 4.1 138 Where kWSRecordNamespaceURI's value is the XML namespace of the type of the complex object you are passing, and kWSRecordType's value is the name of the type. On the WO side, the namespace will be the reverse of the type's class name, and the record type will be the name of the class. For instance, in the example above, the actual class on the server was named com.mdimension.mdtask.extranet.WSCompany .
smmccraw 1.1 139
Pascal Robert 4.1 140 The rest of the dictionary contains attribute=>value mappings. For instance, WSCompany in the example above has a "name" attribute, so the dictionary would also contains a "name" key that maps to the corresponding value.
smmccraw 1.1 141
Pascal Robert 4.1 142 When sending NSDictionary instances from Cocoa, the WO will fire the WOGlobalIDDeserializer and it will not properly parse the nsdictionary or nsarray, it appears that there is no default deserializer on the WO side for those classes.
smmccraw 1.1 143
Pascal Robert 4.1 144 One solution is to add
smmccraw 1.1 145
Pascal Robert 3.1 146 {{code}}
smmccraw 1.1 147
Pascal Robert 3.1 148 @implementation NSObject (NSObject_WOXML)
smmccraw 1.1 149
Pascal Robert 3.1 150 - (NSString*)xmlPlist {
151 NSString* error;
152 NSData* data = [NSPropertyListSerialization dataFromPropertyList:self
153 format:NSPropertyListXMLFormat_v1_0
154 errorDescription:&error];
155 return [[[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding] autorelease];
156 }
smmccraw 1.1 157
Pascal Robert 3.1 158 @end
smmccraw 1.1 159
Pascal Robert 3.1 160 {{/code}}
smmccraw 1.1 161
Pascal Robert 3.1 162 on the cocoa side, than call it when compiling the arguments for the WSMethodInvocationRef
Pascal Robert 4.1 163 Than on the WO side use NSPropertyListSerialization.propertyListFromString(xmlPlist) to recreate the object.
smmccraw 1.1 164
Pascal Robert 3.1 165 = Return Values from WO =
smmccraw 1.1 166
Pascal Robert 4.1 167 One of the other problems WSMakeStubs has is that it doesn't produce a valid identifier for retrieving a WO web service return value. In the generated code, you will see something like
smmccraw 1.1 168
Pascal Robert 3.1 169 {{code}}
smmccraw 1.1 170
Pascal Robert 3.1 171 - (id) resultValue {
172 return [[super getResultDictionary] objectForKey: @"getBillableCompaniesReturn"];
173 }
smmccraw 1.1 174
175
Pascal Robert 3.1 176 {{/code}}
smmccraw 1.1 177
Pascal Robert 4.1 178 however, the actual return value name requires its namspace to be included. The fixed version of the routine looks like:
smmccraw 1.1 179
Pascal Robert 3.1 180 {{code}}
smmccraw 1.1 181
Pascal Robert 3.1 182 - (id) resultValue {
183 return [[super getResultDictionary] objectForKey: @"ns1:getBillableCompaniesReturn"];
184 }
smmccraw 1.1 185
Pascal Robert 3.1 186 {{/code}}
smmccraw 1.1 187
Pascal Robert 4.1 188 Notice the key starts with "ns1:". This value should match the value that appears in your WSDL.
smmccraw 1.1 189
Pascal Robert 3.1 190 = Example Type Wrappers =
smmccraw 1.1 191
Pascal Robert 4.1 192 Here's an example type wrapper I use based on the WSCompany example above. In the static methods that WSMakeStubs creates that wrap my web service methods, I simply initWithDictionary this type with the result dictionary from the web service and return an instance of WSCompany rather than the dictionary. When I send one of these objects back, I simply send [[doc:WO.wsCompany dictionary]] in the wrapper method.
smmccraw 1.1 193
Pascal Robert 3.1 194 {{code}}
195 @interface WSCompany : NSObject {
196 NSMutableDictionary *myDictionary;
197 }
smmccraw 1.1 198
Pascal Robert 3.1 199 -(id)initWithDictionary:(NSDictionary *)_dictionary;
200 -(NSDictionary *)dictionary;
201 -(NSString *)name;
202 -(NSString *)companyID;
203 @end
Pascal Robert 4.1 204 {{/code}}
smmccraw 1.1 205
Pascal Robert 3.1 206 {{code}}
smmccraw 1.1 207
Pascal Robert 3.1 208 @implementation WSCompany
smmccraw 1.1 209
Pascal Robert 3.1 210 -(id)initWithDictionary:(NSDictionary *)_dictionary {
211 self = [super init];
212 myDictionary = [[_dictionary mutableCopy] retain];
213 [myDictionary setObject:@"http://extranet.mdtask.mdimension.com" forKey:(NSString *)kWSRecordNamespaceURI];
214 [myDictionary setObject:@"WSCompany" forKey:(NSString *)kWSRecordType];
215 return self;
216 }
smmccraw 1.1 217
Pascal Robert 3.1 218 -(void)dealloc {
219 [myDictionary release];
220 [super dealloc];
221 }
smmccraw 1.1 222
Pascal Robert 3.1 223 -(NSDictionary *)dictionary {
224 return myDictionary;
225 }
smmccraw 1.1 226
Pascal Robert 3.1 227 -(NSString *)name {
228 return [myDictionary objectForKey:@"name"];
229 }
smmccraw 1.1 230
Pascal Robert 3.1 231 -(NSString *)companyID {
232 return [myDictionary objectForKey:@"companyID"];
233 }
234 @end
smmccraw 1.1 235
Pascal Robert 3.1 236 {{/code}}
smmccraw 1.1 237
Pascal Robert 3.1 238 = Fault Handling =
smmccraw 1.1 239
Pascal Robert 4.1 240 WSMakeStubs doesn't handle the fault properly but it's in the dictionary. In +resultForInvocation: I added a few lines to check for and return the fault
smmccraw 1.1 241
Pascal Robert 3.1 242 {{code}}
smmccraw 1.1 243
Pascal Robert 3.1 244 + (id) resultForInvocation:(WSGeneratedObj*)invocation; {
245 result = [[invocation resultValue] retain];
246 // Added check if a fault occured and return the fault string if so
247 if([invocation isComplete]) {
248 if([invocation isFault]) {
249 result = [[invocation getResultDictionary] valueForKey:@"/FaultString"];
250 }
251 }
252 //
253 [invocation release];
254 return result;
255 }
smmccraw 1.1 256
257
Pascal Robert 3.1 258 {{/code}}
smmccraw 1.1 259
Pascal Robert 3.1 260 = Stateful Services =
smmccraw 1.1 261
262 Below is the necessary code to enable cookie support and stateful session with the files generated by WSMakeStubs. This code also includes changes so the base web services URL is supplied in the init method and allows specifying a timeout value (which I defaulted to 30 seconds). To WSGeneratedObj.h, add three new member variables:
263
Pascal Robert 3.1 264 {{code}}
smmccraw 1.1 265
Pascal Robert 3.1 266 @interface WSGeneratedObj : NSObject {
267 WSMethodInvocationRef fRef;
268 NSDictionary* fResult;
269 NSDictionary* fCookies;
270 NSString fURLString;
271 int fTimeout;
smmccraw 1.1 272
Pascal Robert 3.1 273 id fAsyncTarget;
274 SEL fAsyncSelector;
275 };
smmccraw 1.1 276
Pascal Robert 3.1 277 {{/code}}
278
smmccraw 1.1 279 Here are the new methods to add to WSGeneratedObject.m:
280
Pascal Robert 3.1 281 {{code}}
282 -- (id) initWithWebServicesURLString:(NSString*)urlString
283 {
284 if (self = [super init]) {
285 fURLString = [urlString copy];
286 }
287 return self;
288 }
smmccraw 1.1 289
Pascal Robert 3.1 290 - (NSString*) getWebServicesURLString { return fURLString; }
smmccraw 1.1 291
Pascal Robert 3.1 292 - (NSURL*) getWebServicesURL { return [NSURL URLWithString: [self getWebServicesURLString]]; }
smmccraw 1.1 293
Pascal Robert 3.1 294 - (NSArray*) getReturnedCookies
295 {
296 NSDictionary *results = [self getResultDictionary];
297 if (nil == results)
298 return nil;
299 CFHTTPMessageRef msgRef = (CFHTTPMessageRef)[results objectForKey: (id)kWSHTTPResponseMessage];
300 NSDictionary *headers = (NSDictionary*)CFHTTPMessageCopyAllHeaderFields(msgRef);
301 [headers autorelease];
302 //parse the cookies
303 NSArray *cookies = [NSHTTPCookie cookiesWithResponseHeaderFields: headers forURL: [self getWebServicesURL]];
304 return cookies;
305 }
smmccraw 1.1 306
Pascal Robert 3.1 307 - (void) setCookies:(NSArray*)cookies
308 {
309 [fCookies release];
310 fCookies = [[NSHTTPCookie requestHeaderFieldsWithCookies: cookies] retain];
311 WSMethodInvocationSetProperty([self getRef], kWSHTTPExtraHeaders, fCookies);
312 }
Pascal Robert 4.1 313 {{/code}}
smmccraw 1.1 314
Pascal Robert 3.1 315 {{code}}
smmccraw 1.1 316
Pascal Robert 3.1 317  - (int)timeoutValue { return fTimeout; }
318 - (void)setTimeout:(int)t
319 {
320 if (t >= 0 && t < 600)
321 fTimeout = 30;
322 }
smmccraw 1.1 323
324
Pascal Robert 3.1 325 {{/code}}
smmccraw 1.1 326
Pascal Robert 4.1 327 You will need to modify -dealloc to release fCookies and fURLString. Below is my modified version getCreateInvocationRef. It is modified to get the URL using the new accessor methods above, to get the method name from the class name (which makes a lot more sense than hard-coding it to the class name in every subclass), and to set the timeout. After that is a generic resultValues method so that your generated subclasses can have their -resultValues and -getCreateInvocationRef methods removed~-~--the only methods they require are for setting parameters. There is also a commented out line that you can uncomment to have debug information included in the results dictionary. This is very helpful when trying to debug the transfer of complex objects.
Pascal Robert 3.1 328
329 {{code}}
330
331 - (WSMethodInvocationRef) genCreateInvocationRef
332 {
333 WSMethodInvocationRef invRef = [self createInvocationRef
334 /*endpoint*/: [self getWebServicesURLString]
335 methodName: NSStringFromClass([self class])
336 protocol: (NSString*) kWSSOAP2001Protocol
337 style: (NSString*) kWSSOAPStyleRPC
338 soapAction: @""
339 methodNamespace: @"http://DefaultNamespace"];
340 //set a time-out value
341 if (fTimeout > 0) {
342 WSMethodInvocationSetProperty(invRef, kWSMethodInvocationTimeoutValue, (CFTypeRef)[NSNumber numberWithInt: fTimeout]);
343 // WSMethodInvocationSetProperty(invRef, kWSDebugIncomingBody, (CFTypeRef)kCFBooleanTrue);
344 }
345 return invRef;
346 }
347
348 - (id) resultValue
349 {
350 NSString *key = [NSString stringWithFormat: @"ns1:%@Return", NSStringFromClass([self class])];
351 return [[self getResultDictionary] objectForKey: key];
352 }
353
354
355 {{/code}}
356
smmccraw 1.1 357 To use stateful services, call getReturnedCookies after the first request and store the cookie dictionary. Then call setCookies: with that dictionary on all of your subsequent web services calls. Depending on the cookies you use, you might want to save a new copy of the cookies dictionary after each request.