Installing User Defined Functions (UDF) As An Extension For Built-In Functions (BIF) In Lucee 5.3.2.77
With Adobe ColdFusion, I've always had a fascination with the idea of being able to extend the set of native (or built-in) Functions that are available in the request context. A decade ago, I played around with hacking User Defined Functions (UDFs) into the "hidden scope" of the PageContext; then, realized that I could just hack UDFs right into the url
scope. I was trying to get Adobe ColdFusion to do something that it didn't want to do; all while having people telling me that Lucee (then known as Railo) supported this feature from day-one. Well, a decade later, I'm finally on Lucee CFML! So, I wanted to scratch that old itch and take a quick look at how I can extend the set of Built-in Functions (BIFs) using User Defined Functions (UDFs) and a .lex
file in Lucee 5.3.2.77.
Prologue On The Trade-Offs Of Extending Built-In Functions (BIF)
To be honest, while I am titillated with the idea of extending the set of built-in functions in Lucee, I am not convinced that it is a good idea. As I've gotten older, and racked up a substantial number of failures in my career, I've come to respect the Principle of Least Surprise (or astonishment). The idea being that, the more straightforward and obvious I can make my code, the easier it will be to maintain over time.
Natively-supplied Built-In Functions are documented. They are a known quantity. They are expected. They can be easily identified in the code. By extending the set of BIFs, we are adding "unexpected" functions to that set. For new developers coming into a project, it may be very unclear where these Functions are coming from; or, where they can find documentation in them; or, why they don't come up in any Google searches.
Of course, such issues can be overcome with internal documentation, good on-boarding practices, and solid team communication. But, nothing is going to be as obvious as seeing a scoped method reference, like utils.someMethod()
, right in the code.
Enough Hand-Wringing, Add Some Built-In Functions (BIF)
For this exploration, I wanted to take a look at adding Built-In Functions using an installable Lucee Extension. To be clear, you don't have to use an installable extension to create new BIFs - you can just drop your custom .cfm
templates into special directories within the Lucee Server and / or Web context; this is what installing an Extension is doing for you behind the scenes. I wanted to use the Extension approach because it was a little more fun; and, it allows me to make this demo a bit more interactive.
ASIDE: If you want to manually add new Functions to Lucee, I believe that you can use the following directories:
{lucee-server}/library/function/
{lucee-web}/library/function/
I'm not 100% sure this is accurate - I'm shooting from the hip here a bit.
For the sake of this exploration, I'm going to install two new decision logic functions, isTruthy()
and isFalsy()
. These functions are intended to follow the same Truthy/Falsy rules as found in JavaScript; or, at least, as much as is possible in a ColdFusion context.
These functions are going to be installed as part of a .lex
(Zip) archive file that we're going to compress and copy into the deploy
directory of the Lucee server. Lucee monitors the deploy
directory on an ongoing basis; and, automatically moves the contents of "deployed" .lex
files into the appropriate system directories mentioned in the previous Aside.
To start, I'm going to create a base directory structure, called extension
, for my Lucee Extension code:
/extension/
/extension/functions/
/extension/functions/isfalsy.cfm
/extension/functions/istruthy.cfm
/extension/META-INF/
/extension/META-INF/MANIFEST.MF
The MANIFEST.MF
file contains meta-data information about the extension:
Manifest-Version: 1.0
id: "b6304293-81fa-4bcf-a6aa4bf9bd673697"
version: "1.0.0.0"
name: "TruthyFalsy"
description: "Decision functions based on JavaScript's notion of Truthy and Falsy values."
category: "Decision Logic"
And, the /functions/
files contain the CFML code of the User Defined Functions (UDF) that we want to turn into Built-In Functions (BIF). Here's my isfalsy.cfm
code:
<cfscript>
/**
* I determine if the given value is a Falsy (according to JavaScript rules).
*
* See JavaScript definition on the Mozilla Developer Network (MDN):
* https://developer.mozilla.org/en-US/docs/Glossary/Falsy
*
* @value I am the value being tested.
*/
public boolean function isFalsy( any value ) {
if ( isNull( value ) ) {
return( true );
}
if ( ! isSimpleValue( value ) ) {
return( false );
}
if ( isInstanceOf( value, "java.lang.String" ) ) {
return( ! value.len() );
}
if ( isBoolean( value ) || isNumeric( value ) ) {
return( ! value );
}
return( false );
}
</cfscript>
And, the code for my istruthy.cfm
file:
<cfscript>
/**
* I determine if the given value is a Truthy (according to JavaScript rules). A
* Truthy value is any value that is not explicitly designated as a Falsy.
*
* See JavaScript definition on the Mozilla Developer Network (MDN):
* https://developer.mozilla.org/en-US/docs/Glossary/Truthy
*
* @value I am the value being tested.
*/
public boolean function isTruthy( any value ) {
if ( isNull( value ) ) {
return( false );
}
return( ! isFalsy( value ) );
}
</cfscript>
Notice that my isTruthy()
function is making use of the isFalsy()
function. And, it's doing so with the assumption that isFalsy()
is a Built-In Function.
So, that's the CFML Extension that we want to drop into the Lucee server context. Now, we could manually ZIP the extension
directory, rename the archive as a .lex
file, and then copy it to the deploy
folder. But, what fun would that be? Lucee CFML provides us with all the tools that we need to make this a more hands-off experience. So, let's code this beast up!
The other day, when looking at the Zip Virtual File System (VFS) in Lucee CFML, I mentioned that I have an odd fascination with how easy it is to generate ZIP files in Lucee. Well, that fascination comes to light once again, as we can use the compress()
method to ZIP and copy our extension
directory right into the deploy
directory of the Lucee Server context.
<cfscript>
// The {lucee-server} placeholder brings us to the "context" directory for the
// server. But, for the deployment of extensions, we need to go up one level to find
// the "deploy" directory.
deployPath = expandPath( "{lucee-server}/../deploy/" );
// To deploy our extension, we need to place our LEX file in the deploy directory.
lexPath = ( deployPath & "TruthyFalsy.lex" );
// The cool thing is, we can Zip / Compress the local extension directory directly
// into the server's deploy directory in our CommandBox instance. Woot woot!
compress( "zip", "./extension", lexPath, false );
// ------------------------------------------------------------------------------- //
// ------------------------------------------------------------------------------- //
// Since it takes some time (up to a minute according to a video I watched) for the
// LEX file to get installed, we need to increase the request timeout before we start
// monitoring the file-system for changes.
cfsetting( requestTimeout = ( 2 * 60 ) );
echo( "Waiting for <code>.lex</code> file to be installed " );
// Lucee monitors the deploy directory for changes, and then moves the new files out
// of the deploy directory. As such, we can use the existence of our LEX file as a
// way to determine when the extension has been installed in the server.
while ( fileExists( lexPath ) ) {
echo( "<strong>.</strong> " );
cfflush();
sleep( 500 );
}
// ------------------------------------------------------------------------------- //
// ------------------------------------------------------------------------------- //
// At this point, the LEX file has been installed; however, it appears that the Lucee
// service SOMETIMES needs to be restarted (based on my testing). I am not sure why
// this is inconsistently required.
echo( "<p> The <code>.lex</code> file has been deployed. </p>" );
echo( "<p> You may need to <strong>" );
echo( "<a href='./restart.cfm' target='_blank'>restart the Lucee service</a>" );
echo( "</strong> in order to see the changes.</p>" );
</cfscript>
As you can see, the compress()
call is generating the ZIP archive - as a .lex
file - right into the deploy
directory that Lucee is monitoring. The monitoring of this directory is ongoing, but it is not instantaneous. As such, we can use the existence of our .lex
file to determine when the Extension has been installed. Once the .lex
file disappears, we know that Lucee has extracted our extension code into the appropriate system directories:
Once Lucee installs the extension, I found that I sometimes needed to restart the server in order for the changes to take effect. This was not always the case; and, I don't know why the experience was not consistent. But, restarting the Lucee server is also something we can do programmatically with the CFAdmin
tag:
<p>
<strong>Restarting</strong> the Lucee server context....
</p>
<cfflush />
<cfadmin
action = "restart"
type = "server"
password = "commandbox"
/>
<p>
Restart complete.
</p>
Once the Lucee extension is installed, and the Server is restarted (as needed), we can now consume the isTruthy()
and isFalsy()
functions as if they were Built-In Functions:
<cfscript>
// These should all be true.
echoLine( isFalsy( nullValue() ) === true );
echoLine( isFalsy( false ) === true );
echoLine( isFalsy( "" ) === true );
echoLine( isFalsy( 0 ) === true );
// These should all be false.
echoLine( isFalsy( 1 ) === false );
echoLine( isFalsy( "0" ) === false );
echoLine( isFalsy( [] ) === false );
echoLine( isFalsy( {} ) === false );
echoLine( isFalsy( now() ) === false );
// These should all be true.
echoLine( isTruthy( {} ) === true );
echoLine( isTruthy( [] ) === true );
echoLine( isTruthy( "0" ) === true );
echoLine( isTruthy( true ) === true );
echoLine( isTruthy( 1 ) === true );
echoLine( isTruthy( now() ) === true );
// These should all be false.
echoLine( isTruthy( nullValue() ) === false );
echoLine( isTruthy( false ) === false );
echoLine( isTruthy( 0 ) === false );
echoLine( isTruthy( "" ) === false );
// ------------------------------------------------------------------------------- //
// ------------------------------------------------------------------------------- //
public void function echoLine( required any value ) {
echo( value & "<br />" );
}
</cfscript>
As you can see, we were able to reference isTruthy()
and isFalsy()
as globally-accessible functions. We have successfully turned our User Defined Functions (UDF) into Built-In Functions (BIF) through the extension mechanism in Lucee 5.3.2.77. Lucee Extensions can do a lot more that just install Functions; they can install custom Tags, JAR files, and robust application and admin plug-ins. But, that's far beyond anything that I understand at this time. This was just a fun first step into the world of extending Lucee.
Want to use code from this post? Check out the license.
Reader Comments
Nice experiment. Here's a related Lucee ticket I put in that suggested better (or at least different) truthy/false evaluation in CFML:
https://luceeserver.atlassian.net/browse/LDEV-449
Your next post needs to be publishing your extension to ForgeBox! It's really just a couple more steps and then it will show up automatically for anyone on Lucee 5.3+ to install :)
https://commandbox.ortusbooks.com/package-management/creating-packages/publishing-lucee-extensions-to-forgebox
Also, I think it's very much worth noting that the ID of an extension doesn't have to be a GUID! In fact, I very VERY much hate that the Lucee docs even suggest that. That's the sort of thing I'd suggest to my users if I really hated them. All it needs to be is globally unique. Even something like com.bennadel.truthy would be reasonably unique and 1000 times better than having an extension named b6304293-81fa-4bcf-a6aa4bf9bd673697.
As far as automating the installation-- there is an installation mechanism via cfadmin which is immediate. The deploy folder is more ideal when you want to drop in an extension from an external process. As far as automating a server to install extensions when it comes up, there is always the -Dlucee-extensions=myID,myOtherID JVM arg/env var trick and there's even a cool module someone created that helps automate that from CommandBox:
https://www.forgebox.io/view/docker-lex-install
Anyway, good stuff, I think people will play with extensions more if they see how easy it is so thanks for blogging this. I've actually wanted to create a CommandBox module to help build extensions that would basically store all the metadata in your box.json (instead of that horrible manifest file) and then assemble and publish the extension for you bia a build command or something. Maybe someday I'll get to that.
@Brad,
Oh wow, awesome info! I didn't know any of that, including the UUID caveat :D Will have to dig into it some more. One of the things I tried to do originally, was load an extension that would load some JAVA files in the background (for use in a new BIF). But, I couldn't get that to work. However, I could get the raw BIFs to work, so I figured I would start there.
Re: Truthy / Falsy stuff, the main problem with doing any of it in ColdFusion is CF lacks the core expression evaluation rules that JavaScript has. Part of what makes JavaScript so powerful with this type of evaluation is that you can do things like:
Where JavaScript always returned the short-circuited evaluation of
&&
and||
operators. But, in ColdFusion, this kind of thing just throws an error about being able to cast complex objects to Booleans :DThough, there is the "magic functions" feature of Components, which I haven't even tried playing with yet.
It's been really fun to dig into this -- the new features are forcing me to think about the code differently.
@Brad,
For what it's worth, I tried to install an extension this morning with an ID, like
com.bennadel.lucee.differ
, and the Lucee Admin complained that the ID had to be a "valid UUID". So, maybe they've actually changed to be stricter about it.Yes, at some point the admin started enforcing that :/