RESwitch / RECase ColdFusion Custom Tags For Regular Expression Switch Statements

Posted November 6, 2008 at 10:00 AM

Tags: ColdFusion

As you all know, I LOVE regular expressions. Pattern matching on strings is one of the most powerful weapons that I have in my bat-utility belt. The other day, I was in a situation where it would have been awesome to have a Switch statement that matched on patterns rather than string literals. Seeing as I've built just about every other kind of regular expression ColdFusion custom tag that I could think of, I figured why not give this a try.

The first thing I did was write out the calling page to codify how I wanted to the RESwitch and RECase ColdFusion custom tags to work. One of the most important aspects of pattern matching is being able to not only match patterns (obviously) but to be able to easily access the matched groups within the matched pattern. Here's what I came up with:

 Launch code in new window » Download code as text file »

  • <!--- Set a value to test (quoted string). --->
  • <cfset strValue = """Hey Sugar""" />
  •  
  •  
  • <!--- Check to see what kind of value we are working with. --->
  • <cf_reswitch expression="#strValue#">
  •  
  • <cf_recase
  • pattern="^(\d+)\.(\d)+$"
  • group1="intInteger"
  • group2="intDecimal">
  •  
  • Float found: #intInteger#.#intDecimal#.
  •  
  • </cf_recase>
  •  
  • <cf_recase pattern="^\d+$">
  •  
  • Integer found: #strValue#.
  •  
  • </cf_recase>
  •  
  • <cf_recase
  • pattern="^([^@]+)@(.+)\.(\w{2,})$"
  • group1="strName"
  • group2="strDomain"
  • group3="strExtension">
  •  
  • Email found: #strName#@#strDomain#.#strExtension#.
  •  
  • </cf_recase>
  •  
  • <cf_recase
  • pattern="^(""([^""]+)""|([^""]+))$"
  • group2="strQuotedValue"
  • group3="strNonQuotedValue">
  •  
  • <cfif Len( strQuotedValue )>
  •  
  • Quoted string found: #strQuotedValue#.
  •  
  • <cfelse>
  •  
  • Non-Quoted string found: #strNonQuotedValue#.
  •  
  • </cfif>
  •  
  • </cf_recase>
  •  
  • <!--- Default case. --->
  • <cf_recase pattern="^[\w\W]*$">
  •  
  •  
  • Unknown string value found: #strValue#.
  •  
  • </cf_recase>
  •  
  • </cf_reswitch>

Here, you can see that like the ColdFusion CFSwitch tag, the RESwitch ColdFusion custom tag uses the Expression attribute. Then, the RECase tags use the Pattern attribute to define the pattern we'll be trying to match. Optionally, the RECase tag allows you to define variables into which the matched groups will be stored. This gives us not only the ability to test matches, but to easily access the components of those match.

Running the above test page, we get the following output:

Quoted string found: Hey Sugar.

Notice that even though our test value had quotes around it, we were able to pull out the actual string value by checking to see which group came back with a value. Ideally, a non-matched group would not even have a variable; however, to make this easier to use, I default the non-matched groups to the empty string to keep this a low-entry use tag.

The ColdFusion custom tags that make this happen are actually quite simple. Here is the RESwitch tag:

 Launch code in new window » Download code as text file »

  • <!--- Check to see which tag mode we are in. --->
  • <cfif (THISTAG.ExecutionMode EQ "Start")>
  •  
  • <!--- Start mode. --->
  •  
  • <!--- Check to make sure this tag has an end tag. --->
  • <cfif NOT THISTAG.HasEndTag>
  •  
  • <cfthrow
  • type="RESwitch.MissingEndTag"
  • message="This RESwitch tag requires an end tag."
  • />
  •  
  • </cfif>
  •  
  •  
  • <!--- Param tag attributes. --->
  •  
  • <!---
  • This is the expression that we are testing (the value
  • against which we are going to try and match our patterns).
  • --->
  • <cfparam
  • name="ATTRIBUTES.Expression"
  • type="string"
  • />
  •  
  •  
  • <!---
  • Set the match flag to indicate whether or not any of the
  • child tags has successfully matched a tag.
  • --->
  • <cfset VARIABLES.IsPatternMatched = false />
  •  
  •  
  • <!---
  • Create a Pattern object that we can use to compile our
  • patterns in the child tags.
  • --->
  • <cfset VARIABLES.PatternClass = CreateObject(
  • "java",
  • "java.util.regex.Pattern"
  • ) />
  •  
  • <cfelse>
  •  
  • <!--- End mode. --->
  •  
  • </cfif>

