Thursday, September 25, 2008

OT: NOT Meme(me)

Rey Bango posted a recent entry called "Meme(me)" that gave me a chuckle. Now I know what you are thinking .. but the funny part was not the pictures. It was some of the comments:

".. careful w/ those bag monsters. They're dangerous!"
- Rey Bango

"There are enough horrible images available on the web- I don't think it needs pictures of me making things worse. :)"
- Mark Osbun

"Oh my-- cheesy E-mail fowards just met the blogosphere. :)"
- Brad Wood

(I am with Mark on this one ;)

...Read More

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

...Read More

Experiment with Calling CFFunctions from PDFPageEvents - Part 2

In Part 1 I covered the basics of my java and cfc utilities. In Part 2 I will show an example of them in action. I thought it would be interesting to contrast the differences between the old and new technique. So I used a modified version of the example from Using iText's PdfPageEventHelper with ColdFusion to test the code. (If you have not read the previous entry already, it is worth a quick read. If only so the descriptions below will make more sense ;).

Anyway, my first step was converting the java methods into a CFC with two functions: one that will be called onOpenDocument and the other onEndPage. Both functions have an argument called CF_PDF_EVENT. It is a structure of information that is passed in automatically from the java utility. It contains details about the PDFPageEvent that occurred (writer, document, paragraph position, etcetera).


<cffunction name="initDocument" returntype="void" access="public" output="false">
<cfargument name="CF_PDF_EVENT" type="struct">
...
</cffunction>

Since different page events generate different information, the structure contents vary depending on which event occurred. However, the structure always contains a key named EVENT_PARENT. The value is just a reference to the java utility object. It comes in handy when you need to make properties available to other event functions. Just use the setProp() method to add a property, and getProp() to retrieve it.

For example, the initDocument function below adds a property called "textFont". This property is later used by the addFooter function when generating the footer text.

    <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.COURIER_BOLD,
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>
...
// retrieve the textFont from the event handler object
Local.textFont = arguments.CF_PDF_EVENT.EVENT_PARENT.getProp("textFont");
...
</cfscript>
</cffunction>

All that remains ...


The final step was to modify the original CF example which generated a pdf with page footers. After instantiating the writer, I create a few instances of my components. Both components use the JavaLoader.cfc, so I am passing in an instance as a parameter.


<cfscript>
...

// 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 );
<cfscript>


Next I create an instance of the java utility and link my two functions to the onOpenDocument and onEndPage events. Finally, I register the utility object with the PDFWriter. The rest of the code is unchanged.


<cfscript>
...
// 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
);

// finally, register the page event with the pdfWriter
writer.setPageEvent( eventUtil );

...
<cfscript>

It is worth noting that most of the parameters are objects, not strings. So for instance I am passing in a function object, not a name. I think the parameters are pretty self-explanatory, with the possible exception of "functionContext". In short it accepts the results of the getPageContext() function. When the function is called from java, we need to to provide CF with the context for executing the function. So the link() function extracts the required information from getPageContext() and passes it to the java class. Since I could not find a direct way to access the context from outside a cfc, I added a small helper method to my cfc that returns the context object.

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


Are we there yet??

The final test was to see if all this dynamic stuff actually worked. So I ran the updated code and it produced the same silly pink footers as in the original example.



Now with just a slight change to my CF code, I was able to generate an entirely different footer. I could have also hooked into other events on-the-fly. No recompile of the java class required. Dynamic page events. Pretty cool stuff.




Wrap up ....

There are several ways I could have designed the PDFPageEventHandler.cfc. But I did not want to place a lot of restrictions on how it could be used. So the cfc could be streamlined or tweaked to suit your own needs. Now keep in mind the code is barely tested, so feel free to play around with it. If you have any comments or suggestions on how to improve it I would love to hear them.

A big thank you to Murray for coming up with such an interesting idea!

...Read More

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.

...Read More

Tuesday, September 16, 2008

Debugging Tip - Dump all scopes

I came across a nice debugging tip on Dan Vega's blog. He shows how you can display the contents of most all scopes using getPageContext().getBuiltInScopes()

Obviously the usual cautions apply. 1) It is an undocumented function, so use it at your own risk. 2) It really does dump all scopes. So it is not suitable for production environments where you might accidentally be displaying sensitive information.

...Read More

Sunday, September 14, 2008

Experiment in Tweaking ColdFire (for MS SQL)

Most of us love cfqueryparam .. except when it comes to debugging. Unfortunately, when you use cfqueryparam, the basic CF debugging template displays parameter values separate from the actual sql statements. Understandable, given how queries are handled behind the scenes. But as a developer you typically want to see human readable queries, with the parameter values.

