Distributing a Java Web Start Application via CD-ROM Blog

Version 2


    Isn't Java Web Start (JWS) supposed to allow web-based distribution of applications? So why would one want to distribute a Java Web Start (JWS) application via CD-ROM? Well, for a number of reasons. For larger applications, a complete installation can still be a major download even with high-speed broadband. Secondly, not all desktops are online, and not all can access the internet (for corporate security reasons, for example). And, lastly, some people just like CDs.

    A client company required that their application be distributed worldwide, including to places where broadband coverage is sparse. The application contains information about a large number of products, including detailed drawings and diagrams. All this information constituted a major part of the application, and a complete installation including the JVM amounts to over 40 MB. In addition to this, the company wanted to be able to distribute the application on CDs at trade fairs and with promotional materials; therefore, a CD-based distribution was required. Normally a CD install is possible using either commercial or open source installers, of which there are many. However, when an application is to run with Java Web Start, it needs to be installed in a specific location and not at the user's discretion, as is the norm for installers.

    This article describes the steps involved in installing an application that installs both from CD and the internet. The installation process requires that:

    1. The installed application must check for updates and integrate with the JWS cache.
    2. The installation should work on a machine without an existing or up-to-date version of Java.
    3. The installed application should not require an internet connection.
    4. A installation must be easy to use and must provide a simple user interface.

    Application installation is normally carried out with a generic installer application, but a traditional install process would effectively create a separate application that is unaware of JWS. Each time an update is released, the user would have to download and install the new version, whereas a JWS application only downloads those components that have been updated, making the process far more efficient and reliable. The article therefore also describes a JWS application installer.

    JWS Primer

    Java Web Start allows Java applications to be launched via a link to a JNLPfile. The JNLP file describes the main method or entry point into the application and it references the resources used by the application.

    When a JWS application is launched, the JVM tries to access the required resources, updating them if necessary, and copies the file to its cache. On subsequent attempts to launch the application, JWS can check this cache and skip download of the resources. If the client machine is offline or if the server cannot be contacted, then JWS can run the application in an offline mode.

    If the JWS launch file (the JNLP) were placed on CD, JWS would attempt to contact the server and download any new files. Obviously this would defeat the purpose of distributing the files via CD if the client machine is online. Instead, we need some way to update the JWS cache as though the application had been previously loaded by JWS.

    Updating the JWS Cache

    The Java 5 version of JWS includes a little known-import option that imports a JWS application into the cache from a specified location.

    The CD image at this location is just a copy of what you would normally place on the web server: the JNLP file, plus the .jars and resources referred to by that JNLP file. If you use a servlet to serve up the JNLP, then the CD image will need a self-contained snapshot of the generated JNLP file.

    The CD image can thus be installed into the JWS cache by calling:

    <JAVA_HOME>/jre/bin/javaws -codebase <CACHE_IMAGE> -import <CACHE_IMAGE>/<XXXX>.jnlp

    where <JAVA_HOME> is the root of the (possible new) JVM, <CACHE_IMAGE> is location of the JWS application on the CD, and <XXXX> is the name of the application JNLP file. Later, we will see how this command is automated and wrapped in a simple GUI.

    In installing the cached application, JWS conveniently prompts the user to install desktop and menu shortcuts for starting the application. Once the JWS install has been completed, we can again call JWS to start the newly installed application.

    <JAVA_HOME>/jre/bin/javaws -import <CACHE_IMAGE>/<XXXX>.jnlp

    Again the CD is used, but this time JWS will use the installation referred to by the JNLP file. If the machine is connected to the internet, it will check for updates in the normal way, as part of the process, and then start the application. If there is no network connection, the application will launch as delivered on CD.

    The next time the user starts the application they can use the menu or desktop shortcuts and the CD will no longer be needed. Alternatively, the user can start the application from a link on a web page that points to the same URL/JNLP file combination; i.e., the original version from the website.

    JVM Complications

    One gotcha in all of this is that the above commands require the presence of a JVM, and in some rare cases this may not be installed or may not be available by default on the system path and therefore some extra measures are needed to locate a usable JVM. Furthermore, when a user inserts the CD, the installation should begin, and the installation should check for the presence of an existing JVM. The process of checking for a JVM is then as follows:

    1. Check for a JVM (for the installer).
    2. Install the JVM if not present.
    3. Launch the installer, showing the usual license information.
    4. Install the target JVM (if required by the application and different from 1 above).
    5. Import the JWS cache.
    6. Start the JWS application.

    Some further complications arise from the fact that the minimum JVM for the JWS -import option is Java 5, so even if a JVM is present and usable for the application, this import option will still require a fairly recent JVM. Secondly, the import process takes some time and must complete before the application is launched, and this sort of delayed execution is difficult to achieve with many of the normal installers.

    Given these complications, it was necessary to build a custom launch application that could perform these steps.

    Running the Installation

    As the JWS -import command does most of the real work involved in the installation process, the major task of the launch application is to find and start the JVM with the appropriate commands and display some sort of GUI to the user so that they know something is happening.

    Finding the JVM

    On Windows, the JVM can be located by looking in the system registry under theHKEY_LOCAL_MACHINE\\SOFTWARE\\JavaSoft\\Java Runtime Environment key. The key may contain multiple values, and therefore the launch application iterates over the entries and attempts to find the most recent version of the JVM that is acceptable.

    The following method takes arguments for the minimum and maximum version numbers and attempts to find the JVM path:

    private String getInstalledPath( int majorMin, int minorMin, int revMin, int majorMax, int minorMax, int revMax ) { String installedPath = null; int latestVersion = 0; String keyRoot = "HKEY_LOCAL_MACHINE\\SOFTWARE\\JavaSoft" + "\\Java Runtime Environment"; Vector results = getRegEntries( "\"" + keyRoot + "\" /s" ); int numEntries = results.size(); for ( int i = 0; i < numEntries; i++ ) { String key = results.get( i++ ).toString(); int pos = key.indexOf( "Java Runtime Environment" ); if ( pos > 0 ) { pos += "Java Runtime Environment".length() + 1; String version = key.substring( pos ); String parts[] = version.split( "[._]" ); int majorVersion, minorVersion, revision; majorVersion = Integer.parseInt( parts[ 1 ] ); if ( parts.length > 3 ) minorVersion = Integer.parseInt( parts[ 2 ] ); else minorVersion = 0; if ( parts.length > 4 ) revision = Integer.parseInt( parts[ 3 ] ); else revision = 0; if ((( majorVersion == -1 ) || ( majorVersion >= majorMin )) && (( majorVersion == -1 ) || ( majorVersion <= majorMax ))) { if ((( minorMin == -1 ) || ( minorVersion >= minorMin )) && (( minorMax == -1 ) || ( minorVersion <= minorMax ))) { if ((( revMin == -1 ) || ( revision >= revMin )) && (( revMax == -1 ) || ( revision <= revMax ))) { // Prefer the neweset acceptable version int thisVersion = majorVersion * 10000 + minorVersion * 100 + revision; if ( thisVersion > latestVersion ) { String value = null; while ( i < numEntries ) { value = results.get( i++ ).toString().trim(); if ( value.startsWith( "JavaHome" )) break; } installedPath = value.substring( value.indexOf( "REG_SZ" ) + 6 ).trim(); latestVersion = thisVersion; } } } } } } return installedPath; } 

    The key in the above method is to find the registry entries. There are several APIs for obtaining the registry value, but the most practical in this situation was the simple command-lineREG QUERY <key>, where <key>is the registry path to be queried. The following method issues the command and then reads the entries from the output stream, returning them as a Vector:

    private Vector getRegEntries( String key ) { Vector results = new Vector(); try { Process proc = Runtime.getRuntime().exec( "REG QUERY " + key ); InputStream is = proc.getInputStream(); InputStreamReader isr = new InputStreamReader(is); BufferedReader br = new BufferedReader(isr); String result = ""; String line; while (( line = br.readLine()) != null ) { line = line.trim(); results.add( line ); } return results; } catch ( Exception ex ) { message( language.getString("6") + ex.getMessage() ); ex.printStackTrace(); } return null; } 
    Install the JVM

    If a suitable JVM can't be located, it can be installed from CD, provided that its location is known. The launcher uses a properties file to locate the various resources it needs, and through this mechanism it can be directed to a JVM installation package. Launching the JVM install is again a matter of executing the right command, but this time, as the process is relatively long-running, it is necessary to wait for completion. The execed process' waitFor method causes the thread to wait for completion, but to avoid blocking the UI thread, this is nested within a SwingWorker:

    private void installJre() { try { final String javaInstall = (String)props.get( "jre_installer" ); status.setText( language.getString("3") ); SwingWorker worker = new SwingWorker() { public Object construct() { try { Process process = Runtime.getRuntime().exec( workingDir + File.separatorChar + javaInstall ); process.waitFor(); exitValue = new Integer( process.exitValue()); } catch ( Exception ex ) { exitValue = new Integer( -1 ); ex.printStackTrace(); } return exitValue; } public void finished() { int ev = exitValue.intValue(); if ( exitValue != 0 ) { status.setText( language.getString("Error:_") + exitValue ); message( language.getString("4") ); } else { installedJrePath = getInstalledPath( 5, -1, -1, 5, -1, -1 ); doInstall( installedJrePath ); } status.setText( "" ); } }; worker.start(); } catch ( Exception ex ) { status.setText( language.getString( "5" )); ex.printStackTrace(); } } 
    Launch JWS and Wait

    When the JVM has been located, it can then be launched with the appropriate command line. When invoking JWS, the first invocation of the import may take some time to copy and complete, so again thewaitFor method is used as above, whereas the second invocation just needs to kick off the application (and let it do its own thing). Once the application has launched, the launch application can exit, its work done:

    private void launchWebStart( String javaWSPath, String jnlpPath, String userDir ) { try { String webStartCommand = "\"" + javaWSPath + "\"" + " -wait -codebase file:///" + userDir + "\\"+ appDirectory + " -import " + jnlpPath; Process process = Runtime.getRuntime().exec( webStartCommand ); process.waitFor(); int exitValue = process.exitValue(); if ( exitValue != 0 ) status.setText( language.getString("7") ); else status.setText( language.getString("8") ); int rc = JOptionPane.showConfirmDialog( null, language.getString("9"), language.getString("10"), JOptionPane.YES_NO_OPTION ); if ( rc == JOptionPane.YES_OPTION ) Runtime.getRuntime().exec( javaWSPath + " -offline " + jnlpPath ); status.setText( language.getString("11") ); SwingWorker worker = new SwingWorker() { public Object construct() { try { Thread.currentThread().sleep( 3000L ); } catch ( Exception ex ) { } return null; } public void finished() { System.exit( 0 ); } }; worker.start(); } catch ( Exception ex ) { status.setText( language.getString("12") ); ex.printStackTrace(); } } 

    Packaging it Up

    Once the mechanics of the install have been taken care of, the launcher just needs a simple UI (see Figure 1 below) to let the user know what is going on:

    Cd Installer welcome page

    Figure 1. Installer welcome page (click the image for a larger version)

    The UI is localized and is configurable for logos, app locations, and JVM versions via a config.properties file. The complete application with source is available under an open source license from the Aria project.

    Once the launch application has been created and tested, there is one final step in creating a complete CD install, and that is the autorun feature that starts the install when the CD is inserted. Details of creating the autorun.inf file for Windows can be found in Autorun's Wikipedia entry, but the feature requires a native executable file. Creating such an executable can be accomplished with the Launch4J wrapper. A sample configuration file for Launch4J is included in the source download. When run, Launch4J creates a .exe file for your application. Again, a JVM needs to be bundled with the wrapper, as the launch application outlined above is a Java application and the end user's system may not include a JVM.

    Once the .exe has been created, the autorun.inffile can be created and added to the CD image.

    [autorun] open=XXXX.exe icon=xxxx.ico action=Open XXXX label=My Application 

    Cross-Platform Issues

    The application launcher above relies on some Windows-specific features and therefore the runs only on Windows. The techniques outlined in this article are, however, cross-platform capable, so the main thing to check if you are using an alternative platform is the availability of a native launcher. For example, IzPackincludes launchers for several platforms. Furthermore, if you know the platform in question includes a JVM, as Mac OS X does, then the native launcher may be redundant, as it should be possible to run the launch application directly.


    Combining Java Web Start and Launch4J allows for the creation of CDs that can be distributed to people like trade-show visitors who can then easily and quickly install the application. The full update capabilities of Java Web Start are still available to the user so that they can easily get updates or equally, so that they can use the application where an internet connection is not always available -- the best of both worlds.