Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
I am the chief technical officer at InVision App, Inc - a prototyping and collaboration platform for designers, built by designers. I also rock out in JavaScript and ColdFusion 24x7.
Meanwhile on Twitter
Loading latest tweet...
Ben Nadel at the New York ColdFusion User Group (Sep. 2008) with:

Using A ColdFusion Method Closure In Regular Expression Replace Logic

By Ben Nadel on
Tags: ColdFusion

Earlier today, I explored the ability to create closure-like behaviors between a calling page and a ColdFusion custom tag. While I personally found my last blog post very exciting, I know it didn't do too much to show off the kind of power that something like this would make available. As such, I wanted to make this demo for what might hopefully be a bit of an ooh-ahh moment.

One of the most creative places that I've used closures in Javascript is in the String replace() method. The String replace() method takes the given closure and invokes it once for each matched pattern in the given string, replacing back in the value returned by the closure method. Not only does this allow per-match replacement logic, it provides external access to each pattern match as it happens. This feature in Javascript is so cool, I thought it might be the perfect way to demonstrate Closures in ColdFusion.

First, I created a ColdFusion custom tag, rereplace.cfm, that will act as our Java-powered regular expression replacement function. This tag takes several attributes, which you can think of as method arguments:

  • Result: The caller-based variable into which the resultant string is stored.
  • Text: The target text value in which we will be searching for and replacing pattern matches.
  • Pattern: That Java-powered regular expression pattern for which we are searching.
  • Replacement: The replacement string OR replacement method used to replace the matched patterns.
  • Scope: The scope of replacement (one vs. all).

Notice that the Replacement attribute can take either a string value or a user defined function; this is the attribute that can accept and utilize our closure method. But, before we get into the cool stuff, let's take a quick look at a simple use of this tag:

  • <!--- Perform simple Java-based find / replace. --->
  • <cf_rereplace
  • result="result"
  • text="Dang Joanna! You are looking H-O-T!"
  • pattern="(?i)(a)(n)"
  • replacement="[$1][$2]"
  • scope="all"
  • />
  •  
  • <!--- Output the results. --->
  • <cfoutput>
  •  
  • Result: #result#
  •  
  • </cfoutput>

As you can see, we are searching for a pattern that consists of the letter "a" followed by the letter "n." When matched, we are then replacing the given match with a string containing the individual characters, each one surrounded by brackets. And, when we run the code, we get the following output:

Result: D[a][n]g Jo[a][n]na! You are looking H-O-T!

As you can see, each match of the pattern "an" was replaced with the string, "[a][n]".

OK, now that we're feeling comfortable with the idea of the REReplace.cfm ColdFusion custom tag, let's explore the Closure availability contained within it. In this next demo, we are going to take a string and replace out the "curse" words found within it. The way in which the curse words get replaced will be defined by our Closure method. While that, in and of itself, is not taking advantage of closures, we're also going to collect the curse words as they are found and store them in an array defined in the calling page; that is the power of closures.

The following demo starts off with the definition of our method, doReplacement(); this is our Closure method. Take special notice that the body of the method refers to the array, curses, which is defined in the same template:

  • <cffunction
  • name="doReplacement"
  • access="public"
  • returntype="string"
  • output="false"
  • hint="I take the individual pattern match (broken up into captured groups) and return the replacement value.">
  •  
  • <!--- Define the arguments. --->
  • <cfargument name="$0" hint="Full pattern match." />
  • <cfargument name="$1" hint="First captured group." />
  • <cfargument name="$2" hint="Second captured group." />
  • <cfargument name="$3" hint="Third captured group." />
  •  
  • <!---
  • Append the curset to the curses array.
  •  
  • NOTE: The curses array is able to references the array
  • defined in THIS page, despite the fact that this method
  • is being executed from within another page (custom tag).
  • --->
  • <cfset arrayAppend( curses, arguments.$0 ) />
  •  
  •  
  • <!--- Check the groups to see how to replace the curse. --->
  • <cfif (
  • structKeyExists( arguments, "$1" ) &&
  • structKeyExists( arguments, "$2" )
  • )>
  •  
  • <!--- Replacing "holy crap". --->
  • <cfreturn "#arguments.$2# cow" />
  •  
  • <cfelseif structKeyExists( arguments, "$1" )>
  •  
  • <!--- Replacing "crap". --->
  • <cfreturn "crud" />
  •  
  • <cfelseif structKeyExists( arguments, "$3" )>
  •  
  • <!--- Replacing "goddamn". --->
  • <cfreturn "gosh-darn" />
  •  
  • </cfif>
  •  
  • <!---
  • If we made it this far, we don't have a replacement,
  • so just replace with a * string.
  • --->
  • <cfreturn repeatString(
  • "*",
  • len( arguments.$0 )
  • ) />
  • </cffunction>
  •  
  •  
  • <!--- ----------------------------------------------------- --->
  • <!--- ----------------------------------------------------- --->
  •  
  •  
  • <!---
  • Define an array to keep track of curse words that we have
  • pulled out of the replacement method.
  •  
  • NOTE: This array is defined in THIS template and is being
  • referenced from within the above doReplacement() method even
  • though that method is later invoked in a completed different
  • page context (the reReplace custom tag).
  • --->
  • <cfset curses = [] />
  •  
  •  
  • <!--- Strip out curses and replace them with friendly words. --->
  • <cf_rereplace
  • result="result"
  • text="Holy crap, Tricia! You're looking too goddamn sexy!"
  • pattern="(?i)((holy )?crap)|(goddamn)"
  • replacement="#doReplacement#"
  • scope="all"
  • />
  •  
  • <!--- Output results and collected curses. --->
  • <cfoutput>
  •  
  • Result: #result#<br />
  • <br />
  •  
  • Collected Curses:<br />
  •  
  • <cfloop
  • index="curse"
  • array="#curses#">
  •  
  • - #curse#<br />
  •  
  • </cfloop>
  •  
  • </cfoutput>

