Sunday, September 21, 2008

Experiment with Calling CFFunctions from PDFPageEvents - Part 1



In a previous entry I wrote about using iText's PDFPageEventHelper to perform tasks like adding headers and footers to a PDF. Recently, a reader named Murray raised an interesting question:
    .. It occurs to me that an improvement would be if you could get the onPageEnd listener in the java to call a CF function that you plugged in, and then do all the end page stuff in CF rather than java. For example, you could define what your header and footer looks like dynamically and on a pdf by pdf case instead of having to recompile the java whenever you wanted a different type of header / footer that wasnt covered by the style data you pass in.

Since CF functions are java classes internally, I suspected it was possible. Of course the obvious disadvantage is that it requires venturing into "undocumented" territory, and all the risks that entails. While I am normally not a fan of that, the idea was so interesting I decided to test the theory. Just to find out if it was even possible.

How do I call thee. Let me count the ways
In order to invoke the function from java I needed to figure out what method to call. Fortunately the function class contains a well named "invoke" method, which is what I ended up using.



Unfortunately, the parameter types are very high level (java.lang.Object). So I had to guess which four (4) values should be passed to the invoke method. After poking around Eclipse a bit, I came up with what seemed to be a reasonable guess.
  1. java.lang.Object instance - getPageContext().getFusionContext()
  2. java.lang.String calledName - function name
  3. java.lang.Object parent - getPageContext().getPage()
  4. java.util.Map namedArgs - structure of argument to pass to the function
Mirror, mirror on the wall ..
I decided to use reflection to call the CF functions from my java class. If you are not familiar with it, think of it as a way to execute java code dynamically. One reason I chose reflection is that it is flexible. Plus I thought it was more in line with the goal: create a generic java shell and do all the coding and changes from ColdFusion.

Java Code Overview
There are two parts the code: a java utility class and a CFC. The java class is used to receive PDFPageEvents and essentially re-route them to a CF function. So whenever a page event occurs, the java class will invoke whatever CF function you mapped to that event. If there is no function defined for that event, the event is ignored.

Using the java class is a lot simpler than it looks. Just instantiate it and call the link() method to associate a CF function with a particular page event. All the link() method does is stores the information needed to invoke the CF function in an Object. Then it associates the function with an event, by storing the information in a structure, using the event name as the key.


public void link(String eventName, Object cfFuncObj, Map cfFuncArgs,
String methodName, Class[] paramTypes, Object[] methodArgs) {
// create a new function object
EventFunction func = new EventFunction(cfFuncObj, cfFuncArgs, methodName, paramTypes, methodArgs);
// link the function to the specified event
getEventMap().put(eventName, func);
}


Then whenever an event occurs (like onDocumentOpen), the class checks the structure to see if there is function defined for that event. If there is, it saves information about the event to the function arguments. Then calls the CF function.


public void onOpenDocument(PdfWriter writer, Document document) {
EventFunction cfData = getEventFunction(ON_OPEN_DOCUMENT);
// If there is a CF function mapped to this event
if (cfData != null) {

// Add the event data to the CF function arguments
Map event = createEvent();
event.put(EVENT_WRITER, writer);
event.put(EVENT_DOCUMENT, document);
cfData.addFunctionArg(CF_PDF_EVENT, event);

invokeCFFunction(cfData);
}
}


The method used to call the CF function is quite simple. Nothing very special about it, other than it uses java reflection.


protected void invokeCFFunction(EventFunction cfData) {
try {
// Get the CF function's java class
Class cfClass = cfData.getFunction().getClass();

// Locate the internal method used to invoke the function
Method cfMethod = cfClass.getMethod( cfData.getMethodName(), cfData.getParamTypes() );

// Finally, invoke call the CF function
cfMethod.invoke( cfData.getFunction(), cfData.getMethodArgs());

}
catch (Exception e) {
// convert checked exception into an unchecked exception.
throw new ExceptionConverter( new CFPDFFunctionException(e) );
}
}



