Monday, September 22, 2008

Experiment with Calling CFFunctions from PDFPageEvents - (The Code)

Here is the complete code from my previous entry

Detailed Instructions

Using the JavaLoader.cfc and compiling the Java class with Eclipse
(The instructions for compiling the jar should be the same. Only the java code is different)

Invoke CFFunction from PdfPageEvent Example (MX7+)

Path: {wwwroot}\dev\iText\myTestPage.cfm


<h1>Invoke CFFunction from PdfPageEvent Example (MX7+)</h1>
<cfscript>
savedErrorMessage = "";

fullPathToOutputFile = ExpandPath("./CFToPDFPageEventResult.pdf");
// get instance of javaLoader stored in the server scope
javaLoader = server[application.MyUniqueKeyForJavaLoader];
document = javaLoader.create("com.lowagie.text.Document").init();

try {
outStream = createObject("java", "java.io.FileOutputStream").init(fullPathToOutputFile);
writer = javaLoader.create("com.lowagie.text.pdf.PdfWriter").getInstance(document, outStream);

// get an instance of the CFC containing my cf event functions
eventFuncs = createObject("component", "MyPageEventFunctions").init( javaLoader=javaLoader );

// get an instance of the page event component
eventHandler = createObject("component", "PDFPageEventHandler").init( javaLoader=javaLoader );

// create an instance of the java utility class
eventUtil = eventHandler.createEventUtility();

// link the "initDocument" function to the "onOpenDocument" event
eventHandler.link( eventUtility = eventUtil,
eventName = eventUtil.ON_OPEN_DOCUMENT,
functionContext = eventFuncs.getContext(),
functionObject = eventFuncs.initDocument
);


// link the "addFooter" function to the "onEndPage" event
functionArgs.footerText = "BOREDOM ALERT! BOREDOM ALERT! Page number ";
eventHandler.link( eventUtility = eventUtil,
eventName = eventUtil.ON_END_PAGE,
functionContext = eventFuncs.getContext(),
functionObject = eventFuncs.addFooter,
functionArguments = functionArgs
);


writer.setPageEvent( eventUtil );

// step 4: open the document and add a few sample pages
phrase = javaLoader.create("com.lowagie.text.Phrase");
totalPages = 10;
document.open();
for (i = 1; i LTE totalPages; i = i + 1) {
document.add( phrase.init("The best way to be boring is to leave nothing out. ") );
document.add( phrase.init("The best way to be boring is to leave nothing out. ") );
document.add( phrase.init("The best way to be boring is to leave nothing out. ") );
if (i LT totalPages) {
document.newPage();
}
}
}
catch (Exception e) {
savedErrorMessage = e;
}

// close document and output stream objects
if ( structKeyExists(variables, "document") ) {
document.close();
}
if ( structKeyExists(variables, "outStream") ) {
outStream.close();
}

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

<!--- show any errors --->
<cfif len(savedErrorMessage) gt 0>
Error. Unable to create file
<cfdump var="#savedErrorMessage#">
</cfif>


PDFPageEventHandler.cfc

Path: {wwwroot}\dev\iText\PDFPageEventHandler.cfc
Change the default value of as needed.

<!---
PDFPageEventHandler.cfc

@author http://cfsearching.blogspot.com/
@version 1.0, September 22, 2008
--->
<cfcomponent output="false">
<cfset variables.instance = structNew()>

<cffunction name="init" returntype="PDFPageEventHandler" access="public" output="false">
<cfargument name="javaLoader" type="any" required="true" hint="Instance of the javaLoader.cfc">
<cfargument name="utilityClass" type="string" default="itextutil.CFPDFPageEvent" hint="Dot notation path for the java utility class">

<cfset variables.instance.javaLoader = arguments.javaLoader>
<cfset variables.instance.utilityClass = arguments.utilityClass>

<cfreturn this>
</cffunction>

<cffunction name="getJavaLoader" returntype="any" access="private" output="false">
<cfreturn variables.instance.javaLoader>
</cffunction>

<cffunction name="getUtilityClass" returntype="any" access="private" output="false">
<cfreturn variables.instance.utilityClass>
</cffunction>

<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>

</cfcomponent>


MyPageEventFunctions.cfc

Path: {wwwroot}\dev\iText\MyPageEventFunctions.cfc



<cfcomponent output="false">
<cfset variables.instance = structNew()>

<cffunction name="init" returntype="MyPageEventFunctions" access="public" output="false">
<cfargument name="javaLoader" type="any" required="true">

<cfset variables.instance.javaLoader = arguments.javaLoader>
<cfreturn this>
</cffunction>

