Search code examples
javaclassloaderjava-web-startresourcebundle

MissingResourceException when starting 1.4.2_12 application with webstart 1.6


After I could eventually figure out why JWS 1.6.0_29 failed to launch a 1.4.2_12 application (see this question), I faced another exception when launching a 1.4.2_12 app. with JWS 1.6.0_29.

I get a MissingResourceException when loading a ResourceBundle. Yet a message.properties file do exists in the same package as the class that's loading it.

When JWS 1.4 or 1.5 is used to launch the application, the exception is not raised.
The exception is raised only when launching the app. with JWS 1.6.

Full stackstrace is :

java.lang.ExceptionInInitializerError
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(Unknown Source)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source)
    at java.lang.reflect.Method.invoke(Unknown Source)
    at com.sun.javaws.Launcher.executeApplication(Unknown Source)
    at com.sun.javaws.Launcher.executeMainClass(Unknown Source)
    at com.sun.javaws.Launcher.doLaunchApp(Unknown Source)
    at com.sun.javaws.Launcher.run(Unknown Source)
    at java.lang.Thread.run(Unknown Source)
Caused by: java.util.MissingResourceException: Can't find bundle for base name com.test.hello.messages, locale fr_FR
    at java.util.ResourceBundle.throwMissingResourceException(Unknown Source)
    at java.util.ResourceBundle.getBundleImpl(Unknown Source)
    at java.util.ResourceBundle.getBundle(Unknown Source)
    at com.test.hello.Main.<clinit>(Main.java:10)
    ... 9 more

Test case to reproduce

JNLP descriptor is:

<?xml version="1.0" encoding="utf-8"?>
<jnlp spec="1.0+" codebase="http://localhost:80/meslegacy/apps" href="testJwsXXTo142.jnlp">
    <information>
        <title>JWS TEST 1.6 -> 1.4.2</title>
        <vendor>Hello World Vendor</vendor>
        <description>Hello World</description>
    </information>

    <security>
        <all-permissions />
    </security>

    <resources>
        <j2se version="1.4.2_12" href="http://java.sun.com/products/autodl/j2se" />
        <jar href="jar/helloworld.jar" main="true" />
    </resources>

    <application-desc main-class="com.test.hello.Main" />
</jnlp>

com.test.hello.Main class is:

package com.test.hello;

import java.util.ResourceBundle;

import javax.swing.JFrame;

public class Main {

    private static final ResourceBundle BUNDLE = ResourceBundle.getBundle(Main.class.getPackage().getName()+".messages");

    public static void main(String[] args) {
        JFrame frame = new JFrame("Hello world !");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setSize(800,600);
        frame.setVisible(true);
    }

}

Complementary tests

  • Specifying ClassLoader and Locale to the ResourceBundle.getBundle() method does not fix the problem.
  • Main.class.getClassLaoder() and Thread.currentThread().getContextClassLaoder() have been tested and spawn the same exception.
  • Loading resource "by hand" does work (see below).

Test code to load resource manually :

ClassLoader cl = Main.class.getClassLoader();
String resourcePath = baseName.replaceAll("\\.", "/");
System.out.println(resourcePath);
URL resourceUrl = cl.getResource(resourcePath+".properties");
System.out.println("Resource manually loaded :"+resourceUrl);

Will produce :

com/test/hello/messages.properties
Resource manually loaded :jar:http://localhost:80/meslegacy/apps/jar/helloworld.jar!/com%2ftest%2fhello%2fmessages.properties
  • However, while it is possible to find the resource, get the resource content is not.

Example:

ClassLoader cl = Main.class.getClassLoader();
String resourcePath = baseName.replaceAll("\\.", "/") + ".properties";
URL resourceUrl = cl.getResource(resourcePath);
// here, resourceUrl is not null. Then build bundle by hand
ResourceBundle prb = new PropertyResourceBundle(resourceUrl.openStream());

Which spawns :

java.io.FileNotFoundException: JAR entry com%2ftest%2fhello%2fmessages.properties not found in C:\Documents and Settings\firstname.lastname\Application Data\Sun\Java\Deployment\cache\6.0\18\3bfe5d92-3dfda9ef
    at com.sun.jnlp.JNLPCachedJarURLConnection.connect(Unknown Source)
    at com.sun.jnlp.JNLPCachedJarURLConnection.getInputStream(Unknown Source)
    at java.net.URL.openStream(Unknown Source)
    at com.test.hello.Main.main(Main.java:77)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(Unknown Source)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source)
    at java.lang.reflect.Method.invoke(Unknown Source)
    at com.sun.javaws.Launcher.executeApplication(Unknown Source)
    at com.sun.javaws.Launcher.executeMainClass(Unknown Source)
    at com.sun.javaws.Launcher.doLaunchApp(Unknown Source)
    at com.sun.javaws.Launcher.run(Unknown Source)
    at java.lang.Thread.run(Unknown Source)

Seems to be more a kind of cache issue...

I any of you had a hint, it would be greatly appreciated,

Thanks for reading.


