Tuesday, November 3, 2009

A Better PdfPageEventHandler with JavaLoader/CFCDynamicProxy

Yesterday , I wrote about discovering a true gem in latest version of Mark Mandel's JavaLoader: the CFCDynamicProxy. This tiny, but powerful, class acts as a wrapper around a ColdFusion cfc. Essentially allowing it to mimic a native java object and communicate directly with other java objects in ways it never could before! We all know just about everything in ColdFusion boils down to a java object internally. But until now there were certain things you just could not do from a cfc without taking the r-e-a-l-l-y long way around. Well, the CFCDynamicProxy changes all that.


A perfect example is page events in iText. In order to add custom headers, footers, etcetera with iText you need a custom java class that implements the PdfPageEvent interface. Well as easy and natural as that is in a java environment, it is a bit awkward to have to create and load a new java class in CF every time you wish to create a different set of headers or footers.

Eventually, I came up with a more dynamic method of calling a cffunction to add headers/footers from a java, but it still required that custom java class. But using the CFCDynamicProxy, I was able to simplify and rewrite the code entirely in CF, and let me say... man what a difference! It is now totally dynamic and written in native CF, no extra java classes needed. In comparison to the elegance of using the new Proxy, my previous attempt looks like a decrepit, wart-nosed-hag. To be fair, a lot of what the previous entry was doing manually is still going on in the background. But
the CFCDynamicProxy makes it much, much simpler.

The first step in my code beautification effort was creating a CFC that implemented all of the methods in the iText PdfPageEvent interface. Just as you would if you were implementing a CF interface. Using the PdfPageEvent API, I created a cfc with a function for each of the methods in that interface, taking care to properly align the arguments and data types so they matched their java counterparts.

Mapping the java to ColdFusion data types is pretty simple. Any arguments that are instances of a java class (like PdfWriter, Document, Rectangle) are all mapped to type="any". The rest are usually primitive types like "string" and "float" and the conversions are intuitive, for the most part.






Next I filled in a few functions needed for generating basic page footers. The key function was onPageEnd, which is where all the action takes place. OnPageEnd will be called by iText just before new pages are written to my new pdf document. So inside that function, I grab the writer object and use it to set the desired properties and finally write the footer text onto the pdf.


<cffunction name="onEndPage" access="public" returntype="void" output="true"
hint="Called when a page is finished, just before being written to the document.">
<cfargument name="writer" type="any" required="true" hint="Writer for the target pdf. Instance of com.lowagie.text.pdf.PdfWriter" />
<cfargument name="document" type="any" required="true" hint="Document for target pdf. Instance of com.lowagie.text.Document" />
<cfset var Local = {} />

<cfscript>
if (len(variables.instance.footerText))
{
Local.cb = arguments.writer.getDirectContent();
Local.cb.saveState();

Local.cb.beginText();
Local.cb.setColorFill( variables.instance.textColor );
Local.cb.setFontAndSize( variables.instance.font, javacast("float", variables.instance.fontSize) );
Local.cb.setTextMatrix( arguments.document.left(), arguments.document.bottom() - 10);
Local.text = variables.instance.footerText &" page ["& arguments.writer.getPageNumber() &"]";
Local.cb.showText( Local.text );
Local.cb.endText();

Local.cb.restoreState();
}
</cfscript>

</cffunction>


Having completed my faux-java-class/cfc, the next step was using the CFCDynamicProxy to see if this stuff actually worked. Using the Dynamic proxy is like using any other jar with the JavaLoader. You just add the new cfcdynamicproxy.jar to the array of paths. Then create an instance of the JavaLoader, except this time you set the loadColdFusionClassPath parameter equal to true. This allows the JavaLoader to access ColdFusion's classes to create the proxy object.

Note: The code example included with JavaLoader 1.0 was very easy to follow. So my base code is ripped straight out of Mark's example.



<cfscript>
//add the javaloader dynamic proxy library (and the iText jar) to the javaloader
libpaths = [];
arrayAppend(libpaths, expandPath("/javaLoader/support/cfcdynamicproxy/lib/cfcdynamicproxy.jar"));
arrayAppend(libpaths, expandPath("/dev/itext/iText-2.1.7.jar") );

//we HAVE to load the ColdFusion class path to use the dynamic proxy, as it uses ColdFusion's classes
loader = createObject("component", "javaLoader.JavaLoader").init(loadPaths=libpaths, loadColdFusionClassPath=true);
</cfscript>

