ColdFusion Builder Extensions and Long Processes

A week or so ago Jeff Coughlin came to me with an interesting question. Was it possible for a ColdFusion Builder extension to handle a long running process? By that he meant fire off some type of process and handle the duration without locking up the editor. I got some time to think about this at lunch today and come up with a proof of concept. I then followup the POC with a real, if silly, extension that will scan your ColdFusion code and give you a report on which tags were used the most. Before going any further, note that there is already a good idea of this concept out there - Terry Ryan’s builderStats extension. It makes use of Flash to generate a dialog that nicely waits while the back end code does a bunch of magic bean counting. I wanted a pure HTML solution using jQuery because… well… because. Here’s what I came up with. Feel free to poke multiple holes into this solution.

I began with a new extension, SlowView, that would be as simple as possible. (Both extensions will be attached to this blog entry at the bottom.) SlowView added a menu item to my editor. There was no logic to this - I just wanted a quick and dirty menu item:

<application> <name>SlowView</name> <author>Raymond Camden</author> <version>1</version> <email>ray@camdenfamily.com</email> <license>Buy something off the wishlist! http://www.amazon.com/o/registry/2TCL1D08EZEYE</license> <description>POC for handling a slow process in an extension</description> <menucontributions> <contribution target="editor"> <menu name="Run SlowView"> <action name="Run" handlerid="handler1" showresponse="true"></action> </menu> </contribution> </menucontributions> <handlers> <handler id="handler1" type="CFM" filename="test.cfm" /> </handlers> </application>

Nothing here should be new or special yet. Now let's look at test.cfm:

<cfinclude template="udfs.cfm"> <cfheader name="Content-Type" value="text/xml"> <cfoutput> <response showresponse="true"> <ide url="#getCurrentDir()#/display.cfm"> <view id="slowview1" title="Slow View 1" /> </ide> </response> </cfoutput>

udfs.cfm simply include a few utility functions and are not relevant to this post. Note though that I'm creating a new view called "showview1". Views in CFBuilder must be unique. If not, your output will overwrite another view. Note though that I'm loading another URL for the actual stuff to put in the view - display.cfm. Now let's look at that.

