Tuesday, April 08, 2008

Array types in ColdFusion web services

I get asked questions about publishing web services in ColdFusion often and this is one that many folks run in to that I wanted to post about as I just got asked this. Usually people send mail to Ben Forta, and he just forwards them on to me. :-)

Question - How do I create a function that has an array of custom made objects as an argument or return value?

First, let me recommend reading the ColdFusion 8 Developers guide chapter on web services (http://livedocs.adobe.com/coldfusion/8/htmldocs/webservices_01.html). I would also recommend reading Ben Forta's (et al) book, the Web Application Construction Kit Volume 3, chapter 68 – Creating and Consuming Web Services.

The first piece of information to know is that the CFML complex types, such as they are, might not the best things to use when creating a Web Service. Lets take Struct's for example. When you define an argument to a function as a struct, the XML Schema that is emitted for the WSDL defines an object that has the "any" type as both the key and the value. But this doesn't give the consumer of the web service much information - are there strings or complex types as the key? What kind of values should there be? Should there be various different things contained in the structure?

A better way to create this service is by defining exactly what kind of things you expect or return. To do this you would create a ColdFusion Component (CFC) that used the (mostly useless except for web services) cfproperty tag to describe the structure. Lets say our structure contained a just string pairs. Here is how you would define that:

<cfcomponent>
<cfproperty name="key" type="string">
<cfproperty name="value" type="string">
</cfcomponent>

Now our XML Schema in the WSDL would define a complexType that has two element in it at key (of type string) and value (also of type string). This has the advantage of clarity and also is potnetially much more interoperabile.

But the original question was about arrays. Lets get back to that.

If you want to publish a web service using CFC's then you would define the cffunction that takes an array as an argument or as a returnType. You can define this array to be of a particular CFC type that you have defined using the cfproperty tag as we did above. The example in the Forta book (Volume 3, page 298, listing 68.11 and 68.12) also shows this. The missing piece is how to specify that the argument is an array.

<cffunction name="GetCreditRating" returntype="string" output="no" access="remote">
<cfargument name="person" type="CreditPerson[]" required="yes">

Notice the "[]" after the name of the component. This will indicate that the WSDL should define the CreditPerson complexType and that the argument to the function should be 1 or more (MaxOccurs="unbounded") of these complex type elements i.e. an array. You can use this with types defined by CFCs or even for simple types (e.g. string[]). This syntax is valid in both the returnType attribute and the type attribute of cfargument.

This information is in the ColdFusion 8 documentation at
http://livedocs.adobe.com/coldfusion/8/htmldocs/webservices_20.html

The follow up to this is if you want to consume a web service (such as the one above) you would define an array in CFML and put in it Structs that correspond to the complex type in the WSDL. Here is an example again based on the GetCreditRating that uses the new CF8 syntax for creating stuctures:

<cfscript>
arg = ArrayNew(1);
s1 = { FirstName=Tom, Lastname=Jordahl, …};
s2 = { FirstName=Ben, Lastname=Forta, …};

arg[1] = s1;
arg[2] = s2;
</cfscript>

Then you would pass the “arg” array as the parameter to the invocation of the web service.

I hope this fleshes out a little but about array types for ColdFusion web services.

13 comments:

webRat said...

Wait, so Ben Forta is "Oz" and you're the man behind the curtain?! ;)

Ben Forta said...

Absolutely. The best way to have all the answers is to know who to forward the questions too. :-) Thanks for posting this Tom, I'll refer folks to the link in the future.

--- Ben

Anonymous said...

Hi Tom,

We are stuck at at case where the return type of a webservice is an object (CFC)
One of property of this component is Array

for e.g.

cfcomponent>
cfproperty name="addressList" type="Address[]" />
cfproperty name="exception" type="CustomException" />
/cfcomponent>

First is it the right way of declaring the component.
Secondly when Axis generates the wsdl it generates an element ArrayOfXsdAnytype which holds the collection.
Can you please help us out here

Thanks
SG

Anonymous said...

the previous component is addressRespond.cfc

and the function signature is
cffunction name="getAddress" returnType="AddressRespond" access="remote" output="no">

Another issue is the xml schema generated and soap response dont match.
dint want to clutter with a lot of code, but can provide
-SG

Tom said...

