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 jQuery Conference 2010 (Boston, MA) with:

Creating An ArgumentArray Collection In ColdFusion 9 Using TreeMap

By Ben Nadel on
Tags: ColdFusion

Yesterday, in my post about the contextual nature of the ordered ArgumentCollection method invocation approach, Elliott Sprehn mentioned that you could use a Java TreeMap object to overcome some of the functional "bugs" that I discovered. A TreeMap is a key-value map in which the order of the keys are presented in a "natural" order. That is, the keys are ordered based on their comparator logic, which for our purposes, means alphabetical. Since I had never used the TreeMap before, I wanted to see this work for myself.

Let me start off by reminding you that this is only in ColdFusion 9. When using ColdFusion 8, the TreeMap still results in the same CFScript-invocation bug that I demonstrated in my previous post.

That said, for ColdFusion 9, I wanted to create a function that would abstract the TreeMap. Creating and populating a Java objects takes a few lines of code. As such, I created a function called argumentArray() that would take an array of values and convert it into a TreeMap in which each value was stored at the key represented by its relative array index.

ArgumentArray( valueArray )

  • <cffunction
  • name="argumentArray"
  • access="public"
  • returntype="any"
  • output="false"
  • hint="I take an array and return an ordered-arguments collection.">
  •  
  • <!--- Define arguments. --->
  • <cfargument
  • name="orderedArguments"
  • type="array"
  • required="true"
  • hint="I am array of ordered-arguments."
  • />
  •  
  • <!--- Define the local scope. --->
  • <cfset var local = {} />
  •  
  • <!---
  • Create a TreeMap. This will allow us to invoke both implicit
  • and explicit arguments using a natural-order map. The keys in
  • a TreeMap are access in natural order.
  • --->
  • <cfset local.argumentCollection = createObject( "java", "java.util.TreeMap" )
  • .init()
  • />
  •  
  • <!--- Loop over the arguments and insert them in order. --->
  • <cfloop
  • index="local.argumentIndex"
  • from="1"
  • to="#arrayLen( arguments.orderedArguments )#"
  • step="1">
  •  
  • <!--- Insert the value. --->
  • <cfset local.argumentCollection.put(
  • javaCast( "string", local.argumentIndex ),
  • arguments.orderedArguments[ local.argumentIndex ]
  • ) />
  •  
  • </cfloop>
  •  
  • <!--- Return the populated argument collection. --->
  • <cfreturn local.argumentCollection />
  • </cffunction>

As you can see, this function will take an array like:

[ "Hello", "World" ]

... and convert it into a TreeMap like this:

{ 1:"Hello", 2:"World" }

Now what we have this logic nicely encapsulated, I created two test functions. And, just as in my previous post, I am going to be testing both implicit and explicit argument behavior.

  • <cffunction
  • name="saySomething"
  • access="public"
  • returntype="string"
  • output="false"
  • hint="I build a string with two *assumed* arguments.">
  •  
  • <!--- Argument 1: Name. --->
  • <!--- Argument 2: Description. --->
  •  
  • <cfreturn "Hey, #arguments[ 1 ]#, you look #arguments[ 2 ]# today." />
  • </cffunction>
  •  
  •  
  • <cffunction
  • name="saySomethingElse"
  • access="public"
  • returntype="string"
  • output="false"
  • hint="I build a string with two defined arguments.">
  •  
  • <!--- Define arguments. --->
  • <cfargument
  • name="name"
  • type="string"
  • required="true"
  • hint="I am the name."
  • />
  •  
  • <cfargument
  • name="description"
  • type="string"
  • required="true"
  • hint="I am the description."
  • />
  •  
  • <cfreturn "Hey, #arguments.name#, you look #arguments.description# today." />
  • </cffunction>

As you can see, both functions produce the same result; however, one assumes ordered arguments (saySomething) whereas the other assumes named arguments (saySomethingElse). In the follow tests, I am going to try and invoke both of these functions using the argumentCollection construct. The difference this time, as opposed to my last post, is that I will be assigning a TreeMap, rather than a struct, to the argumentCollection.