This limitation sent me searching for a new debugging tool last week. The search led me to the fantastic Firefox plugin ColdFire (by Raymond Camden, Adam Podolnick and Nathan Mische). It has been around for a while, but I only started using it a few days ago. So far I think it is an amazing tool!

Now since my main interest was database queries, I installed it and jumped right to the DB Queries tab. At first glance the results looked perfect: sql you can copy and paste into an query window. After running a few more queries I noticed a few small things I wanted to tweak. (Yes I know ... give them the moon and they still want more).

  1. CfSQLTypes appear to be case sensitive. So upper case types like "CF_SQL_VARCHAR" are not recognized
  2. NULL values are inserted as an empty string instead of the keyword "NULL"
  3. Binary data is displayed as a comma delimited list



Since I was really curious about the plugin, I took a shot at customizing it.

Item number 1 was easily changed. I looked inside the debug template (WEB-INF\debug\coldfire.cfm) and found a few lines that perform a case sensitive search on cfsqltypes. A simple change from ListFind to ListFindNoCase was all that was needed.


<cfset parameters[x][1] = ListFind(cfsqltypes,attr.sqltype)>

Item 2 was not as easily solved. In fact, I do not really think it can be solved at all. ColdFire retrieves information from the ColdFusion debugger. The problem is the ColdFusion debugger treats NULL parameter values as an [empty string]. That is fine for non-character cfsqltypes like dates, numbers, etcetera. An empty string is obviously not a valid date or number. So you can determine when the parameter is NULL. But with character types, like cf_sql_varchar, it is impossible to determine when the parameter value is really an empty string and when it is really a NULL. So my partial solution was to always treat empty strings as NULL. This was achieved by making a small modification to one of the plugin's javascript files

File: coldfire_v1.2.51.70.xpi\chrome\coldfire.jar\content\lib.js

