views:

288

answers:

5

For my Swing project, I need to support both Java 5 and Java 6. I have defined a custom JComponent (call it Picture) and, after embedding it in a JScrollPane, I put it in a JPanel that uses DesignGridLayout manager.

DesignGridLayout supports baseline alignment thanks to swing-layout open source library (implements baseline support for Java 5 and provides compatibility with the new Java 6 baseline support).

My Picture class overrides public int getBaseline(int width, int height) so that I can define a correct baseline for it. Note that "override" is not completely correct: it overrides the method on Java6 but defines it in Java5.

When I run my sample app on Java5, everything is fine: the Picture baseline I have defined is correctly used.

However, when I use Java6, my Picture#getBaseline() method does not get called! And of course the baseline alignment of my picture is terrible (centered).

After checking in Java6 source, I have seen that, in BasicScrollPaneUI, getBaseline() calls first getBaselineResizeBehavior() on the viewport component (my Picture instance). And it will call getBaseline() only if getBaselineResizeBehavior() returns Component.BaselineResizeBehavior.CONSTANT_ASCENT.

Now my problem is that getBaselineResizeBehavior() is a Java6 method of JComponent that I cannot implement in Java5 because it returns an enum Component.BaselineResizeBehavior which does not exist in Java5.

So my question (finally) is: how can I implement (or simulate?) getBaselineResizeBehavior() so that my class can still compile and run in a Java5 environment?

A: 

You can use reflection to try to get the CONSTANT_ASCENT return value by name. If it can't be reflected, you are J5, otherwise J6. This side-steps the explicit dependency and allows compilation to J5.

Foll. is an example of doing this for dialog modality:

try {
    Field  fld=Class.forName("java.awt.Dialog$ModalExclusionType").getField("TOOLKIT_EXCLUDE");
    Method mth=getClass().getMethod("setModalExclusionType",new Class[]{fld.getType()});
    mth.invoke(this,new Object[]{fld.get(null)});
    }
catch(Throwable thr) {
    log.errorln("Unable to configure window to be unaffected by modal dialogs - dialogs may need to be closed to operate help.");
    log.errorln("Use Java 6 or later to avoid modal dialogs conflicting with the help system.");
    log.errorln("Exception: "+thr);
    }

UPDATE: I originally posted code with the J5 code commented out; I've changed that because I realized it confuses the issue by implying that the J5 code would not work in J6 - it does.

Software Monkey
A: 

Thank you for your answer but unfortunately reflection cannot not solve the problem because I am not the caller but the callee! The caller is BasicScrollPaneUI which I cannot change!

What I need is a way to define the following method in my Picture class:

public Component.BaselineResizeBehavior getBaselineResizeBehavior() {...}

But I cannot because Component.BaselineResizeBehavior is not defined in Java5.

jfpoilpret
Darn - I missed that subtlety.
Software Monkey
A: 

I think that the return type is not treated as part of the a method signature in resolving virtual functions and overloads; it might be that you can define your "overriding" method to return Object, and reflect out the return Enum per my first answer. Since you're compiling in J5 it won't be a compile time conflict, but the JVM should still choose your method to override... It might or it might throw a runtime exception. Still, it's worth a try.

For example:

public Object getBaselineResizeBehavior() {
    Object ret;
    // reflect out the return value
    return ret;
    }

Any error handling can be System.out, purely for debugging, since this will not be invoked unless you are J6, so the reflection, correctly coded, should always work if invoked.

And I would, of course, comment this method to make it very clear what's going on.

Software Monkey
Thanks a lot, I'll give it a try tonight and report back here
jfpoilpret
Unfortunately this approach does not work, it compiles fine (with Java5) but when run under Java6, my method never gets called: it looks it is not recognized as overriding Component#getBaselineResizeBehavior(); the JVM probably checks the return type and thus calls the super-class method.
jfpoilpret
Ah well - that's too bad. No better idea has occurred to me, sorry this wasn't any help.
Software Monkey
+2  A: 

how can I implement (or simulate?) getBaselineResizeBehavior() so that my class can still compile and run in a Java5 environment?

You cannot compile this method declaration with the Java 5 library because the type Component.BaselineResizeBehaviour does not exist:

public Component.BaselineResizeBehavior getBaselineResizeBehavior()

You must compile using Java 6. Your classes can still run on Java 5 if you compile to a 1.5 target, but you must take care that they handle absent types/methods gracefully. Add tests for these cases as you encounter them. Ensure developers attempt to run their code on Java 5 prior to check-in.

For example, this class...

public class MyPanel extends javax.swing.JPanel {

    public java.awt.Component.BaselineResizeBehavior getBaselineResizeBehavior() {
        return java.awt.Component.BaselineResizeBehavior.OTHER;
    }

    public static void main(String[] args) {
        new MyPanel();
        System.out.println("OK");
    }

}

...can be compiled and run as follows using the javac JDK compiler:

X:\fallback>javac -version
javac 1.6.0_05

X:\fallback>javac -target 1.5 MyPanel.java

X:\fallback>"C:\Program Files\Java\jre1.5.0_10\bin\java.exe" -cp . MyPanel
OK

All the popular IDEs offer options for generating older class versions. You can use reflection to test for the existence of methods/types at runtime when you need to make decisions about code paths.

Failure to set the target will result in errors like this:

Exception in thread "main" java.lang.UnsupportedClassVersionError: Bad version n
umber in .class file
    at java.lang.ClassLoader.defineClass1(Native Method)
    at java.lang.ClassLoader.defineClass(Unknown Source)
    at java.security.SecureClassLoader.defineClass(Unknown Source)
    at java.net.URLClassLoader.defineClass(Unknown Source)
    at java.net.URLClassLoader.access$100(Unknown Source)
    at java.net.URLClassLoader$1.run(Unknown Source)
    at java.security.AccessController.doPrivileged(Native Method)
    at java.net.URLClassLoader.findClass(Unknown Source)
    at java.lang.ClassLoader.loadClass(Unknown Source)
    at sun.misc.Launcher$AppClassLoader.loadClass(Unknown Source)
    at java.lang.ClassLoader.loadClass(Unknown Source)
    at java.lang.ClassLoader.loadClassInternal(Unknown Source)
McDowell
Interesting but it only partly answers the problem: I cannot compile it with Java5, I need Java6 with target 1.5.
jfpoilpret
Actually it doesn't work when using swing-layout.jar library: swing-layout uses reflection to find getBaseline() method, but to do so it loops through all methods of the class by calling Class.getMethods() which throws a NoClassDefFoundException due to the getBaselineResizeBehavior() method!
jfpoilpret
Unfortunate - combining this compile target approach with a Factory producer is probably the right way to go; unless you want to get into plugin mechanisms.
McDowell
+2  A: 

I would make a subclass of Picture, perhaps called PictureJava6, which implemented the getBaselineResizeBehaviour(), and when creating instances of Picture, do:

public Component pictureFactory() {
    if(javaVersion > "1.6") {
        return new PictureJava6();
    } else {
        return new Picture();
    }
}
Rolf Rander
Well although I don't like this method much, this is probably the only way to get what I want, although it requires me to compile with JDK6 (and target=1.5). I have just checked it it works. Thanks!
jfpoilpret
Well, the PictureJava6-class will obviously not compile without the java-6 library present. And it is reasonable (maybe not required?) that you need target=1.5 to be backward compatible, so I don't think you can get around this in any other way than making two separate versions compiled separately.
Rolf Rander