Monday 17 February 2014

Railo bug? Or ColdFusion bug...

G'day:
I've had a bit of a break, as you will have noticed. I'm now sitting in my folks' place in Auckland, watching the cricket with me dad. New Zealand are desperately trying to salvage the match against India from certain loss. Currently NZ is on 363/5, with McCullum (183*) and Watling (92*) being the last real line of defence against India. NZ only lead by 118. We kinda need a lead of 250 to not lose (India have another innings yet, and there's still a day and a half to go). We're definitely not gonna win, but we might be able to eke out a draw.

I know hardly any of that will mean anything to most of my readers. However cricket represents "summer" to me.

But enough of the waffle.

Segue alert. One of the most noted wafflers in the CFML community - Scott Stroz - discovered some interesting behaviour when we was migrating some code from ColdFusion to Railo, and initially suspected a bug in Railo. I've had a closer look, and I think it's more likely a bug in ColdFusion, with the behavioural difference with Railo being that it doesn't have the bug. However I'm only 90% convinced of this. Here's the deal...

Scott's ColdFusion code had been using an application-specific mapping to provide pathing information for a <cfimport> tag (detailed here: "Re: cfimport with app specific mappings?").  This was not working on Railo, so superficially seemed like a Railo bug, and Scott raised it on the Railo Google Group accordingly. At the same time I was working through this with him via Google Chat (or whatever it is), because I could not even get it to work on ColdFusion, let alone Railo. But I wanted to get it working on CF then perhaps work out how to get it working on Railo. I drew a blank.

In working through all this Scott found some interesting behaviour, and I independently arrived at the same finding (had I read what Scott had said properly, he'd actually already mentioned this, but I didn't. Oops). Here's my pared-back repro case anyhow.

// Application.cfc
component {

    variables.thisDir = getDirectoryFromPath(getCurrentTemplatePath());

    this.name = "importBug01";
    this.mappings    = {
        "/ui"    = variables.thisDir & "/lib/tags"
    };
    
}

