Calling ColdFusion Components And Methods From Groovy

As you may or may not know, when ColdFusion compiles your code, it doesn't compile down to Java objects that directly mimic your ColdFusion objects; rather, ColdFusion compiles your code down into sets of nested Java classes that make ColdFusion's extreme flexibility possible. In the ColdFusion context, this is irrelevant as it happens seamlessly behind the scenes. However, when you need to use ColdFusion components and methods outside of the ColdFusion context (such as in Groovy), suddenly things get much more complicated.

To help me learn more about Groovy and about how ColdFusion works under the hood, I wanted to see if could create Groovy wrapper classes for ColdFusion components and ColdFusion methods (UDFs - not built-in method) that would facilitate the communication between the two layers. It took me around 5-6 hours, but I think I finally got it working! The Groovy wrappers use undocumented Java methods on the ColdFusion objects, so there was a lot of guess work here; as such, take this with a grain of salt.

Before we look at the Groovy aspect, let's take a look at the ColdFusion component that I was testing with:

Greet.cfc

<cfcomponent

output="false"

hint="I am simple CFC for Groovy testing.">

<!---

Define public values that will act as the default for

the following method call arguments.

--->

<cfset this.firstName = "Joanna" />

<cfset this.lastName = "Smith" />

<cffunction

name="hello"

access="public"

returntype="string"

output="false"

hint="I say hello to the name stored in this CFC context.">

<!--- Define arguments. --->

<cfargument

name="firstName"

type="string"

required="false"

default="#this.firstName#"

hint="I am the first name. NOTE: I default to the public property within this CFC context."

/>

<cfargument

name="lastName"

type="string"

required="false"

default="#this.lastName#"

hint="I am the last name. NOTE: I default to the public property within this CFC context."

/>

<!--- Return the hello message. --->

<cfreturn "Hello #arguments.firstName# #arguments.lastName#" />

</cffunction>

</cfcomponent>

The Greet.cfc ColdFusion component has one method, Hello(), which returns a simple greeting message. As you can see in the code, the Hello() method can take a first name and last name argument which, if not passed-in, will default to the THIS-scoped firstName and lastName properties of the component. The defaulting of the arguments becomes important when we invoke the Hello() method outside of the CFC-binding.

Now that we see the CFC, let's take a look at the Groovy code (powered by CFGroovy). Before the page actually entered the Groovy code, there are two important things to notice: One, we are defining a firstName and lastName variable in the calling page; and Two, we are getting a reference to the current Page Context, which is required when invoking the ColdFusion methods. The beginning bulk of the Groovy code is the CF wrapper class definition - you may want to scroll down to the bottom to see how they are used before you look at the wrappers.

<!--- Import the CFGroovy tag library. --->

<cfimport prefix="g" taglib="../cfgroovy/" />

<!---

Create an instance of our greet CFC. Note that the default

values of the Greet component are:

firstName: Joanna

lastName: Smith

--->

<cfset greet = createObject( "component", "Greet" ) />

<!---

Let's create two variables here (firstName, lastName) that

mimic the THIS-scope properties of our Greet CFC. This will

be used farther down when we invoke the CFC method outside

of the context of the containing ColdFusion component.

--->

<cfset firstName = "Tricia" />

<cfset lastName = "Badonkastein" />

<!---

Get the page context. This is required when we create the

ColdFusion to Groovy bridge objects (it is used in the

method invokation calls).

--->

<cfset pageContext = getPageContext() />

<g:script>

<!--- ------------------------------------------------- --->

<!--- ------------------------------------------------- --->

class CFBridge {

def public variablesScope;

def public pageContext;

<!---

The constructor needs to store the variables scope

and the page context so that it can create the

appropriate ColdFusion to Groovy bridges.

--->

def public CFBridge( variablesScope, pageContext ){

this.variablesScope = variablesScope;

this.pageContext = pageContext;

}

<!---

The call method allows this object to be invoked

as a method (basically, it override the parenthesis

operator). This will take a single ColdFusion object

and wrap it in the appropriate ColdFusion to Groovy

bridge.

--->

def public call( target ){

<!---

Check to see what kind of object we have so

that we can create the appropriate bridge.

--->

if (this.isCFC( target )){

<!--- Target is a CFC object. --->

return( new CFC( this, target ) );

} else if (this.isCFMethod( target )){

<!--- Target is a CF Method reference. --->

return( new CFMethod( this, target ) );

}

<!--- This bridge is not supported yet. --->

throw new Exception( "CF Brdige not supported." );

}

<!---

Determines if the given object is a ColdFusion

component.

--->

def static public isCFC( target ){

return(

target.getClass().getName() == "coldfusion.runtime.TemplateProxy"

);

}

<!---

Determines if the given object is a ColdFusion

method reference.

--->

def static public isCFMethod( target ){

return(

target.getClass().getName().indexOf( "\$func" ) > 0

);

}

}

