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.