And here is the RECase tag:

 Launch code in new window » Download code as text file »

  • <!--- Check to see which tag mode we are in. --->
  • <cfif (THISTAG.ExecutionMode EQ "Start")>
  •  
  • <!--- Start mode. --->
  •  
  • <!--- Check to make sure this tag has an end tag. --->
  • <cfif NOT THISTAG.HasEndTag>
  •  
  • <cfthrow
  • type="RECase.MissingEndTag"
  • message="This RECase tag requires an end tag."
  • />
  •  
  • </cfif>
  •  
  •  
  • <!---
  • Get a reference to the parent tag so that we can update
  • the tag context as needed.
  • --->
  • <cfset VARIABLES.SwitchTag = GetBaseTagData( "cf_reswitch" ) />
  •  
  •  
  • <!---
  • Check to see if a pattern has already been matched. If
  • so, then we want to exit out immediately.
  • --->
  • <cfif VARIABLES.SwitchTag.IsPatternMatched>
  •  
  • <!---
  • Exit out immediately - we don't want any more of the
  • child case tags to execute.
  • --->
  • <cfexit method="exittag" />
  •  
  • </cfif>
  •  
  •  
  • <!---
  • ASSERT: If we have made it this far then we know that
  • no pattern has been matched against our expression yet.
  • --->
  •  
  •  
  • <!--- Param tag attributes. --->
  •  
  • <!---
  • This is the patten that we are going to test against
  • the parent tag expression.
  • --->
  • <cfparam
  • name="ATTRIBUTES.Pattern"
  • type="string"
  • />
  •  
  •  
  • <!--- Compile this regular expression patterns. --->
  • <cfset VARIABLES.Pattern = VARIABLES.SwitchTag.PatternClass.Compile(
  • JavaCast( "string", ATTRIBUTES.Pattern )
  • ) />
  •  
  • <!---
  • Get a matcher for this pattern as it is applied to the
  • expression we are testing.
  • --->
  • <cfset VARIABLES.Matcher = VARIABLES.Pattern.Matcher(
  • JavaCast(
  • "string",
  • VARIABLES.SwitchTag.ATTRIBUTES.Expression
  • )
  • ) />
  •  
  •  
  • <!--- Check to see if this matcher can find a match. --->
  • <cfif VARIABLES.Matcher.Find()>
  •  
  • <!---
  • Update the parent flag to indicate that this tag has
  • matched a pattern.
  • --->
  • <cfset VARIABLES.SwitchTag.IsPatternMatched = true />
  •  
  • <!---
  • Now that we have matched a pattern, let's see if
  • we need to move any of the matched groups into
  • tag attributes. I have picked 20 arbitrarily.
  • --->
  • <cfloop
  • index="VARIABLES.GroupIndex"
  • from="1"
  • to="20"
  • step="1">
  •  
  • <!---
  • Check to see if an attribute exists for this
  • matched group.
  • --->
  • <cfif StructKeyExists( ATTRIBUTES, "group#VARIABLES.GroupIndex#" )>
  •  
  • <!---
  • There is chance that this group did not
  • actually get matched in the pattern. If that
  • is the case, getting it will return a NULL
  • which will remove the variable.
  • --->
  • <cfset VARIABLES.GroupValue = VARIABLES.Matcher.Group(
  • JavaCast( "int", VARIABLES.GroupIndex )
  • ) />
  •  
  • <!--- Check to see if it was matched. --->
  • <cfif StructKeyExists( VARIABLES, "GroupValue" )>
  •  
  • <!--- Store the matched pattern. --->
  • <cfset CALLER[ ATTRIBUTES[ "group#VARIABLES.GroupIndex#" ] ] = VARIABLES.GroupValue />
  •  
  • <cfelse>
  •  
  • <!---
  • That pattern has this group, but it was
  • NOT matched. Let's just store an empty
  • string in the variable. Not the best
  • strategy, but we can always tweak later.
  • --->
  • <cfset CALLER[ ATTRIBUTES[ "group#VARIABLES.GroupIndex#" ] ] = "" />
  •  
  • </cfif>
  •  
  • </cfif>
  •  
  • </cfloop>
  •  
  • <cfelse>
  •  
  • <!---
  • No match was found. Exit out of this tag and let
  • the next child tag execute.
  • --->
  • <cfexit method="exittag" />
  •  
  • </cfif>
  •  
  • <cfelse>
  •  
  • <!--- End mode. --->
  •  
  • </cfif>

