Deploying with Java Web Start - Part 3: Putting the Parts Together
In the first two parts of this series we presented code for use in signed Java Web Start applications running in the "all-permissions" mode. You may have already noticed that all code presented so far can be compiled without referencing the "javaws.jar" file. If you are like many developers, then having to create a special reference to the "javaws.jar" just doesn't feel right. Do you add it to your classpath, or do you pull a copy out of the JRE and place it in your development LIB structure, or do you add it to Netbeans virtual file system, or Eclipse's java build path? Java Web Start is part of the JRE but it's not really available a development time. To use and test Java Web Start you need to actually deploy it. Something just does not feel right from the developer point of view.
It would be nice to have Java Web Start everywhere and not just in a Java Web Start environment. In the ideal world the deployment system should be invisible and just work everywhere. Abstracting and hiding the "javaws.jar" has been a small step in this direction and Part 3 of this series continues the hiding.
Part of the challenge was to create a concrete implementation of the javax.jnlp.DownloadServiceListener interface without having to write "javax.jnlp" anywhere in the code other than in a Class.forName() method. Joshua Bloch (author of Effective Java) would suggest that we can benefit from reflection by using it sparingly. In Listing 4 we create a concrete implementation of the javax.jnlp.DownloadServiceListener interface by reflection and redirect it via the InvocationHandler.
Listing 4: JnlpDownloadListenerProxyclass
package ca.ansir.jnlp;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.net.URL;
/**
* The <code>JnlpDownloadListenerProxy</code> creates a proxy to an object,
* which implements the <code>javax.jnlp.DownloadServiceListener</code>
* interface. This class may be compiled without reference to the javaws.jar.
* The invocation handler redirects calls to an object that implements the
* <code>JnlpDownloadListener</code> interface.
* <p>
* Please feel free to use this code snippet (<code>JnlpDownloadListenerProxy</code>)
* in developing your own commercial or non-commercial applications, but please
* credit the author, Dan Andrews, for any portion of this code that you use,
* and provide a reference to <a href="http://www.ansir.ca">Ansir</a>. Thank
* you.
* </p>
*
* @author Dan Andrews
*/
public final class JnlpDownloadListenerProxy implements InvocationHandler {
/** The listener to redirects calls for. */
private JnlpDownloadListener listener;
/**
* Constructor - private use static methods only.
*
* @param listener
* The <code>JnlpDownloadListener</code> to redirect calls for.
*/
private JnlpDownloadListenerProxy(JnlpDownloadListener listener) {
this.listener = listener;
}
/**
* Creates an object that implements a
* <code>javax.jnlp.DownloadServiceListener</code>.
*
* @param listener
* The <code>JnlpDownloadListener</code> to redirect calls for.
* @return An instance of a <code>javax.jnlp.DownloadServiceListener</code>
* object.
* @throws ClassNotFoundException
*/
public final static Object newInstance(JnlpDownloadListener listener)
throws ClassNotFoundException {
Class downloadServiceListenerClass = Class
.forName("javax.jnlp.DownloadServiceListener");
Object object = Proxy.newProxyInstance(downloadServiceListenerClass
.getClassLoader(),
new Class[] { downloadServiceListenerClass },
new JnlpDownloadListenerProxy(listener));
if (object == null) {
System.out.println("Object is null");
}
return object;
}
/**
* The InvocationHandler implementation.
*
* @param proxy
* The proxy.
* @param m
* The method.
* @param args
* The arguments to the method.
*/
public Object invoke(Object proxy, Method m, Object[] args)
throws Throwable {
Object result = null;
try {
if (m.getName().equals("progress")) {
listener.progress((URL) args[0], (String) args[1],
(Long) args[2], (Long) args[3], (Integer) args[4]);
} else if (m.getName().equals("validating")) {
listener.validating((URL) args[0], (String) args[1],
(Long) args[2], (Long) args[3], (Integer) args[4]);
} else if (m.getName().equals("upgradingArchive")) {
listener.upgradingArchive((URL) args[0], (String) args[1],
(Integer) args[2], (Integer) args[3]);
} else if (m.getName().equals("downloadFailed")) {
listener.downloadFailed((URL) args[0], (String) args[1]);
}
} catch (Exception e) {
throw new RuntimeException("unexpected invocation exception: "
+ e.getMessage());
}
return result;
}
}
In Listing 4 the actual JNLP implementation of a
DownloadListener is hidden. Instead of implementing the DownloadListener directly we
have provided an alternative interface (Listing 5 ) and adapter (Listing 6) to implement or extend. Why is this good? First, by
centralizing and hiding the JNLP implementation we leave open the possibility of swapping
in some future network launching protocol with minimal effort. Second, it now becomes
possible to "stub" out the JNLP implementation's DownloadService, if we chose,
in a manner which could mimic the download service at development time.
Listing 5: JnlpDownloadListener
package ca.ansir.jnlp;
import java.net.URL;
/**
* The <code>JnlpDownloadListener</code> interface.
* <p>
* Please feel free to use this code snippet (<code>JnlpDownloadListener</code>)
* in developing your own commercial or non-commercial applications, but please
* credit the author, Dan Andrews, for any portion of this code that you use,
* and provide a reference to <a href="http://www.ansir.ca">Ansir</a>. Thank
* you.
* </p>
*
* @author Dan Andrews
*/
public interface JnlpDownloadListener {
/**
* This method corresponds to the
* <code>javax.jnlp.DownloadServiceListener</code> object's method named
* <code>progress</code>.
*
* @param url
* The <code>URL</code> resource being downloaded.
*
* @param version
* The version of the resource.
*
* @param readSoFar
* Number of bytes read so far.
*
* @param total
* Number of bytes expected in the download, or -1 if not known.
*
* @param overallPercent
* The percentage, or -1 if not known.
*/
public void progress(URL url, String version, long readSoFar, long total,
int overallPercent);
/**
* This method corresponds to the
* <code>javax.jnlp.DownloadServiceListener</code> object's method named
* <code>validating</code>.
*
* @param url
* The <code>URL</code> resource downloaded.
*
* @param version
* The version of the resource.
*
* @param entry
* The current index in the total for validation.
*
* @param total
* The total number of downloads for validation.
*
* @param overallPercent
* The percentage, or -1 if not known.
*/
public void validating(URL url, String version, long entry, long total,
int overallPercent);
/**
* This method corresponds to the
* <code>javax.jnlp.DownloadServiceListener</code> object's method named
* <code>upgradingArchive</code>.
*
* @param url
* The <code>URL</code> resource being downloaded for the
* patch.
*
* @param version
* The version of the resource for the patch.
*
* @param patchPercent
* The patch percentage, or -1 if not known.
*
* @param overallPercent
* The overall percentage, or -1 if not known.
*/
public void upgradingArchive(URL url, String version, int patchPercent,
int overallPercent);
/**
* This method corresponds to the
* <code>javax.jnlp.DownloadServiceListener</code> object's method named
* <code>downloadFailed</code>.
*
* @param url
* The <code>URL</code> resource being downloaded that failed.
*
* @param version
* The version of the resource that failed.
*/
public void downloadFailed(URL url, String version);
}
Listing 6: JnlpDownloadAdapter
package ca.ansir.jnlp;
import java.net.URL;
/**
* The <code>JnlpDownloadAdapter</code> provides an adapter implementation of
* the <code>JnlpDownloadListener</code> interface.
* <p>
* Please feel free to use this code snippet (<code>JnlpDownloadAdapter</code>)
* in developing your own commercial or non-commercial applications, but please
* credit the author, Dan Andrews, for any portion of this code that you use,
* and provide a reference to <a href="http://www.ansir.ca">Ansir</a>. Thank
* you.
* </p>
*/
public class JnlpDownloadAdapter implements JnlpDownloadListener {
/**
* This method corresponds to the
* <code>javax.jnlp.DownloadServiceListener</code> object's method named
* <code>progress</code>.
*
* @param url
* The <code>URL</code> resource being downloaded.
*
* @param version
* The version of the resource.
*
* @param readSoFar
* Number of bytes read so far.
*
* @param total
* Number of bytes expected in the download, or -1 if not known.
*
* @param overallPercent
* The percentage, or -1 if not known.
*/
public void progress(URL url, String version, long readSoFar, long total,
int overallPercent) {
}
/**
* This method corresponds to the
* <code>javax.jnlp.DownloadServiceListener</code> object's method named
* <code>validating</code>.
*
* @param url
* The <code>URL</code> resource downloaded.
*
* @param version
* The version of the resource.
*
* @param entry
* The current index in the total for validation.
*
* @param total
* The total number of downloads for validation.
*
* @param overallPercent
* The percentage, or -1 if not known.
*/
public void validating(URL url, String version, long entry, long total,
int overallPercent) {
}
/**
* This method corresponds to the
* <code>javax.jnlp.DownloadServiceListener</code> object's method named
* <code>upgradingArchive</code>.
*
* @param url
* The <code>URL</code> resource being downloaded for the
* patch.
*
* @param version
* The version of the resource for the patch.
*
* @param patchPercent
* The patch percentage, or -1 if not known.
*
* @param overallPercent
* The overall percentage, or -1 if not known.
*/
public void upgradingArchive(URL url, String version, int patchPercent,
int overallPercent) {
}
/**
* This method corresponds to the
* <code>javax.jnlp.DownloadServiceListener</code> object's method named
* <code>downloadFailed</code>.
*
* @param url
* The <code>URL</code> resource being downloaded that failed.
*
* @param version
* The version of the resource that failed.
*/
public void downloadFailed(URL url, String version) {
}
}
In Listing 7 the DownloadService is partially stubbed out. The JnlpParts class below
provides an implementation which can be compiled without referencing the javaws.jar.
Listing 7: JnlpParts class
package ca.ansir.jnlp;
import java.lang.reflect.Method;
/**
* The <code>JnlpParts</code> provides a static wrapper methods for the jnlp
* DownloadService object. This code can be compiled without referencing the
* javaws.jar.
* <p>
* Please feel free to use this code snippet (<code>JnlpParts</code>) in
* developing your own commercial or non-commercial applications, but please
* credit the author, Dan Andrews, for any portion of this code that you use,
* and provide a reference to <a href="http://www.ansir.ca">Ansir</a>. Thank
* you.
* </p>
*
* @author Dan Andrews
*/
public final class JnlpParts {
/**
* Constructor - do not construct use static methods only.
*/
private JnlpParts() {
}
/**
* Determines if the named part is cached.
*
* @param partName
* The part name.
* @return True if is cached.
* @throws Exception
*/
public static final boolean isPartCached(String partName) throws Exception {
boolean cached = false;
Object downloadService = JnlpServices.getDownloadService();
Method isPartCachedMethod = downloadService.getClass().getMethod(
"isPartCached", new Class[] { String.class });
Boolean rv = (Boolean) isPartCachedMethod.invoke(downloadService,
new Object[] { partName });
cached = rv.booleanValue();
return cached;
}
/**
* Loads the named part without listening to the DownloadService.
*
* @param partName
* The part name.
* @throws Exception
*/
public static final void load(final String partName) throws Exception {
load(partName, new JnlpDownloadAdapter());
}
/**
* Loads the named part and provides callback to the
* <code>JnlpDownloadListener</code> object.
*
* @param partName
* The part name.
* @param listener
* The <code>JnlpDownloadListener</code> to callback.
* @throws Exception
*/
public static final void load(final String partName,
JnlpDownloadListener listener) throws Exception {
Object downloadService = JnlpServices.getDownloadService();
Object proxy = JnlpDownloadListenerProxy.newInstance(listener);
Method loadPart = downloadService.getClass().getMethod(
"loadPart",
new Class[] { String.class,
Class.forName("javax.jnlp.DownloadServiceListener") });
loadPart.invoke(downloadService, new Object[] { partName, proxy });
}
}
Before concluding this three part series we would like present a synthesis of the parts
to show how all three parts might be used in your deployment of a signed Java Web Start
application running in the "all-permissions" mode.
For a rich client application the first deployment step might be to show a licensing dialog and save the state of whether or not the user agrees to the licensing. Upon uninstalling and reinstalling the application the licensing dialog should be shown again. The preferences implementation (Listing 3) provides a mechanism for storing whether the user agrees, otherwise, the application terminates when the licensing dialog closes. If the user agrees then the licensing dialog is not shown the next time the application is started. When, however, the user uninstalls your application the agreed state is preserved by virtue of the preferences implementation.
Preserving the previous preferences is problematic in this case. It would be desired to show the licensing dialog again if the user reinstalls the application. In order to overcome the problem introduced above a dummy part is used to flag the reinstallation of the application. If the dummy part is not cached by Java Web Start, then any previous agreed state is negated and the part is loaded using the JnlpParts implementation (Listing 7). The logic of showing the licensing dialog is shown in the code below (Listing 8) and in the sample JNLP descriptor (Listing 9). Finally, what rich client application would be complete without a link back to the vendor site using the JnlpLaunchUrl implementation (Listing 2).
Listing 8: Showing the licensing dialog at development, build testing, and deployment.
// Assume JNLP present.
boolean jnlpPresent = true;
try {
Class.forName("javax.jnlp.ServiceManager");
System.setProperty("java.util.prefs.PreferencesFactory",
JnlpPreferencesFactory.class.getName());
} catch (Exception e) {
// Development or build test mode is the exception.
jnlpPresent = false;
}
try {
Preferences prefs = Preferences.userRoot();
// Note you need to manually remove preference to reshow again
// at development or build test times.
boolean accepted = prefs.getBoolean("acceptedLicense",
false);
if (accepted) {
if (jnlpPresent && !JnlpParts.isPartCached("flag")) {
// Application was removed so ensure license is shown again.
prefs.putBoolean("acceptedLicense", false);
accepted = false;
JnlpParts.load("flag");
}
}
if (!accepted) {
// Show your licensing dialog here and exit if not accepted.
// Otherwise remember accepted state and continue.
prefs.putBoolean("acceptedLicense", true);
}
} catch (Exception e) {
// Should not happen as preference impl exist whether or not JNLP is
// present.
}
Listing 9: JNLP descriptor showing use of flag part
<jnlp codebase="$$codebase" href="$$name">
<information>
<title>Your Title</title>
<vendor>Your Company</vendor>
<homepage href="$$codebase"/>
<description kind="short">Your product desription</description>
<description kind="tooltip">Your product tooltip</description>
<icon href="icon32x32.gif" width="32" height="32"/>
<icon kind="splash" href="splash.gif"/>
<offline-allowed />
</information>
<security>
<all-permissions />
</security>
<resources>
<j2se href="http://java.sun.com/products/autodl/j2se" version="1.5+"/>
<jar href="yourproduct.jar" main="true" />
<!-- Note flag.jar can be a dummy empty jar -->
<jar href="flag.jar" part="flag" download="lazy"/>
<package name="flag.*" part="flag" recursive="true"/>
</resources>
<application-desc main-class="your.package.StartUpClass" />
</jnlp>