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.
Being able to call ColdFusion methods from within Java is something that's baffled me for about three years. Now, trying it once again in Groovy, I was finally able to make it work! Of course, I am not 100% sure how the underlying methods work and, it has to be in the greater context of ColdFusion (to get the page context), but they work well enough for this experiment. Groovy has some really awesome stuff in it.
Want to use code from this post? Check out the license.
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.
@Barney,
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:
proxy = new CFCProxy( variables.greet )
proxy.hello()
That is if you didn't want to use the JavaLoader's one.
@Mark,
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 :)
@Ben,
You are right, the JavaProxy.cfc is CF->Java.
What I'm actually referring to is the CFC Dynamic Proxy documented here:
http://www.compoundtheory.com/javaloader/docs/#ColdFusion_Component_Dynamic_P_1999409235798828
And also talked about here:
http://www.compoundtheory.com/?action=displayPost&ID=422
Which enables much more seamless interaction between Java and ColdFusion.
If you were to write your own proxy, you don't need to pass that all in with every call. For one, you can always get at the current PageContext through: FusionContext.getCurrent().pageContext
Also not sure why you are passing through the variables scope.... to invoke a cfc from Java it's just:
getCFC().invoke("methodName", argArray, pageContext);
You can check out my Dynamic Proxy code to see how I am doing it:
http://svn.riaforge.org/javaloader/trunk/cfcdynamicproxy/src/com/compoundtheory/coldfusion/cfc/CFCDynamicProxy.java
@Mark,
Hmmm, I think I *may* have seen that before, but I think it was over my head at the time. I think I may have also thought it was only in regards to Spring integration (your blog post looks familiar).
Where are you getting the FusionContext from? Is that internal to your proxy?
As far as the Variables scope, yeah, I don't need that for the CFC-method invocation; however, when I detatch the method from the CFC as in:
CF( greeting.hello )( .. )
... I use the variables scope as the binding context (as a normal UDF would use)??
I'll read up more on your stuff. Thanks for putting in the links. I really want to wrap my head around this better.
@Mark,
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 ;)
@Ben,
Re: 'is that you have to uphold *some* interface, right'.
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.
@Mark,
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.