<!--- ------------------------------------------------- --->

<!--- ------------------------------------------------- --->

class CFC {

def private bridgeFactory;

def private target;

<!--- Consturctor. --->

def public CFC( bridgeFactory, target ){

this.bridgeFactory = bridgeFactory;

this.target = target;

}

<!---

This method is similar to ColdFusion's

onMissingMethod() event handler and will route the

method calls from Groovy to the underlying ColdFusion

component (CFC).

--->

public methodMissing( String name, args ){

<!---

Check to see if there is only one argument and if

it is a linked hash map. ColdFusion methods can be

invoked either using ordered arguments or named

arguments, so this will try to guess between the

two types.

--->

if (

(args.size() == 1) &&

(args[ 0 ] instanceof java.util.LinkedHashMap)

){

<!---

Invoke ColdFusion method on Component using

hash map approach (the first argument).

--->

return(

this.target.invoke(

name.toString(),

args[ 0 ],

this.bridgeFactory.pageContext

)

);

} else {

<!---

Invoke ColdFusion method on Component using

ordered arguments approach.

--->

return(

this.target.invoke(

name.toString(),

args,

this.bridgeFactory.pageContext

)

);

}

}

}

<!--- ------------------------------------------------- --->

<!--- ------------------------------------------------- --->

class CFMethod {

def private bridgeFactory;

def private target;

<!--- Consturctor. --->

def public CFMethod( bridgeFactory, target ){

this.bridgeFactory = bridgeFactory;

this.target = target;

}

<!---

The call method allows this object to be invoked

as a method (basically, it override the parenthesis

operator). This method will pass use the given

arguments to invoke the underlying ColdFusion method.

--->

def public call( Object... args ){

<!---

Check to see if there is only one argument and if

it is a linked hash map. ColdFusion methods can be

invoked either using ordered arguments or named

arguments, so this will try to guess between the

two types.

--->

if (

(args.size() == 1) &&

(args[ 0 ] instanceof java.util.LinkedHashMap)

){

<!---

Invoke ColdFusion method using hash map

approach (the first argument).

--->

return(

this.target.invoke(

this.bridgeFactory.variablesScope,

"",

this.bridgeFactory.pageContext.getPage(),

args[ 0 ]

)

);

} else {

<!---

Invoke ColdFusion method using ordered

arguments approach.

--->

return(

this.target.invoke(

this.bridgeFactory.variablesScope,

"",

this.bridgeFactory.pageContext.getPage(),

args

)

);

}

}

}

<!--- ------------------------------------------------- --->

<!--- ------------------------------------------------- --->

<!---

Create an instance of the ColdFusion to Groovy bridge

class. This will provide the factory for the rest of

the bridges.

--->

def CF = new CFBridge( variables, variables.pageContext );

<!--- ------------------------------------------------- --->

<!--- ------------------------------------------------- --->

<!--- This is a utility method for testing. --->

def output( value = "" ){

println(

value.toString() + ("<br />" * 2)

);

}

<!--- ------------------------------------------------- --->

<!--- ------------------------------------------------- --->

<!--- ------------------------------------------------- --->

<!--- ------------------------------------------------- --->

<!---

Call the hello method on the current CFC. Notice that we

are feeding the greet CFC instance through our bridge

factory before we call the hello() method.

--->

output(

CF( variables.greet ).hello()

);

<!---

Now, rather than let the hello() method use the default

values, let's override the arguments using an ordered

arguments approach.

--->

output(

CF( variables.greet ).hello( "Kim", "McSinful" )

);

<!---

Now, let's override the arguments again, but this time,

lets use a named-arguments approach.

NOTE: The arguments are being passed-in in reverse order

to demonstrate that order is not being considered.

--->

output(

CF( variables.greet ).hello([

"lastName": "O'Gazmic",

"firstName": "Libby"

])

);

<!---

Now, let's call the hello() method OUTSIDE of the context

of the ColdFusion component in which it was defined.

Notice that this time, we pass the hello() reference to

the bridge factory before we invoke it.

--->

output(

CF( variables.greet.hello )()

);

</g:script>

