JNLP-based applet launcher class for deploying applets that use extension libraries containing native code. : JNLP Web Start « Network Protocol « Java






JNLP-based applet launcher class for deploying applets that use extension libraries containing native code.

 
/*
 * $RCSfile: JNLPAppletLauncher.java,v $
 *
 * Copyright 2007 Sun Microsystems, Inc.  All Rights Reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * - Redistribution of source code must retain the above copyright
 *   notice, this list of conditions and the following disclaimer.
 *
 * - Redistribution in binary form must reproduce the above copyright
 *   notice, this list of conditions and the following disclaimer in
 *   the documentation and/or other materials provided with the
 *   distribution.
 *
 * Neither the name of Sun Microsystems, Inc. or the names of
 * contributors may be used to endorse or promote products derived
 * from this software without specific prior written permission.
 *
 * This software is provided "AS IS," without a warranty of any
 * kind. ALL EXPRESS OR IMPLIED CONDITIONS, REPRESENTATIONS AND
 * WARRANTIES, INCLUDING ANY IMPLIED WARRANTY OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE OR NON-INFRINGEMENT, ARE HEREBY
 * EXCLUDED. SUN MICROSYSTEMS, INC. ("SUN") AND ITS LICENSORS SHALL
 * NOT BE LIABLE FOR ANY DAMAGES SUFFERED BY LICENSEE AS A RESULT OF
 * USING, MODIFYING OR DISTRIBUTING THIS SOFTWARE OR ITS
 * DERIVATIVES. IN NO EVENT WILL SUN OR ITS LICENSORS BE LIABLE FOR
 * ANY LOST REVENUE, PROFIT OR DATA, OR FOR DIRECT, INDIRECT, SPECIAL,
 * CONSEQUENTIAL, INCIDENTAL OR PUNITIVE DAMAGES, HOWEVER CAUSED AND
 * REGARDLESS OF THE THEORY OF LIABILITY, ARISING OUT OF THE USE OF OR
 * INABILITY TO USE THIS SOFTWARE, EVEN IF SUN HAS BEEN ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGES.
 *
 * You acknowledge that this software is not designed, licensed or
 * intended for use in the design, construction, operation or
 * maintenance of any nuclear facility.
 *
 * $Revision: 1.29 $
 * $Date: 2008/10/21 21:33:50 $
 * $State: Exp $
 */


import java.applet.Applet;
import java.applet.AppletContext;
import java.applet.AppletStub;
import java.awt.BorderLayout;
import java.awt.event.ActionListener;
import java.awt.event.MouseEvent;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLConnection;
import java.nio.channels.FileChannel;
import java.nio.channels.FileLock;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.cert.Certificate;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Properties;
import java.util.Set;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.logging.Logger;
import javax.swing.ImageIcon;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JProgressBar;
import javax.swing.SwingUtilities;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;