Notice that the matching algorithm makes use of the Java Pattern and Matcher classes. This means that you can make use of all of the functionality that is provided by the Java regular expression engine (which is more robust and significantly faster than the ColdFusion POSIX regular expression engine).

Download Code Snippet ZIP File

Post Comment  |  Ask Ben  |  Permalink  |  Other Searches  |  Print Page





Reader Comments

Nov 6, 2008 at 10:46 AM // reply »
25 Comments

Hey Ben,

Great article, as always. Good to have you back ;)


Nov 6, 2008 at 10:51 AM // reply »
6,516 Comments

@Francois,

Thanks man :) I am really gonna try to get back into it. Got some stuff in the queue that I want to play with. I think this was a good way to kick it off.


Nov 6, 2008 at 11:02 AM // reply »
25 Comments

I've seen people take longer breaks ;).


Nov 6, 2008 at 7:49 PM // reply »
35 Comments

That is really nice!

I have had a few cases where I wanted to use cfswitch/cfcase, but I needed to check regular expressions instead of strings. I never thought of building a custom tag for it though.

I will definitely check this out next time I run into that situation.


Nov 7, 2008 at 7:55 AM // reply »
6,516 Comments

@Steve,

Let me know if you can think of any way to improve this.


Post Comment  |  Ask Ben

Recent Blog Comments
Nov 20, 2009 at 10:56 PM
Five Months Without Hungarian Notation And I'm Loving It
I use the following and love it: my.namespace.MyComponents.functionMethodsOrUDF() CONSTANT_VALUES_OR_PROPERTIES One thing I always try is to CamelCaseBuiltInColdFusionFunctions() so others can tell ... read »
Nov 20, 2009 at 5:38 PM
Learning ColdFusion 8: CFImage Part I - Reading And Writing Images
Hi Ben, Great article. I've been looking around to see if ColdFusion image engine can programatically create the following "wrap around" effect: http://www.creativepro.com/article/photoshop-s-she ... read »
Nov 20, 2009 at 5:35 PM
Maintaining ColdFusion Sessions Across SMS Text Message Requests Without Cookies
@Dave: I talked to Gert he suggested: <cfhttp method="get" url="http://{some cf website}" result="stuff" addtoken="yes" /> Note the addition of cfhttp attribute addtoken. That should persist y ... read »
Nov 20, 2009 at 5:23 PM
Maintaining ColdFusion Sessions Across SMS Text Message Requests Without Cookies
@Todd, Ahh, gotcha, yeah that makes sense. ... read »
Nov 20, 2009 at 5:17 PM
Maintaining ColdFusion Sessions Across SMS Text Message Requests Without Cookies
Ben, sorry if I didn't make this clear. You can make it work like that if you want, just put <cfset session.foo = 1> (and <cfset application.foo = 1>) in your OnRequestStart() and it reve ... read »
Nov 20, 2009 at 5:07 PM
Maintaining ColdFusion Sessions Across SMS Text Message Requests Without Cookies
@Todd, I have seen tidbits about the way Railo handles session. I can understand that it lazy-loads sessions, but I also think that I might make some things more complicated. For example, often tim ... read »
Nov 20, 2009 at 4:53 PM
Maintaining ColdFusion Sessions Across SMS Text Message Requests Without Cookies
Ben, you can ramp up the security by turning on J2EE session which gives you a third set of numbers other than CFID/CFTOKEN. There's a reason why ACF put this in place (other than just session replic ... read »
Nov 20, 2009 at 4:52 PM
Maintaining ColdFusion Sessions Across SMS Text Message Requests Without Cookies
Case in point, Ben, you may not be aware of this, but in Railo - OnApplicationStart() & OnSessionStart() act differently than in ACF. ACF does: OnApplicationStart (1st hit) OnSessionStart (1st and e ... read »