Once you get to the bottom of the Groovy code, you can see that we are feeding the ColdFusion objects and ColdFusion methods to our ColdFusion-to-Groovy bridge builder before we invoke any methods:

CF( component ).method( ... );CF( component.method )( ... );

When we pass in a ColdFusion component, any methods we call on that bridge will be executed in the context of that ColdFusion component. When we pass in a ColdFusion method (even one that is a property of a component), its execution will be performed in the context of the calling page. This latter point is why we defined firstName and lastName variables at the top of the calling page.

You will also notice that the methods can be invoked using ordered arguments or named-arguments (just as they can be in ColdFusion). This took some fenagling since I basically decided that if the first and only argument passed into the method was a linked hash map, it was being invoked using named arguments - a somewhat arbitrary decision.

Anyway, when we run the above code, we get the following output:

Hello Joanna Smith

Hello Kim McSinful

Hello Libby O'Gazmic

Hello Tricia Badonkastein

As you can see, all of the ColdFusion methods were invoked successfully from the Groovy context. Furthermore, you'll notice that in the last example we invoked the method, Hello(), outside of the CFC-binding; in that case, the firstName and lastName values were pulled out of the calling page context rather than the Greet CFC.

Reader Comments

Nice Ben. There is already a 'pageContext' binding provided for you by CFGroovy, so you don't need an explicit reference the variables scope. Though now that I think about it, it's the PageContext for the g:script tag, not the calling template, so maybe it does matter.

Yeah, I think you're right (re: custom tag scope vs. calling page scope). I'm not really sure... I'm basically just a blind man hacking my way through a jungle of unfamiliar functionality :) This whole things took me hours to figure out, a and lot of that time was just trying to debug the Groovy aspect. Heck, an hour was spent trying to figure out why this caused an infinite loop:

getProperty( name ){return( this[ name ] );}

... I assumed you could use "this" internally to an object without it re-invoking the getProperty() method. Who knew :D

There's some really awesome stuff in Groovy. In particular, I love the operator overloading and just all the awesome operators in general!

Any reason you aren't using my ColdFusion Component Dynamic Proxy, in JavaLoader 1.0?

It works with Java objects, and since Groovy works with Java objects, it should work as well, and would be a much more seamless experience as Groovy won't know the CFC isn't actually just a plain ol' POJO.

Just a thought ;o)

Side idea - since Groovy is untyped, and has it's own version of onMissingMethod, why not just build your own Groovy Proxy class to wrap around a CFC, and then call methods on it. Would save all the CF(obj) calls), and you could just have:

To be honest, I actually looked at your JavaProxy.cfc when I was trying to figure all of this out. Is that what you are referring to? I had a little bit of trouble understanding it, but from what I gathered, I thought it was going the other way (CF to Java).

As far as creating a proxy class, under the hood of the CF() method call, that's what is happening. The reason that I delegate to the call() method on the CF class is that the CF class instance has the reference to the *variables* scope and to the *pageContext*; if I created the proxy classes directly, I would have to pass in the above with every call:

new CFCProxy( variables.greet, variables, variables.pageContext );

... at least as far as I gather. This stuff is really new to me and like I said to Barney, it's like blindly hacking through a jungle :)

I finally downloaded and poked around in your CFC Dynamic Proxy code. Looks very cool. One thing that I was curious about, and I think this might be a pure limitation of Java, is that you have to uphold *some* interface, right? Like this wouldn't work on a CFC that is powered by onMissingMethod() would it?

I wish I knew more Java. Just reading through your code is making me sweat ;)

Right - because Java is typed, you have to tell it is a type of 'something', otherwise it doesn't know what to do with it.

In theory, with Groovy you could probably work around this.

The interesting question would be - could you pass your Groovy->CFC Proxy back to ColdFusion, and would it run? (I have a feeling it would not, due to the way that CF attempts to resove a method to one on a class/interface), whereas with the Java->CFC Dynamic Proxy, you can, as ColdFusion can look up an actual method (via reflection) on the interface the Proxy implements.

Yeah, Groovy can be dynamic because it has a methodMissing() handler, which can act as the pipe into ColdFusion. But, no, you could not pass the Groovy wrapper back into ColdFusion. Or, you could, but it would not work the way you expect. I am pretty sure you would need to manually invoke the "call" method on Groovy object in a ColdFusion context. So yeah, it sounds like the Dynamic Proxy would be rockin that respect.

I am the co-founder and lead engineer at InVision App, Inc — the world's leading prototyping,
collaboration & workflow platform. I also rock out in JavaScript and ColdFusion 24x7 and I dream about
promise resolving asynchronously.