<!--- this is required for event handler --->
<cffunction name="getContext" returntype="any" access="public" output="false">
<cfreturn getPageContext()>
</cffunction>

<cffunction name="getJavaLoader" returntype="any" access="private" output="false">
<cfreturn variables.instance.javaLoader>
</cffunction>

<cffunction name="initDocument" returntype="void" access="public" output="false">
<cfargument name="CF_PDF_EVENT" type="struct">
<cfset var Local = structNew()>
<cfscript>
// create a font object to use for the page footer text
Local.BaseFont = getJavaLoader().create("com.lowagie.text.pdf.BaseFont");
Local.textFont = Local.BaseFont.createFont( Local.BaseFont.HELVETICA,
Local.BaseFont.WINANSI,
Local.BaseFont.EMBEDDED
);
// store the font in the event handler object
arguments.CF_PDF_EVENT.EVENT_PARENT.setProp("textFont", Local.textFont);
</cfscript>
</cffunction>

<cffunction name="addFooter" returntype="void" access="public" output="false">
<cfargument name="footerText" type="string">
<cfargument name="CF_PDF_EVENT" type="struct">
<cfset var Local = structNew()>

<cfscript>
Local.writer = arguments.CF_PDF_EVENT.EVENT_WRITER;
Local.document = arguments.CF_PDF_EVENT.EVENT_DOCUMENT;

Local.textSize = 12;
Local.Color = createObject("java", "java.awt.Color");
Local.textColor = Local.color.decode("##cc0000");
// TEST: change the text color to blue
//Local.textColor = Local.color.decode("##0000ff");

// retrieve the textFont from the event handler object
Local.textFont = arguments.CF_PDF_EVENT.EVENT_PARENT.getProp("textFont");

Local.cb = Local.writer.getDirectContent();
Local.cb.saveState();

Local.cb.beginText();
Local.cb.setColorFill(Local.textColor);
Local.cb.setFontAndSize( Local.textFont, Local.textSize);
Local.cb.setTextMatrix( Local.document.left(), Local.document.bottom() - 10);
Local.text = arguments.footerText & Local.writer.getPageNumber();
// TEST: change the footer text
//Local.text = "www.cluelesscorp.com - What page is this? ["& Local.writer.getPageNumber() &"]";
Local.cb.showText( Local.text );
Local.cb.endText();
Local.cb.restoreState();
</cfscript>
</cffunction>

</cfcomponent>



Java Utility Class Code

/**
* Generic helper class used to invoke a ColdFusion function
* from java when a PDFPageEvent occurs
*
* @author http://cfsearching.blogspot.com
* @version 1.0
*/
package itextutil;

import java.lang.reflect.Method;
import java.util.Hashtable;
import java.util.Map;
import com.lowagie.text.Document;
import com.lowagie.text.ExceptionConverter;
import com.lowagie.text.Paragraph;
import com.lowagie.text.Rectangle;
import com.lowagie.text.pdf.PdfPageEventHelper;
import com.lowagie.text.pdf.PdfWriter;

public class CFPDFPageEvent extends PdfPageEventHelper {
public static final double version = 1.0;

// Keys representing the different pdf page events
public static final String ON_END_PAGE = "onEndPage";
public static final String ON_START_PAGE = "onStartPage";
public static final String ON_CLOSE_DOCUMENT = "onCloseDocument";
public static final String ON_OPEN_DOCUMENT = "onOpenDocument";
public static final String ON_CHAPTER = "onChapter";
public static final String ON_CHAPTER_END = "onChapterEnd";
public static final String ON_GENERIC_TAG = "onGenericTag";
public static final String ON_PARAGRAPH = "onParagraph";
public static final String ON_PARAGRAPH_END = "onParagraphEnd";
public static final String ON_SECTION = "onSection";
public static final String ON_SECTION_END = "onSectionEnd";


public static final String CF_FUNCTION_CLASS = "CF_FUNCTION_CLASS";
public static final String CF_METHOD_NAME = "CF_METHOD_NAME";
public static final String CF_METHOD_PARAMTER_TYPES = "CF_METHOD_NAME";
public static final String CF_METHOD_ARGUMENTS = "CF_METHOD_ARGUMENTS";

// Key for event data passed to the CF functions
public static final String CF_PDF_EVENT = "CF_PDF_EVENT";

// Key for event values passed to the CF functions
public static final String EVENT_PARENT = "EVENT_PARENT";
public static final String EVENT_WRITER = "EVENT_WRITER";
public static final String EVENT_DOCUMENT = "EVENT_DOCUMENT";
public static final String EVENT_POSITION = "EVENT_POSITION";
public static final String EVENT_TITLE = "EVENT_TITLE";
public static final String EVENT_DEPTH = "EVENT_DEPTH";
public static final String EVENT_TEXT = "EVENT_TEXT";
public static final String EVENT_RECTANGLE = "EVENT_RECTANGLE";


private Map props;
private Map eventMap;

public CFPDFPageEvent(){
super();
this.props = new Hashtable();
this.eventMap = new Hashtable();
}


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);
}
}

