Skip to main content

Distributing a Java Web Start Application via CD-ROM

July 10, 2008

{cs.r.title}







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 "http://java.sun.com/products/javawebstart/download-spec.html">JNLP
file. 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:

/jre/bin/javaws -codebase
-import
/.jnlp

where is the root of the
(possible new) JVM, is location of
the JWS application on the CD, and 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.

/jre/bin/javaws -import
/.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 the

HKEY_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:

[prettify]
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;
}
[/prettify]

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-line REG QUERY , where
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:

[prettify] 
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;
}
[/prettify]

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:

[prettify]
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();
  }
}
[/prettify]

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 the
waitFor 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:

[prettify] 
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();
  }
}
[/prettify]

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:

<br "Cd Installer welcome page" src="/images/2008/07/cdinstaller_sm.gif" />

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 "http://downloads.sourceforge.net/aria/CdInstaller20080612.zip?use_mirror=osdn">
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 "http://en.wikipedia.org/wiki/Autorun">Autorun's Wikipedia
entry
, but the feature requires a native executable file.
Creating such an executable can be accomplished with the "http://launch4j.sourceforge.net/">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.inf file can be created and added to the CD
image.

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

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 "http://www.izforge.com/izpack/wiki/native_launcher_and_alternatives">
native launcher
. For example, "http://izpack.org/">IzPack includes 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.

Conclusion

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.

Resources


width="1" height="1" border="0" alt=" " />
Luan O'Carroll is a software developer and lead developer on the Aria project.
Related Topics >> Programming   |