formatParamValue: function( param ) {
var type = param[0];
var value = param[1];
var tmpVal = value;
if (value.length == 0) {
// treat empty strings as NULL
tmpVal = "NULL";
} else if (type == 2){
// ... etcetera
},

I used the MS SQL Profiler to help with item 3 (binary values). The profiler showed that binary parameters were passed to MS SQL as hex. So I made a few small tweaks to the debug template (WEB-INF\debug\coldfire.cfm) to encode any binary values as hex. I also added a few of the other binary types to the list of cfsqltypes.

...
<cfif isBinaryType AND isBinary(attr.value)>
<cfset parameters[x][2] = "0x"& binaryEncode(attr.value, "hex")>
<cfelse>
<cfset parameters[x][2] = attr.value>
</cfif>


The new results were a query I could copy and paste into an MS SQL query window.



Now I doubt these tweaks would be suitable for everyone, but they produced the results I needed. It was also a lot of fun learning about the plugin and trying to understand how it works. While I liked the tool before, delving into the code really increased my appreciation for all of the great work put into it by the creators: Adam Podolnick, Nathan Mische and Raymond Camden.

...Read More

Tuesday, September 9, 2008

OT: If only there had been some clue

Hardly a week goes by without seeing a question that starts with "My query is not returning the right results..." and ends with ".. Oh yeah, and the values in the ColumnXYZ are a list like 1,5,6,18,97. So can anyone tell me why my query does not work?".

{Sigh} The question inevitably elicits a slew of responses about proper database modeling and normalization. Followed by a predictable set of responses from the OP. In confusion they ask "what do you mean by junction table?" Then in disbelief: "how can creating more tables and more records be better?".

You know ... it is almost as if they think you are trying to trick them. As if the fact that they already spent the better part of two hours, working on a single a query, was not clue enough that maybe .. just maybe.. they were headed down the wrong path.

Usually after a few well worded explanations, most are ready to be "saved" and embark on the higher path to database enlightenment. I am a convert myself. But occasionally you run across a true die-hard disbeliever. They scoff at the mere idea of database modeling. Standing unrepentant (and unfortunately .. very vocal) in their ignorance. I suspect they have business cards and t-shirts emblazoned with their name and title:


      John Smith, CEO
      Ignorance Incorporated
      - "Fighting knowledge since 1975"

We all go through periods of ignorance. I am certainly no exception. But it should be a transitory state. Not fervently embraced as a way of life. But for those resolute souls out there, proudly donning their t-shirts and handing out business cards, I can only respond with this bit of absurdity:

Why does your query not work? Because you have angered the database gods. They are powerful, capricious and vengeful. Unfortunately you are now the object of their wrath. They have forever cursed you in punishment for your poor and unworthy database offering. You will spend eternity in database hell, designing increasingly complex queries only a contortionist could admire. Queries that will execute at an agonizingly slow pace. Some will run eternally, never returning results. While others will occasionally drop records or include seemingly unrelated records for no reason you can detect.

Abandon hope. You are doomed. When you reach the gates of database hell, think twice before offering explanations like "my boss prefers it this way" or "changing it was too complicated". The gatekeepers have already heard those cries from other unfortunate residents. Repeating such excuses will only enrage them further.

As you are sitting in your cell, writing queries of infinite complexity, think back to the two hours you spent struggling with that first query. That was the harbinger of this moment.

...Read More

Monday, September 8, 2008

Getting started with iText - Tables

A reader named Turnbull was asking about PDFTables and I realized I did not have any examples on tables posted. So here are the first sections from from the iText tutorial on tables.

Lather, rinse, repeat

The java examples are nice and simple. They demonstrate how easy it is to create a basic table. Simply define a table object, add cells to it. Then add the table to your document. You can apply formats and advanced settings to the cells. But the basic construct consists of those three steps.


Documentation: iText Tutorial - Tables
Source Code: MyFirstTable.java , TableWidthAlignment.java , TableSpacing.java
Requirements:
Some of the examples use Mark Mandel's JavaLoader.cfc to load a newer version of iText


My First Table

Use a PdfPTable to add a table to a PDF document


<h1>My First PdfPTable (MX7 compatible)</h1>
Use a PdfPTable to add a table to a PDF document
<cfscript>
savedErrorMessage = "";


pathToOutputFile = ExpandPath("MyFirstTable.pdf");

try {
// step 1: creation of a document-object
document = createObject("java", "com.lowagie.text.Document").init();

// step 2:
// we create a writer that listens to the document
// and directs a PDF-stream to a file
PdfWriter = createObject("java", "com.lowagie.text.pdf.PdfWriter");
FileOutputStream = createObject("java", "java.io.FileOutputStream");
outStream = FileOutputStream.init( pathToOutputFile );
writer = PdfWriter.getInstance(document, outStream);

// step 3: we open the document
document.open();

// create reusable object
Paragraph = createObject("java", "com.lowagie.text.Paragraph");
PdfPCell = createObject("java", "com.lowagie.text.pdf.PdfPCell");
Color = createObject("java", "java.awt.Color");

// create a table with three columns
PdfPTable = createObject("java", "com.lowagie.text.pdf.PdfPTable");
table = PdfPTable.init( javacast("int", 3) );

// create a header cell
cell = PdfPCell.init( Paragraph.init("header with colspan 3") );
// force header cell to span all three columns
cell.setColspan( javacast("int", 3) );
// add the header to the table
table.addCell( cell );

// add some cells with plain data
table.addCell("1.1");
table.addCell("2.1");
table.addCell("3.1");
table.addCell("1.2");
table.addCell("2.2");
table.addCell("3.2");

// add some formatted cells
cell = PdfPCell.init( Paragraph.init("cell test1") );
cellColor = Color.init( javacast("int", 255), javacast("int", 0), javacast("int", 0) );
cell.setBorderColor( cellColor );
table.addCell( cell );
cell = PdfPCell.init( Paragraph.init("cell test2") );
cell.setColspan( javacast("int", 2) );

// converted java values to decimal RGB values using:
// value = inputBaseN("0xC0", 16);
cellColor = Color.init( javacast("int", 192), javacast("int", 192), javacast("int", 192) );
cell.setBackgroundColor( cellColor );
table.addCell( cell );

// Finally, add the table to the document
document.add(table);

}
catch (com.lowagie.text.DocumentException de) {
savedErrorMessage = de;
}
catch (java.io.IOException ioe) {
savedErrorMessage = ioe;
}

// step 5: always close document and output stream
document.close();
if ( IsDefined("outStream") ) {
outStream.close();
}

WriteOutput("<hr>Finished!");
</cfscript>


<!--- show any errors --->
<cfif len(savedErrorMessage)>
Error creating document
<cfdump var="#savedErrorMessage#">
</cfif>


Table Width Alignment
Changing the width and the alignment of the complete table


<h1>TableWidthAlignment (MX7 compatible)</h1>
Changing the width and the alignment of the complete table
<cfscript>
savedErrorMessage = "";

pathToOutputFile = ExpandPath("TableWidthAlignment.pdf");

try {
// step 1: creation of a document-object
PageSize = createObject("java", "com.lowagie.text.PageSize");
document = createObject("java", "com.lowagie.text.Document").init(PageSize.A4);

// step 2:
// we create a writer that listens to the document
// and directs a PDF-stream to a file
PdfWriter = createObject("java", "com.lowagie.text.pdf.PdfWriter");
FileOutputStream = createObject("java", "java.io.FileOutputStream");
outStream = FileOutputStream.init( pathToOutputFile );
writer = PdfWriter.getInstance(document, outStream);

// step 3: we open the document
document.open();

// create reusable object
Paragraph = createObject("java", "com.lowagie.text.Paragraph");
PdfPCell = createObject("java", "com.lowagie.text.pdf.PdfPCell");
Color = createObject("java", "java.awt.Color");
PdfTable = createObject("java", "com.lowagie.text.pdf.PdfPTable");

// step4
// create a table with three columns
table = PdfTable.init( javacast("int", 3) );
// create a cell to contain the header
cell = PdfPCell.init( Paragraph.init("header with colspan 3") );
// set the header to span all three columns
cell.setColspan( javacast("int", 3) );
// add the header to the table
table.addCell( cell );

// add some cells with plain data
table.addCell("1.1");
table.addCell("2.1");
table.addCell("3.1");
table.addCell("1.2");
table.addCell("2.2");
table.addCell("3.2");

// add some formatted cells
cell = PdfPCell.init( Paragraph.init("cell test1") );
cellColor = Color.init( javacast("int", 255), javacast("int", 0), javacast("int", 0) );
cell.setBorderColor( cellColor );
table.addCell( cell );
cell = PdfPCell.init( Paragraph.init("cell test2") );
cell.setColspan( javacast("int", 2) );
cellColor = Color.init( javacast("int", 192), javacast("int", 192), javacast("int", 192) );
cell.setBackgroundColor( cellColor );
table.addCell( cell );

// step 5: add multiple copies of the table
// to the document to demonstrate different widths and alignment

// add the table to the document at its original size
document.add( table );

// add another copy of the table at 100% width
table.setWidthPercentage( javacast("float", 100) );
document.add( table );
// add another copy at 50% width, and align it to the right
table.setWidthPercentage( javacast("float", 50) );
table.setHorizontalAlignment( PdfTable.ALIGN_RIGHT);
document.add( table );
// add another copy and align it to the left
// note, the table width is still set at 50%
table.setHorizontalAlignment( PdfTable.ALIGN_LEFT );
document.add( table );
}
catch (java.lang.Exception de) {
savedErrorMessage = de;
}

// step 5: always close document and output stream
document.close();
if ( IsDefined("outStream") ) {
outStream.close();
}

WriteOutput("<hr>Finished!");
</cfscript>


<!--- show any errors --->
<cfif len(savedErrorMessage)>
Error creating document
<cfdump var="#savedErrorMessage#">
</cfif>


Table Spacing
Defining the spacing between the table and other content

<h1>TableSpacing (requires newer version of iText)</h1>
Defining the spacing between the table and other content
<cfscript>
savedErrorMessage = "";

pathToOutputFile = ExpandPath("TableSpacing.pdf");
javaLoader = server[application.MyUniqueKeyForJavaLoader];

try {
// step 1: creation of a document-object
PageSize = javaLoader.create("com.lowagie.text.PageSize");
document = javaLoader.create("com.lowagie.text.Document").init(PageSize.A4);

// step 2:
// we create a writer that listens to the document
// and directs a PDF-stream to a file
PdfWriter = javaLoader.create("com.lowagie.text.pdf.PdfWriter");
FileOutputStream = createObject("java", "java.io.FileOutputStream");
outStream = FileOutputStream.init( pathToOutputFile );
writer = PdfWriter.getInstance(document, outStream);

// step 3: we open the document
document.open();

// create reusable object
Paragraph = javaLoader.create("com.lowagie.text.Paragraph");
PdfPCell = javaLoader.create("com.lowagie.text.pdf.PdfPCell");
Color = createObject("java", "java.awt.Color");
PdfTable = javaLoader.create("com.lowagie.text.pdf.PdfPTable");

// step4
// create a table with three columns
table = PdfTable.init( javacast("int", 3) );
// create a cell to contain the header
cell = PdfPCell.init( Paragraph.init("header with colspan 3") );
// set the header to span all three columns
cell.setColspan( javacast("int", 3) );
// add the header to the table
table.addCell( cell );

// add some cells with plain data
table.addCell("1.1");
table.addCell("2.1");
table.addCell("3.1");
table.addCell("1.2");
table.addCell("2.2");
table.addCell("3.2");

// add some formatted cells
cell = PdfPCell.init( Paragraph.init("cell test1") );
cellColor = Color.init( javacast("int", 255), javacast("int", 0), javacast("int", 0) );
cell.setBorderColor( cellColor );
table.addCell( cell );
cell = PdfPCell.init( Paragraph.init("cell test2") );
cell.setColspan( javacast("int", 2) );
cellColor = Color.init( javacast("int", 192), javacast("int", 192), javacast("int", 192) );
cell.setBackgroundColor( cellColor );
table.addCell( cell );

// step 5: add multiple copies of the table to the document
// to demonstrate different spacing options
table.setWidthPercentage( javacast("float", 50) );
document.add( Paragraph.init("We add 2 tables:") );
document.add( table );
document.add( table );

document.add( Paragraph.init("They are glued to eachother") );
document.add(table );
document.add( Paragraph.init("This is not very nice. Turn to the next page to see how we solved this") );
document.newPage();
document.add( Paragraph.init("We add 2 tables, but with a certain 'SpacingBefore':") );
table.setSpacingBefore( javacast("float", 15) );
document.add( table );
document.add( table );
document.add( Paragraph.init("Unfortunately, there was no spacing after.") );
table.setSpacingAfter( javacast("float", 15) );
document.add( table );
document.add( Paragraph.init("This is much better, don't you think so?") );

}
catch (java.lang.Exception de) {
savedErrorMessage = de;
}

// step 5: always close document and output stream
document.close();
if ( IsDefined("outStream") ) {
outStream.close();
}

WriteOutput("<hr>Finished!");
</cfscript>


<!--- show any errors --->
<cfif len(savedErrorMessage)>
Error creating document
<cfdump var="#savedErrorMessage#">
</cfif>

...Read More

Tuesday, September 2, 2008

CFCALENDAR Tip - (..This must be Belgium)

Having a bit of free time the other day, I decided to get in touch with my inner flash/actionscript/flex coder. Let me be honest... I do not have one. But I found a great tip by Paul Robertson (thanks to an entry on coldfusionusers.com) which helped me get started. It showed how cfdumping a flash form reveals the inner workings of cfform. After seeing the raw flex code behind cfform, the Flex documentation suddenly made a whole lot more sense.

Since I was curious about cfcalendar, I create a test form with a calendar and dumped it. While examining the flex output I noticed an interesting property called disabledRanges.



It is the journey that matters

Though cfcalendar only allows you to disable a range of dates, the flex documentation reveals the disabledRanges property can be used to disable individual dates. After a bit of trial and error with date objects I finally got it working. Of course after I figured this out, I found a great entry on just this topic by Nick Watson .. from two years ago ;-) Ah, well.

