Using getApplicationMetadata() To Provide Global Defaults In ColdFusion
A couple of weeks ago on Twitter, James Moberg and I were discussing ColdFusion application metadata. In recent years, ColdFusion has provided a getApplicationMetadata()
function for retrieving the configuration associated with the current request (remember, the Application.cfc
framework component is freshly instantiated on every single request). I've never used the getApplicationMetadata()
function before; but, after my previous post on the ColdFusion custom tags, it occurred to me that I could use the ColdFusion application framework as a hook into providing global defaults for my custom tag attributes.
The Application.cfc
has traditionally been used to configure the built-in ColdFusion features: application timeouts, session management, data-sources, mail servers, Java settings, etc. But, there's no reason that we can't use the Application.cfc
to configure our own custom-built functionality. After all, the getApplicationMetadata()
function returns everything in the this
scope (of the application component). As such, as long as we define our custom configuration within the this
scope, there's no reason that we can't consume this data lower down in the request processing.
To explore this concept, I'm going to create a ColdFusion custom tag that transforms its generated content based on several optional attributes:
trimming
- I determine which whitespace trimming function should be applied. Options are:trim
,ltrim
,rtrim
.casing
- I determine which letter-casing function should be applied. Options are:lcase
,ucase
,ucfirst
,party
.whitespace
- I determine whether or not the remaining whitespace should be replaced with visible characters (.
for space,-
for tab,+
for newline). Options are:visible
.
All of these attributes will have defaults defined within the custom tag logic. But, I'm also going to allow global defaults to be defined within the Application.cfc
component using the public property: transformerAttributeDefaults
. For example, here's the Application.cfc
for this demo:
component
output = false
hint = "I define the application settings and event handlers."
{
this.name = "GlobalDefaultsDemo";
this.applicationTimeout = createTimeSpan( 1, 0, 0, 0 );
this.sessionManagement = false;
this.setClientCookies = false;
// Override (some of) the default attributes for the Transformer.cfm custom tag.
this.transformerAttributeDefaults = {
casing: "party"
};
}
Now, from anywhere within the request processing, I can access these settings by calling:
getApplicationMetadata().transformerAttributeDefaults
Of course, these are only meant as defaults. Which means, if the developer passes-in attributes for a particular custom tag invocation, the invocation instance values should win. The order of precedence therefore becomes:
(lowest) Internal custom tag defaults.
(middlest) Global custom tag default overrides (if they exist).
(highest) Per-instance attribute assignments.
In my custom tag, I can wire this up as such (truncated example):
<cfscript>
tagDefaults = applyGlobalDefaultOverrides({
trimming: "trim",
casing: "lcase",
whitespace: "hidden"
});
// Define tag attributes and defaults.
param name="attributes.variable" type="variableName";
param name="attributes.trimming" type="string" default = tagDefaults.trimming;
param name="attributes.casing" type="string" default = tagDefaults.casing;
param name="attributes.whitespace" type="string" default = tagDefaults.whitespace;
// ... truncated custom tag code ... //
/**
* If the application has overridden the tag attribute defaults, I apply them to the
* given defaults structure before returning it.
*/
private struct function applyGlobalDefaultOverrides( required struct defaults ) {
var appMetadata = getApplicationMetadata();
var appMetadataKey = "transformerAttributeDefaults";
if ( appMetadata.keyExists( appMetadataKey ) ) {
defaults.append( appMetadata[ appMetadataKey] );
}
return defaults;
}
</cfscript>
Here, I'm defining applyGlobalDefaultOverrides()
, a private method local to my ColdFusion custom tag. This method takes the internal default values (lowest precedence) and overrides those defaults using the getApplicationMetadata()
function. The resultant struct is then used to define the defaults within the module's subsequent CFParam
tags. This allows the per-invocation attributes to ultimately take the highest precedence.
If we then invoke this ColdFusion custom tag:
<cf_Transformer variable="request.text" whitespace="visible">
This is Sparta!
</cf_Transformer>
<cfoutput>
<pre>[#encodeForHtml( request.text )#]</pre>
</cfoutput>
... we end up invoking the Transformer.cfm
module with the following attributes:
whitespace="visible"
- highest precedence value provided by the tag invocation code.casing="party"
- middlest precedence value provided by the global defaults in ourApplication.cfc
settings.trimming="trim"
- lowest precedence value provided by the internal defaults of the custom tag.
Now, if we run this ColdFusion code, we get the following page output:
[ThIs.Is.SpArTa!]
It was fully trimmed; the whitespace was made visible; and, the party casing was applied. All of the various attribute inputs were coalesced correctly into a single invocation configuration.
Here's the full code for the ColdFusion custom tag:
<cfscript>
// Since this ColdFusion custom tag deals with generated output, we only care about
// the tag in its "end" mode once we have generated content to consume.
if ( thistag.executionMode == "start" ) {
exit
method = "exitTemplate"
;
}
// ------------------------------------------------------------------------------- //
// ------------------------------------------------------------------------------- //
tagDefaults = applyGlobalDefaultOverrides({
trimming: "trim",
casing: "lcase",
whitespace: "hidden"
});
// Define tag attributes and defaults.
param name="attributes.variable" type="variableName";
param name="attributes.trimming" type="string" default = tagDefaults.trimming;
param name="attributes.casing" type="string" default = tagDefaults.casing;
param name="attributes.whitespace" type="string" default = tagDefaults.whitespace;
// ------------------------------------------------------------------------------- //
// ------------------------------------------------------------------------------- //
switch ( attributes.trimming ) {
case "trim":
thistag.generatedContent = thistag.generatedContent.trim();
break;
case "ltrim":
thistag.generatedContent = thistag.generatedContent.ltrim();
break;
case "rtrim":
thistag.generatedContent = thistag.generatedContent.rtrim();
break;
}
switch ( attributes.casing ) {
case "ucase":
thistag.generatedContent = thistag.generatedContent.ucase();
break;
case "lcase":
thistag.generatedContent = thistag.generatedContent.lcase();
break;
case "ucfirst":
thistag.generatedContent = thistag.generatedContent.ucfirst();
break;
case "party":
thistag.generatedContent = thistag.generatedContent
.reReplace( "(\w)(\w)", "\U\1\L\2", "all" )
;
break;
}
if ( attributes.whitespace == "visible" ) {
thistag.generatedContent = thistag.generatedContent
.reReplace( " ", ".", "all" )
.reReplace( "\t", "-", "all" )
.reReplace( "(\r\n?|\n)", "+\1", "all" )
;
}
// Write the generated content to the calling scope and reset the output.
// --
// NOTE: In Adobe ColdFusion, we could have referenced the caller scope like a
// "magic tunnel" into the calling context. However, Lucee CFML doesn't expose the
// calling context in this manner. As such, we're going to use setVariable() to allow
// for a proper variable path rooted in the calling page scope.
setVariable( "caller.#attributes.variable#", thistag.generatedContent );
// Reset the output.
thistag.generatedContent = "";
// ------------------------------------------------------------------------------- //
// ------------------------------------------------------------------------------- //
/**
* If the application has overridden the tag attribute defaults, I apply them to the
* given defaults structure before returning it.
*/
private struct function applyGlobalDefaultOverrides( required struct defaults ) {
var appMetadata = getApplicationMetadata();
var appMetadataKey = "transformerAttributeDefaults";
if ( appMetadata.keyExists( appMetadataKey ) ) {
defaults.append( appMetadata[ appMetadataKey] );
}
return defaults;
}
</cfscript>
In this exploration, I'm using this as a means to provide ColdFusion custom tag settings. But, there's no reason that this couldn't be used to provide settings for ColdFusion components or even User Defined Functions (UDFs).
That said, whenever you define global behaviors, you have to be cautious. For one thing, it puts a large distance in between the configuration code and the consuming code. This can make the code harder to understand and debug. And, for another thing, it might change the behavior of vendor code in an unexpected way (though, this is more relevant for native ColdFusion settings, such as useJavaAsRegexEngine
, which determines how Regular Expressions are evaluated).
Want to use code from this post? Check out the license.
Reader Comments
I should mention that - over on Twitter - Zac Spitzer mentioned that the
getApplicationMetadata()
has some overhead associated with it. I don't know what the magnitude of that would be; but, he's very familiar with how Lucee CFML is implemented.For reference, here's the built-in function -
getApplicationSettings()
- that Lucee CFML uses to implement this feature. As you can see, it does quite a lot!Post A Comment — ❤️ I'd Love To Hear From You! ❤️
Post a Comment →