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

From version 2.1
edited by smmccraw
on 2007/07/08 09:46
Change comment: There is no comment for this version
To version 4.1
edited by Pascal Robert
on 2007/09/03 19:49
Change comment: There is no comment for this version

Summary

Details

Page properties
Title
... ... @@ -1,1 +1,1 @@
1 -Programming__WebObjects-Web Services-Web Service Provider
1 +Web Services-Web Service Provider
Author
... ... @@ -1,1 +1,1 @@
1 -XWiki.smmccraw
1 +XWiki.probert
Content
... ... @@ -1,6 +1,6 @@
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.
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.
2 2  
3 -= Setting up a WO Web Services Project =
3 += Setting up a WO Web Services Project =
4 4  
5 5  Here are the basic steps for setting up a Web Services producer with WebObjects and Eclipse/WOLips:
6 6  
... ... @@ -14,44 +14,38 @@
14 14  11*. saaj.jar
15 15  11*. jaxrpc.jar
16 16  11. Edit the WO Frameworks collection and add the JavaWebServicesSupport framework from the System frameworks
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.
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.
18 18  1. Edit your Application class and add WOWebServiceRegistrar.registerWebService("PublishedNameOfYourWebService", NameOfTheClassYouJustMade.class, true);
19 19  
20 -That's it. Now when you start your app, you can request http:~/~/yourserver.com/cgi-bin/WebObjects/YourApp.woa/ws/PublishedNameOfYourWebService?wsdl and it will return the autogenerated WSDL document that you can use with any number of web service clients to interact with your server.
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.
21 21  
22 -= Complex Types with WO Web Services =
22 += Complex Types with WO Web Services =
23 23  
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:
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:
25 25  
26 26  {{panel}}
27 -
28 - WOWebServiceRegistrar.registerFactoriesForClassWithQName(new BeanSerializerFactory(_class, _qName), new BeanDeserializerFactory(_class, _qName), _class, _qName);
29 -
27 +WOWebServiceRegistrar.registerFactoriesForClassWithQName(new BeanSerializerFactory(_class, _qName), new BeanDeserializerFactory(_class, _qName), _class, _qName);
30 30  {{/panel}}
31 31  
32 -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.
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.
33 33  
34 -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.
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.
35 35  
36 36  With these registrations, you should now be able to communicate with WO using any standard Web Service client (Axis, .NET, etc).
37 37  
38 -= Sessions and WO Web Services =
36 += Sessions and WO Web Services =
39 39  
40 -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:
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:
41 41  
42 42  {{panel}}
43 -
44 - WOContext context = (WOContext)MessageContext.getCurrentContext().getProperty("com.webobjects.appserver.WOContext");
45 - WOSession session = context.session();
46 -
41 +WOContext context = (WOContext)MessageContext.getCurrentContext().getProperty("com.webobjects.appserver.WOContext");
42 + WOSession session = context.session();
47 47  {{/panel}}
48 48  
49 49  or the shortcut
50 50  
51 51  {{panel}}
52 -
53 - WOSession session = WOWebServiceUtilities.currentWOContext().session();
54 -
48 +WOSession session = WOWebServiceUtilities.currentWOContext().session();
55 55  {{/panel}}
56 56  
57 57  The following additional keys are accessible through the MessageContext:
... ... @@ -58,323 +58,306 @@
58 58  
59 59  * "com.webobjects.appserver.WOContext" = the WOContext for this request
60 60  * "transport.url" = I /believe/ this contains the full request URL up to the query string
61 -* org.apache.axis.transport.http.HTTPConstants.MC//HTTP//SERVLETPATHINFO = contains the request's request handler path
55 +* org.apache.axis.transport.http.HTTPConstants.MC_HTTP_SERVLETPATHINFO = contains the request's request handler path
62 62  * "Authorization" = contains the Authorization header, in the event that you need to process things like Kerberos/SPNEGO, etc.
63 63  * "remoteaddr" = contains the request's remote address
64 64  
65 -= Consuming with Axis in Java =
59 += Consuming with Axis in Java =
66 66  
67 -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 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/ . 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.
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.
68 68  
69 69  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.
70 70  
71 -= Consuming with WebServicesCore.framework =
65 += Consuming with WebServicesCore.framework =
72 72  
73 -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:
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:
74 74  
75 -= WSMakeStubs =
69 += WSMakeStubs =
76 76  
77 -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.
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.
78 78  
79 79  Running WSMakeStubs is very simple:
80 80  
81 -/Developer/Tools/WSMakeStubs --x ObjC --name NameOfServiceClass --url http:~/~/yourserver.com/cgi-bin/WebObjects/YourWOA.woa/ws/YourService?wsdl--
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"]]
82 82  
83 -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.
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.
84 84  
85 -= Service Methods Without Return Values =
79 += Service Methods Without Return Values =
86 86  
87 -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 [[super getResultDictionary]]. The problem with this is that [[super getResultDictionary]] is the code that actually executes the web service method. Simply change the definition for your void method to be:
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:
88 88  
89 -{{panel}}
83 +{{code}}
90 90  
91 - - (id) resultValue {
92 - return [self getResultDictionary];
93 - }
85 + - (id) resultValue {
86 + return [self getResultDictionary];
87 + }
94 94  
95 -{{/panel}}
96 96  
90 +{{/code}}
91 +
97 97  And everything will work as planned.
98 98  
99 -= Bugs and Changes to WSGeneratedObj =
94 += Bugs and Changes to WSGeneratedObj =
100 100  
101 -WSGeneratedObj is MOSTLY bug free. However, there there are a couple changes required to fix a memory leak it generates (from cocoadev.com):
96 +WSGeneratedObj is MOSTLY bug free. However, there there are a couple changes required to fix a memory leak it generates (from cocoadev.com):
102 102  
103 103  At the end of getResultDictionary, add:
104 104  
105 -{{panel}}
100 +{{code}}
106 106  
107 - if (fRef) { // new code
108 - WSMethodInvocationSetCallBack(fRef, NULL, NULL); // new code
109 - } // new code
110 - return fResult; // original code
102 + if (fRef) { // new code
103 + WSMethodInvocationSetCallBack(fRef, NULL, NULL); // new code
104 + } // new code
105 + return fResult; // original code
111 111  
112 -{{/panel}}
107 +{{/code}}
113 113  
114 -
115 115  which now reveals that the NSURL that is used is double-freed, fixable by removing one line from createInvocationRef:
116 116  
117 -{{panel}}
111 +{{code}}
118 118  
119 - NSURL* url = [NSURL URLWithString. endpoint];
120 - if (url == NULL) {
121 - [self handleError. at"NSURL URLWithString failed in createInvocationRef" errorString.NULL errorDomain.kCFStreamErrorDomainMacOSStatus errorNumber.paramErr];
122 - } else {
123 - ref = WSMethodInvocationCreate((CFURLRef) url, (CFStringRef)methodName, (CFStringRef) protocol);
124 - // [url release]; remove this line
125 - ....
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 + ....
126 126  
127 -{{/panel}}
121 +{{/code}}
128 128  
129 -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.
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.
130 130  
131 -= Passing a Complex Type to WO =
125 += Passing a Complex Type to WO =
132 132  
133 -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.
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.
134 134  
135 135  To send a complex type back to WO, you have to set the following keys in your dictionary:
136 136  
137 -{{panel}}
131 +{{code}}
138 138  
139 - [dictionary setObject.at"http.--extranet.mdtask.mdimension.com" forKey.(NSString *)kWSRecordNamespaceURI];
140 - [dictionary setObject.at"WSCompany" forKey.(NSString *)kWSRecordType];
133 + [dictionary setObject:@"http://extranet.mdtask.mdimension.com" forKey:(NSString *)kWSRecordNamespaceURI];
134 + [dictionary setObject:@"WSCompany" forKey:(NSString *)kWSRecordType];
141 141  
142 -{{/panel}}
136 +{{/code}}
143 143  
144 -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 .
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 .
145 145  
146 -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.
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.
147 147  
148 -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.
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.
149 149  
150 -One solution is to add
144 +One solution is to add
151 151  
152 -{{panel}}
146 +{{code}}
153 153  
154 - @implementation NSObject (NSObject_WOXML)
148 +@implementation NSObject (NSObject_WOXML)
155 155  
156 -{{/panel}}
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 +}
157 157  
158 -
158 +@end
159 159  
160 -{{panel}}
160 +{{/code}}
161 161  
162 - - (NSString*)xmlPlist {
163 - NSString* error;
164 - NSData* data = [NSPropertyListSerialization dataFromPropertyList:self
165 - format:NSPropertyListXMLFormat_v1_0
166 - errorDescription:&error];
167 - return [((NSString alloc] initWithData:data encoding:NSUTF8StringEncoding] autorelease];
168 - }
162 +on the cocoa side, than call it when compiling the arguments for the WSMethodInvocationRef
163 + Than on the WO side use NSPropertyListSerialization.propertyListFromString(xmlPlist) to recreate the object.
169 169  
170 -{{/panel}}
165 += Return Values from WO =
171 171  
172 -
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
173 173  
174 -{{panel}}
169 +{{code}}
175 175  
176 - @end
171 + - (id) resultValue {
172 + return [[super getResultDictionary] objectForKey: @"getBillableCompaniesReturn"];
173 + }
177 177  
178 -{{/panel}}
179 179  
180 -on the cocoa side, than call it when compiling the arguments for the WSMethodInvocationRef
176 +{{/code}}
181 181  
182 -Than on the WO side use NSPropertyListSerialization.propertyListFromString(xmlPlist) to recreate the object.
178 +however, the actual return value name requires its namspace to be included. The fixed version of the routine looks like:
183 183  
184 -= Return Values from WO =
180 +{{code}}
185 185  
186 -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
182 + - (id) resultValue {
183 + return [[super getResultDictionary] objectForKey: @"ns1:getBillableCompaniesReturn"];
184 + }
187 187  
188 -{{panel}}
186 +{{/code}}
189 189  
190 - - (id) resultValue {
191 - return [(super getResultDictionary] objectForKey: @"getBillableCompaniesReturn"];
192 - }
188 +Notice the key starts with "ns1:". This value should match the value that appears in your WSDL.
193 193  
194 -{{/panel}}
190 += Example Type Wrappers =
195 195  
196 -however, the actual return value name requires its namspace to be included. The fixed version of the routine looks like:
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.
197 197  
198 -{{panel}}
194 +{{code}}
195 + @interface WSCompany : NSObject {
196 + NSMutableDictionary *myDictionary;
197 + }
199 199  
200 - - (id) resultValue {
201 - return [(super getResultDictionary] objectForKey: @"ns1:getBillableCompaniesReturn"];
202 - }
199 + -(id)initWithDictionary:(NSDictionary *)_dictionary;
200 + -(NSDictionary *)dictionary;
201 + -(NSString *)name;
202 + -(NSString *)companyID;
203 + @end
204 +{{/code}}
203 203  
204 -{{/panel}}
206 +{{code}}
205 205  
206 -Notice the key starts with "ns1:". This value should match the value that appears in your WSDL.
208 + @implementation WSCompany
207 207  
208 -= Example Type Wrappers =
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 + }
209 209  
210 -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 [[wsCompany dictionary]] in the wrapper method.
218 + -(void)dealloc {
219 + [myDictionary release];
220 + [super dealloc];
221 + }
211 211  
212 -{{panel}}
223 + -(NSDictionary *)dictionary {
224 + return myDictionary;
225 + }
213 213  
214 - @interface WSCompany : NSObject {
215 - NSMutableDictionary *myDictionary;
216 - }
217 -
218 - -(id)initWithDictionary:(NSDictionary *)_dictionary;
219 - -(NSDictionary *)dictionary;
220 - -(NSString *)name;
221 - -(NSString *)companyID;
222 - @end
227 + -(NSString *)name {
228 + return [myDictionary objectForKey:@"name"];
229 + }
223 223  
224 -{{/panel}}
231 + -(NSString *)companyID {
232 + return [myDictionary objectForKey:@"companyID"];
233 + }
234 + @end
225 225  
226 - @implementation WSCompany
236 +{{/code}}
227 227  
228 -{{panel}}
238 += Fault Handling =
229 229  
230 -
231 - -(id)initWithDictionary:(NSDictionary *)_dictionary {
232 - self = [super init];
233 - myDictionary = [(_dictionary mutableCopy] retain];
234 - [myDictionary setObject.at"http.--extranet.mdtask.mdimension.com" forKey.(NSString *)kWSRecordNamespaceURI];
235 - [myDictionary setObject.at"WSCompany" forKey.(NSString *)kWSRecordType];
236 - return self;
237 - }
238 -
239 - -(void)dealloc {
240 - [myDictionary release];
241 - [super dealloc];
242 - }
243 -
244 - -(NSDictionary *)dictionary {
245 - return myDictionary;
246 - }
247 -
248 - -(NSString *)name {
249 - return [myDictionary objectForKey.at"name"];
250 - }
251 -
252 - -(NSString *)companyID {
253 - return [myDictionary objectForKey.at"companyID"];
254 - }
255 - @end
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
256 256  
257 -{{/panel}}
242 +{{code}}
258 258  
259 -= Fault Handling =
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 + }
260 260  
261 -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__
262 262  
263 -{{panel}}
258 +{{/code}}
264 264  
265 - + (id) resultForInvocation:(WSGeneratedObj*)invocation; {
266 - result = [(invocation resultValue] retain];
267 - // Added check if a fault occured and return the fault string if so
268 - if([invocation isComplete]) {
269 - if([invocation isFault]) {
270 - result = [(invocation getResultDictionary] valueForKey:@"/FaultString"];
271 - }
272 - }
273 - //
274 - [invocation release];
275 - return result;
276 - }
260 += Stateful Services =
277 277  
278 -{{/panel}}
279 -
280 -= Stateful Services =
281 -
282 282  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:
283 283  
284 -{{panel}}
264 +{{code}}
285 285  
286 - @interface WSGeneratedObj : NSObject {
287 - WSMethodInvocationRef fRef;
288 - NSDictionary* fResult;
289 - NSDictionary* fCookies;
290 - NSString fURLString;
291 - int fTimeout;
292 -
293 - id fAsyncTarget;
294 - SEL fAsyncSelector;
295 - };
266 + @interface WSGeneratedObj : NSObject {
267 + WSMethodInvocationRef fRef;
268 + NSDictionary* fResult;
269 + NSDictionary* fCookies;
270 + NSString fURLString;
271 + int fTimeout;
296 296  
297 -{{/panel}}
273 + id fAsyncTarget;
274 + SEL fAsyncSelector;
275 + };
298 298  
277 +{{/code}}
278 +
299 299  Here are the new methods to add to WSGeneratedObject.m:
300 300  
301 -{{panel}}
281 +{{code}}
282 + -- (id) initWithWebServicesURLString:(NSString*)urlString
283 + {
284 + if (self = [super init]) {
285 + fURLString = [urlString copy];
286 + }
287 + return self;
288 + }
302 302  
303 - - (id) initWithWebServicesURLString:(NSString*)urlString
304 - {
305 - if (self = [super init]) {
306 - fURLString = [urlString copy];
307 - }
308 - return self;
309 - }
310 -
311 - - (NSString*) getWebServicesURLString { return fURLString; }
312 -
313 - - (NSURL*) getWebServicesURL { return [NSURL URLWithString. (self getWebServicesURLString]]; }
314 -
315 - - (NSArray*) getReturnedCookies
316 - {
317 - NSDictionary *results = [self getResultDictionary];
318 - if (nil == results)
319 - return nil;
320 - CFHTTPMessageRef msgRef = (CFHTTPMessageRef)[results objectForKey. (id)kWSHTTPResponseMessage];
321 - NSDictionary *headers = (NSDictionary*)CFHTTPMessageCopyAllHeaderFields(msgRef);
322 - [headers autorelease];
323 - //parse the cookies
324 - NSArray *cookies = [NSHTTPCookie cookiesWithResponseHeaderFields. headers forURL. (self getWebServicesURL]];
325 - return cookies;
326 - }
327 -
328 - - (void) setCookies:(NSArray*)cookies
329 - {
330 - [fCookies release];
331 - fCookies = [(NSHTTPCookie requestHeaderFieldsWithCookies. cookies] retain];
332 - WSMethodInvocationSetProperty([self getRef], kWSHTTPExtraHeaders, fCookies);
333 - }
290 + - (NSString*) getWebServicesURLString { return fURLString; }
334 334  
335 -{{/panel}}
292 + - (NSURL*) getWebServicesURL { return [NSURL URLWithString: [self getWebServicesURLString]]; }
336 336  
337 -* (int)timeoutValue { return fTimeout; }
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 + }
338 338  
339 -{{panel}}
307 + - (void) setCookies:(NSArray*)cookies
308 + {
309 + [fCookies release];
310 + fCookies = [[NSHTTPCookie requestHeaderFieldsWithCookies: cookies] retain];
311 + WSMethodInvocationSetProperty([self getRef], kWSHTTPExtraHeaders, fCookies);
312 + }
313 +{{/code}}
340 340  
341 - - (void)setTimeout:(int)t
342 - {
343 - if (t >= 0 && t < 600)
344 - fTimeout = 30;
345 - }
315 +{{code}}
346 346  
347 -{{/panel}}
317 + - (int)timeoutValue { return fTimeout; }
318 + - (void)setTimeout:(int)t
319 + {
320 + if (t >= 0 && t < 600)
321 + fTimeout = 30;
322 + }
348 348  
349 -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.
350 350  
351 -{{panel}}
325 +{{/code}}
352 352  
353 - - (WSMethodInvocationRef) genCreateInvocationRef
354 - {
355 - WSMethodInvocationRef invRef = [self createInvocationRef
356 - /*endpoint*/: [self getWebServicesURLString]
357 - methodName: NSStringFromClass([self class])
358 - protocol: (NSString*) kWSSOAP2001Protocol
359 - style: (NSString*) kWSSOAPStyleRPC
360 - soapAction: @""
361 - methodNamespace: @"http://DefaultNamespace"];
362 - //set a time-out value
363 - if (fTimeout > 0) {
364 - WSMethodInvocationSetProperty(invRef, kWSMethodInvocationTimeoutValue, (CFTypeRef)[NSNumber numberWithInt. fTimeout]);
365 - // WSMethodInvocationSetProperty(invRef, kWSDebugIncomingBody, (CFTypeRef)kCFBooleanTrue);
366 - }
367 - return invRef;
368 - }
369 -
370 - - (id) resultValue
371 - {
372 - NSString *key = [NSString stringWithFormat%3a at"ns1%3a%atReturn", NSStringFromClass((self class])];
373 - return [(self getResultDictionary] objectForKey: key];
374 - }
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.
375 375  
376 -{{/panel}}
329 +{{code}}
377 377  
378 -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.
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 + }
379 379  
380 -Category:WebObjects
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 +
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.