/**
 * The JNLPAppletLauncher is a general purpose JNLP-based applet
 * launcher class for deploying applets that use extension libraries
 * containing native code. It allows applets to use extensions like
 * Java 3D, JOGL, and JOAL very easily, with just a few additional
 * parameters to the <code>&lt;applet&gt;</code> tag, on Java SE
 * versions as far back as 1.4.2.
 *
 * <p>
 *
 * Like Java Web Start, the JNLPAppletLauncher uses an extension's
 * .jnlp file to locate the native resources for a given extension.
 * The applet developer only needs to specify the platform-independent
 * .jar files containing the .class files for the extension. The
 * platform-specific "nativelib" .jar files are downloaded
 * automatically from the same server that hosts the extension's Java
 * Web Start binaries.
 *
 * <p>
 *
 * Extensions that support JNLPAppletLauncher include Java 3D, JOGL,
 * and JOAL. More can be added without needing to modify the
 * JNLPAppletLauncher. See the section below on <a
 * href="#MODIFYING">modifying extensions to work with the
 * JNLPAppletLauncher</a>.
 *
 * <h2> How to Deploy Applets Using the JNLPAppletLauncher </h2>
 * <p>
 *
 * The <code>applet-launcher.jar</code> file containing the
 * JNLPAppletLauncher class must be signed with the same certificate
 * as the extension's native resources, for example "sun microsystems,
 * inc.". The user will receive a security dialog and will be prompted
 * to accept the certificate for the JLNPAppletLauncher.  The applet
 * being deployed may be either signed or unsigned; if it is unsigned,
 * it runs inside the security sandbox, and if it is signed, the user
 * receives a security dialog to accept the certificate for the applet
 * (in addition to the applet-launcher jar, if it is signed by a
 * different entity).
 *
 * <p>
 *
 * The steps for deploying such applets are straightforward. First,
 * the <code>archive</code> parameter to the applet tag must contain
 * <code>applet-laucher.jar</code>, the extension .jar files, and any
 * jar files associated with your applet. See the section on <a
 * href="#ORGANIZING">organizing jar files</a> for more details.
 *
 * <p>
 *
 * Second, the name of your applet's main class and a textual
 * description must be specified via the applet tag parameters
 * <code>subapplet.classname</code> and
 * <code>subapplet.displayname</code>.
 *
 * <p>
 *
 * Finally, the URLs for the extension .jnlp files being used must be
 * specified as parameters. The <code>jnlpNumExtensions</code>
 * parameter indicates the number of JNLP files that are referenced,
 * and for <code>n</code> such files, their URLs are passed in as
 * parameters <code>jnlpExtension1</code> ...
 * <code>jnlpExtension[n]</code>.
 *
 * <h2><a name="ORGANIZING">Organizing Jar Files</a></h2>
 *
 * <p>
 *
 * Traditionally, applets are specified with a codebase and an archive
 * parameter, the latter which is a list of jar files relative to that
 * codebase. The codebase is optional and defaults to the directory on
 * the web server containing the HTML document which contains the
 * applet tag. See the documentation for the <a
 * href="http://java.sun.com/j2se/1.4.2/docs/guide/misc/applet.html">applet
 * tag</a>.
 *
 * <p>
 *
 * It is not well documented, but at least in the Sun JRE at least as
 * far back as Java SE 1.4.2, it is possible to use absolute URLs in
 * the applet tag's archive parameter. This functionality works on all
 * major operating systems: Windows, Mac OS X, Linux, and Solaris.
 * This means that you can pull code resources from multiple web
 * servers, not just one, in similar fashion to Java Web Start and its
 * extension mechanism. (The security implications are that each
 * unsigned piece of code downloaded from a separate server receives
 * sandboxed permissions to connect back to that server; if there are
 * multiple pieces of unsigned code on the stack during execution of
 * the program then the permissions will be the intersection of all of
 * those on the stack, implying that no programmatic network
 * connections back to the web server(s) will be allowed. See the <a
 * href="http://java.sun.com/sfaq/">Applet Security FAQ</a> for more
 * details.)
 *
 * <p>
 *
 * This capability means that your applets can refer directly to
 * extensions like Java 3D and JOGL hosted on Sun's web servers
 * without having to duplicate their jar files on your web server.
 *
 * <p>
 *
 * To use this capability effectively with the JNLPAppletLauncher, you
 * need to pull in at least three primary pieces of code: the applet
 * launcher itself, your applet's code, and the Java code for the
 * extension, or extensions, your applet depends on. (Remember that
 * the JNLPAppletLauncher's primary function is to automatically
 * download the native code associated with these extensions, and not
 * the Java code for these extensions.)
 *
 * <p>
 *
 * You might choose to specify the codebase of your applet to point to
 * your web server's directory containing the jar files of your
 * applet, and specify absolute URLs to the
 * <code>applet-launcher.jar</code> and the extension jar files. Or
 * you might decide to point the codebase to the server which hosts
 * the applet launcher and specify all of the other resources,
 * including your applet, with absolute URLs. Or you might decide to
 * use all absolute URLs in your archive tag with no codebase. The
 * techniques are basically equivalent. We recommend either pointing
 * the codebase to the directory containing your applet's jars, using
 * relative URLs for your applet, and using absolute URLs for all
 * other resources; or using all absolute URLs.
 *
 * <p>
 *
 * Alternatively, you can re-host the jar files and/or JNLP files and
 * nativelib jars for the extensions you use on your own web
 * server. This has the advantage that your applet will connect to
 * fewer web servers upon startup, but has the disadvantages of
 * requiring additional maintenance on your part and not automatically
 * receiving updates to the extensions when they are published.
 *
 * <p>
 *
 * <b>Note</b> that if you are planning to call into your applet from
 * JavaScript that there are some <a
 * href="#SCRIPTING">scripting-related caveats</a> that you need to be
 * aware of.
 *
 * <p>
 *
 * The <code>jnlpExtension</code> parameters passed to the
 * JNLPAppletLauncher must be specified with absolute URLs.
 *
 * <p>
 *
 * The <a href="#EXAMPLES">examples</a> show how to use the
 * JNLPAppletLauncher in a few different scenarios.
 *
 * <h2>The codebase_lookup parameter</h2>
 *
 * <p>
 *
 * This applet parameter was not well documented until <a
 * href="http://java.sun.com/javase/6/docs/technotes/guides/plugin/developer_guide/special_attributes.html#codebase">recently</a>,
 * but it disables certain legacy behavior of the Java Plug-In. Before
 * the introduction of jar files, applets used to host their class
 * files and resources as flat files on the web server. Once jar files
 * were introduced, it was possible to improve the efficiency of
 * resource loading for applets, but (apparently) not without breaking
 * compatibility. An applet can specify the parameter
 *
 * <pre>
 * &lt;param name="codebase_lookup" value="false"&gt;
 * </pre>
 *
 * <p>
 *
 * to improve efficiency of its loading if it does not rely on
 * fetching flat files from the web server off the codebase.
 * We recommend setting this parameter.
 *
 * <h2>Applets using the OpenGL(r) 3D API</h2>
 *
 * <p>
 *
 * Applets using the OpenGL 3D graphics API, for example through JOGL
 * or Java 3D, may encounter robustness issues on the Windows platform
 * because Sun's Java 2D implementation on Windows uses Microsoft's
 * DirectDraw API. DirectDraw and OpenGL are incompatible at the
 * driver level.
 *
 * <p>
 *
 * As a workaround for this problem, the JNLPAppletLauncher supports
 * disabling the use of DirectDraw. Currently this can only be done on
 * a global basis, for all applets, but doing so is unlikely to slow
 * down other non-3D applets significantly.
 *
 * <p>
 *
 * Specifying the applet parameter
 *
 * <pre>
 * &lt;param name="noddraw.check" value="true"&gt;
 * </pre>
 *
 * <p>
 *
 * will cause the applet launcher, when run on Windows, to check to
 * see whether DirectDraw is enabled and, if so, will prompt the user
 * with a dialog box asking to disable it. A browser restart is
 * required if the setting is changed.
 *
 * <p>
 *
 * If the dialog box is undesirable in a given situation, you can
 * force the noddraw check to always disable DirectDraw with the two
 * applet parameters:
 *
 * <pre>
 * &lt;param name="noddraw.check" value="true"&gt;
 * &lt;param name="noddraw.check.silent" value="true"&gt;
 * </pre>
 *
 * <p>
 *
 * In this case it will not be obvious to the end user that a browser
 * restart might be required for best robustness, but you could
 * potentially document the need to try restarting the browser in case
 * of instability.
 *
 * <h2><a name="SCRIPTING">Scripting Support</a></h2>
 *
 * <p>
 *
 * The JNLPAppletLauncher supports interaction with the sub-applet via
 * the <code>getSubApplet()</code> method. Calling this method from
 * JavaScript will return the subordinate applet with which you can
 * then interact via JavaScript.
 *
 * <p>
 *
 * There are currently some scripting-related caveats associated with
 * <a href="#ORGANIZING">pulling jar files from multiple locations</a>
 * for a particular applet. In particular, it appears that the
 * LiveConnect security model on Mac OS X in the Safari browser
 * prohibits JavaScript from one domain from communicating with Java
 * code (such as an applet) downloaded from another domain. This is
 * correct according to older versions of the LiveConnect
 * specification, although some more recent implementations of
 * LiveConnect allow this, restricting the privileges of such calls in
 * other ways.
 *
 * <p>
 *
 * The workaround for this problem seems to be to host the
 * <code>applet-launcher.jar</code> on your web site if you need to
 * talk to your applet from JavaScript. Your applet's jars will likely
 * also need to be hosted from the same web server. If you talk to
 * extension APIs in your <code>archive</code> tag directly from
 * JavaScript, you may find it necessary to host those jars on your
 * web server as well.
 *
 * <p>
 *
 * From a practical standpoint, most applet developers using
 * JavaScript with the JNLPAppletLauncher will only need to re-host at
 * most <code>applet-launcher.jar</code> on their web site.
 *
 * <h2><a name="EXAMPLES">Examples</a></h2>
 *
 * <p>
 *
 * An applet using JOGL as an extension. Note that this example does
 * not specify a codebase, instead specifying all of its archive tag
 * elements with absolute URLs (split here for readability; in a real
 * applet tag they must be all on one line). Note also the use of the
 * <code>noddraw.check</code> parameter to disable the use of
 * DirectDraw since using JOGL implies the use of OpenGL.
 *
 * <pre>
 * &lt;applet code="org.jdesktop.applet.util.JNLPAppletLauncher"
 *      width=600
 *      height=400
 *      archive="http://download.java.net/media/applet-launcher/applet-launcher.jar,
 *               http://download.java.net/media/jogl/builds/archive/jsr-231-webstart-current/jogl.jar,
 *               http://download.java.net/media/gluegen/webstart/gluegen-rt.jar,
 *               http://download.java.net/media/jogl/builds/archive/jsr-231-webstart-current/jogl-demos.jar"&gt;
 *   &lt;param name="codebase_lookup" value="false"&gt;
 *   &lt;param name="subapplet.classname" value="demos.applets.GearsApplet"&gt;
 *   &lt;param name="subapplet.displayname" value="JOGL Gears Applet"&gt;
 *   &lt;param name="noddraw.check" value="true"&gt;
 *   &lt;param name="progressbar" value="true"&gt;
 *   &lt;param name="jnlpNumExtensions" value="1"&gt;
 *   &lt;param name="jnlpExtension1"
 *          value="http://download.java.net/media/jogl/builds/archive/jsr-231-webstart-current/jogl.jnlp"&gt;
 * &lt;/applet&gt;
 * </pre>
 *
 * <p>
 *
 * An applet using both JOGL and JOAL as extensions. Note again that
 * all code resources are specified with absolute URLs. In this
 * example the unsigned applet pulls in code from both
 * <code>jogl-demos.jar</code> and <code>joal-demos.jar</code>. Note
 * again the use of the <code>noddraw.check</code> parameter.
 *
 * <pre>
 * &lt;applet code="org.jdesktop.applet.util.JNLPAppletLauncher"
 *      width=600
 *      height=400
 *      archive="http://download.java.net/media/applet-launcher/applet-launcher.jar,
 *               http://download.java.net/media/jogl/builds/archive/jsr-231-webstart-current/jogl.jar,
 *               http://download.java.net/media/gluegen/webstart/gluegen-rt.jar,
 *               http://download.java.net/media/jogl/builds/archive/jsr-231-webstart-current/jogl-demos.jar,
 *               http://download.java.net/media/joal/webstart/joal.jar,
 *               http://download.java.net/media/joal/webstart/joal-demos.jar"&gt;
 *   &lt;param name="codebase_lookup" value="false"&gt;
 *   &lt;param name="subapplet.classname" VALUE="demos.applets.GearsJOALApplet"&gt;
 *   &lt;param name="subapplet.displayname" VALUE="JOGL / JOAL Gears Applet"&gt;
 *   &lt;param name="noddraw.check" value="true"&gt;
 *   &lt;param name="progressbar" value="true"&gt;
 *   &lt;param name="jnlpNumExtensions" value="2"&gt;
 *   &lt;param name="jnlpExtension1"
 *          value="http://download.java.net/media/jogl/builds/archive/jsr-231-webstart-current/jogl.jnlp"&gt;
 *   &lt;param name="jnlpExtension2"
 *          value="http://download.java.net/media/joal/webstart/joal.jnlp"&gt;
 * &lt;/applet&gt;
 * </pre>
 *
 * <p>
 * An applet using Java 3D as an extension:
 *
 * <pre>
 * &lt;applet code="org.jdesktop.applet.util.JNLPAppletLauncher"
 *      width=400
 *      height=300
 *      archive="myapplet.jar,
 *               http://download.java.net/media/applet-launcher/applet-launcher.jar,
 *               http://download.java.net/media/java3d/webstart/release/j3d/latest/j3dcore.jar,
 *               http://download.java.net/media/java3d/webstart/release/j3d/latest/j3dutils.jar,
 *               http://download.java.net/media/jogl/builds/archive/jsr-231-webstart-current/jogl.jar,
 *               http://download.java.net/media/gluegen/webstart/gluegen-rt.jar,
 *               http://download.java.net/media/java3d/webstart/release/vecmath/latest/vecmath.jar"&gt;
 *   &lt;param name="codebase_lookup" value="false"&gt;
 *   &lt;param name="subapplet.classname" value="mypkg.MyApplet"&gt;
 *   &lt;param name="subapplet.displayname" value="My Java 3D Applet"&gt;
 *   &lt;param name="jnlpNumExtensions" value="1"&gt;
 *   &lt;param name="jnlpExtension1" value="http://download.java.net/media/java3d/webstart/release/java3d-latest.jnlp"&gt;
 *   &lt;param name="progressbar" value="true"&gt;
 *   &lt;param name="noddraw.check" value="true"&gt;
 * &lt;/applet&gt;
 * </pre>
 *
 * <p>
 * Note that the JOGL jar files are also included in this example. This is
 * necessary in order to run on Mac OS X, for which Java 3D always uses
 * JOGL to render.
 *
 * <h2> Locations of Standard Extensions </h2>
 *
 * <p>
 *
 * This section describes how to set up the <code>archive</code> and
 * <code>jnlpExtension</code> parameters for a few standard
 * extensions.
 *
 * <h4>JNLPAppletLauncher</h4>
 *
 * <p>
 *
 * The master jar file for the JNLPAppletLauncher is located at the following URL:
 * <pre>
 * http://download.java.net/media/applet-launcher/applet-launcher.jar
 * </pre>
 *
 * <p>
 *
 * This jar needs to be added to your archive parameter.
 *
 * <h4>Java 3D</h4>
 *
 * <p>
 *
 * Java 3D 1.5.1 and later supports the
 * JNLPAppletLauncher. You will need to add the following URLs to your
 * archive parameter:
 *
 * <pre>
 * http://download.java.net/media/java3d/webstart/release/j3d/latest/j3dcore.jar
 * http://download.java.net/media/java3d/webstart/release/j3d/latest/j3dutils.jar
 * http://download.java.net/media/java3d/webstart/release/vecmath/latest/vecmath.jar
 * </pre>
 *
 * Then add the following to one of your <code>jnlpExtension</code> parameters:
 *
 * <pre>
 * http://download.java.net/media/java3d/webstart/release/java3d-latest.jnlp
 * </pre>
 *
 * If you want to deploy your applet on Mac OS X, you will also need to
 * include JOGL by adding the following URLs to your archive parameter:
 *
 * <pre>
 * http://download.java.net/media/jogl/builds/archive/jsr-231-webstart-current/jogl.jar
 * http://download.java.net/media/gluegen/webstart/gluegen-rt.jar
 * </pre>
 *
 * Note that this will only work if Java 3D is not installed into the JRE as an
 * extension. Since Apple ships their JRE with Java 3D pre-installed, the
 * end-user must uninstall Java 3D in order for Java 3D applets to work on Mac.
 * <p>
 *
 * Note that the Java 3D .jnlp extension will automatically pull in the
 * native code associated with JOGL and the GlueGen runtime, so you don't have
 * to separately refer to those .jnlp files.
 *
 * <h4>JOGL</h4>
 *
 * <p>
 *
 * JOGL 1.1.1-rc3 and later support the JNLPAppletLauncher. You will
 * need to add the following URL to your archive parameter:
 *
 * <pre>
 * http://download.java.net/media/jogl/builds/archive/jsr-231-webstart-current/jogl.jar
 * </pre>
 *
 * <p>
 *
 * Because JOGL depends on the GlueGen runtime, you will also need to
 * add the following URL to your archive parameter:
 *
 * <pre>
 * http://download.java.net/media/gluegen/webstart/gluegen-rt.jar
 * </pre>
 *
 * <p>
 *
 * Finally, add the following to one of your
 * <code>jnlpExtension</code> parameters:
 *
 * <pre>
 * http://download.java.net/media/jogl/builds/archive/jsr-231-webstart-current/jogl.jnlp
 * </pre>
 *
 * <p>
 *
 * Note that the jogl.jnlp extension will automatically pull in the
 * native code associated with the GlueGen runtime, so you don't have
 * to separately refer to the gluegen-rt.jnlp file.
 *
 * <h4>JOAL</h4>
 *
 * <p>
 *
 * JOAL 1.1.1 and later support the JNLPAppletLauncher. You will need
 * to add the following URL to your archive parameter:
 *
 * <pre>
 * http://download.java.net/media/joal/webstart/joal.jar
 * </pre>
 *
 * <p>
 *
 * Because JOAL, like JOGL, depends on the GlueGen runtime, you will
 * also need to add the following URL to your archive parameter:
 *
 * <pre>
 * http://download.java.net/media/gluegen/webstart/gluegen-rt.jar
 * </pre>
 *
 * <p>
 *
 * (If you are using both JOGL and JOAL, you only need to refer to
 * gluegen-rt.jar once in your archive parameter.)
 *
 * <p>
 *
 * Finally, add the following to one of your
 * <code>jnlpExtension</code> parameters:
 *
 * <pre>
 * http://download.java.net/media/joal/webstart/joal.jnlp
 * </pre>
 *
 * <p>
 *
 * Note that the joal.jnlp extension will automatically pull in the
 * native code associated with the GlueGen runtime, so you don't have
 * to separately refer to the gluegen-rt.jnlp file.
 *
 * <h2><a name="MODIFYING">Modifying Your Extension To Work With The JNLPAppletLauncher</a></h2>
 *
 * <p>
 *
 * If you are the author of an extension like JOGL which requires some
 * native code, with only a simple code change you can make your
 * extension work with the JNLPAppletLauncher. Simply add the
 * following method somewhere in your code:
 *
 * <pre>
 *  private static void loadLibraryInternal(String libraryName) {
 *      String sunAppletLauncher = System.getProperty("sun.jnlp.applet.launcher");
 *      boolean usingJNLPAppletLauncher =
 *          Boolean.valueOf(sunAppletLauncher).booleanValue();
 *
 *      boolean loaded = false;
 *      if (usingJNLPAppletLauncher) {
 *          try {
 *              Class jnlpAppletLauncherClass =
 *                  Class.forName("org.jdesktop.applet.util.JNLPAppletLauncher");
 *              Method jnlpLoadLibraryMethod =
 *                  jnlpAppletLauncherClass.getDeclaredMethod("loadLibrary",
 *                                                            new Class[] { String.class });
 *              jnlpLoadLibraryMethod.invoke(null, new Object[] { libraryName });
 *              loaded = true;
 *          } catch (ClassNotFoundException ex) {
 *              System.err.println("loadLibrary(" + libName + ")");
 *              System.err.println(ex);
 *              System.err.println("Attempting to use System.loadLibrary instead");
 *          } catch (Exception e) {
 *              Throwable t = e;
 *              if (t instanceof InvocationTargetException) {
 *                  t = ((InvocationTargetException) t).getTargetException();
 *              }
 *              if (t instanceof Error)
 *                  throw (Error) t;
 *              if (t instanceof RuntimeException) {
 *                  throw (RuntimeException) t;
 *              }
 *              // Throw UnsatisfiedLinkError for best compatibility with System.loadLibrary()
 *              throw (UnsatisfiedLinkError) new UnsatisfiedLinkError().initCause(e);
 *          }
 *      }
 *
 *      if (!loaded) {
 *          System.loadLibrary(libraryName);
 *      }
 *  }
 * </pre>
 *
 * <p>
 *
 * and wherever you would call <code>System.loadLibrary()</code> (from
 * within an <code>AccessController.doPrivileged()</code> block) to
 * load your extension's native code, call the above
 * <code>loadLibraryInternal</code> method instead.
 *
 * <p>
 *
 * Note again that because the <code>applet-launcher.jar</code> and
 * the nativelib jars for all extensions must currently be signed with
 * the same certificate, this implies that you must resign both the
 * applet launcher as well as any other extensions your applet relies
 * on (unless yours is a Sun-standard extension and can be signed with
 * Sun's code signing certificate).
 *
 * <h2>Acknowledgments</h2>
 *
 * <p>
 *
 * The JNLPAppletLauncher was developed by Kevin Rushforth, Kenneth
 * Russell, and Chien Yang. It is based on the earlier
 * JOGLAppletLauncher developed by Lilian Chamontin.
 */

