Saturday 29 October 2022

CFML: addressing confusion around arrays returned from Java methods and using them with CFML code

G'day:

This has come up often enough that it's probably worth having something clear and googleable around for people to find when this crops up for them.

Context

Sometimes it's useful to call a Java method on a CFML object to benefit from functionality that's in Java that's not in CFML. Being able to call split to split a CFML string on a regular expression pattern is a good example:

string = "abcde"
arrayOfChars = string.split("")
writeDump(arrayOfChars)

Lovely.

Problem

So what's the problem? Often there isn't one. However for people who prefer using CFML's object.method() syntax rather than its function(object) syntax, they might get a surprise:

nextChars = arrayOfChars.map((char) => chr(asc(char) + 1))

This results in:

The map method was not found.
Either there are no methods with the specified method name and argument types
or the map method is overloaded with argument types that ColdFusion cannot decipher reliably.
ColdFusion found 0 methods that match the provided arguments.
If this is a Java object and you verified that the method exists, use the javacast function to reduce ambiguity.

Or Lucee (less info, slightly more helpful as to what the issue is):

No matching Method/Function for [Ljava.lang.String;.map(lucee.runtime.type.Lambda) found

What's the problem? Look at the Lucee error. It's pointing out that arrayOfChars is a [Ljava.lang.String (a Java String[]). So that code is trying to call CFML's map method on a Java String[]. Java String[] doesn't have a map method. one has to recall that when one is using object.method() syntax then the type of the object is what dictates what methods can be called. It's not the same with function(object) when function's implementation can take a "close enough" type and cast it to the type it actually needs.

"Workaround"

It's important to note that this would work no worries:

nextChars = arrayMap(arrayOfChars, (char) => chr(asc(char) + 1))

Solution

However sometimes one might want an actual CFML array (so the object passes type-checks, etc), and this is easy enough to achieve:

arrayOfChars = arrayNew(1).append(arrayOfChars, true)

Here we are creating a CFML array, and then using its append method - which will take a Java array and cast it to a CFML array internally - before appending it to the array it's being called on. Job done. Oh the true argument there just means to append the arrays together, rather than appending the first argument into the last element of the first array. Try it yerself to see the difference: pass it false instead.

Summary / proof

string = "abcde"
arrayOfCharsFromSplit = string.split("")

arrayOfCharsAfterAppend = arrayNew(1).append(arrayOfCharsFromSplit, true)

nextChars = arrayOfCharsAfterAppend.map((char) => chr(asc(char) + 1))

writeDump([
    values = [
        arrayOfCharsFromSplit = arrayOfCharsFromSplit,
        arrayOfCharsAfterAppend = arrayOfCharsAfterAppend,
        nextChars = nextChars
    ], types = [
        arrayOfCharsFromSplit = arrayOfCharsFromSplit.getClass().getName(),
        arrayOfCharsAfterAppend = arrayOfCharsAfterAppend.getClass().getName(),
        nextChars = nextChars.getClass().getName()
    ]
])

And the code is in a trycf.com gist today.

Righto.

--
Adam