This weekend I worked on a little proof of concept concerning persistence and ColdFusion Builder Extensions. Persistence in ColdFusion applications normally involves either cookies, client variables, session variables, or databases. I was curious to see how I could achieve something similar for a CFB extension. My use case for this was preferences. So you could imagine a complex extension perhaps remember what your last choices were. This could make using the extension quicker if you don't have to configure your settings on ever use.
I knew that cookies did not work at all. You can't use client variables without cookies as there is no way to match up the old data with your new request. Session variables can be made to work - but again - I wanted something that would persist. So what about databases? We could make use of Derby if we prompted the extension user for his CF Admin password. That would work - but felt like overkill. Instead, I decided on a simpler idea - using a file.
I decided that a simple file, storing XML, would probably be best. It wouldn't be great for storing a lot of data, but again, we're just talking preferences here. I began by creating a generic component for any CFB extension. I've been meaning to do this for some time now and I finally got around to it. My CFC is named extensionUtility and will be stored in an org/camden/util folder. I've included the complete code below. Note too that I've got a few methods related to finding the current URL. This is useful for building links in forms, JS files, etc.
public string function getCurrentDir() {
var theURL = getCurrentURL();
theURL = listDeleteAt(theURL, listLen(theURL, "/"), "/");
return theURL;
} public string function getCurrentURL() {
var theURL = getPageContext().getRequest().GetRequestUrl().toString();
if(len( CGI.query_string )) theURL = theURL & "?" & CGI.query_string;
return theURL;
} public struct function getSettings(string file="extsettings") {
var realFile = expandPath("./" & arguments.file & ".xml");
if(!fileExists(realFile)) return {};
var settings = {};
var contents = fileRead(realFile);
if(!isXML(contents)) return {};
var settingsXML = xmlParse(contents);
for(var key in settingsXML.settings) {
settings[key] = settingsXML.settings[key].xmlText;
}
return settings;
} public void function setSetting(string name, string value, string file="extsettings") {
var realFile = expandPath("./" & arguments.file & ".xml");
var settings = getSettings(arguments.file);
settings[arguments.name] = arguments.value;
var contents = "<settings>";
for(var key in settings) {
contents &= "<#key#>#xmlFormat(settings[key])#</#key#>";
}
contents &= "</settings>";
fileWrite(realFile, contents);
} }
component {
So the two methods I want to focus in on are getSettings and setSetting. getSetting takes an optional argument that allows you to override the name of a file containing your XML. In most cases you shouldn't need to worry about this. It simply reads in the XML and assumes a flat structure within a settings key. Each node should be a name and the value should be simple text. So for example:
<settings><height>99</height><color>pink</color><width>249</width></settings>
The flip side of getting the settings is to set them. In this case, I'm only allowing you to set one setting at a time, and each time you do we recreate the file. If this were a multi-user application, I'd be concerned with locking and performance. I'd definitely create a way to set multiple values at once. But for our extension, it is essentially single threaded and single user. This makes things much simpler.
So given our utility functions, how do we make use of them? Remember that we can use session variables, but they are a bit of a pain to use. We can make use of the application scope of course, and I do that to load the library in:
<cffunction name="onApplicationStart" returnType="boolean" output="false">
<cfset application.extensionUtility = createObject("component", "org.camden.util.extensionUtility")>
<cfreturn true>
</cffunction> <cffunction name="onRequestStart" returnType="boolean" output="false">
<cfargument name="req" type="string" required="true">
<cfif 1>
<cfset onApplicationStart()>
</cfif>
<cfreturn true>
</cffunction> </cfcomponent>
<cfcomponent>
<cfsetting showdebugoutput="false">
Just to be clear, the code within onRequestStart is only for debugging. There is no need to rerun onApplicationStart on every request. I normally do stuff like this with a "URL hook", but since you can't do that with an extension (well, you could if you requested the extension in your browser), I use a simple "if 1" clause while I test.
For the next step, I need a way to read in my settings and use them. Unlike a traditional web application, extensions give you a defined way to "enter" the application. What I mean is - I know what the first URL request will always be. In my simple extension, I've got a right click menu added for the editor and it always runs one handler, tester.cfm. Here is how I made use of my settings:
<!--- get my setings --->
<cfset settings = application.extensionUtility.getSettings()> <cfif structKeyExists(settings, "width")>
<cfset width = settings.width>
<cfelse>
<cfset width = 250>
</cfif>
<cfif structKeyExists(settings, "height")>
<cfset height = settings.height>
<cfelse>
<cfset height = 100>
</cfif>
<cfif structKeyExists(settings, "color")>
<cfset color = settings.color>
<cfelse>
<cfset color = "red">
</cfif> <cfheader name="Content-Type" value="text/xml">
<cfoutput>
<response showresponse="true">
<ide>
<dialog width="400" height="400" title="Make a Box" />
<body>
<![CDATA[ <h2>Make a Box</h2>
<form action="#application.extensionUtility.getCurrentDir()#/test_response.cfm" method="post">
Width: <input type="text" name="width" value="#width#"><br/>
Height: <input type="text" name="height" value="#height#"><br/>
Color: <input type="text" name="color" value="#color#"><br/>
<input type="submit" value="Do It!">
</form>
]]>
</body>
</ide>
</response></cfoutput>
As you can see, I fetch the settings and see if I have values for width, height, and color. I build up a simple form with these values and post them to the next page. Now let's look at that.
<!--- basic validation, could be better --->
<cfparam name="form.width" default="0">
<cfparam name="form.height" default="0">
<cfparam name="form.color" default="red"> <cfset box = imageNew("", form.width, form.height, "rgb", form.color)>
<cfimage action="writeToBrowser" source="#box#"> <!--- Store these values --->
<cfset application.extensionUtility.setSetting("width", form.width)>
<cfset application.extensionUtility.setSetting("height", form.height)>
<cfset application.extensionUtility.setSetting("color", form.color)>
As you can see, this extension draws boxes (we aren't talking rocket science here!) but at the very end, we use our API to store our settings back. Pretty simple, right? Here is a quick video showing this in action. Notice the first time through my extension will use default settings. The next time though it will have remembered what I did before.
I've attached the entire extension to this blog entry - but obviously the utility CFC is the only thing worthwhile (unless you have some big need to create colored squares!). Obviously this could be tweaked a bit more. Any comments or feedback would be greatly appreciated.