public class JNLPAppletLauncher extends Applet {

    private static final boolean VERBOSE = false;
    private static final boolean DEBUG = false;

    // Indicated that the applet was successfully initialized
    private boolean isInitOk = false;

    // True the first time start is called, false afterwards
    private boolean firstStart = true;

    // Indicates that the applet was started successfully
    private boolean appletStarted = false;

    // The applet we have to start
    private Applet subApplet;

    // Class name of applet to load (required)
    private String subAppletClassName; // from applet PARAM subapplet.classname

    // String representing the name of the applet (optional)
    private String subAppletDisplayName; // from applet PARAM subapplet.displayname

    // URL to an image that we will display while installing (optional)
    private URL subAppletImageURL; // from applet PARAM subapplet.image

    // Panel that will hold the splash-screen image and progress bar while loading
    private JPanel loaderPanel;

    // Helpers for updating deployment.properties with -Dsun.java2d.noddraw=true
    private static final String JRE_PREFIX = "deployment.javapi.jre.";
    private static final String NODDRAW_PROP = "-Dsun.java2d.noddraw=true";
    private static final String DONT_ASK = ".dont_ask";

    // Optional progress bar
    private JProgressBar progressBar = null;

    /*
     * The following variables are defined per-applet, but we can assert that
     * they will not differ for each applet that is loaded by the same
     * ClassLoader. This means we can just cache the values from the first
     * applet. We will check the values for subsequent applets and throw an
     * exception if there are any differences.
     */

    // Flag indicating that this is the first applet
    private static boolean firstApplet = true;

    // List of extension JNLP files. This is saved for the first applet
    // and verified for each subsequent applet.
    private static List/*<URL>*/ jnlpExtensions = null;

    // Code base and archive tag for all applets that use the same ClassLoader
    private static URL codeBase;
    private static String archive = null;

    // Persistent cache directory for storing native libraries and time stamps.
    // The directory is of the form:
    //
    //     ${user.home}/.jnlp-applet/cache/<HOSTNAME>/<DIGEST-OF-CODEBASE-ARCHIVE>
    //
    private static File cacheDir;

    // Set of jar files specified in the JNLP files.
    // Currently unused.
    private static Set/*<URL>*/ jarFiles;

    // Set of native jar files to be loaded. We need to download these
    // native jars, verify the signatures, verify the security certificates,
    // and extract the native libraries from each jar.
    private static Set/*<URL>*/ nativeJars;

    // Native library prefix (e.g., "lib") and suffix (e.g. ".dll" or ".so")
    private static String nativePrefix;
    private static String nativeSuffix;

    // A HashMap of native libraries that can be loaded with System.load()
    // The key is the string name of the library as passed into the loadLibrary
    // call; it is the file name without the directory or the platform-dependent
    // library prefix and suffix. The value is the absolute path name to the
    // unpacked library file in nativeTmpDir.
    private static Map/*<String, String>*/ nativeLibMap;

    /*
     * The following variables are per-ClassLoader static globals.
     */

    // Flag indicating that we got a fatal error in the static initializer.
    // If this happens we will not attempt to start any applets.
    private static boolean staticInitError = false;

    // Base temp directory used by JNLPAppletLauncher. This is set to:
    //
    // ${java.io.tmpdir}/jnlp-applet
    //
    private static File tmpBaseDir;

    // String representing the name of the temp root directory relative to the
    // tmpBaseDir. Its value is "jlnNNNNN", which is the unique filename created
    // by File.createTempFile() without the ".tmp" extension.
    //
    private static String tmpRootPropValue;

    // Root temp directory for this JVM instance. Used to store the individual,
    // per-ClassLoader directories that will be used to load native code. The
    // directory name is:
    //
    // <tmpBaseDir>/<tmpRootPropValue>
    //
    // Old temp directories are cleaned up the next time a JVM is launched that
    // uses JNLPAppletLauncher.
    //
    private static File tmpRootDir;

    // Temporary directory for loading native libraries for this instance of
    // the class loader. The directory name is:
    //
    // <tmpRootDir>/jlnMMMMM
    //
    // where jlnMMMMM is the unique filename created by File.createTempFile()
    // without the ".tmp" extension.
    //
    private static File nativeTmpDir;

    /*
     * IMPLEMENTATION NOTES
     *
     * Assumptions:
     *
     * A. Multiple applets can be launched from the same class loader, and thus
     *    share the same set of statics and same set of native library symbols.
     *    This can only happen if the codebase and set of jar files as specified
     *    in the archive tag are identical. Applets launched from different code
     *    bases or whose set of jar files are different will always get a
     *    different ClassLoader. If this assumption breaks, too many other
     *    things wouldn't work properly, so we can be assured that it will hold.
     *    However, we cannot assume that the converse is true; it is possible
     *    that two applets with the same codebase and archive tag will be loaded
     *    from a different ClassLoader.
     *
     * B. Given the above, this means that we must store the native libraries,
     *    and keep track of which ones have already been loaded statically, that
     *    is, per-ClassLoader rather than per-Applet. This is a good thing,
     *    because it turns out to be difficult (at best) to find the instance of
     *    the Applet at loadLibrary time.
     *
     * Our solution is as follows:
     *
     *    Use the same criteria for determining the cache dir that JPI
     *    uses to determine the class loader to use. More precisely, we will
     *    create a directory based on the codebase and complete set of jar files
     *    specified by the archive tag. To support the case where each applet is
     *    in a unique class loader, we will copy the native libraries into a
     *    unique-per-ClassLoader temp directory and do the System.load() from
     *    there. For a robust solution, we need to lock the cache directory
     *    during validation, since multiple threads, or even multiple processes,
     *    can access it concurrently.
     *
     * TODO: We need a way to clear the cache.
     *
     * We also considered, but rejected, the following solutions:
     *
     * 1. Use a temporary directory for native jars, download, verify, unpack,
     *    and loadLibrary in this temp dir. No persistent cache.
     *
     * 2. Cache the native libraries in a directory based on the codebase and
     *    the extension jars (i.e., the subset of the jars in the archive tag
     *    that also appear in one of the extension JNLP files). Copy the native
     *    libraries into a unique-per-ClassLoader temp directory and load from
     *    there. Note that this has the potential problem of violating the
     *    assertion that two different applets that share the same ClassLoader
     *    must map to the same cache directory.
     *
     * 3. Use the exact criteria for determining the cache dir that JPI
     *    uses to determine which class loader to use (as in our proposed
     *    solution above), unpack the native jars into the cache directory and
     *    load from there. This obviates the need for locking, but it will break
     *    if the JPI ever isolates each applet into its own ClassLoader.
     */

    /**
     * Constructs an instance of the JNLPAppletLauncher class. This is called by
     * Java Plug-in, and should not be called directly by an application or
     * applet.
     */
    public JNLPAppletLauncher() {
    }

    /* @Override */
    public void init() {
        if (VERBOSE) {
            System.err.println();
        }
        if (DEBUG) {
            System.err.println("Applet.init");
        }

        if (staticInitError) {
            return;
        }

        subAppletClassName = getParameter("subapplet.classname");
        if (subAppletClassName == null) {
            displayError("Init failed : Missing subapplet.classname parameter");
            return;
        }

        subAppletDisplayName = getParameter("subapplet.displayname");
        if (subAppletDisplayName == null) {
            subAppletDisplayName = "Applet";
        }

        subAppletImageURL = null;
        try {
            String subAppletImageStr = getParameter("subapplet.image");
            if (subAppletImageStr != null && subAppletImageStr.length() > 0) {
                subAppletImageURL = new URL(subAppletImageStr);
            }
        } catch (IOException ex) {
            ex.printStackTrace();
            // Continue with a null subAppletImageURL
        }

        if (DEBUG) {
            System.err.println("subapplet.classname = " + subAppletClassName);
            System.err.println("subapplet.displayname = " + subAppletDisplayName);
            if (subAppletImageURL != null) {
                System.err.println("subapplet.image = " + subAppletImageURL.toExternalForm());
            }
        }

        initLoaderLayout();

        isInitOk = true;
    }

    /* @Override */
    public void start() {
        if (DEBUG) {
            System.err.println("Applet.start");
        }

        if (isInitOk) {
            if (firstStart) { // first time
                firstStart = false;

                Thread startupThread = new Thread() {
                    public void run() {
                        initAndStartApplet();
                    }
                };
                startupThread.setName("AppletLauncher-Startup");
                startupThread.setPriority(Thread.NORM_PRIORITY - 1);
                startupThread.start();
            } else if (appletStarted) {
                checkNoDDrawAndUpdateDeploymentProperties();

                // We have to start again the applet (start can be called multiple times,
                // e.g once per tabbed browsing
                subApplet.start();
            }
        }

    }