Once you have an instance of the JavaLoader, instantiate your cfc as usual. (In my case, I am instantiating my PdfPageEventHander.cfc) The last step is to wrap the cfc in a proxy, which was incredibly simple. First you create an array of all of the interfaces, your cfc implements. Then grab a reference to the new Dynamic Proxy class and use its createInstance method to create your proxy object. That is all there is to it.


<cfscript>
//....
//intialize the page event handler component
eventHandler = createObject("component", "PdfPageEventHandler").init( font=textFont, fontSize=10, textColor=textColor);
//add a custom footer
eventHandler.setFooterText( "www.clueless.corp * 85 anywhere blvd * lost city" );

//we can pass in an array of strings which name all the interfaces we want out dynamic proxy to implement
interfaces = ["com.lowagie.text.pdf.PdfPageEvent"];

//get a reference to the dynamic proxy class
CFCDynamicProxy = loader.create("com.compoundtheory.coldfusion.cfc.CFCDynamicProxy");

// create a proxy that we will pass to the iText writer
eventHandlerProxy = CFCDynamicProxy.createInstance(eventHandler, interfaces);
</cfscript>

Now for the real test. I passed my proxy object into my iText writer object, just as I would if it were a native java class:
// ...
<cfscript>
fullPathToOutputFile = ExpandPath("./ABetterPdfPageEventHandler.pdf");
document = loader.create("com.lowagie.text.Document").init();
outStream = createObject("java", "java.io.FileOutputStream").init(fullPathToOutputFile);
writer = loader.create("com.lowagie.text.pdf.PdfWriter").getInstance(document, outStream);

// ** register the PROXY as the page event handler **
writer.setPageEvent( eventHandlerProxy );

document.open();
Paragraph = loader.create("com.lowagie.text.Paragraph");
document.add( Paragraph.init("Paragraph Text....") );
document.newPage();
document.add( Paragraph.init("Paragraph Text....") );
document.newPage();
document.close();
outStream.close();

WriteOutput("Done!");
</cfscript>


.. and iText was none the wiser! Voila, instant footers.



A few simple changes to my cfc and I had instant headers instead. It feels like writing java code ... except in CF ;)



You know when I woke up this morning, I thought all of this might have been just a dream. But CFCDynamicProxy is real .. and this class seriously rocks!


4 comments:

Murray,  November 8, 2009 at 3:49 AM  

Thanks for that! I had put my iTextCFC project on hold for a while - partly due to lack of time and partly due to the overheads of implementing the previous way to do dynamic headers and footers.

So, thanks again! I feel inspired once more.

Murray

cfSearching November 8, 2009 at 1:32 PM  

@Murray,

You are welcome. Yes, I am REALLY excited about the Dynamic Proxy because it made this so incredibly easy, and very natural to write! Not to mention you can use it with any interface. Mark's new proxy class is truly cool stuff ;)

Though I did not put it in the sample, I was thinking of structuring the handler cfc's like the iText PdfPageEventHelper class. Basically have a base cfc with all of the empty event functions. Then create custom handlers as needed that extend the base cfc. Then the custom handlers only need to override the events they use.

So many possibilities, so little time!

-Leigh

Webpointz December 17, 2009 at 11:31 AM  

Itext just released v5.0.0 and one of the significant changes to it is that the Package Name has been changed from "com.lowagie" to "com.itextpdf" allowing you to co-exist in CFMX. This is tested using Fully Patched CFMX running on JRockit JVM 1.5 (The new iText is compiled using Java 5, so you will need at least the 1.5 JVM)

Simply download the new iText.jar file and rename to "iTextpdf.jar" and locate it in the proper LIB folder for CFMX.

When you do your createObject calls, simply use the new package name path.

JavaLoader is a great utility, but if you don't load it into server scope (the initial loader with paths to the iText jar) AND ensure it is only loaded ONCE, you can run into severe memory leak issues.

Using the latest refactoring of iText means you don't need to worry about using the Utility.

Thanks for all your work on iText...very helpful!

cfSearching December 17, 2009 at 12:44 PM  

@Webpointz,

That is great news! I know they also changed the license. Do you have any idea what effect that has on projects that use the new version?

-Leigh

  © Blogger templates The Professional Template by Ourblogtemplates.com 2008

Header image adapted from atomicjeep