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?

5 comments:

Steven Klotz said...

What would these calls actually look like in ColdFusion? I'm having some issues invoking web services with complex types, and it sounds like you've got this all figured out...

Jeremy said...

Steven, I will post some sample code that replicates the above as best as possible. Also see my later post about manually running wsdl2java and compiling the stub classes instead of relying on ColdFusion to do it. I ran into issues with certain methods expecting datatypes other than what was documented. Also check out Ben Nadel's GetJavaClassMethods udf. It has been a lifesaver when it comes to debugging (I also posted a cfscript equivalent with error handling in the comments on his original post). This is how I finally figured out that the wsdl methods were screwed up and needed to be recompiled by hand. I passed the web service object to GetJavaClassMethods and dumped the results to see what datatype each method expected.

The key for the ColdFusion equivalent of these calls is using the CreateObject function.

For the above code, here is how I would recreate it (see next comment)...

Jeremy said...

// this is all in cfscript....sorry, but the java classes and methods are fairly specific to Netsuite, but hopefully points in the right direction
RecordRef1 = CreateObject("java", "com.netsuite.webservices.platform.core_2_5.RecordRef"); // 2nd param is the fully qualified java class name
RecordRef2 = CreateObject("java", "com.netsuite.webservices.platform.core_2_5.RecordRef"); // 2nd param is the fully qualified java class name
RecordRef3 = CreateObject("java", "com.netsuite.webservices.platform.core_2_5.RecordRef"); // 2nd param is the fully qualified java class name
RecordType = CreateObject("java", "com.netsuite.webservices.platform.core_2_5.types.Recordtype");

aRecordRef = ArrayNew(1);

RecordRef1.setInternalId("1");
RecordRef2.setInternalId("2");
RecordRef3.setInternalId("3");

RecordRef1.setType(RecordType.customer);
RecordRef2.setType(RecordType.customer);
RecordRef3.setType(RecordType.customer);

ArrayAppend(aRecordRef, RecordRef1);
ArrayAppend(aRecordRef, RecordRef2);
ArrayAppend(aRecordRef, RecordRef3);

SearchMultiSelectField = CreateObject("java", "com.netsuite.webservices.platform.core_2_5.SearchMultiSelectField");
SearchMultiSelectFieldOperator = CreateObject("java", "com.netsuite.webservices.platform.core_2_5.types.SearchMultiSelectFieldOperator");

CustomerSearchBasic = CreateObject("java", "com.netsuite.webservices.platform.common_2_5.CustomerSearchBasic");

SearchMultiSelectField.setSearchValue(getJavaArray(aRecordRef));
SearchMultiSelectField.setOperator(SearchMultiSelectFieldOperator.anyOf);

CustomerSearchBasic.setInternalId(SearchMultiSelectField);

customerSearchBasic.setInternalId(new SearchMultiSelectField(rr, SearchMultiSelectFieldOperator.anyOf));

function getJavaArray(aArray) {
// Given a populated coldfusion array, return the equivalent java array of oJavaClass elements. The elements contained
// in aArray must be of the same java class as oJavaClass.
// PARAMETERS:
// aArray - Required. Array of any valid datatype. Convert this array to a java array.
// oJavaClass - Optional. Object of the datatype of which you want to convert aArray. If not passed, then the first element.
// of aArray will be used to determine the datatype.
// RETURNS: java.lang.reflect.Array
var i = 0;
var javaArray = CreateObject("java", "java.lang.reflect.Array");
var returnArray = "";

if (ArrayLen(Arguments) eq 2) { // Check if optional oJavaClass argument exists.
oJavaClass = Arguments[2];
}
else { // if optional oJavaClass not passed, use the first element of aArray.
oJavaClass = aArray[1];
}
returnArray = javaArray.newInstance(oJavaClass.getClass(), ArrayLen(aArray)); // create the java array of the same class as the object oJavaClass of the same length as aArray
for (i=1; i lte ArrayLen(aArray); i=i+1) { // create the java equivalent of the array
javaArray.set(returnArray, JavaCast("int", i-1), aArray[i]);
} // End for

return returnArray;
} // End function getJavaArray

Jeremy said...

If you are viewing the above, and lines are getting cut off, click here. I haven't quite got a good way to do the posting of code yet.

cluksha said...

I was wondering if you are still working with CF and Netsuite in any way?

I too am pulling my hair out - however I know little to nothing about java classes so your examples are fairly gibberish to me.

Any chance you could point me in the right direction ot simply login and then be able to pull a list of products from Netsuite?