@SG - It lokos like you are using the Array of CFC notation correctly, assuming the "Address" and "CustomException" are also CFCs and do not have a package name. You may need to use a "dot" path (i.e. "com.example.components.Address" for a CFC that lives in wwwroot/com/example/components/Address.cfc).

Anonymous said...

I have opened a topic in the forum regarding this, with all the code
It would be great, if you can post your recommendations there.

Thanks

Shirish said...

http://www.adobe.com/
cfusion/webforums/forum/
messageview.cfm?forumid=1&catid=7
&threadid=1378883&enterthread=y

Steve Parks - Adept Developer said...

This approach works in the main Web Service where you can place CreditPerson[] as the TYPE attribute for CFARGUMENT, but if doesn't work as well with CFPROPERTY. Take the below example:

<Contact>
<InternetInfo>
<Email>
<Addy>guest@host.com</Addy>
<DisplayAs>Guest</DisplayAs>
</Email>
<Email>
<Addy>friend@host.com</Addy>
<DisplayAs>Friend</DisplayAs>
</Email>
</InternetInfo>
</Contact>

So we need to create some CFCs for Contact (the main argument for the Web Service Method), InternetInfo (for the complex type under Contact), Email (has two elements describing each email address).

In the InternetInfo CFC, we'll specify CFPROPERT NAME="Email" Type="Email[]", but the output will be displayed undesirably (imo, because it makes consumption from other languages more difficult):

<InternetInfo>
<Email>
<rpc:item>
<Addy></Addy>
<DisplayAs></DisplayAs>
</rpc:item>
<rpc:item>
<Addy></Addy>
<DisplayAs></DisplayAs>
</rpc:item>
</Email>
</InternetInfo>

Any idea how to accomplish the more desireable result so consumption from other languages isn't so painful?

Aaron Foote said...

Hi Tom

I have an interesting problem related to arrays and webservices I was hoping you could help.

I am attempting to create a WebService in ColdFusion and have been trying to get a .NET program to consume it (tested with vs2005 & vs2008 - CF 7.02 & 8.01)

Returning an array as the only returntype works fine. When that array is nested in a component .NET can not see the returned array - .NET shows it as an array with 0 elements.

Eg

<cfcomponent style=”document” >
<cffunction returntype=”Component1” access=”remote”>

</cffunction>
</cfcomponent>

<cfcomponent name=”Component1” >
<cfproperty name=”comp2” type=”Component2[]”>
<cfpropert name=”status” type=”string”>
</cfcomponet>

<cfcomponent name=”Component2” >
<cfproperty name=”bob” type=”string”>
</cfcomponent>


Any help would be greatly appreciated

Wesley said...

I am trying to do the exact same thing as Aaron. Building web services in ColdFusion to be consumed by MS InfoPath. It seems to see the structure of the property that is an array of objects but it is always empty.

Tom said...

Humm. I am not sure what is going on here, but I would definitely use a monitor tool to intercept or log the SOAP XML that is going to the client to see if CF is sending the right stuff and .NET is not understanding it or if CF/Axis is sending the wrong stuff.

You can use runtime/bin/sniffer.exe (aka tcpmon) in the ColdFusion installation to do this, but you will have to configure it to listen on a port (say 8080) and proxy to the actual CF port (say 80). Then make a copy of the WSDL you feed in to the client and change the URL port number to 8080 so it goes through TCPMon.

Another (simpler) alternative is to turn on the log handler in the Axis client-config.wsdd found in ColdFusion/wwwroot/WEB-INF. Un-comment out the requestFlow and responseFlow elements. I would then run CF from the command line and you should see a log of the SOAP XML that is getting sent.
Hope that helps.

Dan said...

I was wondering, is there a way to cache a web service? If we create a webservice on server 1. It has a large amount of data in it. On server 2, we'd like, in our application, to create an application variable out of the web service information, but don't want the 45 second lag time for the service to run and be turned into an application variable. By caching the service, it seems like it would run fast.
Is this a good approach, or is there something better?
Thanks

Tom said...

@Dan - I am not sure what you are asking for here, but if I get the gist, the answer is no, you do not want to cache a web service. A web service should retrieve the data from its source when asked, otherwise the data would be stale.

You could cache a result set for a period of time, which would save some work on the server if the same requests are run over and over.