CFC Code Overview
The CFC is just a small wrapper used to simplify the task of using the java utility. It contains two main functions: createEventUtility() and link(). The createEventUtility() function just creates an instance of the java class. The second function just converts the arguments into a more acceptable format and passes them along to the utility's link() function.


<cffunction name="createEventUtility" returntype="any" access="public" output="false" hint="Returns a new instance of the java utility class">
<!--- create an instance of the java utility class --->
<cfreturn getJavaLoader().create( getUtilityClass() ).init() >
</cffunction>

<cffunction name="link" returntype="void" access="public" output="false" hint="Links a CFFunction to a PDFPageEvent">
<cfargument name="eventUtility" type="any" required="true" hint="Instance of the java utility class">
<cfargument name="eventName" type="string" required="true" hint="Name of the desired PDFPageEvent">
<cfargument name="functionContext" type="any" required="true" hint="Page context for the CFFunction. ie GetPageContext()">
<cfargument name="functionObject" type="any" required="true" hint="Instance of the desired CFFunction">
<cfargument name="functionArguments" type="struct" default="#structNew()#" hint="Any arguments to pass to the CFFunction">

<cfset var Local = structNew()>

<!--- the method name used to call the CF function internally --->
<cfset Local.internalMethod = "invoke">

<!--- define paramter types required to call the CF function internally --->
<cfset Local.Class = createObject("java", "java.lang.Class")>
<cfset Local.paramTypes = arrayNew(1)>
<cfset Local.paramTypes[1] = Local.Class.forName("java.lang.Object")>
<cfset Local.paramTypes[2] = Local.Class.forName("java.lang.String")>
<cfset Local.paramTypes[3] = Local.Class.forName("java.lang.Object")>
<cfset Local.paramTypes[4] = Local.Class.forName("java.util.Map")>

<!--- define arguments required to call the CF function internally --->
<!--- [1] instance, [2] function name, [3] parent, [4] function arguments --->
<cfset Local.methodArgs = arrayNew(1)>
<cfset Local.methodArgs[1] = arguments.functionContext.getFusionContext()>
<cfset Local.methodArgs[2] = getMetaData(arguments.functionObject).name>
<cfset Local.methodArgs[3] = arguments.functionContext.getPage()>
<cfset Local.methodArgs[4] = arguments.functionArguments >

<!--- using the java class, link the CF function to the specified page event --->
<cfset arguments.eventUtility.link ( arguments.eventName,
arguments.functionObject,
arguments.functionArguments,
Local.internalMethod,
Local.paramTypes,
Local.methodArgs
)>
</cffunction>



Coming up in Part 2 - Putting it all Together.

6 comments:

PaulH September 22, 2008 at 2:24 AM  

cool. itext page/doc events are very powerful but having to write java class is kind of soggy & hard to light especially before marks's java loader. i've tried several times to do it in pure cf but never been successful.

looking forward to the rest of this.

cfSearching September 22, 2008 at 7:36 AM  

@paulh,

Yes. I tried approaching it from a CF only perspective, but did not have much luck either. Thank goodness for the javaLoader. Debugging the java version would have taken a _lot_ longer without it.

I will post the second half later. It still needs a thorough review for potential issues. But at least I know the concept works ;-) I am totally open to any suggestions about how it could be improved or simplified.

Murray,  September 22, 2008 at 1:23 PM  

This is looking really interesting. Thanks for pursuing it. Watching for part 2 ....

cfSearching September 22, 2008 at 2:50 PM  

Okay, Part 2 is finally up and posted ;)

Sami Hoda September 22, 2008 at 3:13 PM  

Interesting... is there a list of events we can use inside a PDF?

cfSearching September 22, 2008 at 3:18 PM  

@sami hoda,

Yes. The events are dictated by the iText PDFPageEvent interface.
http://www.1t3xt.info/api/com/lowagie/text/pdf/PdfPageEvent.html

  © Blogger templates The Professional Template by Ourblogtemplates.com 2008

Header image adapted from atomicjeep