diff --git a/res/appletlogo.png b/res/appletlogo.png new file mode 100644 index 00000000..626e7b4e Binary files /dev/null and b/res/appletlogo.png differ diff --git a/res/appletprogress.gif b/res/appletprogress.gif new file mode 100644 index 00000000..709157cf Binary files /dev/null and b/res/appletprogress.gif differ diff --git a/src/java/org/lwjgl/test/applet/AppletLoaderTest.java b/src/java/org/lwjgl/test/applet/AppletLoaderTest.java new file mode 100644 index 00000000..c270693e --- /dev/null +++ b/src/java/org/lwjgl/test/applet/AppletLoaderTest.java @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2006 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.test.applet; + +import java.applet.Applet; +import java.awt.BorderLayout; +import java.awt.Canvas; + +import org.lwjgl.opengl.AWTInputAdapter; + +public class AppletLoaderTest extends Applet { + + Test test = null; + + public void destroy() { + super.destroy(); + System.out.println("*** destroy ***"); + AWTInputAdapter.destroy(); + } + + public void start() { + super.start(); + System.out.println("*** start ***"); + } + + public void stop() { + super.stop(); + System.out.println("*** stop ***"); + test.stop(); + } + + public void init() { + System.out.println("*** init ***"); + + setLayout(new BorderLayout()); + try { + test = (Test) Class.forName(getParameter("test")).newInstance(); + Canvas canvas = (Canvas) test; + canvas.setSize(getWidth(), getHeight()); + add(canvas); + } catch (Exception e) { + e.printStackTrace(); + } + test.start(); + } +} diff --git a/src/java/org/lwjgl/util/applet/AppletLoader.java b/src/java/org/lwjgl/util/applet/AppletLoader.java new file mode 100644 index 00000000..a813d76a --- /dev/null +++ b/src/java/org/lwjgl/util/applet/AppletLoader.java @@ -0,0 +1,907 @@ +/* + * Copyright (c) 2002-2007 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 java.applet.Applet; +import java.applet.AppletStub; +import java.awt.Color; +import java.awt.FontMetrics; +import java.awt.Graphics; +import java.awt.GridLayout; +import java.awt.Image; +import java.awt.Toolkit; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.InputStream; +import java.io.OutputStream; +import java.lang.reflect.Method; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLClassLoader; +import java.net.URLConnection; +import java.security.AccessControlException; +import java.security.AccessController; +import java.security.PrivilegedExceptionAction; +import java.security.cert.Certificate; +import java.util.Enumeration; +import java.util.StringTokenizer; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; + +/** + *

+ * 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. + *

+ *

+ * The downloaded are extracted to the users temporary directory - and if enabled, cached for + * faster loading in future uses. + *

+ *

+ * The following applet parameters are required: + *

+ *

+ *

+ * Additionally the following parameters can be supplied to tweak the behaviour of the AppletLoader. + *

+ *

+ * @author kappaOne + * @author Brian Matzon + * @version $Revision$ + * $Id$ + */ +public class AppletLoader extends Applet implements Runnable, AppletStub { + /** initializing */ + public static final int STATE_INIT = 1; + + /** determining which packages that are required */ + public static final int STATE_DETERMINING_PACKAGES = 2; + + /** checking for already downloaded files */ + public static final int STATE_CHECKING_CACHE = 3; + + /** downloading packages */ + public static final int STATE_DOWNLOADING = 4; + + /** extracting packages */ + public static final int STATE_EXTRACTING_PACKAGES = 5; + + /** updating the classpath */ + public static final int STATE_UPDATING_CLASSPATH = 6; + + /** switching to real applet */ + public static final int STATE_SWITCHING_APPLET = 7; + + /** initializing real applet */ + public static final int STATE_INITIALIZE_REAL_APPLET = 8; + + /** stating real applet */ + public static final int STATE_START_REAL_APPLET = 9; + + /** done */ + public static final int STATE_DONE = 10; + + /** used to calculate length of progress bar */ + protected int percentage; + + /** current size of download in bytes */ + protected int currentSizeDownload; + + /** total size of download in bytes */ + protected int totalSizeDownload; + + /** 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; + + /** progressbar to render while loading */ + protected Image progressbar; + + /** offscreen image used */ + protected Image offscreen; + + /** background color of applet */ + protected Color bgColor = Color.white; + + /** Color to write errors in */ + protected Color errorColor = Color.red; + + /** color to write forground in */ + protected Color fgColor = Color.black; + + /** urls of the jars to download */ + protected URL[] urlList; + + /** list of jars to download */ + protected String jarList; + + /** actual thread that does the loading */ + protected Thread loaderThread; + + /** animation thread that renders our loaderscreen while loading */ + protected Thread animationThread; + + /** applet to load after all downloads are complete */ + protected Applet lwjglApplet; + + /** whether a fatal error occured */ + protected boolean fatalError; + + /** fatal error that occured */ + protected String fatalErrorDescription; + + /** whether we're running in debug mode */ + protected boolean debugMode; + + /** String to display as a subtask */ + protected String subtaskMessage = ""; + + /** state of applet loader */ + protected int state = STATE_INIT; + + /** generic error message to display on error */ + protected String[] genericErrorMessage = { "An error occured while loading the applet.", + "Plese contact support to resolve this issue.", + ""}; + + /* + * @see java.applet.Applet#init() + */ + public void init() { + + // sanity check + String[] requiredArgs = {"al_main", "al_logo", "al_progressbar", "al_jars"}; + for(int i=0; i 0) { + messageX = (getWidth() - fm.stringWidth(subtaskMessage)) / 2; + og.drawString(subtaskMessage, messageX, messageY+20); + } + + // draw loading bar, clipping it depending on percentage done + int barSize = (progressbar.getWidth(this) * percentage) / 100; + og.clipRect(0, 0, x + barSize, getHeight()); + og.drawImage(progressbar, x, y, null); + } + + og.dispose(); + + // finally draw it all + g.drawImage(offscreen, 0, 0, null); + } + + /** + * 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() { + state = STATE_DETERMINING_PACKAGES; + + StringTokenizer jar = new StringTokenizer(jarList, ", "); + + int jarCount = jar.countTokens() + 1; + + urlList = new URL[jarCount]; + + try { + URL path = getCodeBase(); + + // set jars urls + for (int i = 0; i < jarCount - 1; i++) { + urlList[i] = new URL(path, jar.nextToken()); + } + + // native jar url + String osName = System.getProperty("os.name"); + String nativeJar = null; + + if (osName.startsWith("Win")) { + nativeJar = getParameter("al_windows"); + } else if (osName.startsWith("Linux") || osName.startsWith("FreeBSD") || osName.startsWith("SunOS")) { + nativeJar = getParameter("al_linux"); + } else if (osName.startsWith("Mac")) { + nativeJar = getParameter("al_mac"); + } else { + fatalErrorOccured("OS (" + osName + ") not supported"); + } + + if (nativeJar == null) { + fatalErrorOccured("no lwjgl natives files found"); + } else { + urlList[jarCount - 1] = new URL(path, nativeJar); + } + + } catch (MalformedURLException e) { + fatalErrorOccured(e.getMessage()); + } + } + + /** + * 4 steps + * + * 1) check version of applet and decide whether to download jars + * 2) download the jars + * 3) extract natives + * 4) add to jars to class path + * 5) switch applets + * + */ + public void run() { + + state = STATE_CHECKING_CACHE; + + percentage = 5; + + try { + if(debugMode) { + sleep(2000); + } + + // get path where applet will be stored + String path = (String) AccessController.doPrivileged(new PrivilegedExceptionAction() { + public Object run() throws Exception { + return System.getProperty("java.io.tmpdir") + File.separator + getParameter("al_title") + File.separator; + } + }); + + File dir = new File(path); + + // create directory + if (!dir.exists()) { + dir.mkdir(); + } + dir = new File(dir, "version"); + + // if applet already available don't download anything + boolean cacheAvailable = false; + + // version of applet + String version = getParameter("al_version"); + float latestVersion = 0; + + // if applet version specifed, check if you have latest version of applet + if (version != null) { + + latestVersion = Float.parseFloat(version); + + // if version file exists + if (dir.exists()) { + // compare to new version + if (latestVersion <= readVersionFile(dir)) { + cacheAvailable = true; + percentage = 90; + if(debugMode) { + sleep(2000); + } + } + } + } + + // if jars not available or need updating download them + if (!cacheAvailable) { + // downloads jars from the server + downloadJars(path); // 10-65% + + // Extracts Native Files + extractNatives(path); // 65-85% + + // add version information once jars downloaded successfully + if (version != null) { + percentage = 90; + writeVersionFile(dir, latestVersion); + } + } + + // add the downloaded jars and natives to classpath + updateClassPath(path); + + // switch to LWJGL Applet + switchApplet(); + + state = STATE_DONE; + } catch (AccessControlException ace) { + fatalErrorOccured(ace.getMessage()); + } catch (Exception e) { + fatalErrorOccured(e.getMessage()); + } finally { + loaderThread = null; + } + } + + /** + * read the current version file + * + * @param file the file to read + * @return the version value of saved file + * @throws Exception if it fails to read value + */ + protected float readVersionFile(File file) throws Exception { + DataInputStream dis = new DataInputStream(new FileInputStream(file)); + float version = dis.readFloat(); + dis.close(); + return version; + } + + /** + * write out version file of applet + * + * @param file the file to write out to + * @param version the version of the applet as a float + * @throws Exception if it fails to write file + */ + protected void writeVersionFile(File file, float version) throws Exception { + DataOutputStream dos = new DataOutputStream(new FileOutputStream(file)); + dos.writeFloat(version); + dos.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(String path) throws Exception { + + state = STATE_UPDATING_CLASSPATH; + + percentage = 95; + + Class[] parameters = new Class[] { URL.class}; + + // modify class path by adding downloaded jars to it + for (int i = 0; i < urlList.length; i++) { + // get location of jar as a url + URL u = new URL("file:" + path + getFileName(urlList[i])); + + // add to class path + Method method = URLClassLoader.class.getDeclaredMethod("addURL", parameters); + method.setAccessible(true); + method.invoke((URLClassLoader) ClassLoader.getSystemClassLoader(), new Object[] { u}); + } + + if(debugMode) { + sleep(2000); + } + + // 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"); + + // replace security system to avoid bug where vm fails to + // recognise downloaded jars as signed, when they are + System.setSecurityManager(null); + } + + /** + * replace the current applet with the lwjgl applet + * using AppletStub and initialise and start it + */ + protected void switchApplet() throws Exception { + + state = STATE_SWITCHING_APPLET; + percentage = 100; + + if(debugMode) { + sleep(2000); + } + + Class appletClass = Class.forName(getParameter("al_main")); + lwjglApplet = (Applet) appletClass.newInstance(); + + lwjglApplet.setStub(this); + + setLayout(new GridLayout(1, 1)); + add(lwjglApplet); + validate(); + + state = STATE_INITIALIZE_REAL_APPLET; + lwjglApplet.init(); + + state = STATE_START_REAL_APPLET; + lwjglApplet.start(); + } + + /** + * 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 { + + state = STATE_DOWNLOADING; + + URLConnection urlconnection; + + // calculate total size of jars to download + for (int i = 0; i < urlList.length; i++) { + urlconnection = urlList[i].openConnection(); + totalSizeDownload += urlconnection.getContentLength(); + } + + int initialPercentage = percentage = 10; + + // download each jar + byte buffer[] = new byte[65536]; + for (int i = 0; i < urlList.length; i++) { + if(debugMode) { + sleep(2000); + } + + urlconnection = urlList[i].openConnection(); + + String currentFile = getFileName(urlList[i]); + InputStream inputstream = urlconnection.getInputStream(); + FileOutputStream fos = new FileOutputStream(path + currentFile); + + + int bufferSize; + while ((bufferSize = inputstream.read(buffer, 0, buffer.length)) != -1) { + if(debugMode) { + sleep(10); + } + fos.write(buffer, 0, bufferSize); + currentSizeDownload += bufferSize; + percentage = initialPercentage + ((currentSizeDownload * 55) / totalSizeDownload); + subtaskMessage = "Retrieving: " + currentFile + " " + ((currentSizeDownload * 100) / totalSizeDownload) + "%"; + } + } + subtaskMessage = ""; + } + + /** + * 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 { + + state = STATE_EXTRACTING_PACKAGES; + + int initialPercentage = percentage; + + // get name of jar file with natives from urlList, it will be the last url + String nativeJar = getFileName(urlList[urlList.length - 1]); + + // get the current certificate to compare against native files + Certificate[] certificate = AppletLoader.class.getProtectionDomain().getCodeSource().getCertificates(); + + // create native folder + File nativeFolder = new File(path + "natives"); + if (!nativeFolder.exists()) { + nativeFolder.mkdir(); + } + + // open jar file + JarFile jarFile = new JarFile(path + nativeJar, true); + + // get list of files in jar + Enumeration entities = jarFile.entries(); + + totalSizeExtract = 0; + + // 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 + } + } + + if(debugMode) { + sleep(1000); + } + + InputStream in = jarFile.getInputStream(jarFile.getEntry(entry.getName())); + OutputStream out = new FileOutputStream(path + "natives" + File.separator + entry.getName()); + + int bufferSize; + byte buffer[] = new byte[65536]; + + while ((bufferSize = in.read(buffer, 0, buffer.length)) != -1) { + if(debugMode) { + sleep(10); + } + out.write(buffer, 0, bufferSize); + currentSizeExtract += bufferSize; + + // update progress bar + percentage = initialPercentage + ((currentSizeExtract * 20) / totalSizeExtract); + subtaskMessage = "Extracting: " + entry.getName() + " " + ((currentSizeExtract * 100) / totalSizeExtract) + "%"; + } + + // validate if the certificate for native file is correct + validateCertificateChain(certificate, entry.getCertificates()); + + in.close(); + out.close(); + } + subtaskMessage = ""; + + jarFile.close(); + } + + /** + * Validates the certificate chain for a single file + * + * @param ownCerts Chain of certificates to check against + * @param native_certs Chain of certificates to check + * @return true if the chains match + */ + protected static void validateCertificateChain(Certificate[] ownCerts, Certificate[] native_certs) throws Exception { + if (native_certs == null) + throw new Exception("Unable to validate certificate chain. Native entry did not have a certificate chain at all"); + + if (ownCerts.length != native_certs.length) + throw new Exception("Unable to validate certificate chain. Chain differs in length [" + ownCerts.length + " vs " + native_certs.length + "]"); + + for (int i = 0; i < ownCerts.length; i++) { + if (!ownCerts[i].equals(native_certs[i])) { + throw new Exception("Certificate mismatch: " + ownCerts[i] + " != " + native_certs[i]); + } + } + } + + /** + * Get Image from path provided + * + * @param s location of the image + * @return the Image file + */ + protected Image getImage(String s) { + try { + DataInputStream datainputstream = new DataInputStream(getClass().getResourceAsStream(s)); + byte abyte0[] = new byte[datainputstream.available()]; + datainputstream.readFully(abyte0); + datainputstream.close(); + return Toolkit.getDefaultToolkit().createImage(abyte0); + } catch (Exception e) { + /* */ + } + return null; + } + + + /** + * 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 color Color to load + * @param defaultColor Default color to use if no color to load + * @return Color to use + */ + protected Color getColor(String color, Color defaultColor) { + String param_color = getParameter(color); + if (param_color != null) { + return new Color(Integer.parseInt(param_color, 16)); + } + return defaultColor; + } + + /** + * Retrieves the boolean 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 boolean getBooleanParameter(String name, boolean defaultValue) { + String parameter = getParameter(name); + if(parameter != null) { + return Boolean.parseBoolean(parameter); + } + return defaultValue; + } + + /** + * Sets the state of the loaded and prints some debug information + * + * @param error Error message to print + * @param state State to enter + */ + protected void fatalErrorOccured(String error) { + fatalError = true; + fatalErrorDescription = "Fatal error occured (" + state + "): " + error; + System.out.println(fatalErrorDescription); + repaint(); + } + + /** + * Utility method for sleeping + * @param ms milliseconds to sleep + */ + protected void sleep(long ms) { + try { + Thread.sleep(ms); + } catch (Exception e) { + /* ignored */ + } + } +} \ No newline at end of file