Distributing a Java Web Start Application via CD-ROM
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:
- The installed application must check for updates and integrate with the JWS cache.
- The installation should work on a machine without an existing or up-to-date version of Java.
- The installed application should not require an internet connection.
- 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 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:
<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:
- Check for a JVM (for the installer).
- Install the JVM if not present.
- Launch the installer, showing the usual license information.
- Install the target JVM (if required by the application and different from 1 above).
- Import the JWS cache.
- 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:
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-line REG 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 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:
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:
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.inf file 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, 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
- Sample code for this article
- Autorun.inf description
- Launch4J executable wrapper
- Login or register to post comments
- Printer-friendly version
- 3681 reads