First, I am going to test with a CFScript-based invocation:

  • <!--- Define our arguments as an ordered-array. --->
  • <cfset orderedArguments = [ "Jill", "amazing" ] />
  •  
  • <!---
  • Invoke the method using an argumentArray rather than a typical
  • struct. This will allow us to more consistently use dynamic,
  • ordered-arguments.
  • --->
  • <cfset something = saySomething(
  • argumentCollection = argumentArray( orderedArguments )
  • ) />
  •  
  • <!---
  • Invoke the method using an argumentArray rather than a typical
  • struct. This will allow us to more consistently use dynamic,
  • ordered-arguments.
  • --->
  • <cfset somethingElse = saySomethingElse(
  • argumentCollection = argumentArray( orderedArguments )
  • ) />
  •  
  •  
  • <!--- Output both invocation results. --->
  • <cfoutput>
  •  
  • <h1>
  • Ordered CFScript Invocation
  • </h1>
  •  
  • Something: #something#<br />
  • <br />
  •  
  • Something Else: #somethingElse#
  •  
  • </cfoutput>

As you can see, we are converting a standard ColdFusion array into a TreeMap and then passing the resultant value into the given function using the argumentCollection. When we run the above code, we get the following output:

Ordered CFScript Invocation

Something: Hey, Jill, you look amazing today.

Something Else: Hey, Jill, you look amazing today.

Awesome! This worked perfectly regardless of how the arguments are defined within the target function. As we've seen before, however, there tends to be a difference between CFScript and CFInvoke behavior. As such, I am now going to test the same function using argumentCollection in a CFInvoke tag context:

  • <!--- Define our arguments as an ordered-array. --->
  • <cfset orderedArguments = [ "Anna", "stunning" ] />
  •  
  • <!--- Invoke the method using implicit arguments. --->
  • <cfinvoke
  • returnvariable="something"
  • method="saySomething"
  • argumentcollection="#argumentArray( orderedArguments )#"
  • />
  •  
  • <!--- Invoke the method using explicit arguments. --->
  • <cfinvoke
  • returnvariable="somethingElse"
  • method="saySomethingElse"
  • argumentcollection="#argumentArray( orderedArguments )#"
  • />
  •  
  •  
  • <!--- Output both invocation results. --->
  • <cfoutput>
  •  
  • <h1>
  • Ordered CFInvoke Invocation
  • </h1>
  •  
  • Something: #something#<br />
  • <br />
  •  
  • Something Else: #somethingElse#
  •  
  • </cfoutput>

As you can see, it's the same exact concept. The only thing we've changed is the actual mode of invocation. When we run this code, we get the following output:

Ordered CFInvoke Invocation

Something: Hey, stunning, you look Anna today.

Something Else: Hey, Anna, you look stunning today.

This time, we start to see some inconsistent behavior. While explicit arguments continue to work (they worked in my previous blog post too), when using the CFInvoke tag with an implicit-argument function, the arguments are still not mapping in their natural TreeMap order.

Using TreeMap to create an ordered argumentCollection in CF9 is definitely interesting and it does fix some bugs. But, it doesn't quite get us to where we want to be. While this approach fixes the CFScript-based problems, it still leaves CFInvoke-based bugs unresolved.



Looking For A New Job?

100% of job board revenue is donated to Kiva. Loans that change livesFind out more »

Reader Comments

@Ben, just as an aside on maps:

For all intents and purposes, you can think of java.util.HashMap as a ColdFusion struct and java.util.TreeMap as a struct on which you've done x = ListSort(StructKeyList(struct)) and you're using x to iterate over the struct. That is, TreeMap is like an ordered struct.

But on the Java level, they have a very interesting other usage. If you have a very large number of incoming things to sort, you can use HashMap to gather them together quickly. (When the number of elements gets large, doing a put to a HashMap is much faster than a TreeMap.) Then, when you're done, you can build a TreeMap from the HashMap all at once like so:

  • TreeMap map2 = new TreeMap (map1);

... where map1 is the HashMap.

All subclasses of Map (that I know of) have a constructor that takes another Map as its only argument. The cool thing about that is, the TreeMap does a one-time sort that's "of order n log n", if you know what that means. For everyday situations, sorts of order n log n are pretty much the best you can do (fastest available).

The key phrase there was "one-time". If you do lots and lots of puts to a TreeMap that you're building from scratch, you're having to maintain the sorted order on every stinking put.

