Compound Theory

v2.0

Categories

  1. Transfer
  2. ColdFusion
  3. JRuby
  4. Java
  5. ColdSpring
  6. Squabble
  7. JavaLoader
  8. ColdDoc
  9. 2ddu
  10. AsyncHTTP
  11. OO Analysis and Design
  12. Flex
  13. Railo
  14. XML / XSL
  15. Hibernate
  16. ColdFusion Builder
  17. Fall
  18. Ubuntu
  19. XHTML / CSS
  20. Eclipse
  21. Git
  22. Oracle Database
  23. Usability / UI Design
  24. webDU
  25. cf.Objective()
  26. LWJGL
  27. cf.Objective(ANZ)
  28. Captcha
  29. MAX
  30. Melbourne CFUG
  31. Martial Arts
  32. Random Things
  33. Conduit

Recent Posts

Projects

Recent Comments

15 June 2004 02:32 PM 28 Comments

Reasons I hate XMLTransform()

Okay, maybe hate is a bit of a strong word, but seriously, having done some (serious?) XSL development under Java, the XMLTransform() of Coldfusion does leave a little to be desired.

Inability to pass in xsl:param values externally
This one is my primary gripe. I don't understand why you can't do this natively in ColdFusion. In Java, when you create your XML transformation via XSL you can pass in values that are defined in the top of the XSL document like so:

<xsl:param name="root"/>

Which means I can use that variable later on in my XSL stylesheet - the primary example being being able to pass in the root URL of your application, so you can build links from it, so regardless of where you run this XSL stylesheet, you know images and links will still be relative to the root. (Admitedly there are other ways to do this, but this is just an example).

Within the current power of CF, you would have to build your XSL at runtime, and add in these elements yourself. Personally I see this as kind of cludgy, as I prefer to keep my XSL files in a flat file somewhere.

XSL Files must be read in to a variable before they can be used
This means that before you use an XSL file, you have to <CFFILE> it in before you use it. 'No big deal' I hear you say, cache it in a scope somewhere and then reuse it later... yes yes, this is all valid, except for one small thing

<xsl:import href="modularXSL.xsl"/>

If you do this - you can no longer do relative XSL imports, which pretty much destroys any chance of doing modular XSL development. Considering that you can't use a <xsl:param> to dynamically pass through the root path, that does mean you are left with developing your XSL at compile time... (which means it's not a file anymore anyway, and can't be used by other xsl stylesheets), or hardcoding your logical path into the XSL stylesheet itself (yuck!).

But what do we do now?
Okay, so I've had my major gripe, and I did have a good winge for a little while about this before I got mad enough to actually look for a solution. There is a solution to one of the issues, there is a xslt() function that can be found at cflib.org.

This runs natively from CF, however does not handle my issue with using native <xsl:import>.

So of course, I didn't get even more mad - I decided to get down and dirty with some Java and came up with my own XSLT() funtion that uses the underlying Java engine (CF uses Xalan it seems under the hood for anyone that cares) that can take either (a) a XML / XSL string, or (b) a file path to a XML / XSL file.

This means it can do BOTH parameters, AND relative xsl importing!

The documentation looks like this:

Syntax:
XSLT(xmlsource, xslsource [, stParameters])

Arguments:
xmlSource - either a valid xml document as a string, or a absolute file path to a XML file.
xslSource - either a valid XSL document as a string, or a absolute file path to a XSL file.
stParameters (optional) - a structure of xsl:param elements to pass through where the key is the name of the param, and the value is the value of the param being passed through. Do note that StructInsert() will need to be used as param names are case sensitive, and otherwise the struct key value will be in uppercase.

Example:
This can be run a variety of ways:

<cfxml variable="xml">
<!--- valid xml doc --->
</cfxml>

<cfxml variable="xsl">
<!--- valid xsl doc --->
</cfxml>

<cfscript>
stParams = StructNew();
StructInsert(stParams, "root", "http://www.mysite.com");
</cfscript>

<cfoutput>#xslt(xml, xsml, stParams)#</cfoutput>

OR

<cfoutput>#xslt("c:\xmlFile.xml", xsl, stParams)#</cfoutput>

OR

<cfoutput>#xslt(xml, "c:\xslFile.xsl", stParams)#</cfoutput>

OR

<cfoutput>#xslt(("c:\xmlFile.xml", "c:\xslFile.xsl", stParams)#</cfoutput>

Code:
<cffunction name="xslt" returntype="string" output="No">
<cfargument name="xmlSource" type="string" required="yes">
<cfargument name="xslSource" type="string" required="yes">
<cfargument name="stParameters" type="struct" default="#StructNew()#" required="No">

<cfscript>
var source = ""; var transformer = ""; var aParamKeys = ""; var pKey = "";
var xmlReader = ""; var xslReader = ""; var pLen = 0;
var xmlWriter = ""; var xmlResult = ""; var pCounter = 0;
var tFactory = createObject("java", "javax.xml.transform.TransformerFactory").newInstance();

//if xml use the StringReader - otherwise, just assume it is a file source.
if(Find("<", arguments.xslSource) neq 0)
{
xslReader = createObject("java", "java.io.StringReader").init(arguments.xslSource);
source = createObject("java", "javax.xml.transform.stream.StreamSource").init(xslReader);
}
else
{
source = createObject("java", "javax.xml.transform.stream.StreamSource").init("file:///#arguments.xslSource#");
}

transformer = tFactory.newTransformer(source);

//if xml use the StringReader - otherwise, just assume it is a file source.
if(Find("<", arguments.xmlSource) neq 0)
{
xmlReader = createObject("java", "java.io.StringReader").init(arguments.xmlSource);
source = createObject("java", "javax.xml.transform.stream.StreamSource").init(xmlReader);
}
else
{
source = createObject("java", "javax.xml.transform.stream.StreamSource").init("file:///#arguments.xmlSource#");
}

//use a StringWriter to allow us to grab the String out after.
xmlWriter = createObject("java", "java.io.StringWriter").init();

xmlResult = createObject("java", "javax.xml.transform.stream.StreamResult").init(xmlWriter);

if(StructCount(arguments.stParameters) gt 0)
{
aParamKeys = structKeyArray(arguments.stParameters);
pLen = ArrayLen(aParamKeys);
for(pCounter = 1; pCounter LTE pLen; pCounter = pCounter + 1)
{
//set params
pKey = aParamKeys[pCounter];
transformer.setParameter(pKey, arguments.stParameters[pKey]);
}
}

transformer.transform(source, xmlResult);

return xmlWriter.toString();
</cfscript>
</cffunction>

There you go - copy paste that funtion, and now you have every all funtionality you could probably ever want when doing an XSL transforamtion.

I will probably shoot this off to cflib.org at some point soon, so you can search for it there as well.

I must say that I am loving the Java integration with CF, it has definately enabled me to do many more interesting things with CF I was previously never able to do before.

If you have any questions / comments / bugs, drop me a line, or post a comment.