    /* @Override */
    public void stop(){
        if (subApplet != null) {
            subApplet.stop();
        }
    }

    /* @Override */
    public void destroy(){
        if (subApplet != null) {
            subApplet.destroy();
        }
    }

    /** Helper method to make it easier to call methods on the
        sub-applet from JavaScript. */
    public Applet getSubApplet() {
        return subApplet;
    }

    //----------------------------------------------------------------------
    // Support for forwarding notifications about dragged-out applets
    //

    public void appletDragStarted() {
        try {
            Method m = getSubApplet().getClass().getMethod("appletDragStarted", null);
            m.invoke(getSubApplet(), null);
        } catch (Throwable t) {
        }
    }

    public void appletDragFinished() {
        try {
            Method m = getSubApplet().getClass().getMethod("appletDragFinished", null);
            m.invoke(getSubApplet(), null);
        } catch (Throwable t) {
        }
    }

    public void appletRestored() {
        try {
            Method m = getSubApplet().getClass().getMethod("appletRestored", null);
            m.invoke(getSubApplet(), null);
        } catch (Throwable t) {
        }
    }

    public boolean isAppletDragStart(MouseEvent e) {
        try {
            Method m = getSubApplet().getClass().getMethod("isAppletDragStart",
                                                           new Class[] { MouseEvent.class });
            return ((Boolean) m.invoke(getSubApplet(), new Object[] { e })).booleanValue();
        } catch (Throwable t) {
            // Throw an exception back to the Java Plug-In to cause it
            // to use the default functionality
            throw new RuntimeException(t);
        }
    }

    public void setAppletCloseListener(ActionListener l) {
        try {
            Method m = getSubApplet().getClass().getMethod("setAppletCloseListener",
                                                           new Class[] { ActionListener.class });
            m.invoke(getSubApplet(), new Object[] { l });
        } catch (Throwable t) {
            // Throw an exception back to the Java Plug-In to cause it
            // to use the default functionality
            throw new RuntimeException(t);
        }
    }

    /**
     * This method is called by the static initializer to create / initialize
     * the temp root directory that will hold the temp directories for this
     * instance of the JVM. This is done as follows:
     *
     *     1. Synchronize on a global lock. Note that for this purpose we will
     *        use System.out in the absence of a true global lock facility.
     *        We are careful not to hold this lock too long.
     *
     *     2. Check for the existence of the "jnlp.applet.launcher.tmproot"
     *        system property.
     *
     *         a. If set, then some other thread in a different ClassLoader has
     *            already created the tmprootdir, so we just need to
     *            use it. The remaining steps are skipped.
     *
     *         b. If not set, then we are the first thread in this JVM to run,
     *            and we need to create the the tmprootdir.
     *
     *     3. Create the tmprootdir, along with the appropriate locks.
     *        Note that we perform the operations in the following order,
     *        prior to creating tmprootdir itself, to work around the fact that
     *        the file creation and file lock steps are not atomic, and we need
     *        to ensure that a newly-created tmprootdir isn't reaped by a
     *        concurrently running JVM.
     *
     *            create jlnNNNN.tmp using File.createTempFile()
     *            lock jlnNNNN.tmp
     *            create jlnNNNN.lck while holding the lock on the .tmp file
     *            lock jlnNNNN.lck
     *
     *        Since the Reaper thread will enumerate the list of *.lck files
     *        before starting, we can guarantee that if there exists a *.lck file
     *        for an active process, then the corresponding *.tmp file is locked
     *        by that active process. This guarantee lets us avoid reaping an
     *        active process' files.
     *
     *     4. Set the "jnlp.applet.launcher.tmproot" system property.
     *
     *     5. Add a shutdown hook to cleanup jlnNNNN.lck and jlnNNNN.tmp. We
     *        don't actually expect that this shutdown hook will ever be called,
     *        but the act of doing this, ensures that the locks never get
     *        garbage-collected, which is necessary for correct behavior when
     *        the first ClassLoader is later unloaded, while subsequent Applets
     *        are still running.
     *
     *     6. Start the Reaper thread to cleanup old installations.
     */
    private static void initTmpRoot() throws IOException {
        if (VERBOSE) {
            System.err.println("---------------------------------------------------");
        }

        synchronized (System.out) {

            // Get the name of the tmpbase directory.
            String tmpBaseName = System.getProperty("java.io.tmpdir") +
                    File.separator + "jnlp-applet";
            tmpBaseDir = new File(tmpBaseName);

            // Get the value of the tmproot system property
            final String tmpRootPropName = "jnlp.applet.launcher.tmproot";

            tmpRootPropValue = System.getProperty(tmpRootPropName);

            if (tmpRootPropValue == null) {
                // Create the tmpbase directory if it doesn't already exist
                tmpBaseDir.mkdir();
                if (!tmpBaseDir.isDirectory()) {
                    throw new IOException("Cannot create directory " + tmpBaseDir);
                }

                // Create ${tmpbase}/jlnNNNN.tmp then lock the file
                File tmpFile = File.createTempFile("jln", ".tmp", tmpBaseDir);
                if (VERBOSE) {
                    System.err.println("tmpFile = " + tmpFile.getAbsolutePath());
                }
                final FileOutputStream tmpOut = new FileOutputStream(tmpFile);
                final FileChannel tmpChannel = tmpOut.getChannel();
                final FileLock tmpLock = tmpChannel.lock();

                // Strip off the ".tmp" to get the name of the tmprootdir
                String tmpFileName = tmpFile.getAbsolutePath();
                String tmpRootName = tmpFileName.substring(0, tmpFileName.lastIndexOf(".tmp"));

                // create ${tmpbase}/jlnNNNN.lck then lock the file
                String lckFileName = tmpRootName + ".lck";
                File lckFile = new File(lckFileName);
                if (VERBOSE) {
                    System.err.println("lckFile = " + lckFile.getAbsolutePath());
                }
                lckFile.createNewFile();
                final FileOutputStream lckOut = new FileOutputStream(lckFile);
                final FileChannel lckChannel = lckOut.getChannel();
                final FileLock lckLock = lckChannel.lock();

                // Create tmprootdir
                tmpRootDir = new File(tmpRootName);
                if (DEBUG) {
                    System.err.println("tmpRootDir = " + tmpRootDir.getAbsolutePath());
                }
                if (!tmpRootDir.mkdir()) {
                    throw new IOException("Cannot create " + tmpRootDir);
                }

                // Add shutdown hook to cleanup the OutputStream, FileChannel,
                // and FileLock for the jlnNNNN.lck and jlnNNNN.lck files.
                // We do this so that the locks never get garbage-collected.
                Runtime.getRuntime().addShutdownHook(new Thread() {
                    /* @Override */
                    public void run() {
                        // NOTE: we don't really expect that this code will ever
                        // be called. If it does, we will close the output
                        // stream, which will in turn close the channel.
                        // We will then release the lock.
                        try {
                            tmpOut.close();
                            tmpLock.release();
                            lckOut.close();
                            lckLock.release();
                        } catch (IOException ex) {
                            // Do nothing
                        }
                    }
                });

                // Set the system property...
                tmpRootPropValue = tmpRootName.substring(tmpRootName.lastIndexOf(File.separator) + 1);
                System.setProperty(tmpRootPropName, tmpRootPropValue);
                if (VERBOSE) {
                    System.err.println("Setting " + tmpRootPropName + "=" + tmpRootPropValue);
                }

                // Start a new Reaper thread to do stuff...
                Thread reaperThread = new Thread() {
                    /* @Override */
                    public void run() {
                        deleteOldTempDirs();
                    }
                };
                reaperThread.setName("AppletLauncher-Reaper");
                reaperThread.start();
            } else {
                // Make sure that the property is not set to an illegal value
                if (tmpRootPropValue.indexOf('/') >= 0 ||
                        tmpRootPropValue.indexOf(File.separatorChar) >= 0) {
                    throw new IOException("Illegal value of: " + tmpRootPropName);
                }

                // Set tmpRootDir = ${tmpbase}/${jnlp.applet.launcher.tmproot}
                if (VERBOSE) {
                    System.err.println("Using existing value of: " +
                            tmpRootPropName + "=" + tmpRootPropValue);
                }
                tmpRootDir = new File(tmpBaseDir, tmpRootPropValue);
                if (DEBUG) {
                    System.err.println("tmpRootDir = " + tmpRootDir.getAbsolutePath());
                }
                if (!tmpRootDir.isDirectory()) {
                    throw new IOException("Cannot access " + tmpRootDir);
                }
            }
        }
    }

    /**
     * Called by the Reaper thread to delete old temp directories
     * Only one of these threads will run per JVM invocation.
     */
    private static void deleteOldTempDirs() {
        if (VERBOSE) {
            System.err.println("*** Reaper: deleteOldTempDirs in " +
                    tmpBaseDir.getAbsolutePath());
        }

        // enumerate list of jnl*.lck files, ignore our own jlnNNNN file
        final String ourLockFile = tmpRootPropValue + ".lck";
        FilenameFilter lckFilter = new FilenameFilter() {
            /* @Override */
            public boolean accept(File dir, String name) {
                return name.endsWith(".lck") && !name.equals(ourLockFile);
            }
        };

        // For each file <file>.lck in the list we will first try to lock
        // <file>.tmp if that succeeds then we will try to lock <file>.lck
        // (which should always succeed unless there is a problem). If we can
        // get the lock on both files, then it must be an old installation, and
        // we will delete it.
        String[] fileNames = tmpBaseDir.list(lckFilter);
        if (fileNames != null) {
            for (int i = 0; i < fileNames.length; i++) {
                String lckFileName = fileNames[i];
                String tmpDirName = lckFileName.substring(0, lckFileName.lastIndexOf(".lck"));
                String tmpFileName = tmpDirName + ".tmp";

                File lckFile = new File(tmpBaseDir, lckFileName);
                File tmpFile = new File(tmpBaseDir, tmpFileName);
                File tmpDir = new File(tmpBaseDir, tmpDirName);

                if (lckFile.exists() && tmpFile.exists() && tmpDir.isDirectory()) {
                    FileOutputStream tmpOut = null;
                    FileChannel tmpChannel = null;
                    FileLock tmpLock = null;

                    try {
                        tmpOut = new FileOutputStream(tmpFile);
                        tmpChannel = tmpOut.getChannel();
                        tmpLock = tmpChannel.tryLock();
                    } catch (Exception ex) {
                        // Ignore exceptions
                        if (DEBUG) {
                            ex.printStackTrace();
                        }
                    }

                    if (tmpLock != null) {
                        FileOutputStream lckOut = null;
                        FileChannel lckChannel = null;
                        FileLock lckLock = null;

                        try {
                            lckOut = new FileOutputStream(lckFile);
                            lckChannel = lckOut.getChannel();
                            lckLock = lckChannel.tryLock();
                        } catch (Exception ex) {
                            if (DEBUG) {
                                ex.printStackTrace();
                            }
                        }

                        if (lckLock != null) {
                            // Recursively remove the old tmpDir and all of
                            // its contents
                            removeAll(tmpDir);

                            // Close the streams and delete the .lck and .tmp
                            // files. Note that there is a slight race condition
                            // in that another process could open a stream at
                            // the same time we are trying to delete it, which will
                            // prevent deletion, but we won't worry about it, since
                            // the worst that will happen is we might have an
                            // occasional 0-byte .lck or .tmp file left around
                            try {
                                lckOut.close();
                            } catch (IOException ex) {
                            }
                            lckFile.delete();
                            try {
                                tmpOut.close();
                            } catch (IOException ex) {
                            }
                            tmpFile.delete();
                        } else {
                            try {
                                // Close the file and channel for the *.lck file
                                if (lckOut != null) {
                                    lckOut.close();
                                }
                                // Close the file/channel and release the lock
                                // on the *.tmp file
                                tmpOut.close();
                                tmpLock.release();
                            } catch (IOException ex) {
                                if (DEBUG) {
                                    ex.printStackTrace();
                                }
                            }
                        }
                    }
                } else {
                    if (VERBOSE) {
                        System.err.println("    Skipping: " + tmpDir.getAbsolutePath());
                    }
                }
            }
        }
    }

