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.

19 comments:

webRat said...

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

Anonymous 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 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?

Anonymous 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.

DK said...

Hi Tom,

We migrated one of our web service APIs from CF7 to CF8.1 and we are now having issues with the case of the complex data types that are returned by the web service.

Basically, this is what is happening:

mycustomobject.cfc

<cfcomponent bindingname="mycustomobject">
<cfproperty name="message" type="string">
<cfproperty name="messagecode" type="numeric">
</cfcomponent>

My API:
<cfcomponent>
<cffunction name="testmethod" access="remote" returntype="mycustomobject">
<cfset var myobj = createObject("component", "mycustomobject")>
<cfset myobj.message = "Foo">
<cfset myobj.messagecode = 22>
</cffunction>
</cfcomponent>


Webservice Calling page:
<cfset b = createObject("webservice", "http://xx.xx.xx.xx/testapi.cfc?wsdl")>

<cfset result = b.testmethod()>
<cfdump var="#result#">

The wsdl that is generated has this definition of the complex type:
<complexType name="mycustomobject">
<sequence>
<element name="message" nillable="true" type="xsd:string"/>
<element name="messagecode" nillable="true" type="xsd:double"/>
</sequence>
</complexType>

Here you will see that the wsdl shows the complex type as "mycustomobject" (small case). But the call to the web service returns "Mycustomobject" (titlecase).

This results in non-CF platforms not being able to call the webservice. Any ideas if this is configurable or if there's a way to affect this behaviour?

Anonymous said...

Ben Forta, best-selling ColdFusion author is coming to India this August at India's largest Adobe Flash Platform Conference. Ben Forta will conduct a visionary keynote on the opening day of the summit. For more information and to register log on to www.adobesummit.com

Unknown said...

How do I specify that the return value of my web service is an array of strings?

Saying

cffunction name="getInfoSources" access="remote" returntype="string[]">

produces the error:

coldfusion.xml.rpc.CFCInvocationException: [coldfusion.xml.rpc.SkeletonClassLoader$UnresolvedCFCDataTypeException : Cannot resolve CFC datatype: string.]

when I try to create the WSDL

Thanks!

-- Mabel :-)
mliang@datacollaborative.com

Tim Brown said...

Thanks a lot! This was very helpful. Just one question about how the request is structured (i assume this would apply to the response as well).

I have created a component with properties called participant.cfc

<cfcomponent>
cfproperty name="FirstName" type="string"
cfproperty name="LastName" type="string"
</cfcomponent>

in my saveParticipants function that has this argument:

<cfargument name="participants" type="participant[]" required="true">

the getSOAPRequest() method call renders a SOAP envelope with the following inner structure for the participants.

<participants soapenc:arrayType="ns2:participant[2]" xmlns:ns2="http://soap.mynamespace.com" xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/" xsi:type="soapenc:Array">
<participants xsi:type="ns2:participant">
<firstName xsi:type="xsd:string">
Jane
</firstName>
<lastName xsi:type="xsd:string">
Doe
</lastName>
</participants>
<participants xsi:type="ns2:participant">
<firstName xsi:type="xsd:string">
John
</firstName>
<lastName xsi:type="xsd:string">
Doe
</lastName>
</participants>
</participants>


Maybe this is just semantics, but why doesn't it create the inner elements of the < participants> tag as < participant> (singular) as specified in type of my argument? Any way to acheive this so that the rendered requests and other service methods will return XML that is as self-describing as possible?

Thanks again. This saved me hours of time.

coldfusion 9 developer said...

Thanks for the post. I too faced the same problem that milag said. Please feel free to reply me how to do it.

Andy Ewings said...

Hi. This is exactly what I am doing to define an array of complex types in a web service that I am posting however I am experiencing a sporadic problem. Every now and then (no pattern to it) the web service fails with a error message implying there is an issue invoking the objects. If I restart Coldfusion server it starts to work again. We are using CF 8 at the mo, which is soon to be upgraded to 10, but I haven't seen anything on line to imply its a recognised issue that has been fixed?