Another interesting property in the documentation is disabledDays. Say you want to display a calendar that only allows Tuesdays to be selected. You can use this property to do just that.

So if you were unaware of them, these two properties can come in handy with cfcalendar.

Disable Days of the Week

<cfsavecontent variable="disableDates">
// disable all days EXCEPT Tuesday (2)
testCalendar.disabledDays = [0,1,3,4,5,6];
</cfsavecontent>

<cfform name="testForm" format="flash" onLoad="#disableDates#">
<cfformitem type="text">Disable all days except Tuesday</cfformitem>
<cfcalendar name="testCalendar">
</cfform>


Disable Individual Days

<cfset selectedDate = "2008-09-30">
<cfset datesToDisable = "2008-09-01,2008-09-15,2008-09-26">
<cfoutput>
<cfsavecontent variable="disableDates">
// convert the cf list to an actionscript variable
var #toScript(datesToDisable, "list", false, true)#;
var dates:Array = [];
// convert each value to an actionscript date object
<cfloop list="#datesToDisable#" index="currDate">
var #ToScript( parseDateTime(currDate), "d", false, true )#;
// add each object to the array of disabled dates
dates.push( d );
</cfloop>
// set the dates as disabled
testCalendar.disabledRanges = dates;
</cfsavecontent>
</cfoutput>

<cfform name="testForm" format="flash" onLoad="#disableDates#">
<cfformitem type="text">Disable non-contiguous dates</cfformitem>
<cfcalendar name="testCalendar" selecteddate="#selectedDate#">
</cfform>

...Read More

  © Blogger templates The Professional Template by Ourblogtemplates.com 2008

Header image adapted from atomicjeep