Monday, August 28, 2006

Netsuite, ColdFusion, Java, and a lot of gray hair

Ok, so I'm more likely to lose my hair over this than have it turn gray, but I'm working with creating a ColdFusion interface with Netsuite's web service. I'm running into the source of a common complaint from other developers trying to use Netsuite's web service: the documentation is spotty, and what is available is often outright wrong, leaving us to a maddening cycle of herky jerky trial and error. Some of my issues have been solved handily by Netsuite's support staff through their user forum and a conversation with one of their engineers, but this one is particularly puzzling and troubling.

Here is a paraphrased version of an issue I posted in the Netsuite support forum (note that I had moved to writing the code directly in Java since most of the sample code in the documentation is in Java, and I wanted to ensure my problems were not ColdFusion-specific). If you have any ideas, I'm all ears, but if I find or am given a solution, I will be certain to post it here.

I'm trying to write what I thought would be a simple search that will return all of the subcustomers of a given customer. I'm trying to get my feet wet with the following Java sample code from page 91 of the Platform Guide. I'm using the 2.0 wsdl, and the Platform Guide document I'm reading also says it's for 2.0.

RecordRef[] rr = new RecordRef[] {new RecordRef("1", Recordtype.customer), new RecordRef("2", RecordType.customer), new RecordRef("3", RecordType.customer)};
CustomerSearchBasic customerSearchBasic = new CustomerSearchBasic();
customerSearchBasic.setInternalId(new SearchMultiSelectField(rr, SearchMultiSelectFieldOperator.anyOf));


When I try to run this code, it complains about my trying to pass a SearchMultiSelectField object to the setInternalId method. Looking at the java source generated by Axis 1.2 wsdl2java, it does look like the setInternalId method for CustomerSearchBasic is supposed to accept a SearchMultiSelectField object as a parameter. Here is the code snippet from the generated CustomerSearchBasic.java file:

public void setInternalId(com.netsuite.webservices.platform.core_2_0.SearchMultiSelectField internalId) {
this.internalId = internalId;
}



However, I became suspicious and found some code to retrieve the list of available methods and parameter types from the CustomerSearchBasic object. It appeared that the setInternalId parameter actually is expecting an array of RecordRef objects.

So I compiled and ran the following. No errors!

RecordRef[] rr = new RecordRef[] {new RecordRef("1", Recordtype.customer), new RecordRef("2", RecordType.customer), new RecordRef("3", RecordType.customer)};
CustomerSearchBasic customerSearchBasic = new CustomerSearchBasic();
customerSearchBasic.setInternalId(rr);


Am I missing something? Is it possible I'm somehow referencing an old java class from the 1.3.2 wsdl?

Tuesday, August 01, 2006

XMLSearch, XPath, and XML namespaces in ColdFusion

Sample code used in this posting is ColdFusion-specific, but the XPath syntax will likely apply to many XPath implementations in other languages.
I was looking a specific set of elements in a SOAP response and kept getting an empty array returned from XMLSearch. Turns out this was related to how the namespaces in the response were defined and/or assigned to certain elements. (see this Talking Tree posting)
The information in the Talking Tree posting helped me identify my problem, but in my case, it wasn't about noname namespaces. In fact, what if you want to find all elements of a given name and don't care about the namespace or even where it lies in the hierarchy? In the example below, I want to quickly retrieve all of the Response elements. Note that one Response element is a child of Other, while the remaining are direct descendants of ResponseList.

<ResponseList xmlns="urn:shama.lama.dingdong.net">
<Response>
<ns1:success xmlns:ns1="urn:core.shama.lama.dingdong.net">true</ns1:success>
<baseRef internalId="1234" xmlns:ns2="urn:core.shama.lama.dingdong.net"/>
</Response>
<Response>
<ns3:success xmlns:ns3="urn:core.shama.lama.dingdong.net">false</ns3:success>
<ns3:statusDetail type="ERROR">
<ns3:code>USER_ERROR</ns3:code>
<ns3:message>That record does not exist.</ns3:message>
</ns3:statusDetail>
</ns3:status>
<baseRef internalId="4421" xmlns:ns4="urn:core.shama.lama.dingdong.net"/>
</Response>
<Other>
<Response>
<ns3:success xmlns:ns3="urn:core.shama.lama.dingdong.net">false</ns3:success>
<ns3:statusDetail type="ERROR">
<ns3:code>RECORDNOTFOUND_ERROR</ns3:code>
<ns3:message>That record does not exist.</ns3:message>
</ns3:statusDetail>
</ns3:status>
</Response>
<warning>Import timed out briefly and process was restarted. No further errors were reported.</warning>
</Other>
</ResponseList>

Now if it weren't for the namespaces, I could simply use the following:
<cfset MyArray = XMLSearch(MyXMLDoc, "//Response")> 

...but in this case that would return an empty array.

Ok, but what about the noname namespace syntax:
<cfset MyArray = XMLSearch(MyXMLDoc, "//:Response")> 

That's fine if there's an actual noname namespace assigned to that element, but in this case there isn't one assigned.

After trying countless variations of XPath syntax, I still couldn't get anything other than a blank array returned. I became desperate and quickly wrote a function that strips all of the namespace definitions and labels out of the XML, then searched on the results of that. But I felt this was rather convoluted and added too much overhead. Surely there was a better way. I kept searching, and lo and behold, came across the local-name function. If you want to ignore namespaces and hierarchical context completely, you can search by the local name of the element:
<cfset MyArray = XMLSearch(MyXMLDoc, "//*[local-name()='Response']" 

And now you have your array containing the three Response elements.

March 11, 2008 UPDATE: Ryan commented that he had tried the syntax below with similar success. I have not tried this myself, but give it a shot:
<cfset MyArray = XMLSearch(MyXMLDoc, "//*:Response")>