Saturday, 6 June 2015

Not a Lucee bug, but a ColdFusion one. And I learn more about Java

G'day:
OK so remember that glitch I had in my earlier blog article: "CFML: learning something new about directoryList()":


I finally managed to work out WTF is going on. And despite appearances it actually seems like a bug in ColdFusion, not a bug in Lucee. But I'm unsure.

But there was certainly a bug in my recollection of how Java works, which did not help.



I had this wee Java class, and it was working fine via ColdFusion, but giving the above error in Lucee:

package me.adamcameron.miscellany;

import java.io.File;

class FileFilterOnMinimumLength implements java.io.FileFilter {

    private long length;

    public FileFilterOnMinimumLength(long length){
        this.length = length;
    }
    
    public boolean accept(File pathname){
        return pathname.length() >= this.length;
    }

}

I've spent the last threefour hours to try to come up with a pared back repro case to give to Lucee as a bug. I finally pared it back enough to get a repro case, and it seemed like Lucee was not handling any class that needed a constructor. It simply wasn't allowing me to call the constructor. Any static methods were fine, just not object methods. Obviously there was still more to it because if the bug was what it seemed to be, everyone else would have spotted it too. But my code worked on CF, and failed on Lucee. Hmmmm.

Then the actual problem occurred to me. And it's not a bug in Lucee at all. It was a bug in my code, but one that ColdFusion was letting me get away with. I varied my repro case slightly, and proved the problem. And obviously detected where the issue is, and have now fixed it.

Here's my final repro case:

package me.adamcameron.bug;

class UnqualifiedPerson {

    private String name;

    public UnqualifiedPerson(){
        
    }
    
    public void setName(String name){
        this.name = name;
    }
    
    public String getName(){
        return this.name;
    }
    
    public static String greet(String name){
        return "G'day " + name;
    }

}

package me.adamcameron.bug;

public class PublicPerson {

    private String name;

    public PublicPerson(){
        
    }
    
    public void setName(String name){
        this.name = name;
    }
    
    public String getName(){
        return this.name;
    }
    
    public static String greet(String name){
        return "G'day " + name;
    }

}

// constructorIssue.cfm

package = "me.adamcameron.bug";
classes = ["UnqualifiedPerson", "PublicPerson"];

classes.each(function(class){
    var qualifiedClass = "#package#.#class#";

    writeOutput("<h4>Testing with #class#</h4>");

    runSafe(function(){
        writeOutput("Static only<br>");
        var staticPersonProxy = createObject("java", qualifiedClass);
        writeOutput(staticPersonProxy.greet("Zachary"));
    });


    runSafe(function(){
        writeOutput("No init()<br>");
        var personWithoutInit = createObject("java", qualifiedClass);
        personWithoutInit.setName("Zachary");
        writeOutput(personWithoutInit.getName());
    });


    runSafe(function(){
        writeOutput("With init()<br>");
        var personWithoutInit = createObject("java", qualifiedClass).init();
        personWithoutInit.setName("Zachary");
        writeOutput(personWithoutInit.getName());
    });
});

function runSafe(task){
    try {
        task();
    } catch (any e){
        writeOutput("Type: #e.type#<br>Message: #e.message#<br>Detail:#e.detail#");
    } finally {
        writeOutput("<hr>");
    }
}

The CFML simply works with each class in turn, and runs the static method, with static proxy object; then tries to run the object methods via both an un-init()-ed object instance, and an init()-ed one.

Here are the results on ColdFusion:

Testing with UnqualifiedPerson

Static only
G'day Zachary


No init()
Type: Object
Message: Object instantiation exception.
Detail:An exception occurred while instantiating a Java object. The class must not be an interface or an abstract class. If the class has a constructor that accepts an argument, you must call the constructor explicitly using the init(args) method. Error : Class coldfusion.runtime.java.JavaProxy can not access a member of class me.adamcameron.bug.UnqualifiedPerson with modifiers "public"


With init()
Zachary


Testing with PublicPerson

Static only
G'day Zachary


No init()
Zachary


With init()
Zachary



And on Lucee:

Testing with UnqualifiedPerson

Static only
G'day Zachary


No init()
Type: java.lang.IllegalAccessException
Message: Class lucee.runtime.reflection.pairs.ConstructorInstance can not access a member of class me.adamcameron.bug.UnqualifiedPerson with modifiers "public"
Detail:


With init()
Type: java.lang.IllegalAccessException
Message: Class lucee.runtime.reflection.pairs.ConstructorInstance can not access a member of class me.adamcameron.bug.UnqualifiedPerson with modifiers "public"
Detail:


Testing with PublicPerson

Static only
G'day Zachary


No init()
Zachary


With init()
Zachary



And did you catch what the problem is?

My UnqualifiedPerson class does not declare the access level on the class itself. It's just this:

class UnqualifiedPerson {
    // ...
}

And class access rules are that unqualified access is only public in a stand-alone class. However because my class is in a package... unqualified access means package-private. And the CFML server should be operating from a different package, so... it should not have access to the class. To be honest, I don't quite get why the static method within UnqualifiedPerson is accessible... shouldn't its access be dictated by the class' own access?

Anyway, you'll now note that PublicPerson is actively declared public:

public class PublicPerson {
    // ...
}

And this enables Lucee to get at its constructor, and create objects.

This kinda makes sense. But I might need someone to explain why it is ColdFusion doesn't have this same issue with package-private classes that Lucee has. And also why Lucee still has access to static methods in a package-private class. Something still doesn't seem quite right here. I just dunno where the problem lies. Definitely there's a gap in my understanding here, but I'm not sure that Lucee is getting it entirely right; or how wrong ColdFusion is getting it. I mean... ColdFusion has odd behaviour with the way it initialises classes. It seems the act of creating the Java object with createObject() automatically attempts to call a no-arg constructor explicitly... which it can't do on a non-public class. However it's absolutely fine calling it explicitly via init(). I have to admit I dunno how CFML is doing the explicit constructor calls; I suspect it's some reflection shenanigans, and just that ColdFusion and Lucee are making these calls slightly differently.

Hmmm. Any Java experts out there know what the story is?

Cheers.

--
Adam