The only relevant thing here is the mapping. BTW, my file system structure for this application is as follows (this'll make the mapping be easier to follow):

import/
    lib/
        tags/
            strong.cfm
    site/
        testMapping.cfm
        viaImport.cfm
        viaInclude.cfm
        viaModule.cfm
    Application.cfc

The custom tag in question is very simple, and its code is pretty much irrelevant here, but here it is:

// tag.cfm
if (thisTag.executionMode == "end"){
    writeOutput("<strong>#trim(thisTag.generatedContent)#</strong>");
    thisTag.generatedContent = "";
}

So all this does is to apply <strong> tags to the text within the tags.

[Watling has just got his century with a boundary: 103* (297 deliveries, 422min). NZ 384/5, lead by 138]

Next we have a baseline test which calls the tag (to demonstrate it works), but without using the mapping:

<!--- viaModule.cfm --->
<cfmodule template = "/ui/strong.cfm">
    G'day World
</cfmodule>

And this code, predictably, yields this:

G'day World!

OK, so the tag works fine.

What about this mapping: is that loading in OK?

<!--- testMapping.cfm --->
<cfoutput>#expandPath('/ui')#</cfoutput>

This outputs the correct value, which for me is:

C:\apps\adobe\ColdFusion\10\cfusion\wwwroot\shared\scratch\blogExamples\coldfusion\bugs\import\lib\tags

If I comment out the mapping from Application.cfc, I see the results:

C:\apps\adobe\ColdFusion\10\cfusion\wwwroot\ui

That's just ColdFusion guessing I must mean that /ui is a directory off the ColdFusion root.

Right. So everything is working correctly / predictably. So far.

Now I try to use that mapping in a <cfimport> tag.

<!--- viaImport.cfm --->
<cfimport taglib="/ui" prefix="ui">
<ui:strong>G'day World!</ui:strong>

[Brendon McCullum double-century: 203* (395 deliveries. They didn't say how long he's been in there). NZ 399/5, lead by 153]

And now we get something less predictable:

Cannot import the tag library specified by /ui.

The following error was encountered: C:/apps/adobe/ColdFusion/10/cfusion/wwwroot/ui. Ensure that you have specified a valid tag library.The CFML compiler was processing:

  • A cfimport tag beginning on line 2, column 2.

Hmmm. Well we know the directory is fine, as we've accessed it in other adjacent code. And we know the mapping is fine too, as we have used that.

Let's now see what happens if we use a CFAdmin mapping instead:



Active ColdFusion Mappings
ActionsLogical PathDirectory Path
/CFIDE C:/apps/adobe/ColdFusion/10/cfusion/wwwroot/CFIDE 
Edit Delete  /ui C:/apps/adobe/ColdFusion/10/cfusion/wwwroot/shared/scratch/blogExamples/coldfusion/bugs/import/lib/tags 

Result:

G'day World!

Hmmm. Time to RTFM. The docs for <cfimport> have this to say:

Attributes

Attribute
Req/Opt
Default
Description
taglib
Required

Tag library URI. The path must be relative to the web root (and start with /), the current page location, or a directory specified in the Administrator ColdFusion mappings page.
And this is what we are seeing here. A mapping in CFAdmin works. A mapping in Application.cfc does not. Nor, it seems, is it expected to.

But that's not all. This would not be interesting if that was the end of the story.

There's one last file mentioned in that file listing above:

<!--- viaInclude.cfm --->
<cfinclude template="./viaImport.cfm">

All this does is call that viaImport.cfm file I mentioned above, via a <cfinclude>. So we should expect the same behaviour as calling viaImport.cfm directly: an error.

Before hitting the file, I remove the mapping from ColdFusion Administrator, restart ColdFusion, and re-hit viaImport.cfm, and confirm it is erroring. Yup.

Now I browse to viaInclude.cfm:

G'day World!

Huh??? How is that working? What's "worse" is that now if I browse to viaImport.cfm... that works too.

And if I restart again, and repeat the process:
  • browse to viaImport.cfm: error
  • browse to viaInclude.cfm (which simply calls viaImport.cfm, remember!): works OK
  • browse to viaImport.cfm again: now it works OK.
That's just lunacy.

I went to have a look at what was going on in the compiled files, and this cast some light on the scene. The compiled files after browsing to viaImport.cfm are as follows:

cfudf2ecfm1426863377.class
cfdetail2ecfm164248976.class
cfexception_en2exml1817986200.class
cfgettemplate2ecfm1834882185.class
cfParseException2ecfm1024591355.class
cfudf2ecfm1426863377$funcENCODEFORERROR.class
cfudf2ecfm1426863377$funcENCODEFORERRORSMART.class

None of those are anything to do with viaImport.cfm (or Application.cfc, which also should be getting compiled), so it looks like things are erroring-out before any code gets actually run. Basically it seems ColdFusion is attempting to compile viaImport.cfm without first looking up, compiling and executing Application.cfc. But it needs to run Application.cfc so as to load the mappings for the /ui in the <cfimport> tag to resolve. So viaImport.cfm can't compile, so we error out. To me this is entirely reasonable. We can't expect runtime considerations like mappings in Application.cfc to be relevant at the compile time for a file. Even if ColdFusion did compile and run Application.cfc before attempting to compile viaImport.cfm.

But how, then, is viaInclude.cfm working? It shouldn't be. If we clear that lot out, restart, and hit viaInclude.cfm, we get this lot:

cfviaImport2ecfm2014207820.class
cfviaInclude2ecfm1189608697.class
cfApplication2ecfc217786993.class
cfcomponent2ecfc932966791.class
cfstrong2ecfm244369874.class

There we see everything has been compiled as we'd expect (given the code runs). We still don't know why it compiled and ran OK then. How did viaImport.cfm compile unless somehow Application.cfc had compiled and been executed before it was run?

I'm going to do a bit of an experiment here. I've altered Application.cfc and viaInclude.cfm to wait for a minute at key points in their execution:

// Application.cfc
component {

    variables.thisDir = getDirectoryFromPath(getCurrentTemplatePath());

    this.name = "importBug01";
    this.mappings    = {
        "/ui"    = variables.thisDir & "/lib/tags"
    };

    sleep(60*1000);

}

<!--- viaInclude.cfm --->
<cfset sleep(60*1000)>
<cfinclude template="./viaImport.cfm">

My hypothesis is that I'll see Application.cfc compiling, 60sec passing, then viaInclude.cfm compiling, another minute then viaImport.cfm and strong.cfm being compiled. Results:

cfApplication2ecfc217786993.class (15:54:53)
cfviaInclude2ecfm1189608697.class (15:54:53)
cfviaImport2ecfm2014207820.class (15:56:53)
cfstrong2ecfm244369874.class (15:56:53)

[McCullum 229* and Watling 122* now on a partnership of 350*, off 729 deliveries. NZ 444/5 lead by 198. New ball in one over. Gulp.]


OK, so it's not quite what I expected, but viaInclude.cfm and Application.cfc are definitely compiling and executing before viaImport.cfm and strong.cfm are even compiled.

[Watling out lbw for 124. NZ 446/6 lead by 200. Watling and McCullum have batted NZ back into this game, but we're definitely more likely to lose than win or even draw at this stage]

And... I've just twigged what's going on.

In viaInclude.cfm, we have this code:

<cfinclude template="./viaImport.cfm">

Now the path for the template in the <cfinclude> is a static value there, but it could be a dynamic value. And the dynamic value could utilise mappings, so ColdFusion has to run through any code that might include a mapping before compiling viaInclude.cfm. Not least of all because it needs to be able to find the file being included.

On the other hand, a <cfimport> tag cannot take a dynamic value for the path, as evidenced by this code:

<!--- viaImport.cfm --->
<cfset importPath = "/ui">
<cfimport taglib="#importPath#" prefix="ui">
<ui:strong>G'day World!</ui:strong>

And this error when I try to run it:

This expression must have a constant value.

The CFML compiler was processing:

  • A cfimport tag beginning on line 3, column 2.

So given that value cannot be dynamic, the ColdFusion compiler doesn't seem to think it might need to resolve mapping values either. Which is... wrong.

There's no reason for compilation to act any differently with a <cfimport> as with a <cfinclude> (or indeed a <cfmodule>) vis-a-vis the resolution of any mappings that might be in play. It'd make sense if runtime mappings absolutely didn't work; but clearly they do. Just not properly. They will be used when compiling a <cfimport> tag if they already exist in memory, but the compiler won't go look them up if it doesn't have them. If the compiler can do this for a <cfinclude>, it should do it with a <cfimport> as well.



In conclusion:
  • there's definitely a difference in behaviour here between ColdFusion in Railo;
  • ColdFusion has a bug (raised as 3708694) in that it doesn't handle the compilation of <cfimport> tags uniformly;
  • Railo does handle this uniformly, erring towards how ColdFusion documents the behaviour to be;
  • ColdFusion should support runtime mappings (ie: those in Application.cfc) for <cfimport> tags, just like it does for <cfinclude> and <cfmodule>;
  • as could/should Railo.
I think Scott has inadvertantly leveraged incoherent behaviour that ColdFusion has, and has been bitten on the bum by Railo being more uniform about things, and doing things they correct way. I don't think we can legitimately call for Railo to start behaving incoherently here just because ColdFusion does, for the sake of sideways compatibility.

Thoughts?

--
Adam

Oh, and NZ are 492/6. McCullum 251*; Neesham  23*. Lead by 246. 20 overs remaining until stumps on day 4. McCullum and Watling have batted NZ back into this game. Fantastic work. Utterly fantastic.