lwjgl/src/java/org/lwjgl/util/applet/AppletLoader.java

2265 lines
68 KiB
Java

/*
* Copyright (c) 2002-2008 LWJGL Project
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* * Redistributions 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 'LWJGL' nor the names of
* its contributors may be used to endorse or promote products derived
* from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package org.lwjgl.util.applet;
import org.lwjgl.LWJGLUtil;
import java.applet.Applet;
import java.applet.AppletStub;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.EventQueue;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Image;
import java.awt.MediaTracker;
import java.awt.image.ImageObserver;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FilePermission;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.io.Writer;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.net.HttpURLConnection;
import java.net.JarURLConnection;
import java.net.SocketPermission;
import java.net.URL;
import java.net.URLClassLoader;
import java.net.URLConnection;
import java.security.AccessControlException;
import java.security.AccessController;
import java.security.AllPermission;
import java.security.CodeSource;
import java.security.PermissionCollection;
import java.security.Permissions;
import java.security.PrivilegedExceptionAction;
import java.security.cert.Certificate;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.Queue;
import java.util.StringTokenizer;
import java.util.Vector;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.JarOutputStream;
import java.util.jar.Pack200;
import java.util.zip.CRC32;
import java.util.zip.CheckedInputStream;
import java.util.zip.GZIPInputStream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
/**
* <p>
* The AppletLoader enables deployment of LWJGL to applets in an easy
* and polished way. The loader will display a configurable logo and progressbar
* while the relevant jars (generic and native) are downloaded from a specified source.
* </p>
* <p>
* The downloaded jars are extracted to the users temporary directory - and if enabled, cached for
* faster loading in future uses.
* </p>
* <p>
* The following applet parameters are required:
* <ul>
* <li>al_main - [String] Full package and class the applet to instantiate and display when loaded.</li>
* <li>al_jars - [String] Comma separated list of jars to download.</li>
* <p>
* <li>al_windows - [String] Jar containing native files for windows.</li>
* <li>al_linux - [String] Jar containing native files for linux.</li>
* <li>al_mac - [String] Jar containing native files for mac.</li>
* <li>al_solaris - [String] Jar containing native files for solaris.</li>
* <li>al_freebsd - [String] Jar containing native files for freebsd.</li>
* <li>al_openbsd - [String] Jar containing native files for openbsd.</li>
* </ul>
* </p>
* <p>
* Additionally the following parameters can be supplied to tweak the behaviour of the AppletLoader.
* <ul>
* <li>al_cache - [boolean] Whether to use cache system. <i>Default: true</i>.</li>
* <li>al_version - [int or float] Version of deployment. If this is specified, the jars will be cached and
* reused if the version matches. If version doesn't match all of the files are reloaded.</li>
*
* <li>al_debug - [boolean] Whether to enable debug mode. <i>Default: false</i>.</li>
* <li>al_min_jre - [String] Specify the minimum jre version that the applet requires, should be in format like 1.8.0_291-b10 or a subset like 1.8 <i>Default: 1.8</i>.</li>
* <li>al_prepend_host - [boolean] Whether to limit caching to this domain, disable if your applet is hosted on multiple domains and needs to share the cache. <i>Default: true</i>.</li>
* <li>al_lookup_threads - [int] Specify the number of concurrent threads to use to get file information before downloading. <i>Default: 1</i>.</li>
* <p>
* <li>al_windows64 - [String] If specified it will be used instead of al_windows on 64bit windows systems.</li>
* <li>al_windows32 - [String] If specified it will be used instead of al_windows on 32bit windows systems.</li>
* <li>al_linux64 - [String] If specified it will be used instead of al_linux on 64bit linux systems.</li>
* <li>al_linux32 - [String] If specified it will be used instead of al_linux on 32bit linux systems.</li>
* <li>al_mac32 - [String] If specified it will be used instead of al_mac on 64bit mac systems.</li>
* <li>al_mac64 - [String] If specified it will be used instead of al_mac on 32bit mac systems.</li>
* <li>al_macppc - [String] If specified it will be used instead of al_mac on PPC mac systems.</li>
* <p>
* <li>boxbgcolor - [String] any String AWT color ("red", "blue", etc), RGB (0-255) or hex formated color (#RRGGBB) to use as background. <i>Default: #ffffff</i>.</li>
* <li>boxfgcolor - [String] any String AWT color ("red", "blue", etc), RGB (0-255) or hex formated color (#RRGGBB) to use as foreground. <i>Default: #000000</i>.</li>
* <p>
* <li>al_logo - [String Path of of the logo resource to paint while loading.<i>Default: "appletlogo.gif"</i>.</li>
* <li>al_progressbar - [String] Path of the progressbar resource to paint on top of the logo, width clipped by percentage.<i>Default: "appletprogress.gif"</i>.</li>
* <p>
* <li>lwjgl_arguments - </li> [String] used to pass LWJGL parameters to LWJGL e.g. ("-Dorg.lwjgl.input.Mouse.allowNegativeMouseCoords=true -Dorg.lwjgl.util.Debug=true").</li>
* </ul>
* </p>
* @author kappaOne <one.kappa@gmail.com>
* @author Brian Matzon <brian@matzon.dk>
* @version $Revision$
* $Id$
*
* Contributors:
* <ul>
* <li>Arielsan</li>
* <li>Bobjob</li>
* <li>Dashiva</li>
* <li>Dr_evil</li>
* <li>Elias Naur</li>
* <li>Kevin Glass</li>
* <li>Matthias Mann</li>
* <li>Mickelukas</li>
* <li>NateS</li>
* <li>Pelle Johnsen</li>
* <li>Riven</li>
* <li>Ruben01</li>
* <li>Shannon Smith</li>
* </ul>
*
*/
public class AppletLoader extends Applet implements Runnable, AppletStub {
/** initializing */
public static final int STATE_INIT = 1;
/** checking version of jre */
public static final int STATE_CHECK_JRE_VERSION = 2;
/** determining which packages that are required */
public static final int STATE_DETERMINING_PACKAGES = 3;
/** checking for already downloaded files */
public static final int STATE_CHECKING_CACHE = 4;
/** checking if any updates are available for cache files */
public static final int STATE_CHECKING_FOR_UPDATES = 5;
/** downloading packages */
public static final int STATE_DOWNLOADING = 6;
/** extracting packages */
public static final int STATE_EXTRACTING_PACKAGES = 7;
/** validating packages */
public static final int STATE_VALIDATING_PACKAGES = 8;
/** updating the classpath */
public static final int STATE_UPDATING_CLASSPATH = 9;
/** switching to real applet */
public static final int STATE_SWITCHING_APPLET = 10;
/** initializing real applet */
public static final int STATE_INITIALIZE_REAL_APPLET = 11;
/** stating real applet */
public static final int STATE_START_REAL_APPLET = 12;
/** done */
public static final int STATE_DONE = 13;
/** used to calculate length of progress bar */
protected volatile int percentage;
/** total size of download in bytes */
protected int totalDownloadSize;
/** current size of extracted in bytes */
protected int currentSizeExtract;
/** total size of extracted in bytes */
protected int totalSizeExtract;
/** logo to be shown while loading */
protected Image logo, logoBuffer;
/** progressbar to render while loading */
protected Image progressbar, progressbarBuffer;
/** offscreen image used */
protected Image offscreen;
/** set to true while painting is done */
protected boolean painting;
/** background color of applet */
protected Color bgColor = Color.white;
/** color to write foreground in */
protected Color fgColor = Color.black;
/** urls of the jars to download */
protected URL[] urlList;
/** classLoader used to add downloaded jars to the classpath */
protected ClassLoader classLoader;
/** actual thread that does the loading */
protected Thread loaderThread;
/** animation thread that renders our load screen while loading */
protected Thread animationThread;
/** applet to load after all downloads are complete */
protected Applet lwjglApplet;
/** whether we're running in debug mode */
protected boolean debugMode;
/** whether to prepend host to cache path */
protected boolean prependHost;
/** Used to store file names with lastModified time */
protected HashMap<String, Long> filesLastModified;
/** Sizes of files to download */
protected int[] fileSizes;
/** Number of native jars */
protected int nativeJarCount;
/** whether to use caching system, only download files that have changed */
protected boolean cacheEnabled;
/** String to display as a subtask */
protected String subtaskMessage = "";
/** state of applet loader */
protected volatile int state = STATE_INIT;
/** whether lzma is supported */
protected boolean lzmaSupported;
/** whether pack200 is supported */
protected boolean pack200Supported;
/** whether to run in headless mode */
protected boolean headless = false;
/** whether to switch applets in headless mode or wait longer */
protected boolean headlessWaiting = true;
/** messages to be passed via liveconnect in headless mode */
protected String[] headlessMessage;
/** threads to use when fetching information of files to be downloaded */
protected int concurrentLookupThreads;
/** whether a fatal error occurred */
protected boolean fatalError;
/** whether a certificate refused error occurred */
protected boolean certificateRefused;
/** whether the minimum required JRE version is not found */
protected boolean minimumJreNotFound;
/** generic error message to display on error */
protected String[] genericErrorMessage = { "An error occured while loading the applet.",
"Please contact support to resolve this issue.",
"<placeholder for error message>"};
/** error message to display if user refuses to accept certificate*/
protected String[] certificateRefusedMessage = { "Permissions for Applet Refused.",
"Please accept the permissions dialog to allow",
"the applet to continue the loading process."};
/** error message to display if minimum JRE version is not met */
protected String[] minimumJREMessage = { "Your version of Java is out of date.",
"Visit java.com to get the latest version.",
"Java <al_min_jre> or greater is required."};
/** fatal error message to display */
protected String[] errorMessage;
/** have natives been loaded by another instance of this applet */
protected static boolean natives_loaded;
/*
* @see java.applet.Applet#init()
*/
public void init() {
setState(STATE_INIT);
// sanity check
String[] requiredArgs = {"al_main", "al_jars"};
for ( String requiredArg : requiredArgs ) {
if ( getParameter(requiredArg) == null ) {
fatalErrorOccured("missing required applet parameter: " + requiredArg, null);
return;
}
}
// whether to use cache system
cacheEnabled = getBooleanParameter("al_cache", true);
// whether to run in debug mode
debugMode = getBooleanParameter("al_debug", false);
// whether to prepend host to cache path
prependHost = getBooleanParameter("al_prepend_host", true);
// whether to run in headless mode
headless = getBooleanParameter("al_headless", false);
// obtain the number of concurrent lookup threads to use
concurrentLookupThreads = getIntParameter("al_lookup_threads", 1); // defaults to 1
// get colors of applet
bgColor = getColor("boxbgcolor", Color.white);
setBackground(bgColor);
fgColor = getColor("boxfgcolor", Color.black);
if (!headless) {
// load logos
logo = getImage(getStringParameter("al_logo", "appletlogo.gif"));
progressbar = getImage(getStringParameter("al_progressbar", "appletprogress.gif"));
}
// check for lzma support
try {
Class.forName("LZMA.LzmaInputStream");
lzmaSupported = true;
} catch (Throwable e) {
/* no lzma support */
}
// check pack200 support
try {
java.util.jar.Pack200.class.getSimpleName();
pack200Supported = true;
} catch (Throwable e) {
/* no pack200 support */
}
}
/**
* Generates a stacktrace in the form of a string
* @param exception Exception to make stacktrace of
* @return Stacktrace of exception in the form of a string
*/
private static String generateStacktrace(Exception exception) {
Writer result = new StringWriter();
PrintWriter printWriter = new PrintWriter(result);
exception.printStackTrace(printWriter);
return result.toString();
}
/*
* @see java.applet.Applet#start()
*/
public void start() {
if (lwjglApplet != null) {
lwjglApplet.start();
}
else {
if(loaderThread == null && !fatalError) {
loaderThread = new Thread(this);
loaderThread.setName("AppletLoader.loaderThread");
loaderThread.start();
if (!headless) {
animationThread = new Thread() {
public void run() {
while(loaderThread != null) {
repaint();
AppletLoader.this.sleep(100);
}
animationThread = null;
}
};
animationThread.setName("AppletLoader.animationthread");
animationThread.start();
}
}
}
}
/*
* @see java.applet.Applet#stop()
*/
public void stop() {
if (lwjglApplet != null) {
lwjglApplet.stop();
}
}
/*
* @see java.applet.Applet#destroy()
*/
public void destroy() {
if (lwjglApplet != null) {
lwjglApplet.destroy();
}
}
/**
* Clean up resources
*/
protected void cleanUp() {
progressbar = null;
logo = null;
logoBuffer = null;
progressbarBuffer = null;
offscreen = null;
}
/**
* Retrieves the applet that has been loaded. Useful for liveconnect.
*/
public Applet getApplet() {
return lwjglApplet;
}
/**
* Retrieves the current status of the AppletLoader and is
* used by liveconnect when running in headless mode.
*
* This method will return the current progress of the AppletLoader
* as a value from 0-100. In the case of a fatal error it will
* return -1. If the certificate is refused it will return -2.
* If the minimum jre requirement is not met will return -3.
*
* When method returns 100 the AppletLoader will sleep until the
* method is called again. When called again it will switch to the
* LWJGL Applet. This is a useful trigger to start the LWJGL applet
* when needed.
*/
public int getStatus() {
if (fatalError) {
headlessMessage = errorMessage;
if (certificateRefused) return -2;
if (minimumJreNotFound) return -3;
return -1;
}
if (percentage == 100 && headlessWaiting) {
headlessWaiting = false;
}
if (percentage == 95) {
percentage = 100; // ready to switch applet
}
String[] message = {getDescriptionForState(), subtaskMessage};
headlessMessage = message;
return percentage;
}
/**
* Retrieves the current message for the current status.
* Used by liveconnect when running in headless mode.
*/
public String[] getMessages() {
return headlessMessage;
}
/**
* Transfers the call of AppletResize from the stub to the lwjglApplet.
*/
public void appletResize(int width, int height) {
resize(width, height);
}
/*
* @see java.awt.Container#update(java.awt.Graphics)
*/
public final void update(Graphics g) {
paint(g);
}
/*
* @see java.awt.Container#paint(java.awt.Graphics)
*/
public void paint(Graphics g) {
// don't paint loader if applet loaded
if(state == STATE_DONE) {
cleanUp(); // clean up resources
return;
}
// no drawing in headless mode
if (headless) return;
// create offscreen if missing
if (offscreen == null) {
offscreen = createImage(getWidth(), getHeight());
// create buffers for animated gifs
if (logo != null) {
logoBuffer = createImage(logo.getWidth(null), logo.getHeight(null));
// add image observer, it will notify when next animated gif frame is ready
offscreen.getGraphics().drawImage(logo, 0, 0, this);
// in case image is not animated fill image buffer once
imageUpdate(logo, ImageObserver.FRAMEBITS, 0, 0, 0, 0);
}
if (progressbar != null) {
progressbarBuffer = createImage(progressbar.getWidth(null), progressbar.getHeight(null));
// add image observer, it will notify when next animated gif frame is ready
offscreen.getGraphics().drawImage(progressbar, 0, 0, this);
// in case image is not animated fill image buffer once
imageUpdate(progressbar, ImageObserver.FRAMEBITS, 0, 0, 0, 0);
}
}
// draw everything onto an image before drawing to avoid flicker
Graphics og = offscreen.getGraphics();
FontMetrics fm = og.getFontMetrics();
// clear background color
og.setColor(bgColor);
og.fillRect(0, 0, offscreen.getWidth(null), offscreen.getHeight(null));
og.setColor(fgColor);
// if we had a failure of some sort, notify the user
if (fatalError) {
for(int i=0; i<errorMessage.length; i++) {
if(errorMessage[i] != null) {
int messageX = (offscreen.getWidth(null) - fm.stringWidth(errorMessage[i])) / 2;
int messageY = (offscreen.getHeight(null) - (fm.getHeight() * errorMessage.length)) / 2;
og.drawString(errorMessage[i], messageX, messageY + i*fm.getHeight());
}
}
} else {
og.setColor(fgColor);
painting = true;
// get position at the middle of the offscreen buffer
int x = offscreen.getWidth(null)/2;
int y = offscreen.getHeight(null)/2;
// draw logo
if (logo != null) {
og.drawImage(logoBuffer, x-logo.getWidth(null)/2, y-logo.getHeight(null)/2, this);
}
// draw message
String message = getDescriptionForState();
int messageX = (offscreen.getWidth(null) - fm.stringWidth(message)) / 2;
int messageY = y + 20;
if (logo != null) messageY += logo.getHeight(null)/2;
else if (progressbar != null) messageY += progressbar.getHeight(null)/2;
og.drawString(message, messageX, messageY);
// draw subtaskmessage, if any
if(subtaskMessage.length() > 0) {
messageX = (offscreen.getWidth(null) - fm.stringWidth(subtaskMessage)) / 2;
og.drawString(subtaskMessage, messageX, messageY+20);
}
// draw loading progress bar, clipping it depending on percentage done
if (progressbar != null) {
int barSize = (progressbar.getWidth(null) * percentage) / 100;
og.clipRect(x-progressbar.getWidth(null)/2, 0, barSize, offscreen.getHeight(null));
og.drawImage(progressbarBuffer, x-progressbar.getWidth(null)/2, y-progressbar.getHeight(null)/2, this);
}
painting = false;
}
og.dispose();
// finally draw it all centred
g.drawImage(offscreen, (getWidth() - offscreen.getWidth(null))/2, (getHeight() - offscreen.getHeight(null))/2, null);
}
/**
* When an animated gif frame is ready to be drawn the ImageObserver
* will call this method.
*
* The Image frame is copied into a buffer, which is then drawn.
* This is done to prevent image tearing on gif animations.
*/
public boolean imageUpdate(Image img, int flag, int x, int y, int width, int height) {
// finish with this ImageObserver
if (state == STATE_DONE) return false;
// if image frame is ready to be drawn and is currently not being painted
if (flag == ImageObserver.FRAMEBITS && !painting) {
Image buffer;
// select which buffer to fill
if (img == logo) buffer = logoBuffer;
else buffer = progressbarBuffer;
Graphics g = buffer.getGraphics();
// clear background on buffer
g.setColor(bgColor);
g.fillRect(0, 0, buffer.getWidth(null), buffer.getHeight(null));
// buffer background is cleared, so draw logo under progressbar
if (img == progressbar && logo != null) {
g.drawImage(logoBuffer, progressbar.getWidth(null)/2-logo.getWidth(null)/2,
progressbar.getHeight(null)/2-logo.getHeight(null)/2, null);
}
g.drawImage(img, 0, 0, this);
g.dispose();
repaint();
}
return true;
}
/**
* @return string describing the state of the loader
*/
protected String getDescriptionForState() {
switch (state) {
case STATE_INIT:
return "Initializing loader";
case STATE_CHECK_JRE_VERSION:
return "Checking version";
case STATE_DETERMINING_PACKAGES:
return "Determining packages to load";
case STATE_CHECKING_CACHE:
return "Calculating download size";
case STATE_CHECKING_FOR_UPDATES:
return "Checking for updates";
case STATE_DOWNLOADING:
return "Downloading packages";
case STATE_EXTRACTING_PACKAGES:
return "Extracting downloaded packages";
case STATE_VALIDATING_PACKAGES:
return "Validating packages";
case STATE_UPDATING_CLASSPATH:
return "Updating classpath";
case STATE_SWITCHING_APPLET:
return "Switching applet";
case STATE_INITIALIZE_REAL_APPLET:
return "Initializing real applet";
case STATE_START_REAL_APPLET:
return "Starting real applet";
case STATE_DONE:
return "Done loading";
default:
return "unknown state";
}
}
/**
* Trims the passed file string based on the available capabilities
* @param file string of files to be trimmed
* @return trimmed string based on capabilities of client
*/
protected String trimExtensionByCapabilities(String file) {
if (!pack200Supported) {
file = file.replace(".pack", "");
}
if (!lzmaSupported && file.endsWith(".lzma")) {
file = file.replace(".lzma", "");
System.out.println("LZMA decoder (lzma.jar) not found, trying " + file + " without lzma extension.");
}
return file;
}
/**
* Reads list of jars to download and adds the urls to urlList
* also finds out which OS you are on and adds appropriate native
* jar to the urlList
*/
protected void loadJarURLs() throws Exception {
setState(STATE_DETERMINING_PACKAGES);
// jars to load
String jarList = getParameter("al_jars");
String nativeJarList = null;
String osName = System.getProperty("os.name");
if (osName.startsWith("Win")) {
// check if arch specific natives have been specified
if (System.getProperty("os.arch").endsWith("64")) {
nativeJarList = getParameter("al_windows64");
} else {
nativeJarList = getParameter("al_windows32");
}
if (nativeJarList == null) {
nativeJarList = getParameter("al_windows");
}
} else if (osName.startsWith("Linux") || osName.startsWith("Unix")) {
// check if arch specific natives have been specified
if (System.getProperty("os.arch").endsWith("64")) {
nativeJarList = getParameter("al_linux64");
} else {
nativeJarList = getParameter("al_linux32");
}
if (nativeJarList == null) {
nativeJarList = getParameter("al_linux");
}
} else if (osName.startsWith("Mac") || osName.startsWith("Darwin")) {
// check if arch specific natives have been specified
if (System.getProperty("os.arch").endsWith("64")) {
nativeJarList = getParameter("al_mac64");
} else if (System.getProperty("os.arch").contains("ppc")) {
nativeJarList = getParameter("al_macppc");
} else {
nativeJarList = getParameter("al_mac32");
}
if (nativeJarList == null) {
nativeJarList = getParameter("al_mac");
}
} else if (osName.startsWith("Solaris") || osName.startsWith("SunOS")) {
nativeJarList = getParameter("al_solaris");
} else if (osName.startsWith("FreeBSD")) {
nativeJarList = getParameter("al_freebsd");
} else if (osName.startsWith("OpenBSD")) {
nativeJarList = getParameter("al_openbsd");
} else {
fatalErrorOccured("OS (" + osName + ") not supported", null);
return;
}
if (nativeJarList == null) {
fatalErrorOccured("no lwjgl natives files found", null);
return;
}
jarList = trimExtensionByCapabilities(jarList);
StringTokenizer jars = new StringTokenizer(jarList, ", ");
nativeJarList = trimExtensionByCapabilities(nativeJarList);
StringTokenizer nativeJars = new StringTokenizer(nativeJarList, ", ");
int jarCount = jars.countTokens();
nativeJarCount = nativeJars.countTokens();
urlList = new URL[jarCount+nativeJarCount];
URL path = getCodeBase();
// set jars urls
for (int i = 0; i < jarCount; i++) {
urlList[i] = new URL(path, jars.nextToken());
}
for (int i = jarCount; i < jarCount+nativeJarCount; i++) {
urlList[i] = new URL(path, nativeJars.nextToken());
}
}
/**
* 9 steps
*
* 1) check jre version meets minimum requirements
* 2) check applet cache and decide which jars to download
* 3) download the jars
* 4) extract native files
* 5) validate jars for any corruption
* 6) save applet cache information
* 7) add jars to class path
* 8) set any lwjgl properties
* 9) switch to loaded applet
*/
public void run() {
percentage = 5;
try {
debug_sleep(2000);
// check JRE version meets minimum requirements
if (!isMinJREVersionAvailable()) {
minimumJreNotFound = true;
fatalErrorOccured("Java " + getStringParameter("al_min_jre", "1.8") + " or greater is required.", null);
return;
}
// parse the urls for the jars into the url list
loadJarURLs();
// get path where applet files will be stored
String path = getCacheDirectory();
File dir = new File(path);
// create directory
if (!dir.exists()) {
dir.mkdirs();
}
File versionFile = new File(dir, "version");
// if specified applet version already available don't download anything
boolean versionAvailable = false;
// version string of applet
String version = getParameter("al_version");
// if applet version specifed, compare with version in the cache
if (version != null) {
versionAvailable = compareVersion(versionFile, version.toLowerCase());
}
// if jars not available or need updating download them
if (!versionAvailable) {
// get jars file sizes and check cache
getJarInfo(dir); // 5-15%
// downloads jars from the server
downloadJars(path); // 15-55%
// Extract Pack and LZMA files
extractJars(path); // 55-65%
// Extracts Native Files
extractNatives(path); // 65-80%
// Validate Jars // 80-90%
validateJars(path);
// save version information once jars downloaded successfully
if (version != null) {
percentage = 90;
writeObjectFile(versionFile, version.toLowerCase());
}
// save file names with last modified info once downloaded successfully
writeObjectFile(new File(dir, "timestamps"), filesLastModified);
}
// add the downloaded jars and natives to classpath
updateClassPath(path);
// set lwjgl properties
setLWJGLProperties();
// if headless mode then sleep, until told to continue
if (headless) {
while(headlessWaiting) {
Thread.sleep(100);
}
}
// make applet switch on the EDT as an AWT/Swing permission dialog could be called
EventQueue.invokeAndWait(new Runnable() {
public void run() {
try {
switchApplet();
} catch (Exception e) {
fatalErrorOccured("This occurred while '" + getDescriptionForState() + "'", e);
}
setState(STATE_DONE);
repaint();
}
});
} catch (Exception e) {
certificateRefused = e instanceof AccessControlException;
fatalErrorOccured("This occurred while '" + getDescriptionForState() + "'", e);
} finally {
loaderThread = null;
}
}
/**
* When this method is supplied with a JRE version it will compare it to the
* current JRE version.
*
* minimum requried JRE version is set using al_min_jre parameter, if not
* this is not set then the value will default to version 1.8
*
* The minimumVersion should follow a structure such as x.x.x_x
* Example values would include 1.6.0_10 or a subset like 1.6.0 or 1.6
*
* @return returns true if the available version is greater or equal to the
* minimum version required
*
* @throws Exception a NumberFormatException is thrown if the string is not valid
*/
public boolean isMinJREVersionAvailable() throws Exception {
setState(STATE_CHECK_JRE_VERSION);
String minimumVersion = getStringParameter("al_min_jre", "1.8");
String javaVersion = System.getProperty("java.version");
// remove dash and anything after it (letters) from version string e.g. 1.8.0_291-b10
minimumVersion = javaVersion.split("-")[0];
javaVersion = minimumVersion.split("-")[0];
// split version string into a string arrays
String[] jvmVersionData = javaVersion.split("[_\\.]");
String[] minVersionData = minimumVersion.split("[_\\.]");
int maxLength = Math.max(jvmVersionData.length, minVersionData.length);
// convert string arrays into int arrays
int[] jvmVersion = new int[maxLength];
int[] minVersion = new int[maxLength];
for (int i = 0; i < jvmVersionData.length; i++) {
jvmVersion[i] = Integer.parseInt(jvmVersionData[i]);
}
for (int i = 0; i < minVersionData.length; i++) {
minVersion[i] = Integer.parseInt(minVersionData[i]);
}
// compare versions
for (int i = 0; i < maxLength; i++) {
if (jvmVersion[i] < minVersion[i]) return false; // minVersion is greater then jvmVersion
}
return true;
}
/**
* This method will return true if the version stored in the file
* matches the supplied String version.
*
* @param versionFile - location to file containing version information
* @param version - String version that needs to be compared
* @return returns true if the version in file matches specified version
*/
protected boolean compareVersion(File versionFile, String version) {
// if version file exists
if (versionFile.exists()) {
String s = readStringFile(versionFile);
// compare to version with file
if (s != null && s.equals(version)) {
percentage = 90; // not need to download cache files again
if(debugMode) {
System.out.println("Loading Cached Applet Version: " + version);
}
debug_sleep(2000);
return true; // version matches file
}
}
return false;
}
/**
* Parses the java_arguments list and sets lwjgl specific
* properties accordingly, before the launch.
*/
protected void setLWJGLProperties() {
String lwjglArguments = getParameter("lwjgl_arguments");
if(lwjglArguments != null && lwjglArguments.length() > 0) {
int start = lwjglArguments.indexOf("-Dorg.lwjgl");
while(start != -1) {
int end = lwjglArguments.indexOf(" ", start);
if(end == -1) {
end = lwjglArguments.length();
}
String[] keyValue = lwjglArguments.substring(start+2, end).split("=");
System.setProperty(keyValue[0], keyValue[1]);
if(debugMode) {
System.out.println("Setting property " + keyValue[0] + " to " + keyValue[1]);
}
start = lwjglArguments.indexOf("-Dorg.lwjgl", end);
}
}
}
/**
* This method will return the location of the cache directory. All the
* applet files will be downloaded and stored here. A folder will be
* created inside the LWJGL cache directory from the al_title parameter.
* This folder will also be prepended by the host name of the codebase
* to avoid conflict with same named applets on other hosts.
*
* @return path to applets cache directory
* @throws Exception if access is denied
*/
protected String getCacheDirectory() throws Exception {
String path = AccessController.doPrivileged(new PrivilegedExceptionAction<String>() {
public String run() throws Exception {
// we append the code base to avoid naming collisions with al_title
String codebase = "";
if(prependHost) {
codebase = getCodeBase().getHost();
if(codebase == null || codebase.length() == 0) {
codebase = "localhost";
}
codebase += File.separator;
}
return getLWJGLCacheDir() + File.separator + codebase + getParameter("al_title") + File.separator;
}
});
return path;
}
/**
* Get path to the lwjgl cache directory. This location will be where
* the OS keeps temporary files.
*
* @return path to the lwjgl cache directory
*/
protected String getLWJGLCacheDir() {
String cacheDir = System.getProperty("deployment.user.cachedir");
if (cacheDir == null || System.getProperty("os.name").startsWith("Win")) {
cacheDir = System.getProperty("java.io.tmpdir");
}
return cacheDir + File.separator + "lwjglcache";
}
/**
* read String object from File
*
* @param file to be read
* @return the String stored in the file or null if it fails
*/
protected String readStringFile(File file) {
try {
return (String)readObjectFile(file);
} catch (Exception e) {
// failed to read version file
e.printStackTrace();
}
// return null if failed to read file
return null;
}
/**
* read the HashMap from File
*
* @param file the file to read
* @return the hashmap stored in the file or an empty hashmap if it fails
*/
@SuppressWarnings("unchecked")
protected HashMap<String, Long> readHashMapFile(File file) {
try {
return (HashMap<String, Long>) readObjectFile(file);
} catch (Exception e) {
// failed to read hashmap from file
e.printStackTrace();
}
// return an empty map if failed to read file
return new HashMap<String, Long>();
}
/**
* read the object from the File
*
* @param file the file to read
* @return the object contained in the file or null if it fails
* @throws Exception if it fails to read object from file
*/
protected Object readObjectFile(File file) throws Exception {
FileInputStream fis = new FileInputStream(file);
try {
ObjectInputStream dis = new ObjectInputStream(fis);
Object object = dis.readObject();
dis.close();
return object;
} catch (Exception e) {
// failed to read file
throw e;
} finally {
fis.close();
}
}
/**
* write object to specified File
*
* @param file the file to write out to
* @param object the contents of the file
* @throws Exception if it fails to write file
*/
protected void writeObjectFile(File file, Object object) throws Exception {
FileOutputStream fos = new FileOutputStream(file);
try {
ObjectOutputStream dos = new ObjectOutputStream(fos);
dos.writeObject(object);
dos.close();
} finally {
fos.close();
}
}
/**
* Edits the ClassPath at runtime to include the jars
* that have just been downloaded and then adds the
* lwjgl natives folder property.
*
* @param path location where applet is stored
* @throws Exception if it fails to add classpath
*/
protected void updateClassPath(final String path) throws Exception {
setState(STATE_UPDATING_CLASSPATH);
percentage = 95;
URL[] urls = new URL[urlList.length];
for (int i = 0; i < urlList.length; i++) {
String file = new File(path, getJarName(urlList[i])).toURI().toString();
// fix JVM bug where ! is not escaped
file = file.replace("!", "%21");
urls[i] = new URL(file);
}
// get AppletLoader certificates
final Certificate[] certs = getCurrentCertificates();
// detect if we are running on a mac and save result as boolean
String osName = System.getProperty("os.name");
final boolean isMacOS = (osName.startsWith("Mac") || osName.startsWith("Darwin"));
// add downloaded jars to the classpath with required permissions
classLoader = new URLClassLoader(urls) {
protected PermissionCollection getPermissions (CodeSource codesource) {
PermissionCollection perms = null;
try {
// no permissions
perms = new Permissions();
// if certificates match the AppletLoader certificates then we should be all set
if (certificatesMatch(certs, codesource.getCertificates())) {
perms.add(new AllPermission());
return perms;
}
String host = getCodeBase().getHost();
if (host != null && (host.length() > 0)) {
// add permission for downloaded jars to access host they were from
perms.add(new SocketPermission(host, "connect,accept"));
}
else if ( "file".equals(codesource.getLocation().getProtocol()) ) {
// if running locally add file permission
String path = codesource.getLocation().getFile().replace('/', File.separatorChar);
perms.add(new FilePermission(path, "read"));
}
} catch (Exception e) {
e.printStackTrace();
}
return perms;
}
// allow non lwjgl native to be found from cache directory
protected String findLibrary (String libname) {
String libPath = path + "natives" + File.separator + LWJGLUtil.mapLibraryName(libname);
if (new File(libPath).exists()) {
return libPath;
}
return super.findLibrary(libname);
}
};
debug_sleep(2000);
// unload natives loaded by a previous instance of this lwjgl applet
unloadNatives(path);
// add natives files path to native class path
System.setProperty("org.lwjgl.librarypath", path + "natives");
// Make sure jinput knows about the new path too
System.setProperty("net.java.games.input.librarypath", path + "natives");
// set the library path, useful for non lwjgl natives
System.setProperty("java.library.path", path + "natives");
// mark natives as loaded
natives_loaded = true;
}
/**
* Unload natives loaded by a different classloader.
*
* Due to limitations of the jvm, native files can only
* be loaded once and only be used by the classloader
* they were loaded from.
*
* Due to the way applets on plugin1 work, one jvm must
* be used for all applets. We need to use multiple
* classloaders in the same jvm due to LWJGL's static
* nature. In order to solve this we simply remove the
* natives from a previous classloader allowing a new
* classloader to use those natives in the same jvm.
*
* This method will only attempt to unload natives from a
* previous classloader if it detects that the natives have
* been loaded in the same jvm.
*
* @param nativePath directory where natives are stored
*/
private void unloadNatives(String nativePath) {
// check whether natives have been loaded into this jvm
if (!natives_loaded) {
return;
}
try {
Field field = ClassLoader.class.getDeclaredField("loadedLibraryNames");
field.setAccessible(true);
Vector libs = (Vector) field.get(getClass().getClassLoader());
String path = new File(nativePath).getCanonicalPath();
for (int i = 0; i < libs.size(); i++) {
String s = (String) libs.get(i);
// if a native from the nativePath directory is loaded, unload it
if (s.startsWith(path)) {
libs.remove(i);
i--;
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* replace the current applet with the lwjgl applet
* using AppletStub and initialise and start it
*/
protected void switchApplet() throws Exception {
setState(STATE_SWITCHING_APPLET);
percentage = 100;
debug_sleep(2000);
// set correct context classloader for lwjgl applet
Thread.currentThread().setContextClassLoader(classLoader);
Class appletClass = classLoader.loadClass(getParameter("al_main"));
lwjglApplet = (Applet) appletClass.newInstance();
lwjglApplet.setStub(this);
lwjglApplet.setSize(getWidth(), getHeight());
setLayout(new BorderLayout());
add(lwjglApplet);
validate();
setState(STATE_INITIALIZE_REAL_APPLET);
lwjglApplet.init();
setState(STATE_START_REAL_APPLET);
lwjglApplet.start();
}
/**
* This method will get the files sizes of the files to download.
* It wil further get the lastModified time of files
* and save it in a hashmap, if cache is enabled it will mark
* those files that have not changed since last download to not
* redownloaded.
*
* @param dir - location to read cache file from
* @throws Exception - if fails to get infomation
*/
protected void getJarInfo(File dir) throws Exception {
setState(STATE_CHECKING_CACHE);
filesLastModified = new HashMap<String, Long>();
// store file sizes and mark which files not to download
fileSizes = new int[urlList.length];
File timestampsFile = new File(dir, "timestamps");
// if timestamps file exists, load it
if (timestampsFile.exists()) {
setState(STATE_CHECKING_FOR_UPDATES);
filesLastModified = readHashMapFile(timestampsFile);
}
// calculate total size of jars to download
ExecutorService executorService = Executors.newFixedThreadPool(concurrentLookupThreads);
Queue<Future> requests = new LinkedList<Future>();
// create unique object to sync code in requests
final Object sync = new Integer(1);
for (int j = 0; j < urlList.length; j++) {
final int i = j;
Future request = executorService.submit(new Runnable() {
public void run() {
try {
URLConnection urlconnection = urlList[i].openConnection();
urlconnection.setDefaultUseCaches(false);
if (urlconnection instanceof HttpURLConnection) {
((HttpURLConnection) urlconnection).setRequestMethod("HEAD");
}
fileSizes[i] = urlconnection.getContentLength();
long lastModified = urlconnection.getLastModified();
String fileName = getFileName(urlList[i]);
if (cacheEnabled && lastModified != 0 && filesLastModified.containsKey(fileName)) {
long savedLastModified = filesLastModified.get(fileName);
// if lastModifed time is the same, don't redownload
if (savedLastModified == lastModified) {
fileSizes[i] = -2; // mark it to not redownload
}
}
if (fileSizes[i] >= 0) {
synchronized (sync) {
totalDownloadSize += fileSizes[i];
}
}
// put key and value in the hashmap
filesLastModified.put(fileName, lastModified);
} catch (Exception e) {
throw new RuntimeException("Failed to fetch information for " + urlList[i], e);
}
}});
requests.add(request);
}
while (!requests.isEmpty()) {
Iterator<Future> iterator = requests.iterator();
while (iterator.hasNext()) {
Future request = iterator.next();
if (request.isDone()) {
request.get(); // will throw an exception if request thrown an exception.
iterator.remove();
// update progress bar
percentage = 5 + (int) (10 * (urlList.length - requests.size()) / (float) urlList.length);
}
}
Thread.sleep(10);
}
executorService.shutdown();
}
/**
* Will download the jars from the server using the list of urls
* in urlList, while at the same time updating progress bar
*
* @param path location of the directory to save to
* @throws Exception if download fails
*/
protected void downloadJars(String path) throws Exception {
setState(STATE_DOWNLOADING);
URLConnection urlconnection;
int initialPercentage = percentage = 15;
int amountDownloaded = 0;
// download each jar
byte buffer[] = new byte[65536];
for (int i = 0; i < urlList.length; i++) {
// skip file if marked as -2 (already downloaded and not changed)
if (fileSizes[i] == -2) continue;
int unsuccessfulAttempts = 0;
int maxUnsuccessfulAttempts = 3;
boolean downloadFile = true;
String currentFile = getFileName(urlList[i]);
// download the jar a max of 3 times
while(downloadFile) {
downloadFile = false;
debug_sleep(2000);
try {
urlconnection = urlList[i].openConnection();
urlconnection.setUseCaches(false);
if (urlconnection instanceof HttpURLConnection) {
urlconnection.setRequestProperty("Cache-Control", "no-store,max-age=0,no-cache");
urlconnection.connect();
}
InputStream inputstream = getJarInputStream(currentFile, urlconnection);
FileOutputStream fos = new FileOutputStream(path + currentFile);
int bufferSize;
int currentDownload = 0;
long downloadStartTime = System.currentTimeMillis();
int downloadedAmount = 0;
String downloadSpeedMessage = "";
try {
while ((bufferSize = inputstream.read(buffer, 0, buffer.length)) != -1) {
debug_sleep(10);
fos.write(buffer, 0, bufferSize);
currentDownload += bufferSize;
int totalDownloaded = amountDownloaded + currentDownload;
percentage = initialPercentage + ((totalDownloaded * 45) / totalDownloadSize);
subtaskMessage = "Retrieving: " + currentFile + " " + ((totalDownloaded * 100) / totalDownloadSize) + "%";
downloadedAmount += bufferSize;
long timeLapse = System.currentTimeMillis() - downloadStartTime;
// update only if a second or more has passed
if (timeLapse >= 1000) {
// get kb/s, nice that bytes/millis is same as kilobytes/seconds
float downloadSpeed = (float) downloadedAmount / timeLapse;
// round to two decimal places
downloadSpeed = ((int)(downloadSpeed*100))/100f;
// set current speed message
downloadSpeedMessage = " - " + downloadSpeed + " KB/sec";
// reset downloaded amount
downloadedAmount = 0;
// reset start time
downloadStartTime = System.currentTimeMillis();
}
subtaskMessage += downloadSpeedMessage;
}
} finally {
inputstream.close();
fos.close();
}
// download complete, verify if it was successful
if (urlconnection instanceof HttpURLConnection) {
if (currentDownload == fileSizes[i]) {
// successful download
}
else if (fileSizes[i] <= 0 && currentDownload != 0) {
// If contentLength for fileSizes[i] <= 0, we don't know if the download
// is complete. We're going to guess the download is complete.
}
else {
throw new Exception("size mismatch on download of " + currentFile +
" expected " + fileSizes[i] + " got " + currentDownload);
}
}
// successful file download, update total amount downloaded
amountDownloaded += fileSizes[i];
} catch (Exception e) {
e.printStackTrace(); // output exception to console
// Failed to download the file
unsuccessfulAttempts++;
// download failed try again
if (unsuccessfulAttempts < maxUnsuccessfulAttempts) {
downloadFile = true;
Thread.sleep(100); // wait a bit before retrying
}
else {
// retry attempts exhasted, download failed
throw new Exception("failed to download " + currentFile +
" after " + maxUnsuccessfulAttempts + " attempts");
}
}
}
}
subtaskMessage = "";
}
/**
* Retrieves a jar files input stream. This method exists primarily to fix an Opera hang in getInputStream
* @param urlconnection connection to get input stream from
* @return InputStream or null if not possible
*/
protected InputStream getJarInputStream(final String currentFile, final URLConnection urlconnection) throws Exception {
final InputStream[] is = new InputStream[1];
// try to get the input stream 3 times.
// Wait at most 5 seconds before interrupting the thread
for (int j = 0; j < 3 && is[0] == null; j++) {
Thread t = new Thread() {
public void run() {
try {
is[0] = urlconnection.getInputStream();
} catch (IOException e) {
/* ignored */
}
}
};
t.setName("JarInputStreamThread");
t.start();
int iterationCount = 0;
while(is[0] == null && iterationCount++ < 5) {
try {
t.join(1000);
} catch (InterruptedException inte) {
/* ignored */
}
}
if(is[0] == null) {
try {
t.interrupt();
t.join();
} catch (InterruptedException inte) {
/* ignored */
}
}
}
if(is[0] == null) {
throw new Exception("Unable to get input stream for " + currentFile);
}
return is[0];
}
/**
* Extract LZMA File
* @param in Input path to pack file
* @param out output path to resulting file
* @throws Exception if any errors occur
*/
protected void extractLZMA(String in, String out) throws Exception {
File f = new File(in);
FileInputStream fileInputHandle = new FileInputStream(f);
// use reflection to avoid hard dependency
Class<?> clazz = Class.forName( "LZMA.LzmaInputStream" );
Constructor constructor = clazz.getDeclaredConstructor(InputStream.class);
InputStream inputHandle = (InputStream) constructor.newInstance(fileInputHandle);
OutputStream outputHandle = new FileOutputStream(out);
byte [] buffer = new byte [1<<14];
try {
int ret = inputHandle.read(buffer);
while (ret >= 1) {
outputHandle.write(buffer,0,ret);
ret = inputHandle.read(buffer);
}
} finally {
inputHandle.close();
outputHandle.close();
}
// delete LZMA file, as it is no longer needed
f.delete();
}
/**
* Extract GZip File
* @param in Input path to pack file
* @param out output path to resulting file
* @throws Exception if any errors occur
*/
protected void extractGZip(String in, String out) throws Exception {
File f = new File(in);
FileInputStream fileInputHandle = new FileInputStream(f);
InputStream inputHandle = new GZIPInputStream(fileInputHandle);
OutputStream outputHandle = new FileOutputStream(out);
try {
byte [] buffer = new byte [1<<14];
int ret = inputHandle.read(buffer);
while (ret >= 1) {
outputHandle.write(buffer,0,ret);
ret = inputHandle.read(buffer);
}
} finally {
inputHandle.close();
outputHandle.close();
}
// delete GZip file, as it is no longer needed
f.delete();
}
/**
* Extract Pack File
* @param in Input path to pack file
* @param out output path to resulting file
* @throws Exception if any errors occur
*/
protected void extractPack(String in, String out) throws Exception {
File f = new File(in);
FileOutputStream fostream = new FileOutputStream(out);
JarOutputStream jostream = new JarOutputStream(fostream);
try {
Pack200.Unpacker unpacker = Pack200.newUnpacker();
unpacker.unpack(f, jostream);
} finally {
jostream.close();
fostream.close();
}
// delete pack file as its no longer needed
f.delete();
}
/**
* Extract all jars from any lzma/gz/pack files
*
* @param path output path
* @throws Exception if any errors occur
*/
protected void extractJars(String path) throws Exception {
setState(STATE_EXTRACTING_PACKAGES);
float increment = (float) 10.0 / urlList.length;
// extract all gz, lzma, pack.gz and pack.lzma files
for (int i = 0; i < urlList.length; i++) {
// if file has not changed, skip it
if (fileSizes[i] == -2) continue;
percentage = 55 + (int) (increment * (i+1));
String filename = getFileName(urlList[i]);
if (filename.endsWith(".pack.lzma")) {
subtaskMessage = "Extracting: " + filename + " to " + replaceLast(filename, ".lzma", "");
debug_sleep(1000);
extractLZMA(path + filename, path + replaceLast(filename, ".lzma", ""));
subtaskMessage = "Extracting: " + replaceLast(filename, ".lzma", "") + " to " + replaceLast(filename, ".pack.lzma", "");
debug_sleep(1000);
extractPack(path + replaceLast(filename, ".lzma", ""), path + replaceLast(filename, ".pack.lzma", ""));
}
else if (filename.endsWith(".pack.gz")) {
subtaskMessage = "Extracting: " + filename + " to " + replaceLast(filename, ".gz", "");
debug_sleep(1000);
extractGZip(path + filename, path + replaceLast(filename, ".gz", ""));
subtaskMessage = "Extracting: " + replaceLast(filename, ".gz", "") + " to " + replaceLast(filename, ".pack.gz", "");
debug_sleep(1000);
extractPack(path + replaceLast(filename, ".gz", ""), path + replaceLast(filename, ".pack.gz", ""));
}
else if (filename.endsWith(".pack")) {
subtaskMessage = "Extracting: " + filename + " to " + replaceLast(filename, ".pack", "");
debug_sleep(1000);
extractPack(path + filename, path + replaceLast(filename, ".pack", ""));
}
else if (filename.endsWith(".lzma")) {
subtaskMessage = "Extracting: " + filename + " to " + replaceLast(filename, ".lzma", "");
debug_sleep(1000);
extractLZMA(path + filename, path + replaceLast(filename, ".lzma", ""));
}
else if (filename.endsWith(".gz")) {
subtaskMessage = "Extracting: " + filename + " to " + replaceLast(filename, ".gz", "");
debug_sleep(1000);
extractGZip(path + filename, path + replaceLast(filename, ".gz", ""));
}
}
}
/**
* This method will extract all file from the native jar and extract them
* to the subdirectory called "natives" in the local path, will also check
* to see if the native jar files is signed properly
*
* @param path base folder containing all downloaded jars
* @throws Exception if it fails to extract files
*/
protected void extractNatives(String path) throws Exception {
setState(STATE_EXTRACTING_PACKAGES);
float percentageParts = 15f/nativeJarCount; // parts for each native jar from 15%
// create native folder
File nativeFolder = new File(path + "natives");
if (!nativeFolder.exists()) {
nativeFolder.mkdir();
}
// get the current AppletLoader certificates to compare against certificates of the native files
Certificate[] certificate = getCurrentCertificates();
for (int i = urlList.length - nativeJarCount; i < urlList.length; i++) {
// if a new native jar was not downloaded, no extracting needed
if (fileSizes[i] == -2) {
continue;
}
// get name of jar file with natives from urlList
String nativeJar = getJarName(urlList[i]);
// open jar file
JarFile jarFile = new JarFile(path + nativeJar, true);
// get list of files in jar
Enumeration entities = jarFile.entries();
totalSizeExtract = 0;
int jarNum = i - (urlList.length - nativeJarCount); // used for progressbar
// calculate the size of the files to extract for progress bar
while (entities.hasMoreElements()) {
JarEntry entry = (JarEntry) entities.nextElement();
// skip directories and anything in directories
// conveniently ignores the manifest
if (entry.isDirectory() || entry.getName().indexOf('/') != -1) {
continue;
}
totalSizeExtract += entry.getSize();
}
currentSizeExtract = 0;
// reset point to begining by getting list of file again
entities = jarFile.entries();
// extract all files from the jar
while (entities.hasMoreElements()) {
JarEntry entry = (JarEntry) entities.nextElement();
// skip directories and anything in directories
// conveniently ignores the manifest
if (entry.isDirectory() || entry.getName().indexOf('/') != -1) {
continue;
}
// check if native file already exists if so delete it to make room for new one
// useful when using the reload button on the browser
File f = new File(path + "natives" + File.separator + entry.getName());
if (f.exists()) {
if (!f.delete()) {
continue; // unable to delete file, it is in use, skip extracting it
}
}
debug_sleep(1000);
InputStream in = jarFile.getInputStream(jarFile.getEntry(entry.getName()));
OutputStream out = new FileOutputStream(path + "natives" + File.separator + entry.getName());
try {
int bufferSize;
byte buffer[] = new byte[65536];
while ((bufferSize = in.read(buffer, 0, buffer.length)) != -1) {
debug_sleep(10);
out.write(buffer, 0, bufferSize);
currentSizeExtract += bufferSize;
// update progress bar
percentage = 65 + (int)(percentageParts * (jarNum + currentSizeExtract/(float)totalSizeExtract));
subtaskMessage = "Extracting: " + entry.getName() + " " + ((currentSizeExtract * 100) / totalSizeExtract) + "%";
}
} finally {
in.close();
out.close();
}
// validate the certificate for the native file being extracted
if (!certificatesMatch(certificate, entry.getCertificates())) {
f.delete(); // delete extracted native as its certificates doesn't match
throw new Exception("The certificate(s) in " + nativeJar + " do not match the AppletLoader!");
}
}
subtaskMessage = "";
jarFile.close();
// delete native jar as it is no longer needed
File f = new File(path + nativeJar);
f.delete();
}
}
/**
* Compare two certificate chains to see if they match
*
* @param certs1 first chain of certificates
* @param certs2 second chain of certificates
*
* @return true if the certificate chains are the same
*/
protected static boolean certificatesMatch(Certificate[] certs1, Certificate[] certs2) throws Exception {
if (certs1 == null || certs2 == null) {
return false;
}
if (certs1.length != certs2.length) {
System.out.println("Certificate chain differs in length [" + certs1.length + " vs " + certs2.length + "]!");
return false;
}
for (int i = 0; i < certs1.length; i++) {
if (!certs1[i].equals(certs2[i])) {
System.out.println("Certificate mismatch found!");
return false;
}
}
return true;
}
/**
* Returns the current certificate chain of the AppletLoader
*
* @return - certificate chain of AppletLoader
*/
protected static Certificate[] getCurrentCertificates() throws Exception {
// get the current certificate to compare against native files
Certificate[] certificate = AppletLoader.class.getProtectionDomain().getCodeSource().getCertificates();
// workaround for bug where cached applet loader does not have certificates!?
if (certificate == null) {
URL location = AppletLoader.class.getProtectionDomain().getCodeSource().getLocation();
// manually load the certificate
JarURLConnection jurl = (JarURLConnection) (new URL("jar:" + location.toString() + "!/org/lwjgl/util/applet/AppletLoader.class").openConnection());
jurl.setDefaultUseCaches(true);
certificate = jurl.getCertificates();
jurl.setDefaultUseCaches(false);
}
return certificate;
}
/**
* Check and validate jars which will be loaded into the classloader to make
* sure that they are not corrupt. This ensures corrupt files are never marked
* as successful downloadeds by the cache system.
*
* @param path - where the jars are stored
* @throws Exception if a corrupt jar is found
*/
protected void validateJars(String path) throws Exception {
setState(STATE_VALIDATING_PACKAGES);
percentage = 80;
float percentageParts = 10f / urlList.length; // percentage for each file out of 10%
for (int i = 0; i < urlList.length - nativeJarCount; i++) {
debug_sleep(1000);
// if file not downloaded, no need to validate again
if (fileSizes[i] == -2) continue;
subtaskMessage = "Validating: " + getJarName(urlList[i]);
File file = new File(path, getJarName(urlList[i]));
if (!isZipValid(file)) {
throw new Exception("The file " + getJarName(urlList[i]) + " is corrupt!");
}
percentage = 80 + (int)(percentageParts * i);
}
subtaskMessage = "";
}
/**
* This method will check if a zip file is valid by running through it
* and checking for any corruption and CRC failures
*
* @param file - zip file to test
* @return boolean - runs false if the file is corrupt
*/
protected boolean isZipValid(File file) {
try {
ZipFile zipFile = new ZipFile(file);
try {
Enumeration e = zipFile.entries();
byte[] buffer = new byte[4096];
while(e.hasMoreElements()) {
ZipEntry zipEntry = (ZipEntry) e.nextElement();
CRC32 crc = new CRC32();
BufferedInputStream bis = new BufferedInputStream(zipFile.getInputStream(zipEntry));
CheckedInputStream cis = new CheckedInputStream(bis, crc);
while(cis.read(buffer, 0, buffer.length) != -1) {
// scroll through zip entry
}
if (crc.getValue() != zipEntry.getCrc()) {
return false; // CRC match failed, corrupt zip
}
}
return true; // valid zip file
} finally {
zipFile.close();
}
} catch (IOException e) {
e.printStackTrace();
return false;
}
}
/**
* Get Image from path provided
*
* @param s location of the image
* @return the Image file
*/
protected Image getImage(String s) {
// if s is "" then don't load an image
if (s.length() == 0) return null;
Image image = null;
try {
image = getImage(new URL(getCodeBase(), s));
} catch (Exception e) {
/* */
}
// if image failed to load, try another method
if (image == null) {
image = getImage(Thread.currentThread().getContextClassLoader().getResource(s));
}
// if image loaded sucessfully return it
if (image != null) {
return image;
}
// show error as image could not be loaded
fatalErrorOccured("Unable to load the logo/progressbar image: " + s, null);
return null;
}
/**
* Get Image from path provided
*
* @param url location of the image
* @return the Image file
*/
public Image getImage(URL url) {
try {
MediaTracker tracker = new MediaTracker(this);
Image image = super.getImage(url);
// wait for image to load
tracker.addImage(image, 0);
tracker.waitForAll();
// if no errors return image
if (!tracker.isErrorAny()) {
return image;
}
} catch (Exception e) {
/* */
}
return null;
}
/**
* Get jar name from URL.
*
* @param url Get jar file name from this url
* @return file name as string
*/
protected String getJarName(URL url) {
String fileName = url.getFile();
if (fileName.endsWith(".pack.lzma")) {
fileName = replaceLast(fileName, ".pack.lzma", "");
} else if (fileName.endsWith(".pack.gz")) {
fileName = replaceLast(fileName, ".pack.gz", "");
} else if (fileName.endsWith(".pack")) {
fileName = replaceLast(fileName, ".pack", "");
} else if (fileName.endsWith(".lzma")) {
fileName = replaceLast(fileName, ".lzma", "");
} else if (fileName.endsWith(".gz")) {
fileName = replaceLast(fileName, ".gz", "");
}
return fileName.substring(fileName.lastIndexOf('/') + 1);
}
/**
* Get file name portion of URL.
*
* @param url Get file name from this url
* @return file name as string
*/
protected String getFileName(URL url) {
String fileName = url.getFile();
return fileName.substring(fileName.lastIndexOf('/') + 1);
}
/**
* Retrieves the color
*
* @param param Color to load
* @param defaultColor Default color to use if no color to load
* @return Color to use
*/
protected Color getColor(String param, Color defaultColor) {
String color = getParameter(param);
if (color == null) return defaultColor;
// Check if RGB format
if (color.indexOf(",") != -1) {
StringTokenizer st = new StringTokenizer(color, ",");
// We've got three components for the color
try {
return new Color(Integer.parseInt(st.nextToken().trim()),
Integer.parseInt(st.nextToken().trim()),
Integer.parseInt(st.nextToken().trim()));
} catch (Exception e) {
// failed to parse
return defaultColor;
}
}
// Check & decode if the color is in hexadecimal color format (i.e. #808000)
try {
return Color.decode(color);
} catch (NumberFormatException e) {
// ignore exception
}
// Get the color by name if it exists
try {
return (Color)Color.class.getField(color).get(null);
} catch (Exception e) {
return defaultColor;
}
}
/**
* Replaces the last occurrence of the specified target substring with
* the specified replacement string in a string.
*
* @param original - String to search
* @param target - substring to find
* @param replacement - what to replace target substring with
* @return - return the modified string, if target substring not found return original string
*/
public String replaceLast(String original, String target, String replacement) {
int index = original.lastIndexOf(target);
if(index == -1) {
return original;
}
return original.substring(0, index) + replacement + original.substring(index + target.length());
}
/**
* Retrieves the String value for the parameter
* @param name Name of parameter
* @param defaultValue default value to return if no such parameter
* @return value of parameter or defaultValue
*/
protected String getStringParameter(String name, String defaultValue) {
String parameter = getParameter(name);
if (parameter != null) {
return parameter;
}
return defaultValue;
}
/**
* Retrieves the boolean value for the parameter
* @param name Name of parameter
* @param defaultValue default value to return if no such parameter
* @return value of parameter or defaultValue
*/
protected boolean getBooleanParameter(String name, boolean defaultValue) {
String parameter = getParameter(name);
if (parameter != null) {
return Boolean.parseBoolean(parameter);
}
return defaultValue;
}
/**
* Retrieves the int value for the applet
* @param name Name of parameter
* @param defaultValue default value to return if no such parameter
* @return value of parameter or defaultValue
*/
protected int getIntParameter(String name, int defaultValue) {
String parameter = getParameter(name);
if (parameter != null) {
return Integer.parseInt(parameter);
}
return defaultValue;
}
/**
* Sets the error message and print debug information
*
* @param error Error message to print
*/
protected void fatalErrorOccured(String error, Exception e) {
fatalError = true;
if (minimumJreNotFound) {
errorMessage = minimumJREMessage;
errorMessage[errorMessage.length-1] = error;
}
else if (certificateRefused) {
errorMessage = certificateRefusedMessage;
}
else {
errorMessage = genericErrorMessage;
errorMessage[errorMessage.length-1] = error;
}
System.out.println(error);
if(e != null) {
System.out.println(e.getMessage());
System.out.println(generateStacktrace(e));
}
repaint();
}
/**
* set the state of applet loader
* @param state new state of applet loader
* */
protected void setState(int state) {
this.state = state;
if(debugMode) {
System.out.println(getDescriptionForState());
}
}
/**
* Utility method for sleeping
* Will only really sleep if debug has been enabled
* @param ms milliseconds to sleep
*/
protected void debug_sleep(long ms) {
if(debugMode) {
sleep(ms);
}
}
/**
* Utility method for sleeping
* @param ms milliseconds to sleep
*/
protected void sleep(long ms) {
try {
Thread.sleep(ms);
} catch (Exception e) {
/* ignored */
}
}
}