This time, when we call the rereplace.cfm ColdFusion custom tag, rather than passing in a string for the replacement attribute, we are passing in a reference to our doReplacement() method. This method then evaluates the captured groups, as they are found, and returns the replacement text as required. Each matched curse words is subsequently being stored in the curses array which is then output at the end.

When we run the above code, we get the following output:

Result: Holy cow, Tricia! You're looking too gosh-darn sexy!

Collected Curses:
- Holy crap
- goddamn

As you can see, not only were the pattern matches replaced with the Closure method return values, our Closure method was able to act as if it were still in the calling page context, storing each pattern match in the curses array. This doesn't look too pretty because we have to jump through a lot of hoops to get this to work in ColdFusion; hopefully, in future releases of the language, functionality like this will be more readily available.

Here is the REReplace.cfm ColdFusion custom tag that is responsible for calling the method as a Closure:

rereplace.cfm

  • <!--- Define custom tag attributes. --->
  •  
  • <!---
  • This is the caller-based result into which the final
  • string value will be stored (after all the replacements have
  • been made).
  • --->
  • <cfparam
  • name="attributes.result"
  • type="string"
  • default=""
  • />
  •  
  • <!---
  • This is the text value in which we are going to be searching
  • for and replacing values.
  • --->
  • <cfparam
  • name="attributes.text"
  • type="string"
  • />
  •  
  • <!---
  • This is the regular expression pattern that we are going
  • to be matching.
  • --->
  • <cfparam
  • name="attributes.pattern"
  • type="string"
  • />
  •  
  • <!---
  • This is the replace-with value that we are going to be
  • using to replace the matched pattern. This can be a string
  • value or a function reference.
  •  
  • NOTE: If this is the a FUNCTION, it will be executed in the
  • context of the CALLER scope, with each group being passed
  • in as a subsequent argument.
  • --->
  • <cfparam
  • name="attributes.replacement"
  • type="any"
  • />
  •  
  • <!---
  • This is the scope of the replacement. This can be ALL
  • or ONE (ALL being a global find and replace).
  • --->
  • <cfparam
  • name="attributes.scope"
  • type="string"
  • default="one"
  • />
  •  
  •  
  • <!--- ----------------------------------------------------- --->
  • <!--- ----------------------------------------------------- --->
  •  
  •  
  • <!--- Prepare the Java regular expression pattern. --->
  • <cfset pattern = createObject( "java", "java.util.regex.Pattern" )
  • .compile(
  • javaCast( "string", attributes.pattern )
  • )
  • />
  •  
  • <!---
  • Get the pattern matcher for this pattern on the target
  • value text.
  • --->
  • <cfset matcher = pattern.matcher(
  • javaCast( "string", attributes.text )
  • ) />
  •  
  •  
  • <!---
  • As we replace the pattern matches, we are going to need to
  • store the results in a string buffer.
  • --->
  • <cfset buffer = createObject( "java", "java.lang.StringBuffer" )
  • .init()
  • />
  •  
  •  
  • <!---
  • While the Java-pattern-matcher aspects of the two replacement
  • methods are the same (string vs. method), the method-based
  • replacement has a bunch of additional logic. As such, we are
  • going to split the two approaches.
  • --->
  • <cfif isCustomFunction( attributes.replacement )>
  •  
  • <!---
  • The repalcement value is a function; which mean, we are
  • going to need to be able to execute in the context of the
  • calling page (CALLER).
  • --->
  •  
  • <!---
  • Get the meta data for the caller page. This will give us
  • the CLASS used for the caller scope; from this, we can
  • use reflection to access the given field (page context)
  • from a given instance of that class (caller).
  • --->
  • <cfset callerMetaData = getMetaData( caller ) />
  •  
  • <!---
  • Get the class field that would hold a reference to the
  • pageContext object.
  • --->
  • <cfset contextField = callerMetaData.getDeclaredField( "pageContext" ) />
  •  
  • <!---
  • This is a private field so we have to explicitly change
  • its access (I don't fully understand things at this level
  • - this is just what Elliott had... and, if you don't do
  • it, it tells you that the field is private and throws
  • an error).
  • --->
  • <cfset contextField.setAccessible( true ) />
  •  
  • <!---
  • Now that we have the Field object that represents the
  • PageContext property, use reflection to get that property
  • from the CALLER instance.
  • --->
  • <cfset callerPageContext = contextField.get( caller ) />
  •  
  •  
  • <!---
  • Now that we have the constructs ready to make the
  • calling-page context-bind, let's keep looping while the
  • matcher can find additional matches.
  • --->
  • <cfloop condition="matcher.find()">
  •  
  • <!---
  • For each match that we find, we are going to pass
  • the individual groups to the replacement method as
  • individual arguments. Therefore, we have to create
  • an array for our argument collection.
  • --->
  • <cfset argumentCollection = [] />
  •  
  • <!---
  • Resize the arguments to be the same as the number
  • of groups (this will leave undefined values if we
  • don't have captured groups).
  •  
  • NOTE: We are doing the +1 because the full match is
  • always passed in as the first argument.
  • --->
  • <cfset arrayResize(
  • argumentCollection,
  • (matcher.groupCount() + 1)
  • ) />
  •  
  • <!--- Loop over the groups to create each match. --->
  • <cfloop
  • index="groupIndex"
  • from="0"
  • to="#matcher.groupCount()#"
  • step="1">
  •  
  • <!--- Set the group value. --->
  • <cfset argumentCollection[ groupIndex + 1 ] = matcher.group(
  • javaCast( "int", groupIndex )
  • ) />
  •  
  • </cfloop>
  •  
  • <!---
  • Now that we have the argument collection for the
  • replacement, invoke the replacement method and pass
  • them in.
  •  
  • NOTE: We are executing the replacement method in
  • the context of the calling page using the underlying,
  • undocumented Java methods for invoking ColdFusion
  • methods.
  • --->
  • <cfset replacementValue = attributes.replacement.invoke(
  • caller.variables,
  • javaCast( "string", getMetaData( attributes.replacement ).name ),
  • callerPageContext.getPage(),
  • javaCast( "java.lang.Object[]", argumentCollection )
  • ) />
  •  
  • <!---
  • Check to see if we have a replaced value (if the
  • replacement method does not return a value, we are
  • going to consider this an empty string replacement).
  • --->
  • <cfif (
  • structKeyExists( variables, "replacementValue" ) &&
  • isSimpleValue( replacementValue )
  • )>
  •  
  • <!---
  • We are going to consider the replaced value a
  • "final" string; meaning, we don't want to let it
  • use any back references. As such, we need to
  • escape special characters.
  • --->
  • <cfset matcher.appendReplacement(
  • buffer,
  • javaCast(
  • "string",
  • replacementValue.replaceAll(
  • javaCast( "string", "([\\\$])" ),
  • javaCast( "string", "\\$1" )
  • )
  • )
  • ) />
  •  
  • <cfelse>
  •  
  • <!--- Empty string replacement. --->
  • <cfset matcher.appendReplacement(
  • buffer,
  • javaCast( "string", "" )
  • ) />
  •  
  • </cfif>
  •  
  • </cfloop>
  •  
  • <cfelse>
  •  
  • <!---
  • The replacement value is a string; as such, all we have
  • to do is handle standard string replacement.
  • --->
  •  
  • <!---
  • Keep looping while the matcher can find additional
  • matches.
  • --->
  • <cfloop condition="matcher.find()">
  •  
  • <!---
  • Replace the matched text and append it to the buffer.
  • NOTE: We are going to let any embedded slashes (\) and
  • dollar signs ($) act normally.
  • --->
  • <cfset matcher.appendReplacement(
  • buffer,
  • javaCast( "string", attributes.replacement )
  • ) />
  •  
  • <!---
  • Check the scope of the replacement. If the scope is
  • ONE, we can break out of the loop.
  • --->
  • <cfif (attributes.scope eq "one")>
  •  
  • <!---
  • We found the first match and replaced it. Break
  • out of the loop to prevent further replacement.
  • --->
  • <cfbreak />
  •  
  • </cfif>
  •  
  • </cfloop>
  •  
  • </cfif>
  •  
  •  
  • <!---
  • Regardless of our type of replacement, we will need to take
  • care of the tail of our target text.
  • --->
  • <cfset matcher.appendTail( buffer ) />
  •  
  •  
  • <!---
  • Check to see if the calling context has supplied a result
  • variable into which to store the final text value.
  • --->
  • <cfif len( attributes.result )>
  •  
  • <!--- Store the final text value. --->
  • <cfset caller[ attributes.result ] = buffer.toString() />
  •  
  • </cfif>
  •  
  • <!--- Exit out of tag. --->
  • <cfexit method="exittag" />

I like this example because it brings together a few of my very favorite things: ColdFusion, regular expressions, Java Pattern / Matcher, custom tags, and Closures. Again, it's not the most attractive demo because the language is not really designed to allow for this; hopefully in the future, this kind of functionality will be easier to implement. Of course, the ability to create Closures is somewhat less helpful unless more of the built-in ColdFusion functions are updated to allow for function arguments.

Anyway, this is all just a fun little exploration - trying to stretch the language as far as she'll go.




Reader Comments