tisdag 14 december 2010

Extbrowser in Netbeans platform jnlp / webstart app

The project I'm working on right now is using java web start to deploy. It is also using the extbrowser platform module that enables urls to open in the users default browser. Setting up the building of this project in netbeans, however, was not very straight forward. If it can be done more easily, I'm all ears, but this is how I did it. By the way, I'm in a Ubuntu 10.04, Netbeans 6.9.1, java 1.6.0.22 environment building for Win/OsX/Linux platforms. The below solution is verified on ubuntu 10.04/10.10, Win 7 Professional and Os X 10.5.8

1) After including External HTML Browser (found in project properties -> Libraries -> ide modules), building jnlp application does not work until modules/lib/extbrowser.dll, modules/lib/extbrowser64.dll is added to the verifyexcludes line in jnlp.xml found in the netbeans-6.9.1/harness directory. Now you can build the jnlp project again.

2) Extbrowser uses native code to access default browser, and to facilitate this in Windows, two files are included in your netbeans installation (netbeans-6.9.1/ide/modules/lib/extbrowser.dll & extbrowser64.dll). The main part of the rest of this solution is about including these two files in your project and making them available to your run-time when necessary.

2.1) I wrote the following class to handle the dll relating tasks. It basically takes the two dll's and saves them to the local filesystem to make them available to windows and then loads them (since I really only load the one corresponding to 32 or 64 bit os, I should only save one - good opportunity for refactoring...). At //http://nicklothian.com/blog/2008/11/19/modify-javalibrarypath-at-runtime/ I found an excellent piece of code to make this newly saved file (at least its containing directory) a part of the java.library.path at runtime. This has to be done to be able to load the dll. Depending on file system rights, the files can of course be saved to a directory that is already a part of java.library.path and in this case, this part should, of course be omitted...

public class WindowsDllLoader {

    public static void loadExtbrowserDlls(boolean os64Bit) {
        if (!new File(System.getProperty("user.home") + "extbrowser.dll").exists()
                || !new File(System.getProperty("user.home") + "extbrowser64.dll").exists()) {
            try {
                copyFromJar("extbrowser.dll");
                copyFromJar("extbrowser64.dll");
            } catch (Exception ex) {
                throw new RuntimeException(ex);
            }
        }
        loadDll(os64Bit?"extBrowser64.dll":"extBrowser.dll");
        try {
            addDir(System.getProperty("user.home"));
        } catch (IOException ex) {
            throw new RuntimeException(ex);
        }
    }

    private static void copyFromJar(String file) {
        try {
            InputStream in = WindowsDllLoader.class.getClassLoader().getResourceAsStream(file);
            File fileOut = new File(System.getProperty("user.home") + file);
            OutputStream out = new FileOutputStream(fileOut);
            while (true) {
                int data = in.read();
                if (data == -1) {
                    break;
                }
                out.write(data);
            }
            in.close();
            out.close();
        } catch (Exception e) {
            System.out.println(e.toString());
        }
    }

    private static void loadDll(String file) {
        System.load(System.getProperty("user.home") + file);
    }

    public static void addDir(String s) throws IOException {
        try {
            Field field = ClassLoader.class.getDeclaredField("usr_paths");
            field.setAccessible(true);
            String[] paths = (String[]) field.get(null);
            for (int i = 0; i < paths.length; i++) {
                if (s.equals(paths[i])) {
                    return;
                }
            }
            String[] tmp = new String[paths.length + 1];
            System.arraycopy(paths, 0, tmp, 0, paths.length);
            tmp[paths.length] = s;
            field.set(null, tmp);
            System.setProperty("java.library.path", System.getProperty("java.library.path") + File.pathSeparator + s);
        } catch (IllegalAccessException e) {
            throw new IOException("Failed to get permissions to set library path");
        } catch (NoSuchFieldException e) {
            throw new IOException("Failed to get field handle to set library path");
        }
    }
}