public void onCloseDocument(PdfWriter writer, Document document) {
EventFunction cfData = getEventFunction(ON_CLOSE_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);
}
}

public void onStartPage(PdfWriter writer, Document document) {
EventFunction cfData = getEventFunction(ON_START_PAGE);
// 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);
}
}

public void onEndPage(PdfWriter writer, Document document) {
EventFunction cfData = getEventFunction(ON_END_PAGE);
// 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);
}
}

public void onChapter(PdfWriter writer, Document document, float position, Paragraph title) {
EventFunction cfData = getEventFunction(ON_CHAPTER);
// 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);
event.put(EVENT_POSITION, String.valueOf(position));
event.put(EVENT_TITLE, title);
cfData.addFunctionArg(CF_PDF_EVENT, event);

invokeCFFunction(cfData);
}
}

public void onChapterEnd(PdfWriter writer, Document document, float position) {
EventFunction cfData = getEventFunction(ON_CHAPTER_END);
// 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);
event.put(EVENT_POSITION, String.valueOf(position));
cfData.addFunctionArg(CF_PDF_EVENT, event);

invokeCFFunction(cfData);
}
}

public void onGenericTag(PdfWriter writer, Document document, Rectangle rect, String text) {
EventFunction cfData = getEventFunction(ON_GENERIC_TAG);
// 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);
event.put(EVENT_RECTANGLE, rect);
event.put(EVENT_TEXT, text);
cfData.addFunctionArg(CF_PDF_EVENT, event);

invokeCFFunction(cfData);
}
}

public void onParagraph(PdfWriter writer, Document document, float position) {
EventFunction cfData = getEventFunction(ON_PARAGRAPH);
// 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);
event.put(EVENT_POSITION, String.valueOf(position));
cfData.addFunctionArg(CF_PDF_EVENT, event);

invokeCFFunction(cfData);
}
}
public void onParagraphEnd(PdfWriter writer, Document document, float position) {
EventFunction cfData = getEventFunction(ON_PARAGRAPH_END);
// 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);
event.put(EVENT_POSITION, String.valueOf(position));
cfData.addFunctionArg(CF_PDF_EVENT, event);

invokeCFFunction(cfData);
}
}

public void onSection(PdfWriter writer, Document document, float position, int depth, Paragraph title) {
EventFunction cfData = getEventFunction(ON_SECTION);
// 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);
event.put(EVENT_POSITION, String.valueOf(position));
event.put(EVENT_DEPTH, String.valueOf(depth));
event.put(EVENT_TITLE, title);
cfData.addFunctionArg(CF_PDF_EVENT, event);

invokeCFFunction(cfData);
}
}

public void onSectionEnd(PdfWriter writer, Document document, float position) {
EventFunction cfData = getEventFunction(ON_SECTION_END);
// 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);
event.put(EVENT_POSITION, String.valueOf(position));
cfData.addFunctionArg(CF_PDF_EVENT, event);

invokeCFFunction(cfData);
}
}

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) );
}
}

protected Map createEvent() {
Map event = new Hashtable();
event.put(EVENT_PARENT, this);
return event;
}


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);
}

public Object getProp(String key) {
return (this.props.containsKey(key) ? this.props.get(key) : "");
}

public void setProp(String key, Object value) {
this.props.put(key, value);
}

protected Map getEventMap() {
return this.eventMap;
}
protected EventFunction getEventFunction(String eventName) {
return (EventFunction)getEventMap().get(eventName);
}

public static void main(String[] args) {
}

/**
* Custom exception designed to make it easier to detect and catch errors from CF
*/
class CFPDFFunctionException extends Exception {
public CFPDFFunctionException(Exception e) {
super(e);
}
}

/**
* This class represents the information required to call a CF Function
*/
class EventFunction {
private Object funcInstance;
private Map funcArgs;
private String methodName;
private Class[] paramTypes;
private Object[] methodArgs;

public EventFunction( Object funcInstance, Map funcArgs, String methodName,
Class[] paramTypes, Object[] methodArgs) {

this.funcInstance = funcInstance;
this.funcArgs = funcArgs;
this.methodName = methodName;
this.paramTypes = paramTypes;
this.methodArgs = methodArgs;
}

public Object getFunction() {
return this.funcInstance;
}
public Map getFunctionArgs() {
return this.funcArgs;
}
public void addFunctionArg(Object key, Object value) {
this.funcArgs.put(key, value);
}
public String getMethodName() {
return this.methodName;
}
public Class[] getParamTypes() {
return this.paramTypes;
}
public Object[] getMethodArgs() {
return this.methodArgs;
}
}
}