<cfinclude template="udfs.cfm"> <html> <head> <script src="jquery-1.5.1.min.js"></script> <script> var watcher; var lastmsg = ""; function checkProcess() { $.get("checkprocess.cfm", {}, function(res,code) { if(res.MESSAGE != lastmsg) { $("#console").append(res.MESSAGE+"<br>"); lastmsg = res.MESSAGE; } if(res.MESSAGE == "Done.") { clearInterval(watcher); $("#result").html("The result is "+res.RESULT); } }, "json"); } $(document).ready(function() { $("#submitBtn").click(function() { var input = $("#number").val(); if(input == "" || isNaN(input)) return; //fire off the request $("#result").html("Beginning your process - please stand by."); $.get("process.cfm",{input:input}); //now begin polling watcher = setInterval(checkProcess,1000); }); }); </script> </head> <body> <form> Input number: <input type="text" name="number" id="number"> <input type="button" id="submitBtn" value="Slowly Double it"> <div id="result"></div> </form> <div id="console"></div> </body> </html>

Ok, now we've got some stuff going on! Let's look at the bottom first. I've got a basic form with a button. Below that is a result div and below that is a console div. As you can guess, the console div is mainly going to be used for debugging. Now let's go up a bit.

My document.ready block adds a listener to the button click event. I grab the value out of the text field and check if it is numeric. If it is, I make a GET request to process.cfm. Notice that I do not have a result handler. I will not be hanging around for the end of this call. Instead, I set up an interval to run every second.

If we go higher then into checkProcess, you can see I'm performing a request to checkprocess.cfm. This is going to return a result structure to me that I can use to check the status of my process. As you can see, I look for a new message, and if one exists, I print it out. If the message is "Done.", I remove my interval and tell the user. Now let's look at our two server side files. First, process.cfm:

<cfparam name="url.input" default="0"> <cfset url.input = val(url.input)> <!--- begin my super slow response ---> <cfset application.status = {}> <cfset application.status.message = "Begun"> <cfset sleep(3000)> <cfset application.status.message = "Part of the way done."> <cfset sleep(3000)> <cfset application.status.message = "Mostly done."> <cfset sleep(3000)> <cfset application.status.message = "Done."> <cfset application.status.result = url.input * 2>

This code takes the input and does quick validation on it. I then create an Application-scoped structure to hold the status. Remember - a CFB extension is much like a single user application. So using the Application scope is (pretty much) ok for this. I then use a few sleep methods to delay the completion of the file. Once it's all the way done I store my result. Here then is checkprocess.cfm:

<cfset json = serializeJSON(application.status)> <cfcontent type="application/json" reset="true"><cfoutput>#json#</cfoutput>

As you can see, all it does it spit out the Application variable. So how does it work? Here is a quick Jing video:

</embed>

Notice that I'm able to use the editor while this is going on. Polling isn't the most effective way to talk with the server, but in this case, it works just fine I'd say. Nice and simple. Now let's look at a more advanced example, TagCounter. First, it's ide_config.xml file:

<application> <name>Tag Counter</name> <author>Raymond Camden</author> <version>1</version> <email>ray@camdenfamily.com</email> <license>Buy something off the wishlist! http://www.amazon.com/o/registry/2TCL1D08EZEYE</license> <description>I count your ColdFusion tags.</description> <menucontributions> <contribution target="editor"> <menu name="Count Tags"> <action name="Do It" handlerid="startView" showresponse="true"></action> </menu> </contribution> <contribution target="projectview"> <menu name="Count Tags"> <action name="Do It" handlerid="startView" showResponse="true"></action> </menu> </contribution> </menucontributions> <handlers> <handler id="startView" type="CFM" filename="start.cfm" /> </handlers> </application>

Of note is that this extension supports both the project view and the editor view. Now let's look at start.cfm, where I went ahead and used my builderHelper utility.

<cfset helper = createObject("component", "builderHelper").init(ideeventinfo)> <cfset application.res = helper.getSelectedResource()> <cfheader name="Content-Type" value="text/xml"> <cfoutput> <response showresponse="true"> <ide url="#helper.getRootURL()#/display.cfm"> <view id="tagcounter" title="Tag Counter" /> </ide> </response> </cfoutput>

Outside of using the utility, the other change here is that I store the selected resource to an application variable. Was it a click from the project view or the editor? Who cares. builderHelper figures it out for me. Now let's look at display.cfm:

<html> <head> <script src="jquery-1.5.1.min.js"></script> <script> var watcher; var lastmsg = ""; function fixTag(s) { s = s.replace("<","<"); s = s.replace(">",">"); return s; } function checkProcess() { $.get("checkprocess.cfm", {}, function(res,code) { if(res.MESSAGE != lastmsg) { $("#result").html(res.MESSAGE+"<br>"); lastmsg = res.MESSAGE; } if(res.CODE == 1) { clearInterval(watcher); var s = "<h2>Results</h2><br/>"; s += "Scanned "+res.RESULT.TOTALFILES+" total file(s).<br/>"; s += "<table border=\"1\" width=\"300\">"; for(var x = 0; x<res.RESULT.SORTEDTAGS.length; x++) { s+= "<tr><td>"+fixTag(res.RESULT.SORTEDTAGS[x])+"</td><td>"+res.RESULT.TAGS[res.RESULT.SORTEDTAGS[x]]+"</td></tr>"; } s += "</table>"; $("#result").html(s); } }, "json"); } $(document).ready(function() { $("#result").html("Beginning your process - please stand by."); $.get("process.cfm"); //now begin polling watcher = setInterval(checkProcess,1000); }); </script> </head> <body> <cfoutput> <p id="main"> <cfif application.res.type is "file"> Scanning file #application.res.path#. <cfelse> Scanning folder #application.res.path#. </cfif> </p> </cfoutput> <p id="result"></p> </body> </html>

Now this is a bit more complex than before. I begin (and as always, I'm starting at the bottom) by reporting on the type of scan being made. Unlike the form-based extension, this one begins the process immediately. Now I'm assuming a nice clean CODE result from the back end so I've got a simpler way to tell when done. Everything else is just vanilla layout of the data. Let's look at the back end.

<cfset files = []> <cfset application.status = {}> <cfset application.status.code = 0> <cfset application.status.result = {}> <cfset application.status.result.tags = {}> <cfset application.status.message = ""> <cfif application.res.type is "file"> <cfset files[1] = application.res.path> <cfelse> <cfset files = directoryList(application.res.path, true, "path","*.cfm|*.cfc")> </cfif> <cfloop index="x" from="1" to="#arrayLen(files)#"> <cfset application.status.message = "Processing file #x# out of #arrayLen(files)#."> <cfset contents = fileRead(files[x])> <cfset tags = reMatchNoCase("<cf.*?>",contents)> <!--- remove attributes ---> <cfloop index="x" from="1" to="#arrayLen(tags)#"> <cfset tag = tags[x]> <cfset tag = reReplace(tag,"[[:space:]].*?>",">")> <cfset tags[x] = tag> </cfloop> <cfloop index="tag" array="#tags#"> <cfif not structKeyExists(application.status.result.tags, tag)> <cfset application.status.result.tags[tag] = 0> </cfif> <cfset application.status.result.tags[tag]++> </cfloop> </cfloop> <cfset application.status.result.totalFiles = arrayLen(files)> <cfset application.status.result.sortedTags = structSort(application.status.result.tags, "numeric", "desc")> <cfset application.status.code = 1>

The idea here is simple - create an error of either one file or all the files in a folder. Note that the docs are wrong about directoryList. You can provide multiple filters. Once I have my array, I begin looping over it. For each file I read in the contents and use some simple regex to extract the tags. This isn't rock solid but works ok. Once done I can use structSort to get a list of tags in descending order of use. And that's it. Here's a video of it scanning BlogCFC.

</embed>

Enjoy!

Download attached file.

Raymond Camden's Picture

About Raymond Camden

Raymond is a developer advocate. He focuses on JavaScript, serverless and enterprise cat demos. If you like this article, please consider visiting my Amazon Wishlist or donating via PayPal to show your support.

Lafayette, LA https://www.raymondcamden.com

Comments