Saturday 6 June 2015

CFML: learning something new about directoryList()

G'day:
I need to shuffle some files around in my GitHub repos, as they're getting a bit of a mess. I decided to knock together a script to separate files out by file type, so I can rearrange my "scratch" repo which pretty much contains all my shite in it, into separate repos for CFML, PHP, etc.

I should be doing this with PHP to give myself some practice, but the thought of it made my will to live start ebbing away, so I decided to stick to CFML.



I can never remember the order of the arguments for directoryList() (well: after the first arg, anyhow), and decided on a whim I'd start relying on Lucee's own docs instead of always using ColdFusion's ones. Firstly I googled "lucee docs directorylist", and the first result was from Mark Drew's automated Lucee doc generation thingey @ www.luceedocs.org. I think it's unhelpful to have that site still in existence now that Lucee have their own official docs site up... I shall pursue that. However at least Mark's managed to get Google on his side. The official docs site page for directoryList() doesn't make the first page of results. Or the second page. Or the third page. It's "interesting" that my own file on GitHub for CFScript docs does make the first page results, despite not really being focused on directoryList() (it does mention it in passing). There is just one link to the top level of the Lucee docs function A-Z. It seems the Lucee bods might need some help with SEO.

Here's a back link anyhow: the Lucee documentation page for directoryList() is here.

All that's boring. The interesting bit is this...

This is what it says for the filter argument:

Filter to be used to filter the results:
  • A string that uses "*" as a wildcard, for example, "*.cfm"
  • a UDF (User defined Function) using the following pattern "functioname(String path):boolean", the function is run for every single file, if the function returns true, then the file is will be added to the list otherwise it will be omitted


Huh? I didn't know about that. Cool. I initially wondered if this was a Lucee-specific thing, so headed over to the ColdFusion docs to see what they said:

File extension filter applied to returned names, for example, *.cfm.
[...]
Also, you can pass a function in the filter argument:
[...]
Additionally, it can also accept the instances of Java FileFilter Objects.

Every bloody better! Not only has it got the callback option, it's also got a FileFilter based option too!

And I didn't know about any of this. I just thought one could pass a file spec, and that was it.

OK, I need to test this.

First a baseline, just using an expression:

// filterByExpression.cfm
dir = expandPath("./testFiles");
result = directoryList(dir, true, "query", "*.txt");
writeDump(result);

Nothing surprising there, or in its output:


Right. So now a callback. Here's the Lucee version:

// filterByCallbackLucee.cfm

dir = expandPath("./testFiles");
result = directoryList(
    dir,
    true,
    "query",
    function(filePath){
        return reFind("\\(five|ten).txt$", arguments.filePath);
    }
);
writeDump(result);

Note that the Lucee version only passes one argument to the callback: the full file path. TBH... this isn't of much use, IMO. I think it should pass in the whole file object so one can check other attributes of the file (there's more to a file than its path, after all). Still: it's slightly more use than just using an expression. Here I'm - in a rather contrived fashion - just grabbing files called five.txt or ten.txt.

The ColdFusion version of this is a bit better:

// filterByCallbackCF.cfm

dir = expandPath("./testFiles");
result = directoryList(
    dir,
    true,
    "query",
    function(filePath, fileName, fileExtension){
        return reFind("\\(five|ten).txt$", arguments.filePath);
    }
);
writeDump(result);

As well as the file path we also get just the name, and just the extension. Well: I said it was a bit better. but the same observation applies here... the underlying engine clearly has a file object it could pass into the callback, but it doesn't. At the very least it should pass in each of the values that the resultant query ends up with: Attributes, DateLastModified, Directory, Link, Mode, Name, Size, Type.

But ColdFusion wins out here by doing something actually useful. It has this final option of being able to pass a FileFilter object as the filter argument. FileFilter is a Java interface which dictates a single method: accept(), which takes a file object and returns a boolean.

I polished up my Java skills (I know one should not polish a turd, but hey), and came up with this:

package me.adamcameron.miscellany;

import java.io.File;

class FileFilterOnMinimumLength implements java.io.FileFilter {

    private long length;

    public FileFilterOnMinimumLength(long length){
        this.length = length;
    }
    
    public boolean accept(File pathname){
        return pathname.length() >= this.length;
    }

}

This simply gets initialised with a minimum length, and the accept() method compares the file's length to the initialised one, and returns true if it makes the cut. Simple.

Here it is in action with CFML:

// filterByFileFilterViaJar.cfm
param URL.length = 5;
filterByLength = createObject("java", "me.adamcameron.miscellany.FileFilterOnMinimumLength").init(URL.length);

dir = expandPath("./testFiles");
result = directoryList(
    dir,
    true,
    "query",
    filterByLength
);
writeDump(result);

And it works fine (the dump is mostly the same as above, so I'll spare you). Even if one needs to use Java to make one of these FileFilters, it's dead easy. How cool.

On a whim, I decided to see if Lucee also supported this, but they had simply not documented it. And the results were disappointing, but not for the reason one might expect:


Note it's not erroring when I come to use the FileFilter object... it's erroring when I simply try to create it. I'd blame myself for bad Java if it wasn't for the fact the exact same class works on ColdFusion. I got the same results on Lucee 4.5 and Railo 4.2 as well. I also tried it as an individual class on Railo and Lucee 4.5 (this is no longer supported on Lucee 5. Frown), but got the same problem. It's no-doubt some idiosyncrasy of what I'm doing, but I'm buggered if I know what. Anyway, this leaves me unable to test whether Lucee can take a FileFilter here.

I'm quite happy to have learned something new, and also I'm probably gonna be able to leverage this for my file-re-organisation task I have in front of me today.

I think I'll raise an enhancement request for both Lucee and ColdFusion, so that Lucee at least mirrors the arguments passed into the callback that ColdFusion provides for, but both systems should really avail more values still, here. And Lucee should support passing a FileFilter too.

I'm gonna try to work out what this Lucee error is first though.

Righto.

--
Adam