    /**
     * Remove the specified file or directory. If "path" is a directory, then
     * recursively remove all entries, then remove the directory itself.
     */
    private static void removeAll(File path) {
        if (VERBOSE) {
            System.err.println("removeAll(" + path + ")");
        }

        if (path.isDirectory()) {
            // Recursively remove all files/directories in this directory
            File[] list = path.listFiles();
            if (list != null) {
                for (int i = 0; i < list.length; i++) {
                    removeAll(list[i]);
                }
            }
        }

        path.delete();
    }


    /**
     * This method is executed from outside the Event Dispatch Thread. It
     * initializes, downloads, and unpacks the required native libraries into
     * the cache, and then starts the applet on the EDT.
     */
    private void initAndStartApplet() {
        // Parse the extension JNLP files and download the native resources
        try {
            initResources();
        } catch (Exception ex) {
            ex.printStackTrace();
            displayError(toErrorString(ex));
            return;
        }

        // Indicate that we are starting the applet
        displayMessage("Starting applet " + subAppletDisplayName);
        setProgress(0);

        // Now schedule the starting of the subApplet on the EDT
        SwingUtilities.invokeLater(new Runnable() {
            public void run() {
                // start the subapplet
                startSubApplet();
            }
        });

    }

    /**
     * Initializes, downloads, and extracts the native resources needed by this
     * applet.
     */
    private void initResources() throws IOException {
        synchronized (JNLPAppletLauncher.class) {
            if (firstApplet) {
                // Save codeBase and archive parameter for assertion checking
                codeBase = getCodeBase();
                assert codeBase != null;
                archive = getParameter("archive");
                if (archive == null || archive.length() == 0) {
                    throw new IllegalArgumentException("Missing archive parameter");
                }

                // Initialize the collections of resources
                jarFiles = new HashSet/*<URL>*/();
                nativeJars = new HashSet/*<URL>*/();
                nativeLibMap = new HashMap/*<String, String>*/();
            } else {
                // The following must hold for applets in the same ClassLoader
                assert getCodeBase().equals(codeBase);
                assert getParameter("archive").equals(archive);
            }

            int jnlpNumExt = -1;
            String numParamString = getParameter("jnlpNumExtensions");
            if (numParamString != null) {
                try {
                    jnlpNumExt = Integer.parseInt(numParamString);
                } catch (NumberFormatException ex) {
                }

                if (jnlpNumExt <= 0) {
                    throw new IllegalArgumentException("Missing or invalid jnlpNumExtensions parameter");
                }
            }

            List/*<URL>*/ urls = new ArrayList/*<URL>*/();
            for (int i = 1; i <= jnlpNumExt; i++) {
                String paramName = "jnlpExtension" + i;
                String urlString = getParameter(paramName);
                if (urlString == null || urlString.length() == 0) {
                    throw new IllegalArgumentException("Missing " + paramName + " parameter");
                }
                URL url = new URL(urlString);
                urls.add(url);
            }

            // If this is the first time, process the list of extensions and
            // save the results. Otherwise, verify that the list of extensions
            // is the same as the first applet.
            if (firstApplet) {
                jnlpExtensions = urls;
                parseJNLPExtensions(urls);

                if (VERBOSE) {
                    System.err.println();
                    System.err.println("All files successfully parsed");
                    printResources();
                }

                if (nativeJars.size() > 0) {
                    // Create the cache directory if not already created.
                    // Create the temporary directory that will hold a copy of
                    // the extracted native libraries, then copy each native
                    // library into the temp dir so we can call System.load().
                    createCacheDir();
                    createTmpDir();

                    // Download and validate the set of native jars, if the
                    // cache is out of date. Then extract the native DLLs,
                    // creating a list of native libraries to be loaded.
                    for (Iterator iter = nativeJars.iterator(); iter.hasNext(); ) {
                        URL url = (URL) iter.next();
                        processNativeJar(url);
                    }
                }

                // Set a system property that libraries can use to know when to call
                // JNLPAppletLauncher.loadLibrary instead of System.loadLibrary
                System.setProperty("sun.jnlp.applet.launcher", "true");

            } else {
                // Verify that the list of jnlpExtensions is the same as the
                // first applet
                if (!jnlpExtensions.equals(urls)) {
                    throw new IllegalArgumentException(
                            "jnlpExtension parameters do not match previously loaded applet");
                }
            }

            firstApplet = false;
        }
    }

    /**
     * Detemine the cache directory location based on the codebase and archive
     * tag. Create the cache directory if not already created.
     */
    private void createCacheDir() throws IOException {
        StringBuffer cacheBaseName = new StringBuffer();
        cacheBaseName.append(System.getProperty("user.home")).append(File.separator).
                append(".jnlp-applet").append(File.separator).
                append("cache");
        File cacheBaseDir = new File(cacheBaseName.toString());
        if (VERBOSE) {
            System.err.println("cacheBaseDir = " + cacheBaseDir.getAbsolutePath());
        }

        cacheDir = new File(cacheBaseDir, getCacheDirName());
        if (VERBOSE) {
            System.err.println("cacheDir = " + cacheDir.getAbsolutePath());
        }

        // Create cache directory and load native library
        if (!cacheDir.isDirectory()) {
            if (!cacheDir.mkdirs()) {
                throw new IOException("Cannot create directory " + cacheDir);
            }
        }

        assert cacheBaseDir.isDirectory();
    }

    /**
     * Returns a directory name of the form: hostname/hash(codebase,archive)
     */
    private String getCacheDirName() {
        final String codeBasePath = getCodeBase().toExternalForm();

        // Extract the host name; replace characters in the set ".:\[]" with "_"
        int hostIdx1 = -1;
        int hostIdx2 = -1;
        String hostNameDir = "UNKNOWN";
        hostIdx1 = codeBasePath.indexOf("://");
        if (hostIdx1 >= 0) {
            hostIdx1 += 3; // skip the "://"
            // Verify that the character immediately following the "://"
            // exists and is not a "/"
            if (hostIdx1 < codeBasePath.length() &&
                    codeBasePath.charAt(hostIdx1) != '/') {
                hostIdx2 = codeBasePath.indexOf('/', hostIdx1);
                if (hostIdx2 > hostIdx1) {
                    hostNameDir = codeBasePath.substring(hostIdx1, hostIdx2).
                            replace('.', '_').
                            replace(':', '_').
                            replace('\\', '_').
                            replace('[', '_').
                            replace(']', '_');
                }
            }
        }

        // Now concatenate the codebase and the list of jar files in the archive
        // Separate them by an "out-of-band" character which cannot appear in
        // either the codeBasePath or archive list.
        StringBuffer key = new StringBuffer();
        key.append(codeBasePath).
                append("\n").
                append(getParameter("archive"));
        if (VERBOSE) {
            System.err.println("key = " + key);
        }

        StringBuffer result = new StringBuffer();
        result.append(hostNameDir).
                append(File.separator).
                append(sha1Hash(key.toString()));
        if (VERBOSE) {
            System.err.println("result = " + result);
        }

        return result.toString();
    }

    /**
     * Produces a 40-byte SHA-1 hash of the input string.
     */
    private static String sha1Hash(String str) {
        MessageDigest sha1 = null;
        try {
            sha1 = MessageDigest.getInstance("SHA-1");
        } catch (NoSuchAlgorithmException ex) {
            throw new RuntimeException(ex);
        }

        byte[] digest = sha1.digest(str.getBytes());
        if (digest == null || digest.length == 0) {
            throw new RuntimeException("Error reading message digest");
        }
        StringBuffer res = new StringBuffer();
        for (int i = 0; i < digest.length; i++) {
            int val = (int)digest[i] & 0xFF;
            if (val < 0x10) {
                res.append("0");
            }
            res.append(Integer.toHexString(val));
        }
        return res.toString();
    }

    /**
     * Create the temp directory in tmpRootDir. To do this, we create a temp
     * file with a ".tmp" extension, and then create a directory of the
     * same name but without the ".tmp". The temp file, directory, and all
     * files in the directory will be reaped the next time this is started.
     * We avoid deleteOnExit, because it doesn't work reliably.
     */
    private void createTmpDir() throws IOException {

        if (VERBOSE) {
            System.err.println("---------------------------------------------------");
        }

        File tmpFile = File.createTempFile("jln", ".tmp", tmpRootDir);
        String tmpFileName = tmpFile.getAbsolutePath();
        String tmpDirName = tmpFileName.substring(0, tmpFileName.lastIndexOf(".tmp"));
        nativeTmpDir = new File(tmpDirName);
        if (VERBOSE) {
            System.err.println("tmpFile = " + tmpFile.getAbsolutePath() +
                    "  tmpDir = " + nativeTmpDir.getAbsolutePath());
        }
        if (!nativeTmpDir.mkdir()) {
            throw new IOException("Cannot create " + nativeTmpDir);
        }
    }