It's a space versus time tradeoff. You're using twice as much Map space to achieve an improvement in time. Presumably, the objects pointed-to by both maps would be shared. That is, I'm pretty sure that it's a "shallow copy". The difference is that, if you define an Iterator over a HashMap, you get unsorted, but if you define an Iterator over a TreeMap, you get sorted.

Just an idea to hold in reserve for when you have a large collection to sort. I know you love ideas.

Reply to this Comment

@WebManWalking,

I vaguely understanding order of magnitude and processing time. They were always talking about it in my Algorithms class in college many years ago, particularly in terms of sorting algorithms (I guess those are the first type that everyone learns at an academic level).

That's a very interesting use case - the one time sort of keys. I don't typically ever need ordered keys in ColdFusion, so I am not completely sure what I would use this for; but I love having juicy goodness on the back burner for when it's needed.

Reply to this Comment

Here's what I used it for:

When the size of an XML file gets really big, if there's any error at all, the sender complains that they did everything right(!!), and my software is goofing up. They were never right. It was always their lack of familiarity with XML rules that caused the problem. But by saying it was our fault, to prove them wrong, I had to do their debugging for them.

They would always apologize profusely for their XML generation mistake, but in the meantime, I had done their debugging for them. So naturally they kept complaining first, before even looking at their own code. As the number of people sending me XML multiplied, I had to find a way to keep from having to do everyone else's debugging for them. I needed to isolate the cause of the problem in software.

So I hit on the idea of using a SAX parser. That way, I could quote them chapter and verse: the exact tag number where their XML generator screwed up. Also, SAX parsers parse XML much faster than DOM parsers, making them more appropriate for large XML submissions.

The problem is that, after parsing, you want to process the data according to particular tag names, not sequential order. So I hit on the idea of sorting the parsed data by a string that represented tag inclusion hierarchy (not XPath, more simplistic). That string was the HashMap key, which became the sorted TreeMap key.

Now, if they mess up, we tell them "You Bozo! You messed up at tag 1234 (MiddleInitial), whose parent (FirstName) is a data tag, not a group tag!", and they realize that they forgot to close FirstName.

(Well, we don't exactly call them Bozo, but you get the idea. And TreeMap made it possible.)

Reply to this Comment

@WebManWalking,

Ha ha, very nice. I've always wanted to play around with the SAX parser, but every time I've looked into it, you need to implement a whole event handler interface, right? Are you programming that in Java or Groovy or something?

Reply to this Comment

The core of what you have to do is extend javax.xml.parsers.HandlerBase (and in my case, also implement CustomTag). Extending HandlerBase defines empty callbacks for you, the most useful being:
* startDocument,
* endDocument,
* startElement,
* endElement,
* characters (to grab data between elements) and
* error (if you want to implement a parser that validates against a DTD/XSD).

When I say empty, I mean {}, no code. But since you're subclassing HandlerBase, you can implement your own methods by those same names, and they override the empty ones. Any methods you don't want to implement, don't. (The HandlerBase empty method will be called, and do nothing.)

A minimalistic SAX parser would implement only startElement, endElement and characters. Here's some pseudocode:

  • public void startElement (String tagName, AttributeList a)
  • throws SAXException
  • {
  • // set up a property to receive the data
  • }
  • public void characters(char buf[], int offset, int len)
  • throws SAXException
  • {
  • // Do something with the characters,
  • // based on the property that startElement
  • // set up.
  • }
  • public void endElement (String tagName)
  • throws SAXException
  • {
  • // Do something with the data you captured.
  • }

These are called repeatedly as start and end elements are encountered. All the real work is in startElement, which cleverly figures out where to store the element's upcoming data.

That's why they call it SAX, the Simple API for XML. The API's as simple as can be. But startElement can get pretty complex, depending on your XML format.

There's a LITTLE more to it than that (instantiating with SAXParserFactory), but the core of the work is in those 3 methods.

Reply to this Comment

@WebManWalking,

Pretty cool stuff. I do get questions a lot about how to parse XML that is too big for xmlParse(). I tell people to look into this kind of thing, but have no field experience with it myself.

Reply to this Comment

Post A Comment

You — Get Out Of My Dreams, Get Into My Comments
Live in the Now
Oops!
Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.