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);
        }
    }

fredag 19 november 2010

Nimbus with JNLP

We want to use the Nimbus look and feel in our project and use the following code in our Installer.restored method (as described at http://download.oracle.com/javase/tutorial/uiswing/lookandfeel/nimbus.html):

try {
    for (LookAndFeelInfo info : UIManager.getInstalledLookAndFeels()) {
        if ("Nimbus".equals(info.getName())) {
            UIManager.setLookAndFeel(info.getClassName());
            break;
        }
    }
} catch (Exception e) {
    // If Nimbus is not available, you can set the GUI to another look and feel.
}

And this works nicelly! However, due to 
https://netbeans.org/bugzilla/show_bug.cgi?id=190175 this 
does not really work with jnlp yet... I'll be back!
 
*** 2010-12-09 ***
By adding  the following to you master.jnlp Nimbus is run on webstart too 
(wouldnt mind having 190175 fixed anyway but still...):
  <application-desc>
    <argument>--laf</argument>
    <argument>com.sun.java.swing.plaf.nimbus.NimbusLookAndFeel</argument>
  </application-desc>
 

onsdag 17 november 2010

Netbean Platform Locale in zip-build

Ok. I want to run our netbeans platform-based application in Swedish regardles of locale settings used in development IDE. Turns out that if I use Netbeans IDE in Swedish everything looks fine while executing the application from within the IDE (it starts and run in Swedish) but if I build an independet zip, it starts in english (15 minutes of googling lets me know that this is a pretty common complaint). However, there is an uggly fix while we wait for the following to bugs to be completed:
http://netbeans.org/bugzilla/show_bug.cgi?id=133901 (vote for them)
http://netbeans.org/bugzilla/show_bug.cgi?id=157031

After you  have build your zip-distribution (or jnlp distribution), you need to copy
the following libraries from your netbeans installation into corresponding locations in your application:
platform/core/locale
platform/lib/locale
platform/modules/locale
platform/modules/ext/locale

You also have to edit your .conf file located in your_app/etc/your_app.conf
find default_options and add this: -J-Duser.language=sv --locale sv (change sv to your locale setting).

All of the above is automated with Ant and only applicable if you have the original IDE with your desired locale to begin with...

Edit:
To enable locale translations in zip builds and jnlp builds I ended up using the build in 
support for Branding. Here you can enter your own values to pretty much any property-file 
in your project. And the locale data is stored in files accessible here.
 
However, entering all those translations here just isn't a realistic option so what I did was that I 
changed one or two properties here so that I could see what and how this changed was 
stored in my project. It turned out that it is stored in file and directory hierarchy much similar 
to how the localized jars are stored. 

So, what I did was this:
I downloaded the platform locale project from http://hg.netbeans.org/main/l10n/ using Mercury.
From there I chose the sv branch since I'm interested in the swedish locale. Then I copied 
the each sub directory corresponding to the jar I was wanting to translate into the projects 
branding directory. After copy there was two things that had to be done: 
1) rename the top directory (add ".jar" to its name) 
2) rename the Bundle_sv.properties file that contains the translations.

This all may sound complicated but in reality was quite simple 
(but it took about 1.5 hours to do it manually for my project). Take a look at the branding tool 
and what it does when you change a value in one of the properties (it's browsable in the 
branding directory in the Files tab of your project) if you need a hand.

And just like that, you have locale data build in to you zip and jnlp builds!

torsdag 11 november 2010

Encoding magic

Ok, so the other week I spent two days on trying to lure my Netbeans project to execute with the possibility to read utf-encoded xml on windows platforms. Reading through, and trying, multiple sites with advice on how to set the file.encoding in a variety of project and platform settings files, I finally changed my xml-reading implementation from getClass().getResourceAsStream("guide.xml") to
new InputStreamReader(getClass().getResourceAsStream("guide.xml"), "UTF-8").

Just like that. If this is usefull to someone else I'm happy...

måndag 18 oktober 2010

Programmatically selecting Next in NetBeans Wizard

After searching for quite a while for a way to programmatically select Next step in a wizard (if for some reason using the ordinary Next button is not enough), I found this (at http://forums.netbeans.org/ptopic23811.html):

yourWizardVisualPanel.getRootPane().getDefaultButton().doClick()

A place to call home

I'm currently working (in Näsviken, Sweden) on a NetBeans Platform based project. The goal is to produce a business application (bookkeeping, invoice sending, and so on) that can be run both as a stand-alone and remotely connected application. We're using hibernate based JPA for our persistence needs and use alternatively Apache Derby and MySQL for database.

My idea for this blog is to try to collect and share any findings regarding all aspects of this development as it proceeds. Sometimes I can feel that easily available documentation for the NetBeans Platform is lacking and possibly this will be my little contribution...