2.2) The WindowsDllLoader class was put in a separate project that also included the two dll's. The pre-build jar from this project was included in the platform app to be a part of the jnlp build signing process. When it was signed in this shape, it was called ext-windows.jar ( not a deliberate choice by me - maybe because of where I placed the included jar...). Anyways, this jar file had to be found by my jnlp-files when loading the app from the web server, so I modified the org-netbeans-modules-extbrowser.jnlp file in my build:

<?xml version='1.0' encoding='UTF-8'?>
<!DOCTYPE jnlp PUBLIC "-//Sun Microsystems, Inc//DTD JNLP Descriptor 6.0//EN" "http://java.sun.com/dtd/JNLP-6.0.dtd">
<jnlp spec='1.0+' codebase='http://***my_app***/netbeans/'>
  <information>
   <title>External HTML Browser</title>
   <vendor>NetBeans</vendor>
   <description kind='one-line'>Enables integration of external web browsers with the IDE.</description>
   <description kind='short'>The External Browser module enables the integration of external web browsers with the IDE for viewing Javadoc documentation and testing applets and web (JSP and servlet) applications. It provides an alternative to the built-in HTML Browser.</description>
  </information>
<security><all-permissions/></security>
  <resources>
    <jar href='org-netbeans-modules-extbrowser/org-netbeans-modules-extbrowser.jar'/>
    <nativelib href="http://***my_app***/app/org-jdesktop-swingx/ext-windows.jar" />
  </resources>
  <component-desc/>
</jnlp>

2.3) I then save a copy of this and included it in the project and added the following line to the project build.xml:
 <copy file="org-netbeans-modules-extbrowser.jnlp" overwrite="true" todir="${release.dir}/netbeans"/>
Where this file overwrites the vanilla version of the jnlp file that does not include the nativelib reference or directs the download to the signed version of the dll-containing ext-windows jar file.


2.4) I use the following code to call the above methods (and this code, in turn, is run in the application's module installer at start up):
public class ExtBrowser {

    public static void load() {
        if (Utilities.isWindows()) {
            WindowsDllLoader.loadExtbrowserDlls(is64BitWindows());
        }
    }

    private static boolean is64BitWindows() {
        // should be 32 or 64 bit, but it may not be present on some jdks
        String sunDataModel = System.getProperty("sun.arch.data.model"); //NOI8N
        if (sunDataModel != null) {
            return "64".equals(sunDataModel);
        } else {
            String javaVMName = System.getProperty("java.vm.name"); //NOI8N
            return (javaVMName != null) && (javaVMName.indexOf("64") > -1);
        }
}

I think this was pretty much it. It requires some more testing on more 32/64 bit cases, but I think this is how its done. Th solution can most probably also be used to include other native codes/dlls and such.



Oh and here is another nifty piece of code that I came across (at http://forums.netbeans.org/post-71323.html) that is very useful if you just want to open the system default browser programatically. This has nothing to do with the ExtBrowser logic and can, as far as I know, be used in any kind of project:
    static final String[] browsers = {"firefox", "opera", "konqueror", "epiphany","seamonkey", "galeon", "kazehakase", "mozilla", "netscape", "chrome"};
    public static void openURL(String url) {
        String osName = System.getProperty("os.name");
        try {
            if (osName.startsWith("Mac OS")) {
                Class<?> fileMgr = Class.forName("com.apple.eio.FileManager");
                Method openURL = fileMgr.getDeclaredMethod("openURL",
                        new Class[]{String.class});
                openURL.invoke(null, new Object[]{url});
            } else if (osName.startsWith("Windows")) {
                Runtime.getRuntime().exec("rundll32 url.dll,FileProtocolHandler " + url);
            } else { //assume Unix or Linux
                boolean found = false;
                for (String browser : browsers) {
                    if (!found) {
                        found = Runtime.getRuntime().exec(new String[]{"which", browser}).waitFor() == 0;
                        if (found) {
                            Runtime.getRuntime().exec(new String[]{browser, url});
                        }
                    }
                }
                if (!found) {
                    throw new Exception(Arrays.toString(browsers));
                }
            }
        } catch (Exception e) {
            throw new RuntimeException("Error attempting to launch web browser", e);
        }
    }