6 comments:

Murray,  September 22, 2008 at 4:39 PM  

Wow, that was a lot more involved than I imagined! Thanks for doing that. I am not across all the detail yet and I may be back with questions!

I am working on an iTextCFC which is a wrapper for iText that allows the user to have CCS-type styles and "stylesheets" that are applied to the page elements - thus making it a bit easier to construct the pdf.

eg to give an idea:
// Create some styles
objPDF.addNamedStyle("p1","font-family:TIMES_ROMAN; font-size:12; color:ff0000; margin-bottom:10");
objPDF.addNamedStyle("h1","font-family:helvetica; font-size:14; font-style:bold; text-align:center; margin-bottom:5;");

// Create a new document (returns an iText document object)
myDoc1 = objPDF.newDocument(ExpandPath("test4.pdf"),"A4","P","","margin:20,30,20,30");

// Use the named style and also add/override the style (last parameter)
objPDF.addParagraph("Heading","h1","margin-bottom:2");
objPDF.addParagraph("my version is:"&objPDF.iTextVersion(),"p1","color:blue");


I will build your pagehelper into this. Once I have a more robust version I will post iTextCFC for evaluation and suggestions.

Thanks again,
Murray

cfSearching September 22, 2008 at 6:04 PM  

@Murray,

You are welcome, but I was rather intrigued with the idea myself ;-) I think it can be improved, but at least it is something to build upon. If anyone has any suggestions for improvements, I am all ears.

Hard-coding might simplify things a bit, but I preferred not to go that route. If the underlying classes change, this way you at least have a _hope_ of fixing it without changing the jar.

I also read entry on Ben Nadel's blog that might offer another option involving evaluate(). On the one hand it _might_ be simpler. But on the other it might also be a debugging nightmare ;)

The stylesheets idea sounds very interesting. I definitely look forward to seeing the first version.

Cheers,
Leigh

Murray,  October 18, 2008 at 5:46 PM  

Hi Leigh,

Firstly, sorry for the silence since you posted this - I have been dragged off onto other development but I am back onto this now.

I have implemented your solution and am using it with my iTextCFC. All good, and thanks again!

Now, I have hit a weird problem and I cant see what is going on.

For my use I want to write a table for the header and use writeSelectedRows to write it out. That is what I was doing in my java version of the onEndPage method and it all worked correctly there.

However, when I try to do the same in my CF onEndPage method (which is a replacement for your addFooter method that has been linked in and works for a plain text header like your footer example), CF throws the following error:

java.lang.reflect.InvocationTargetException

and the stack shows:
Caused by: coldfusion.runtime.java.MethodSelectionException: The selected method writeSelectedRows was not found.

I have cfdumped the table just before the call to writeSelectedRows and it looks normal (ie is an object of com.lowagie.text.pdf.PdfPTable) and I can see the 4 instances of the writeSelectedRows method.

Here is part of the function (your blog wont accept the cf tags):

---------------------------
Local.writer = arguments.CF_PDF_EVENT.EVENT_WRITER;
Local.document = arguments.CF_PDF_EVENT.EVENT_DOCUMENT;
Local.cb = Local.writer.getDirectContent();

// Local.cb works fine when used for text eg Local.cb.beginText(); etc so it seems to be ok

Local.table = getJavaLoader().create("com.lowagie.text.pdf.PdfPTable");
Local.table.init(1);
Local.table.addCell("test header");

Local.cb.saveState();
Local.table.writeSelectedRows(0, -1, 50, 800, Local.cb);
Local.cb.restoreState();

--------------------------

So, I can see the method in the cfdump but CF insists that it cant be found.

Do you have any ideas?

Thanks,
Murray

cfSearching October 18, 2008 at 8:06 PM  

@Murray,

Yes, sometimes CF gets confused with overloaded methods. Say you have one method that accepts "int" and the other "float". Since a value like 0 can be either an int OR float, CF does not know which method it should call.

Try using javacast to reduce the ambiguity. See if that helps.

Local.table.writeSelectedRows(
javacast("int", 0),
javacast("int", -1),
....,
Local.cb);

Murray,  October 19, 2008 at 2:07 PM  

@Leigh,

You are a *(^%^&#$ genius!!

That was it!

Thanks again,
Murray

  © Blogger templates The Professional Template by Ourblogtemplates.com 2008

Header image adapted from atomicjeep