/* * Copyright (c) 2004, P. Simon Tuffs (simon@simontuffs.com) * All rights reserved. * * See the full license at http://www.simontuffs.com/one-jar/one-jar-license.html * This license is also included in the distributions of this software * under doc/one-jar-license.txt */ package com.simontuffs.onejar; import java.io.BufferedReader; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.lang.reflect.Constructor; import java.lang.reflect.Method; import java.net.MalformedURLException; import java.net.URL; import java.security.AccessController; import java.security.PrivilegedAction; import java.util.Arrays; import java.util.Enumeration; import java.util.HashSet; import java.util.Properties; import java.util.Set; import java.util.jar.Attributes; import java.util.jar.JarEntry; import java.util.jar.JarFile; import java.util.jar.JarInputStream; import java.util.jar.Manifest; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; /** * Run a java application which requires multiple support jars from inside * a single jar file. * *

* Developer time JVM properties: *

 *   -Done-jar.main.class={name}  Use named class as main class to run. 
 *   -Done-jar.record[=recording] Record loaded classes into "recording" directory.
 *                                Flatten jar.names into directory tree suitable 
 * 								  for use as a classpath.
 *   -Done-jar.jar.names          Record loaded classes, preserve jar structure
 *   -Done-jar.verbose            Run the JarClassLoader in verbose mode.
 * 
* @author simon@simontuffs.com (http://www.simontuffs.com) */ public class Boot { /** * The name of the manifest attribute which controls which class * to bootstrap from the jar file. The boot class can * be in any of the contained jar files. */ public final static String BOOT_CLASS = "Boot-Class"; public final static String ONE_JAR_CLASSLOADER = "One-Jar-Class-Loader"; public final static String ONE_JAR_MAIN_CLASS = "One-Jar-Main-Class"; public final static String MANIFEST = "META-INF/MANIFEST.MF"; public final static String MAIN_JAR = "main/main.jar"; public final static String WRAP_CLASS_LOADER = "Wrap-Class-Loader"; public final static String WRAP_DIR = "wrap"; public final static String WRAP_JAR = "/" + WRAP_DIR + "/wraploader.jar"; // System properties. public final static String PROPERTY_PREFIX = "one-jar."; public final static String P_MAIN_CLASS = PROPERTY_PREFIX + "main.class"; public final static String P_RECORD = PROPERTY_PREFIX + "record"; public final static String P_JARNAMES = PROPERTY_PREFIX + "jar.names"; public final static String P_VERBOSE = PROPERTY_PREFIX + "verbose"; public final static String P_INFO = PROPERTY_PREFIX + "info"; public final static String P_STATISTICS = PROPERTY_PREFIX + "statistics"; public final static String P_SHOW_PROPERTIES = PROPERTY_PREFIX + "show.properties"; public final static String P_JARPATH = PROPERTY_PREFIX + "jar.path"; public final static String P_ONE_JAR_CLASS_PATH = PROPERTY_PREFIX + "class.path"; public final static String P_JAVA_CLASS_PATH = "java.class.path"; public final static String P_PATH_SEPARATOR = "|"; public final static String P_EXPAND_DIR = PROPERTY_PREFIX + "expand.dir"; // Command-line arguments public final static String HELP = "--one-jar-help"; public final static String VERSION = "--one-jar-version"; public final static String[] HELP_PROPERTIES = { P_MAIN_CLASS, "Specifies the name of the class which should be executed \n(via public static void main(String[])", P_RECORD, "true: Enables recording of the classes loaded by the application", P_JARNAMES, "true: Recorded classes are kept in directories corresponding to their jar names.\n" + "false: Recorded classes are flattened into a single directory. \nDuplicates are ignored (first wins)", P_VERBOSE, "true: Print verbose classloading information", P_INFO, "true: Print informative classloading information", P_STATISTICS, "true: Shows statistics about the One-Jar Classloader", P_JARPATH, "Full path of the one-jar file being executed. \nOnly needed if java.class.path does not contain the path to the jar, e.g. on Max OS/X.", P_ONE_JAR_CLASS_PATH, "Extra classpaths to be added to the execution environment. \nUse platform independent path separator '" + P_PATH_SEPARATOR + "'", P_EXPAND_DIR, "Directory to use for expanded files.", P_SHOW_PROPERTIES, "true: Shows the JVM system properties.", }; public final static String[] HELP_ARGUMENTS = { HELP, "Shows this message, then exits.", VERSION, "Shows the version of One-JAR, then exits.", }; protected static boolean info, verbose, statistics; protected static String myJarPath; protected static long startTime = System.currentTimeMillis(); protected static long endTime = 0; // Singleton loader. This must not be changed once it is set, otherwise all // sorts of nasty class-cast exceptions will ensue. Hence we control // access to it strongly. private static JarClassLoader loader = null; /** * This method provides access to the bootstrap One-JAR classloader which * is needed in the URL connection Handler when opening streams relative * to classes. * @return */ public synchronized static JarClassLoader getClassLoader() { return loader; } /** * This is the single point of entry for setting the "loader" member. It checks to * make sure programming errors don't call it more than once. * @param $loader */ public synchronized static void setClassLoader(JarClassLoader $loader) { if (loader != null) throw new RuntimeException("Attempt to set a second Boot loader"); loader = $loader; setProperties(loader); } protected static void VERBOSE(String message) { if (verbose) System.out.println("Boot: " + message); } protected static void WARNING(String message) { System.err.println("Boot: Warning: " + message); } protected static void INFO(String message) { if (info) System.out.println("Boot: Info: " + message); } protected static void PRINTLN(String message) { System.out.println("Boot: " + message); } public static void main(String[] args) throws Exception { run(args); } public static void run(String args[]) throws Exception { processArgs(args); // Is the main class specified on the command line? If so, boot it. // Othewise, read the main class out of the manifest. String mainClass = null; { // Default properties are in resource 'one-jar.properties'. Properties properties = new Properties(); String props = "/one-jar.properties"; InputStream is = Boot.class.getResourceAsStream(props); if (is != null) { INFO("loading properties from " + props); properties.load(is); } // Merge in anything in a local file with the same name. props = "file:one-jar.properties"; is = Boot.class.getResourceAsStream(props); if (is != null) { INFO("loading properties from " + props); properties.load(is); } // Set system properties only if not already specified. Enumeration _enum = properties.propertyNames(); while (_enum.hasMoreElements()) { String name = (String)_enum.nextElement(); if (System.getProperty(name) == null) { System.setProperty(name, properties.getProperty(name)); } } } if (Boolean.valueOf(System.getProperty(P_SHOW_PROPERTIES, "false")).booleanValue()) { // What are the system properties. Properties props = System.getProperties(); String keys[] = (String[])props.keySet().toArray(new String[]{}); Arrays.sort(keys); for (int i=0; i), or use " + ONE_JAR_MAIN_CLASS + " in the manifest"); if (mainClass != null) { // Guard against the main.jar pointing back to this // class, and causing an infinite recursion. String bootClass = Boot.class.getName(); if (bootClass.equals(mainClass)) throw new Exception(getMyJarName() + " main class (" + mainClass + ") would cause infinite recursion: check main.jar/META-INF/MANIFEST.MF/Main-Class attribute: " + mainClass); // Set the context classloader in case any classloaders delegate to it. // Otherwise it would default to the sun.misc.Launcher$AppClassLoader which // is used to launch the jar application, and attempts to load through // it would fail if that code is encapsulated inside the one-jar. Thread.currentThread().setContextClassLoader(loader); Class cls = loader.loadClass(mainClass); endTime = System.currentTimeMillis(); showTime(); Method main = cls.getMethod("main", new Class[]{String[].class}); main.invoke(null, new Object[]{args}); } } public static void showTime() { long endtime = System.currentTimeMillis(); if (statistics) { PRINTLN("Elapsed time: " + (endtime - startTime) + "ms"); } } public static void setProperties(IProperties jarloader) { INFO("setProperties(" + jarloader + ")"); if (getProperty(P_RECORD)) { jarloader.setRecord(true); jarloader.setRecording(System.getProperty(P_RECORD)); } if (getProperty(P_JARNAMES)) { jarloader.setRecord(true); jarloader.setFlatten(false); } if (getProperty(P_VERBOSE)) { jarloader.setVerbose(true); jarloader.setInfo(true); verbose = true; } if (getProperty(P_INFO)) { jarloader.setInfo(true); info = true; } statistics = getProperty(P_STATISTICS); } public static boolean getProperty(String key) { return Boolean.valueOf(System.getProperty(key, "false")).booleanValue(); } public static String getMyJarName() { String name = getMyJarPath(); int last = name.lastIndexOf("/"); if (last >= 0) { name = name.substring(last+1); } return name; } public static String getMyJarPath() { if (myJarPath != null) { return myJarPath; } myJarPath = System.getProperty(P_JARPATH); if (myJarPath == null) { try { // Hack to obtain the name of this jar file. String jarname = System.getProperty(P_JAVA_CLASS_PATH); // Open each Jar file looking for this class name. This allows for // JVM's that place more than the jar file on the classpath. String jars[] =jarname.split(System.getProperty("path.separator")); for (int i=0; i=)"); for (int i=0; i