    /**
     * Download, cache, verify, and unpack the specified native jar file.
     * Before downloading, check the cached time stamp for the jar file
     * against the server. If the time stamp is valid and matches that of the
     * server, then we will use the locally cached files. This method assumes
     * that cacheDir and nativeTmpDir both exist.
     *
     * An IOException is thrown if the files cannot loaded for some reason.
     */
    private void processNativeJar(URL url) throws IOException {
        assert cacheDir.isDirectory();
        assert nativeTmpDir.isDirectory();

        // 6618105: Map '\' to '/' prior to stripping off the path
        String urlString = url.toExternalForm().replace('\\', '/');
        String nativeFileName = urlString.substring(urlString.lastIndexOf("/") + 1);
        File nativeFile = new File(cacheDir, nativeFileName);
        // Make sure the file is not "." or ".."
        if (nativeFile.isDirectory()) {
            throw new IOException(nativeFile + " is a directory");
        }

        String tmpStr = nativeFileName;
        int idx = nativeFileName.lastIndexOf(".");
        if (idx > 0) {
            tmpStr = nativeFileName.substring(0, idx);
        }
        String indexFileName = tmpStr + ".idx";
        File indexFile = new File(cacheDir, indexFileName);

        if (VERBOSE) {
            System.err.println("nativeFile = " + nativeFile);
            System.err.println("indexFile = " + indexFile);
        }

        displayMessage("Loading: " + nativeFileName);
        setProgress(0);

        URLConnection conn = url.openConnection();
        conn.connect();

        Map/*<String,List<String>>*/ headerFields = conn.getHeaderFields();
        if (VERBOSE) {
            for (Iterator iter = headerFields.entrySet().iterator(); iter.hasNext(); ) {
                Entry/*<String,List<String>>*/ e = (Entry) iter.next();
                for (Iterator iter2 = ((List/*<String>*/) e.getValue()).iterator(); iter2.hasNext(); ) {
                    String s = (String) iter2.next();
                    if (e.getKey() != null) {
                        System.err.print(e.getKey() + ": ");
                    }
                    System.err.print(s + " ");
                }
                System.err.println();
            }
            System.err.println();
        }

        // Validate the cache, download the jar if needed
        // TODO: rather than synchronizing on System.out during cache validation,
        // we should use a System property as a lock token (protected by
        // System.out) so we don't hold a synchronized lock on System.out during
        // a potentially long download operation.
        synchronized (System.out) {
            validateCache(conn, nativeFile, indexFile);
        }

        // Unpack the jar file
        displayMessage("Unpacking: " + nativeFileName);
        setProgress(0);

        // Enumerate the jar file looking for native libraries
        JarFile jarFile = new JarFile(nativeFile);
        Set/*<String>*/ rootEntries = getRootEntries(jarFile);
        Set/*<String>*/ nativeLibNames = getNativeLibNames(rootEntries);

        // Validate certificates; throws exception upon validation error
        validateCertificates(jarFile, rootEntries);

        // Extract native libraries from the jar file
        extractNativeLibs(jarFile, rootEntries, nativeLibNames);

        if (VERBOSE) {
            System.err.println();
        }
    }

    // Validate the cached file. If the cached file is out of date or otherwise
    // invalid, download the file and store the new time stamp.
    // This method must be called with a global lock being held such that
    // no other thread -- even in another class loader -- can executed this
    // method concurrently.
    private void validateCache(URLConnection conn,
            File nativeFile,
            File indexFile) throws IOException {

        // Lock the cache directory
        final String lckFileName = "cache.lck";
        File lckFile = new File(cacheDir, lckFileName);
        lckFile.createNewFile();
        final FileOutputStream lckOut = new FileOutputStream(lckFile);
        final FileChannel lckChannel = lckOut.getChannel();
        final FileLock lckLock = lckChannel.lock();

        try {
            // Check to see whether the cached jar file exists and is valid
            boolean valid = false;
            long cachedTimeStamp = readTimeStamp(indexFile);
            long urlTimeStamp = conn.getLastModified();

            if (nativeFile.exists() &&
                    urlTimeStamp > 0 &&
                    urlTimeStamp == readTimeStamp(indexFile)) {

                valid = true;
            }

            // Validate the cache, download the jar if needed
            if (!valid) {
                if (VERBOSE) {
                    System.err.println("processNativeJar: downloading " + nativeFile.getAbsolutePath());
                }
                indexFile.delete();
                nativeFile.delete();

                // Copy from URL to File
                int len = conn.getContentLength();
                if (VERBOSE) {
                    System.err.println("Content length = " + len + " bytes");
                }

                int totalNumBytes = copyURLToFile(conn, nativeFile);
                if (DEBUG) {
                    System.err.println("processNativeJar: " + conn.getURL().toString() +
                            " --> " + nativeFile.getAbsolutePath() + " : " +
                            totalNumBytes + " bytes written");
                }

                // Write timestamp to index file.
                writeTimeStamp(indexFile, urlTimeStamp);

            } else {
                if (DEBUG) {
                    System.err.println("processNativeJar: using previously cached: " +
                            nativeFile.getAbsolutePath());
                }
            }
        } finally {
            // Unlock the cache directory
            lckLock.release();
        }
    }

    private long readTimeStamp(File indexFile) {
        try {
            BufferedReader reader = new BufferedReader(new FileReader(indexFile));
            try {
                String str = reader.readLine();
                return Long.parseLong(str);
            } finally {
                reader.close();
            }
        } catch (Exception ex) {
        }
        return -1;
    }

    private void writeTimeStamp(File indexFile, long timestamp) {
        try {
            BufferedWriter writer = new BufferedWriter(new FileWriter(indexFile));
            try {
                writer.write("" + timestamp + "\n");
                writer.flush();
            } finally {
                writer.close();
            }
        } catch (Exception ex) {
            displayError("Error writing time stamp for native libraries");
        }
    }

    // Copy the specified URL to the specified File
    private int copyURLToFile(URLConnection inConnection,
            File outFile) throws IOException {

        int totalNumBytes = 0;
        InputStream in = new BufferedInputStream(inConnection.getInputStream());
        try {
            OutputStream out = new BufferedOutputStream(new FileOutputStream(outFile));
            try {
                totalNumBytes = copyStream(in, out, inConnection.getContentLength());
            } finally {
                out.close();
            }
        } finally {
            in.close();
        }

        return totalNumBytes;
    }

    /**
     * Copy the specified input stream to the specified output stream. The total
     * number of bytes written is returned. If the close flag is set, both
     * streams are closed upon completeion.
     */
    private int copyStream(InputStream in, OutputStream out,
            int totalNumBytes) throws IOException {

        int numBytes = 0;

        final int BUFFER_SIZE = 1000;
        final float pctScale = 100.0f / (float)totalNumBytes;
        byte[] buf = new byte[BUFFER_SIZE];
        setProgress(0);
        while (true) {
            int count;
            if ((count = in.read(buf)) == -1) {
                break;
            }
            out.write(buf, 0, count);
            numBytes += count;
            if (totalNumBytes > 0) {
                setProgress((int)Math.round((float)numBytes * pctScale));
            }
        }
        setProgress(100);

        return numBytes;
    }

    /**
     * Enumerate the list of entries in the jar file and return those that are
     * the root entries.
     */
    private Set/*<String>*/ getRootEntries(JarFile jarFile) {
        if (VERBOSE) {
            System.err.println("getRootEntries:");
        }

        Set/*<String>*/ names = new HashSet/*<String>*/();
        Enumeration/*<JarEntry>*/ entries = jarFile.entries();
        while (entries.hasMoreElements()) {
            JarEntry entry = (JarEntry) entries.nextElement();
            String entryName = entry.getName();

            if (VERBOSE) {
                System.err.println("JarEntry : " + entryName);
            }

            // only look at entries with no "/"
            if (entryName.indexOf('/') == -1 &&
                entryName.indexOf(File.separatorChar) == -1) {
                names.add(entryName);
            }
        }

        return names;
    }


    /**
     * Filter the root entries in the jar file and return those that
     * are native library names.
     */
    private Set/*<String>*/ getNativeLibNames(Set/*<String>*/ entryNames) {
        if (VERBOSE) {
            System.err.println("getNativeLibNames:");
        }

        Set/*<String>*/ names = new HashSet/*<String>*/();
        for (Iterator iter = entryNames.iterator(); iter.hasNext(); ) {
            String name = (String) iter.next();
            String lowerCaseName = name.toLowerCase();

            // Match entries with correct prefix and suffix (ignoring case)
            if (lowerCaseName.startsWith(nativePrefix) &&
                lowerCaseName.endsWith(nativeSuffix)) {
                names.add(name);
            }
        }

        return names;
    }

    /**
     * Validate the certificates for each native Lib in the jar file.
     * Throws an IOException if any certificate is not valid.
     */
    private void validateCertificates(JarFile jarFile,
            Set/*<String>*/ nativeLibNames) throws IOException {

        if (DEBUG) {
            System.err.println("validateCertificates:");
        }

        byte[] buf = new byte[1000];
        Enumeration/*<JarEntry>*/ entries = jarFile.entries();
        while (entries.hasMoreElements()) {
            JarEntry entry = (JarEntry) entries.nextElement();
            String entryName = entry.getName();

            if (VERBOSE) {
                System.err.println("JarEntry : " + entryName);
            }

            if (nativeLibNames.contains(entryName)) {

                if (DEBUG) {
                    System.err.println("VALIDATE: " + entryName);
                }

                if (!checkNativeCertificates(jarFile, entry, buf)) {
                    throw new IOException("Cannot validate certificate for " + entryName);
                }
            }
        }

    }

    /**
     * Check the native certificates with the ones in the jar file containing the
     * certificates for the JNLPAppletLauncher class (all must match).
     */
    private boolean checkNativeCertificates(JarFile jar, JarEntry entry,
            byte[] buf) throws IOException {

        // API states that we must read all of the data from the entry's
        // InputStream in order to be able to get its certificates

        InputStream is = jar.getInputStream(entry);
        int totalLength = (int) entry.getSize();
        int len;
        while ((len = is.read(buf)) > 0) {
        }
        is.close();

        // locate JNLPAppletLauncher certificates
        Certificate[] appletLauncherCerts = JNLPAppletLauncher.class.getProtectionDomain().
                getCodeSource().getCertificates();
        if (appletLauncherCerts == null || appletLauncherCerts.length == 0) {
            throw new IOException("Cannot find certificates for JNLPAppletLauncher class");
        }

        // Get the certificates for the JAR entry
        Certificate[] nativeCerts = entry.getCertificates();
        if (nativeCerts == null || nativeCerts.length == 0) {
            return false;
        }

        int checked = 0;
        for (int i = 0; i < appletLauncherCerts.length; i++) {
            for (int j = 0; j < nativeCerts.length; j++) {
                if (nativeCerts[j].equals(appletLauncherCerts[i])){
                    checked++;
                    break;
                }
            }
        }
        return  (checked == appletLauncherCerts.length);
    }

    /**
     * Extract the specified set of native libraries in the given jar file.
     */
    private void extractNativeLibs(JarFile jarFile,
                                   Set/*<String>*/ rootEntries,
                                   Set/*<String>*/ nativeLibNames) throws IOException {

        if (DEBUG) {
            System.err.println("extractNativeLibs:");
        }

        Enumeration/*<JarEntry>*/ entries = jarFile.entries();
        while (entries.hasMoreElements()) {
            JarEntry entry = (JarEntry) entries.nextElement();
            String entryName = entry.getName();

            if (VERBOSE) {
                System.err.println("JarEntry : " + entryName);
            }

            // In order to be compatible with Java Web Start, we need
            // to extract all root entries from the jar file. However,
            // we only allow direct loading of the previously
            // discovered native library names.
            if (rootEntries.contains(entryName)) {
                // strip prefix & suffix
                String libName = entryName.substring(nativePrefix.length(),
                        entryName.length() - nativeSuffix.length());
                if (DEBUG) {
                    System.err.println("EXTRACT: " + entryName + "(" + libName + ")");
                }

                File nativeLib = new File(nativeTmpDir, entryName);
                InputStream in = new BufferedInputStream(jarFile.getInputStream(entry));
                OutputStream out = new BufferedOutputStream(new FileOutputStream(nativeLib));
                int numBytesWritten = copyStream(in, out, -1);
                in.close();
                out.close();
                if (nativeLibNames.contains(entryName)) {
                    nativeLibMap.put(libName, nativeLib.getAbsolutePath());
                }
            }
        }
    }

