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.

Leave a Comment

Comments

  • Dirk Eismann | August 9, 2004

    Great shot! I am currently evaluating the functionality of XmlTransform() in terms of the requirements of an upcoming project. I think your article points out some very important things to watch for.

    Thanks,
    Dirk.

  • Bill Rawlinson | October 8, 2004

    very cool – one of the better – and more useful – UDF’s I’ve seen someone create and share with the public.

    I’ll have to add this to my library – thanks

  • Kevin Webb | November 2, 2004

    I could not get the function you provided to work. I would get "Local variable source on line 29 must be grouped at the top of the function body." It was complaining about the use of cfscript within the function. When I converted to cfscript function I did not get errors. However, I cannot get the passing of params to work.

    Help, please.
    Kevin

  • Mark | November 3, 2004

    Kevin – It was written for CFMX 6.1, in which you can use ‘var’ at the top of cfscript blocks.

    Upgrade your server from 6.0 to 6.1.

    (Failing that, you can just take the ‘var’d variables and move them out to cfset blocks)

  • Stephen W. | January 20, 2005

    Wow, this is better then Excedrine for the headache of a problem I have been working on.

    By they way, I was looking for your Paypal button but could not find it 😉

  • Mark | January 20, 2005

    Uhh… Paypal?

    Never even thought about it. ;o)

    Maybe one day – but for now, just enjoy the code!

  • Courtney | March 20, 2005

    This problem was almost going to derail my conversion of a .Net project to ColdFusion for a very important client.

    Thank you so much.

  • Craig | March 30, 2005

    You sir, are a god. I was investigating doing something similar, but you’ve a) already done it, and b) done more with it than I would have ever thought to.

  • netdragon | August 10, 2005

    Does Coldfusion 7 require your workaround? We are thinking of going to 7 and wonder if 7 fixed xsl:import to allow relative paths. If Coldfusion 7 would require this workaround, would the workaround work on 7?

  • Mark | August 10, 2005

    Wow – can’t believe I’ve still getting questions on this.

    As per CF7 – I’ve not had a real decent look at CF7, but at first glance, it doesn’t look like it would support relative xsl:import because you still have to cffile in your XSL before you use it.

    However, I don’t know any reason why the above code would not work on CF7, although it hasn’t been tested.

  • netdragon | August 11, 2005

    Since we now have a test server up, we can test your workaround. I’ll share with you what I’ve been doing, though, as it might be helpful to some that haven’t had time yet to apply your workaround. I created a directory on my system with the same absolute path the server uses for the includes. I also wrote a perl script that can change the path in the xsl:import statement from one path to another, depending on whether I’m putting it on the test or live system:

    use File::Find;
    use strict;
    # Script to update files so that the <xsl:import> section points to the right place
    # Change from D:webrootadWdrVideoSpacedesignThemeXsltsinclude
    # to C:WebSphereAppServerinstalledAppsadWdrVideoSpacedesignThemeXsltsinclude
    my $newval = "C:WebSphereAppServerinstalledAppsadWdrVideoSpacedesignThemeXsltsinclude";
    print "Changing values to $newvaln";
    find (&process, ".");
    sub process
    {
    my $fname = $File::Find::name;
    my $fdir = $File::Find::dir;
    if ( $fname =~ /.xslt$/ && $fdir =~ /^.$/)
    {
    print "Reading $fname: ";
    open (INFILE, "< $fname") || die ("Could not open file <br> $!");
    my @text = <INFILE>;
    my $len = scalar(@text);
    my $counter = 0;
    my $changed = 0;
    for($counter=0 ; $counter < $len ; $counter++)
    {
    if ($text[$counter] =~ m/href="([a-z]|[A-Z]):.*(.*)"/)
    {
    my $a = "$text[$counter]";
    $a =~ s/href="([a-z]|[A-Z]):.*(.*)"/href="$newval$2"/g;
    $text[$counter]=$a;
    print "n Changed line $counter of $fname";
    $changed = 1;
    }
    }
    close(INFILE);
    if ($changed == 1)
    {
    print "n Updating… ";
    open(OUTFILE, "> $fname");
    print OUTFILE @text;
    close(OUTFILE);
    print "Donen";
    }
    else
    {
    print "No changes needed.n";
    }
    }
    }

  • netdragon | August 12, 2005

    I found my script above wasn’t robust enough, as it could possibly change any href="C:somedir" or href="D:somedir" and although that would be rare, I decided that it’s still not worth the risk. therefore, I made the regular expression include xsl:import. Of course, since leading tabs are taken out on the above code, you’re responsible for fixing the formatting yourself. In the above, change the if($text[$counter =~ match statement on line 23 to:

    if ($text[$counter] =~ m/xsl:imports+href="([a-z]|[A-Z]):.*(.*)"/)

    You might also want to put a #!/usr/bin/perl shebang statement in the top of the file if you aren’t using activestate. I added the shebang statement for when I run the script on cygwin.

    If you want the script to span more directories, remove or modify && $fdir =~ /^.$/ on line 13.

    Also, you have to have the C: or D: in the pathname. Otherwise, if you want to have relative paths for local testing (on Altova XMLSpy, etc), you’d need to modify the regexes on line 23 and 26. Still, you don’t need to use relative paths for local testing since you can create a directory on your local system to match your includes absolute path on the server.

    Mark: Has there been a bug report sent to Macromedia about this issue?

  • Ethan Cane | October 5, 2005

    For anyone that does care that ColdFusion implements its XSL functionality using Apache Xalan…

    Reading your post at 3:40am somewhere in the world and still awake enough to write that this sort of key information is invaluable to developers working with any kind of technology.

    The more details you can provide in future posts the better.

    Kind regards…

    PS. Very nice post!

    Ethan Cane
    Web Developer

  • RegularGuy | April 4, 2006

    Hi there
    Can someone please help me? I am evaluating this for a project but seem to be getting a massive error. I get the following :

    Unable to find a constructor for class java.io.StringReader that accepts parameters of type ( coldfusion.xml.XmlNodeList )

    I am really really keen to get this working, so if someone could help me it would be great as I don’t have a fallback plan at the moment. I am using CF 7.01 (7,0,1,116466)

    TIA

  • Mark | April 4, 2006

    RegularGuy –

    If you look at the docs, you will see that you need to pass in the XML as a *string*, not a CF XML doc.

    I think you’ll find that will solve your problem.

  • Regularguy | April 5, 2006

    Hi Mark
    Thanks very much. That was a DOH moment. I have one question for you and anyone else here. We are developing a site for an online dating agency in CF 7 /SQL server on Win 2003. We are about to use Mach-ii to break up our reams of spaghetti code into objects. One of the objectives in the redevelopment is the ability for this site to have ‘white labels’ i.e. media partners who have their own look and feel, but our engine and hosted by us. While we have managed define the data structure and business logic calls, we are struggling to find a way to make the UI as flexible as we can. For instance we might have a shopping cart ‘pod’ that needs to be on the right and not the left, or a white label might need a permanent right hand side banner when there is not one on our main site.

    Do any of you guys have a suggestion for this ? We have been looking at tons of code in the past week but have hit a wall. Any help will be highly appreciated

    Regards

  • Ethan Cane | April 5, 2006

    I used to contract for a company called SpeedDater who tried to implement this very approach. A whitelabel on their site.

    They eventually ended up sacrificing their code by bundling <cfif/> blocks all over the shop based on a URL query parameter.

    They totally screwed up in this regard and just wanted to get the extra revenue, but really sacrificed their codebase in doing so.

    DO NOT FOLLOW THAT APPROACH.

    Not too sure what the right course of action might be.

    Perhaps get in touch (ethan@xmlstandards.org) and maybe we can come up with a better solution.

    I am fairly busy during the days but my evenings are mostly free.

    Ethan

  • Bill | April 5, 2006

    You can always set it up with an administrative feature where the page is broken down into a grid, and the adminstrator of the white-label could then move "pods" around in the grid – this info would be stored (in a database most likely) and would be used to construct the page.

    You could cache the page once it is constructed and clear out the cache when the admin edits the organization of the pods again.

    Another option might be to use CSS to position all of the "pods" and hide/make visible pods that only show up on certain sites.

  • J-P Stacey | March 29, 2007

    This function is great: Coldfusion only really comes alive when you start to rip down to the Java guts beneath. Thanks for squirreling the classes out for us.

    Incidentally, I wrote a componentized version of it a month or two back. I think I emailed your contact email but probably got caught by a spam-trap.

    It’s called CFJavaXML (you can find it on teh Goggle), and being a component it has hooks for grabbing the compiled XSL and cacheing it. I’ve had (variably) good performance improvements that way.

  • Nick Van Kleeck | June 7, 2007

    Thanks for solving a problem that exists to this day.

  • charlie arehart | September 8, 2007

    Folks, since people do still seem to be finding this new 3-year old blog entry and commenting on it recently, I think it’s important to point out some changes since then. (Not knocking what Mark wrote here then, just sharing some updates. In fact, it may well be that the CF engineers heard his plea!)

    First, CF7 did in fact fix this problem of not being able to pass in arguments. There is now an optional 3rd argument which works just as above, taking a structure which names keys and values to be substituted for parameters during the transform.

    Second, since a comment was made about not being able to use an XML document object for the first argument (the XML being transformed), that too has since been fixed, at least since 6.1, since it’s listed in the 6.1 docs online (http://livedocs.adobe.com/coldfusion/6.1/htmldocs/funca127.htm).

    Hope that’s helpful to readers.

  • Charlie Arehart | September 8, 2007

    Obviously, I meant to say "now 3-year old", not "new". 🙂

  • King | March 18, 2008

    I am not still able to get this to work. I got this error:
    Transformation Error.
    javax.xml.transform.TransformerException
    java.lang.ClassCastException: org.apache.xpath.objects.XString
    cannot be cast to org.apache.xpath.objects.XNodeSet

    Please do help, its really frustrating

  • vara | May 13, 2008

    Please some one help me, I got this error:
    java.lang.ClassCastException: org.apache.xpath.objects.XString cannot be cast to org.apache.xpath.objects.XNodeSet

  • Hatem Jaber | June 19, 2008

    I just ran across this post, it’s still alive and kicking!

    Mark, great article, it inspired to play with xmltransform() and try something different. I am rewriting a code generator and decided to store the data in a database vs. files like I had been doing. It’s giving me more flexibility with working with it and I was quite surprised that xmltransform() didn’t complain about the data coming from a query.

    Great post, great blog, keep it up!

  • netdragon | June 19, 2008

    Has anyone tried Saxon w/ Coldfusion? Does this issue still apply?

  • Robert | September 23, 2008

    Excellent! I couldn’t get my very first try on transformation to work using XmlTransform(). This tag did the trick.
    Thank you!!

  • Matt | June 17, 2011

    Great function…. I was looking to preserve indents and new line returns after using xmlTransform for a provider who for some reason required them… I used your code and added to the top
    <cfargument name="doIndent" type="boolean" default="false" required="no">
    <!— optional doIndent attribute —>

    var OutputKeys = createObject("java", "javax.xml.transform.OutputKeys");

    then down below…
    if (doIndent) {
    transformer.setOutputProperty(OutputKeys.INDENT, "yes");
    transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2");
    }

    below the stParameters loop and
    immediately above the
    transformer.transform(source, xmlResult);

    Thanks a lot for your code which opened the door to solving this annoying problem for me.