RESwitch / RECase ColdFusion Custom Tags For Regular Expression Switch Statements
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:
<!--- 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:
<!--- 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:
<!--- 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).
Want to use code from this post? Check out the license.
Reader Comments
Hey Ben,
Great article, as always. Good to have you back ;)
@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.
I've seen people take longer breaks ;).
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.
@Steve,
Let me know if you can think of any way to improve this.