Sunday, 26 April 2015

CFML: just cos something looks like an array doesn't mean it is an array

One of my Aussie chums hit me up the other day, asking me for some help with some quirky code he was seeing. We worked out what the issue was, but it stands repeating because it was non-obvious from a CFMLer's point of view.

The original code was along these lines:

// original.cfm
timezoneClass = createObject( "java", "java.util.TimeZone" );
allZones = timezoneClass.getAvailableIds();
writeDump(var=allZones, top=10);

arraySort(allZones, "text");
writeDump(var=allZones, top=10);

He was getting an array of all the time zones Java supports, and sorting them for use. However this is what was being output:

That's not particularly helpful, ColdFusion. How about saying what the frickin' error actually was!!

However I'm used to this sort of error when something goes wrong with some sort of Java operation. ColdFusion just doesn't report the error back very well.

So what's wrong here?

Well... I dunno the minutiae of the detail of the problem, but it's basically that arraySort() is designed to work on a CFML array, and what's coming back from that getAvailableIds() call is a Java array. Those are two different things. And, for one thing, a CFML "array" actually isn't an array in the Java sense of the notion.

When we looked at the data in the dump it looks like an array, so we didn't think twice about it. However let's look more closely:

cfmlArray = ["tahi", "rua", "toru", "wha"];
javaArray = cfmlArray.toList().split(",");

    java = {
    cfml = {

Here I do this:

This outputs:

Note that the CFML array is a coldfusion.runtime.Array, and the Java array is literally a String array. So not the same type of data at all. Now if we use ClassViewer to use reflection to look at what methods each type provides for, we see that a coldfusion.runtime.Array has a bunch of arraySort() methods mapping to the CFML arraySort() function, whereas - obviously - normal Java array will not have those methods. So arraySort() doesn't find the method, so it fails.
I'll not waste space here with the ClassViewer output, but they're here if you're interested:

So what's the solution then?

Well on a whim I figured Java arrays don't support sorting, but ArrayLists do, so I'd just convert the Array to an ArrayList. I didn't initially know how to do this, but Google did, and it's via java.util.Arrays, specifically the asList() method. So I arrived at this code:

javaArrayList = createObject("java", "java.util.Arrays").asList(javaArray);
arraySort(javaArrayList, "text");

And this works:

Note that an ArrayList also doesn't have all the same arraySort() methods that a CFML Array has, so I guess ColdFusion is just wise enough to look for a sort() method if the collection passed to arraySort() is an AbstractList (which an ArrayList is). A CFML array is also a subclass of AbstractList.

Because the ArrayList does its own sorting, my mate ended up using that method directly, rather than using CFML's arraySort().

All of this shows up a bug in Lucee, btw. Whilst trying to work out what the initial error was, I ran my repro code (full code: sortJavaArrayInline.cfm) through Lucee. Here's the result:

Note the result of doing the arraySort() on the Java array: it didn't error, but it also didn't work: the array still has its original order. This isn't so cool. If it didn't work, it should throw an error like ColdFusion does.

I'll raise a ticket for that, once I press send on this, so I have a URL to point back to this article to include in the ticket. Here it is: LDEV-320.

Righto. On with my Sunday.