Solution

  • Here is the explanation and workarround for this problem.

    1 - Explanation

    The problem comes from the URLs returned by the system ClassCloader (JWS6 system ClassLoader).

    With JWS 1.6, URL returned by the system ClassLoader contain escape sequences such as the one shown in the following :

    jar:http://localhost:80/meslegacy/apps/jar/helloworld.jar!/com%2ftest%2fhello%2fmessages.properties
    

    Locating resources in classpath is possible but when it comes to actually access the content of that resource a FileNotFoundException is raised: This is what causes the FileNotFoundException in ResourceBundle.

    Please note that when no escape sequence appears in the URL, for example when the resource is at the root of the claspath, there is no problem to access the resource content. Problem appears only when you get %xx stuff in the URL path part.

    2 - Workarround

    Once the problem had been focused (it took me days to figure this out !), it was time to find a workarround for this. While it would have been possible for me to fix my problem on specific localized code parts, it quickly turned out that is was possible to fix the issue globaly by coding a specific ClassLoader to "replace" the JNLPClassLoader. I don't acutally "replace" because it seems impossible to me but I rather do the following :

    1. Disable SecurityManager to be abled to play with my custom classloader
    2. Code my own classloader derived from URLClassLoader that fix URL when they are returned
    3. Set its classpath with the claspath extracted from the JNLPClassLoader
    4. Set this custom classloader to be the context classloader
    5. Set this custom classloader to be the AWT-Event-Thread context classloader
    6. Use this custom classloader to load my application entry point.

    This gives the following ClassLoader

    public class JwsUrlFixerClassLoader extends URLClassLoader {
    
        private final static Logger LOG = Logger.getLogger(JwsUrlFixerClassLoader.class);
    
        private static String SIMPLE_CLASS_NAME = null;
    
        private static boolean LOG_ENABLED = "true".equals(System.getProperty("classloader.debug"));
    
        static {
            SIMPLE_CLASS_NAME = JwsUrlFixerClassLoader.class.getName();
            int idx = SIMPLE_CLASS_NAME.lastIndexOf('.');
            if (idx >= 0 && idx < SIMPLE_CLASS_NAME.length()-1) {
                SIMPLE_CLASS_NAME = SIMPLE_CLASS_NAME.substring(idx + 1);
            }
        }
    
        public JwsUrlFixerClassLoader(URL[] urls, ClassLoader parent) {
            super(urls, parent);
        }
    
        public URL getResource(String name) {
            if (LOG.isDebugEnabled()) {
                LOG.debug("getResource(): getResource(" + name + ")");
            }
            if (LOG_ENABLED) {
                login("getResource(" + name + ")");
            }
            URL out = super.getResource(name);
            if (out != null) {
                out = URLFixerTool.fixUrl(out);
            }
            if (LOG_ENABLED) {
                logout("getResource returning " + out);
            }
            return out;
        }
    
        public URL findResource(String name) {
            if (LOG_ENABLED) {
                login("findResource(" + name + ")");
            }
            URL out = super.findResource(name);
            if (out != null) {
                out = URLFixerTool.fixUrl(out);
            }
            if (LOG_ENABLED) {
                logout("findResource returning " + out);
            }
            return out;
        }
    
        public InputStream getResourceAsStream(String name) {
            if (LOG_ENABLED) {
                login("getResourceAsStream(" + name + ")");
            }
            InputStream out = super.getResourceAsStream(name);
            if (LOG_ENABLED) {
                logout("getResourceAsStream returning " + out);
            }
            return out;
        }
    
        protected synchronized Class loadClass(String name, boolean resolve) throws ClassNotFoundException {
            if (LOG_ENABLED) {
                login("loadClass(" + name + ")");
            }
            // First, check if the class has already been loaded
            Class c = findLoadedClass(name);
            if (c == null) {
                try {
                    c = findClass(name);
                } catch (ClassNotFoundException cnfe) {
                    if (getParent() == null) {
                        // c = findBootstrapClass0(name);
                        Method m = null;
                        try {
                            m = URLClassLoader.class.getMethod("findBootstrapClass0", new Class[] {});
                            m.setAccessible(true);
                            c = (Class) m.invoke(this, new Object[] { name });
                        } catch (Exception e) {
                            throw new ClassNotFoundException();
                        }
                    } else {
                        c = getParent().loadClass(name);
                    }
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            if (LOG_ENABLED) {
                logout("loadClass returning " + c);
            }
            return c;
        }
    
        private static void login(String message) {
            System.out.println("---> [" + Thread.currentThread().getName() + "] " + SIMPLE_CLASS_NAME + ": " + message);
        }
    
        private static void logout(String message) {
            System.out.println("<--- [" + Thread.currentThread().getName() + "] " + SIMPLE_CLASS_NAME + ": " + message);
        }
    
    }
    

    Now in a AppBoostrap class which I set to be the main-class in the JNLP descriptor, I do the following :

        System.setSecurityManager(null);
        ClassLoader parentCL = AppBootstrap.class.getClassLoader();
        URL[] classpath = new URL[] {};
        if (parentCL instanceof URLClassLoader) {
            URLClassLoader ucl = (URLClassLoader) parentCL;
            classpath = ucl.getURLs();
        }
        final JwsUrlFixerClassLoader vlrCL = new JwsUrlFixerClassLoader(classpath, parentCL);
        Thread.currentThread().setContextClassLoader(vlrCL);
        try {
            SwingUtilities.invokeAndWait(new Runnable() {
    
                public void run() {
                    Thread.currentThread().setContextClassLoader(vlrCL);
                }
            });
        } catch (Exception e) {
            LOG.error("main(): Failed to set context classloader !", e);
        }
    

    In the previous excerpt I get the ClassLoader that loaded my AppBootstrap class and use it as the parent classloader of my JwsUrlFixerClassLoader.

    I had to fix the problem of the default parent delegation strategy of the URLClassLodaer.loadClass() and replace it with the "try my classpath first then parent".

    After that has been done everything went right and a couple of other bugs that we so far couldn't explain have disapeared.

    That's magic ! After a lot of pain though...

    Hope this can help someone one day...