    /**
     * The true start of the sub applet (invoked in the EDT)
     */
    private void startSubApplet() {
        try {
            subApplet = (Applet)
                Class.forName(subAppletClassName,
                              true,
                              Thread.currentThread().getContextClassLoader()).newInstance(); 
            subApplet.setStub(new AppletStubProxy());
        } catch (ClassNotFoundException ex) {
            ex.printStackTrace();
            displayError("Class not found: " + subAppletClassName);
            return;
        } catch (Exception ex) {
            ex.printStackTrace();
            displayError("Unable to start " + subAppletDisplayName);
            return;
        }

        add(subApplet, BorderLayout.CENTER);

        try {
            subApplet.init();
            remove(loaderPanel);
            validate();
            checkNoDDrawAndUpdateDeploymentProperties();
            subApplet.start();
            appletStarted = true;
        } catch (Exception ex) {
            ex.printStackTrace();
        }
    }

    /**
     * Method called by an extension such as JOGL or Java 3D to load the
     * specified library. Applications and applets should not call this method.
     *
     * @param libraryName name of the library to be loaded
     *
     * @throws SecurityException if the caller does not have permission to
     * call System.load
     */
    public static void loadLibrary(String libraryName) {
        if (VERBOSE) {
            System.err.println("-----------");
            Thread.dumpStack();
        }

        if (DEBUG) {
            System.err.println("JNLPAppletLauncher.loadLibrary(\"" + libraryName + "\")");
        }

        String fullLibraryName = (String) nativeLibMap.get(libraryName);
        if (fullLibraryName == null) {
            // Throw UnsatisfiedLinkError to try to match behavior of System.loadLibrary()
            throw new UnsatisfiedLinkError(libraryName);
        }

        if (DEBUG) {
            System.err.println("    loading: " + fullLibraryName + "");
        }

        System.load(fullLibraryName);
    }
    
    public static void removeLibrary(String libraryName) {
        String fullLibraryName = (String) nativeLibMap.get(libraryName);
        if (fullLibraryName == null) {
            return;
        }

        if (DEBUG) {
            System.err.println("    removing: " + fullLibraryName + "");
        }

        new File(fullLibraryName).delete();
        nativeLibMap.remove(libraryName);
    }

    private static String toErrorString(Throwable throwable) {
        StringBuffer errStr = new StringBuffer(throwable.toString());
        Throwable cause = throwable.getCause();
        while (cause != null) {
            errStr.append(": ").append(cause);
            cause = cause.getCause();
        }
        return errStr.toString();
    }

    private void displayMessage(final String message) {
        if (progressBar != null) {
            SwingUtilities.invokeLater(new Runnable() {
                public void run() {
                    progressBar.setString(message);
                }
            });
        }
    }

    private void displayError(final String errorMessage) {
        // Log message on Java console and display in applet progress bar
        Logger.getLogger("global").severe(errorMessage);
        if (progressBar != null) {
            SwingUtilities.invokeLater(new Runnable() {
                public void run() {
                    progressBar.setString("Error : " + errorMessage);
                }
            });
        }
    }

    private void setProgress(final int value) {
        if (progressBar != null) {
            SwingUtilities.invokeLater(new Runnable() {
                public void run() {
                    progressBar.setValue(value);
                }
            });
        }
    }

    private void initLoaderLayout() {
        setLayout(new BorderLayout());
        loaderPanel = new JPanel(new BorderLayout());
        if (getBooleanParameter("progressbar")) {
            progressBar = new JProgressBar(0, 100);
            progressBar.setBorderPainted(true);
            progressBar.setStringPainted(true);
            progressBar.setString("Loading...");
        }
        boolean includeImage = false;
        ImageIcon image = null;
        if (subAppletImageURL != null) {
            image = new ImageIcon(subAppletImageURL);
            includeImage = true;
        }
        add(loaderPanel, BorderLayout.SOUTH);
        if (includeImage) {
            loaderPanel.add(new JLabel(image), BorderLayout.CENTER);
            if (progressBar != null) {
                loaderPanel.add(progressBar, BorderLayout.SOUTH);
            }
        } else {
            if (progressBar != null) {
                loaderPanel.add(progressBar, BorderLayout.CENTER);
            }
        }
    }

    private void parseJNLPExtensions(List/*<URL>*/ urls) throws IOException {
        for (Iterator iter = urls.iterator(); iter.hasNext(); ) {
            URL url = (URL) iter.next();
            JNLPParser parser = new JNLPParser(this, url);
            parser.parse();
        }
    }

    private void addJarFile(URL jarFile) {
        jarFiles.add(jarFile);
    }

    private void addNativeJar(URL nativeJar) {
        nativeJars.add(nativeJar);
    }

    /*
     * Debug method to print out resources from the JNLP file
     */
    private static void printResources() {
        System.err.println("  Resources:");
        System.err.println("    Class Jars:");
        doPrint(jarFiles);
        System.err.println();
        System.err.println("    Native Jars:");
        doPrint(nativeJars);
    }

    /*
     * Debug method to print out resources from the JNLP file
     */
    private static void doPrint(Collection/*<URL>*/ urls) {
        for (Iterator iter = urls.iterator(); iter.hasNext(); ) {
            URL url = (URL) iter.next();
            String urlString = url.toExternalForm();
            System.err.println("      " + urlString);
        }
    }

    // Static initializer for JNLPAppletLauncher
    static {
        System.err.println("JNLPAppletLauncher: static initializer");

        String systemOsName = System.getProperty("os.name").toLowerCase();

        if (systemOsName.startsWith("mac")) {
            // Mac OS X
            nativePrefix = "lib";
            nativeSuffix = ".jnilib";
        } else if (systemOsName.startsWith("windows")) {
            // Microsoft Windows
            nativePrefix = "";
            nativeSuffix = ".dll";
        } else {
            // Unix of some variety
            nativePrefix = "lib";
            nativeSuffix = ".so";
        }

        if (DEBUG) {
            System.err.println("os.name = " + systemOsName);
            System.err.println("nativePrefix = " + nativePrefix + "  nativeSuffix = " + nativeSuffix);
        }

        // Create / initialize the temp root directory, starting the Reaper
        // thread to reclaim old installations if necessary. If we get an
        // exception, set an error code so we don't try to start the applets.
        try {
            initTmpRoot();
        } catch (Exception ex) {
            ex.printStackTrace();
            staticInitError = true;
        }
    }

    private static final String _javaVersionProperty =
            System.getProperty("java.version");
    private static final boolean _atLeast13 =
            (!_javaVersionProperty.startsWith("1.2"));
    private static final boolean _atLeast14 = (_atLeast13 &&
            !_javaVersionProperty.startsWith("1.3"));
    private static final boolean _atLeast15 = (_atLeast14 &&
            !_javaVersionProperty.startsWith("1.4"));
    private static final String vendorURL =
            System.getProperty("java.vendor.url");
    private static final String sunVendorURL = "http://java.sun.com/";

    private static boolean isJavaVersionAtLeast15() {
        return _atLeast15;
    }

    private static boolean isJavaVersion142Family() {
        return _javaVersionProperty.startsWith("1.4.2");
    }

    // -----------------------------------------------------------------------

    /**
     * Proxy implementation class of AppletStub. Delegates to the
     * JNLPAppletLauncher class.
     */
    private class AppletStubProxy implements AppletStub {
        public boolean isActive() {
            return JNLPAppletLauncher.this.isActive();
        }

        public URL getDocumentBase() {
            return JNLPAppletLauncher.this.getDocumentBase();
        }

        public URL getCodeBase() {
            return JNLPAppletLauncher.this.getCodeBase();
        }

        public String getParameter(String name) {
            return JNLPAppletLauncher.this.getParameter(name);
        }

        public AppletContext getAppletContext() {
            return JNLPAppletLauncher.this.getAppletContext();
        }

        public void appletResize(int width, int height) {
            JNLPAppletLauncher.this.resize(width, height);
        }
    }

    /**
     * Parser class for JNLP files for the applet launcher. For simplicitly, we
     * assume that everything of interest is within a single "jnlp" tag and
     * that the "resources" tags are not nested.
     */
    private static class JNLPParser {

        // The following represents the various states we can be in
        private static final int INITIAL = 1;
        private static final int STARTED = 2;
        private static final int IN_JNLP = 3;
        private static final int IN_RESOURCES = 4;
        private static final int SKIP_ELEMENT = 5;

        private static SAXParserFactory factory;
        private static String systemOsName;
        private static String systemOsArch;

        private JNLPAppletLauncher launcher;
        private URL url;
        private InputStream in;
        private JNLPHandler handler;
        private String codebase = "";
        private int state = INITIAL;
        private int prevState = INITIAL;
        private int depth = 0;
        private int skipDepth = -1;

        private JNLPParser(JNLPAppletLauncher launcher, URL url) throws IOException {
            this.launcher = launcher;
            this.url = url;
            this.handler = new JNLPHandler();
        }

        private void parse() throws IOException {
            if (VERBOSE) {
                System.err.println("JNLPParser: " + url.toString());
            }
            try {
                URLConnection conn = url.openConnection();
                conn.connect();
                InputStream in = new BufferedInputStream(conn.getInputStream());

                SAXParser parser = factory.newSAXParser();
                parser.parse(in, handler);
                in.close();
            } catch (ParserConfigurationException ex) {
                throw (IOException) new IOException().initCause(ex);
            } catch (SAXException ex) {
                throw (IOException) new IOException().initCause(ex);
            }
        }

        // Static initializer for JNLPParser
        static {
            if (sunVendorURL.equals(vendorURL)) {
                // set the "javax.xml.parsers.SAXParserFactory" to the default
                // implementation, so we won't pull down all the JARs listed
                // in the archive tag to look for a SAXParserFactory implementation
                if (isJavaVersionAtLeast15()) {

                    System.setProperty("javax.xml.parsers.SAXParserFactory",
                            "com.sun.org.apache.xerces.internal.jaxp.SAXParserFactoryImpl");

                } else if (isJavaVersion142Family()) {

                    System.setProperty("javax.xml.parsers.SAXParserFactory",
                            "org.apache.crimson.jaxp.SAXParserFactoryImpl");
                }
            }

            factory = SAXParserFactory.newInstance();
            systemOsName = System.getProperty("os.name").toLowerCase();
            systemOsArch = System.getProperty("os.arch").toLowerCase();
            if (DEBUG) {
                System.err.println("os.name = " + systemOsName);
                System.err.println("os.arch = " + systemOsArch);
            }
        }


        /**
         * Handler class containing callback methods for the parser.
         */
        private class JNLPHandler extends DefaultHandler {

            JNLPHandler() {
            }

            /* @Override */
            public void startDocument() {
                if (VERBOSE) {
                    System.err.println("START DOCUMENT: " + url);
                }

                state = STARTED;
                if (VERBOSE) {
                    System.err.println("state = " + state);
                }
            }

            /* @Override */
            public void endDocument() {
                if (VERBOSE) {
                    System.err.println("END DOCUMENT");
                }

                state = INITIAL;
                if (VERBOSE) {
                    System.err.println("state = " + state);
                }
            }

            /* @Override */
            public void startElement(String uri,
                    String localName,
                    String qName,
                    Attributes attributes) throws SAXException {

                ++depth;

                if (VERBOSE) {
                    System.err.println("<" + qName + ">" + " : depth=" + depth);

                    for (int i = 0; i < attributes.getLength(); i++) {
                        System.err.println("    [" + i + "]  " + attributes.getQName(i) +
                                " = \"" + attributes.getValue(i) + "\"");
                    }
                }

                // Parse qName based on current state
                switch (state) {
                case STARTED:
                    if (qName.equals("jnlp")) {
                        state = IN_JNLP;

                        codebase = attributes.getValue("codebase");
                        if (codebase == null) {
                            throw new SAXException("<jnlp> unable to determine codebase");
                        }
                        if (codebase.lastIndexOf('/') != codebase.length()-1) {
                            codebase = codebase + "/";
                        }
                        if (VERBOSE) {
                            System.err.println("JNLP : codebase=" + codebase);
                        }

                        if (VERBOSE) {
                            System.err.println("state = " + state);
                        }
                    } else if (qName.equals("resources")) {
                        throw new SAXException("<resources> tag not within <jnlp> tag");
                    }
                    // Ignore all other tags
                    break;

                case IN_JNLP:
                    if (qName.equals("jnlp")) {
                        throw new SAXException("Nested <jnlp> tags");
                    } else if (qName.equals("resources")) {
                        String osName = attributes.getValue("os");
                        String osArch = attributes.getValue("arch");
                        if ((osName == null || systemOsName.startsWith(osName.toLowerCase())) &&
                                (osArch == null || systemOsArch.startsWith(osArch.toLowerCase()))) {
                            if (VERBOSE) {
                                System.err.println("Loading resources : os=" + osName + "  arch=" + osArch);
                            }
                            state = IN_RESOURCES;
                        } else {
                            prevState = state;
                            skipDepth = depth - 1;
                            state = SKIP_ELEMENT;
                        }

                        if (VERBOSE) {
                            System.err.println("Resources : os=" + osName + "  arch=" + osArch + "  state = " + state);
                        }
                    }
                    break;

                case IN_RESOURCES:
                    try {
                        if (qName.equals("jnlp")) {
                            throw new SAXException("Nested <jnlp> tags");
                        } else if (qName.equals("resources")) {
                            throw new SAXException("Nested <resources> tags");
                        } else if (qName.equals("jar")) {
                            String str = attributes.getValue("href");
                            if (str == null || str.length() == 0) {
                                throw new SAXException("<jar> tag missing href attribute");
                            }
                            String jarFileStr = codebase + str;
                            if (VERBOSE) {
                                System.err.println("Jar: " + jarFileStr);
                            }
                            URL jarFile = new URL(jarFileStr);
                            launcher.addJarFile(jarFile);
                        } else if (qName.equals("nativelib")) {
                            String str = attributes.getValue("href");
                            if (str == null || str.length() == 0) {
                                throw new SAXException("<nativelib> tag missing href attribute");
                            }
                            String nativeLibStr = codebase + str;
                            if (VERBOSE) {
                                System.err.println("Native Lib: " + nativeLibStr);
                            }
                            URL nativeLib = new URL(nativeLibStr);
                            launcher.addNativeJar(nativeLib);
                        } else if (qName.equals("extension")) {
                            String extensionURLString = attributes.getValue("href");
                            if (extensionURLString == null || extensionURLString.length() == 0) {
                                throw new SAXException("<extension> tag missing href attribute");
                            }
                            if (VERBOSE) {
                                System.err.println("Extension: " + extensionURLString);
                            }
                            URL extensionURL = new URL(extensionURLString);
                            JNLPParser parser = new JNLPParser(launcher, extensionURL);
                            parser.parse();
                        } else {
                            prevState = state;
                            skipDepth = depth - 1;
                            state = SKIP_ELEMENT;

                            if (VERBOSE) {
                                System.err.println("state = " + state);
                            }
                        }
                    } catch (IOException ex) {
                        throw (SAXException) new SAXException(ex).initCause(ex);
                    }
                    break;

                case INITIAL:
                case SKIP_ELEMENT:
                default:
                    break;
                }

            }

            /* @Override */
            public void endElement(String uri,
                    String localName,
                    String qName) throws SAXException {

                --depth;

                if (VERBOSE) {
                    System.err.println("</" + qName + ">");
                }

                // Parse qName based on current state
                switch (state) {
                case IN_JNLP:
                    if (qName.equals("jnlp")) {
                        state = STARTED;
                        if (VERBOSE) {
                            System.err.println("state = " + state);
                        }
                    }
                    break;

                case IN_RESOURCES:
                    if (qName.equals("resources")) {
                        state = IN_JNLP;
                        if (VERBOSE) {
                            System.err.println("state = " + state);
                        }
                    }
                    break;

                case SKIP_ELEMENT:
                    if (depth == skipDepth) {
                        state = prevState;
                        skipDepth = -1;
                        if (VERBOSE) {
                            System.err.println("state = " + state);
                        }
                    }
                    break;

                case INITIAL:
                case STARTED:
                default:
                    break;
                }

            }

        }
    }

    //----------------------------------------------------------------------
    // Helper routines for adding -Dsun.java2d.noddraw=true to deployment.properties

    // Get a "boolean" parameter
    private boolean getBooleanParameter(String parameterName) {
        return Boolean.valueOf(getParameter(parameterName)).booleanValue();
    }

    private String getDeploymentPropsDir() {
        final String osName = System.getProperty("os.name").toLowerCase();
        StringBuffer result = new StringBuffer();

        result.append(System.getProperty("user.home"));
        if (osName.startsWith("windows")) {
            if (osName.indexOf("vista") != -1) {
                result.append(File.separator).append("AppData").
                        append(File.separator).append("LocalLow");
            } else {
                result.append(File.separator).append("Application Data");
            }
            result.append(File.separator).append("Sun").
                    append(File.separator).append("Java").
                    append(File.separator).append("Deployment");
        } else if (osName.startsWith("mac")) {
            result.append(File.separator).append("Library").
                    append(File.separator).append("Caches").
                    append(File.separator).append("Java");
        } else {
            result.append(File.separator).append(".java").
                    append(File.separator).append("deployment");
        }

        return result.toString();
    }

    private void checkNoDDrawAndUpdateDeploymentProperties() {
        if (!getBooleanParameter("noddraw.check"))
            return;
        if (System.getProperty("os.name").toLowerCase().startsWith("windows") &&
            !"true".equalsIgnoreCase(System.getProperty("sun.java2d.noddraw"))) {
            if (!SwingUtilities.isEventDispatchThread()) {
                try {
                    SwingUtilities.invokeAndWait(new Runnable() {
                            public void run() {
                                updateDeploymentPropertiesImpl();
                            }
                        });
                } catch (Exception e) {
                }
            } else {
                updateDeploymentPropertiesImpl();
            }
        }
    }

    private void updateDeploymentPropertiesImpl() {
        String userHome = System.getProperty("user.home");
        File dontAskFile = new File(userHome + File.separator + ".jnlp-applet" +
                                    File.separator + DONT_ASK);
        if (dontAskFile.exists())
            return; // User asked us not to prompt again

        int option = 0;

        if (!getBooleanParameter("noddraw.check.silent")) {
            option = JOptionPane.showOptionDialog(null,
                                                  "For best robustness of OpenGL applets on Windows,\n" +
                                                  "we recommend disabling Java2D's use of DirectDraw.\n" +
                                                  "This setting will affect all applets, but is unlikely\n" +
                                                  "to slow other applets down significantly. May we update\n" +
                                                  "your deployment.properties to turn off DirectDraw for\n" +
                                                  "applets? You can change this back later if necessary\n" +
                                                  "using the Java Control Panel, Java tab, under Java\n" +
                                                  "Applet Runtime Settings.",
                                                  "Update deployment.properties?",
                                                  JOptionPane.YES_NO_CANCEL_OPTION,
                                                  JOptionPane.QUESTION_MESSAGE,
                                                  null,
                                                  new Object[] {
                                                      "Yes",
                                                      "No",
                                                      "No, Don't Ask Again"
                                                  },
                                                  "Yes");
        }

        if (option < 0 ||
            option == 1)
            return; // No

        if (option == 2) {
            try {
                dontAskFile.createNewFile();
            } catch (IOException e) {
            }
            return; // No, Don't Ask Again
        }

        try {
            // Must update deployment.properties
            File propsDir = new File(getDeploymentPropsDir());
            if (!propsDir.exists()) {
                // Don't know what's going on or how to set this permanently
                return;
            }

            File propsFile = new File(propsDir, "deployment.properties");
            if (!propsFile.exists()) {
                // Don't know what's going on or how to set this permanently
                return;
            }

            Properties props = new Properties();
            InputStream input = new BufferedInputStream(new FileInputStream(propsFile));
            props.load(input);
            input.close();
            // Search through the keys looking for JRE versions
            Set/*<String>*/ jreVersions = new HashSet/*<String>*/();
            for (Iterator/*<String>*/ iter = props.keySet().iterator(); iter.hasNext(); ) {
                String key = (String) iter.next();
                if (key.startsWith(JRE_PREFIX)) {
                    int idx = key.lastIndexOf(".");
                    if (idx >= 0 && idx > JRE_PREFIX.length()) {
                        String jreVersion = key.substring(JRE_PREFIX.length(), idx);
                        jreVersions.add(jreVersion);
                    }
                }
            }

            // Make sure the currently-running JRE shows up in this set to
            // avoid repeated displays of the dialog. It might not in some
            // upgrade scenarios where there was a pre-existing
            // deployment.properties and the new Java Control Panel hasn't
            // been run yet.
            jreVersions.add(System.getProperty("java.version"));

            // OK, now that we know all JRE versions covered by the
            // deployment.properties, check out the args for each and update
            // them
            for (Iterator/*<String>*/ iter = jreVersions.iterator(); iter.hasNext(); ) {
                String version = (String) iter.next();
                String argKey = JRE_PREFIX + version + ".args";
                String argVal = props.getProperty(argKey);
                if (argVal == null) {
                    argVal = NODDRAW_PROP;
                } else if (argVal.indexOf(NODDRAW_PROP) < 0) {
                    argVal = argVal + " " + NODDRAW_PROP;
                }
                props.setProperty(argKey, argVal);
            }

            OutputStream output = new BufferedOutputStream(new FileOutputStream(propsFile));
            props.store(output, null);
            output.close();

            if (!getBooleanParameter("noddraw.check.silent")) {
                // Tell user we're done
                JOptionPane.showMessageDialog(null,
                                              "For best robustness, we recommend you now exit and\n" +
                                              "restart your web browser. (Note: clicking \"OK\" will\n" +
                                              "not exit your browser.)",
                                              "Browser Restart Recommended",
                                              JOptionPane.INFORMATION_MESSAGE);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

   
  








Related examples in the same category

1.Java Web Start: File Chooser Demo Project
2.Java Web Start Demo
3.JNLP Demo
4.JNLP Download Servlet
5.This sample is the Java Web Start JRE auto-download installer
6.This sample demonstrates features of the JNLPRandomAccessFile API
7.Webpad: word processing application allows users to Create, Open, Modify, Save and Print files
8.This sample demonstrates on how to use CORBA within Java Web Start
9.Hello Java Web start
10.Web start and JNLP demoWeb start and JNLP demo