/*
* 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;
/**
* Run a java application which requires multiple support jars from inside
* a single jar file.
*
* <p>
* Developer time JVM properties:
* <pre>
* -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.
* </pre>
* @author simon@simontuffs.com (<a href="http://www.simontuffs.com">http://www.simontuffs.com</a>)
*/
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_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";
// 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 (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. Duplicates are ignored (first wins)",
P_VERBOSE, "true: Print verbose classloading information",
P_INFO, "true: Print informative classloading information"
};
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;
protected static String myJarPath;
// 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);
}
public static void main(String[] args) throws Exception {
run(args);
}
public static void run(String args[]) throws Exception {
if (false) {
// What are the system properties.
Properties props = System.getProperties();
Enumeration _enum = props.keys();
while (_enum.hasMoreElements()) {
String key = (String)_enum.nextElement();
System.out.println(key + "=" + props.get(key));
}
}
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));
}
}
}
// Process developer properties:
mainClass = System.getProperty(P_MAIN_CLASS);
// If no main-class specified, check the manifest of the main jar for
// a Boot-Class attribute.
if (mainClass == null) {
String jar = getMyJarPath();
JarFile jarFile = new JarFile(jar);
Manifest manifest = jarFile.getManifest();
Attributes attributes = manifest.getMainAttributes();
mainClass = attributes.getValue(ONE_JAR_MAIN_CLASS);
if (mainClass == null) {
mainClass = attributes.getValue(BOOT_CLASS);
if (mainClass != null) {
WARNING("The manifest attribute " + BOOT_CLASS + " is deprecated in favor of the attribute " + ONE_JAR_MAIN_CLASS);
}
}
}
if (mainClass == null) {
// Still don't have one (default). One final try: look for a jar file in a
// main directory. There should be only one, and it's manifest
// Main-Class attribute is the main class. The JarClassLoader will take
// care of finding it.
InputStream is = Boot.class.getResourceAsStream("/" + MAIN_JAR);
if (is != null) {
JarInputStream jis = new JarInputStream(is);
Manifest manifest = jis.getManifest();
jis.close();
Attributes attributes = manifest.getMainAttributes();
mainClass = attributes.getValue(Attributes.Name.MAIN_CLASS);
} else {
// There is no main jar. Warning.
WARNING("Unable to locate " + MAIN_JAR + " in the JAR file " + getMyJarPath());
}
}
// Do we need to create a wrapping classloader? Check for the
// presence of a "wrap" directory at the top of the jar file.
URL url = Boot.class.getResource(WRAP_JAR);
if (url != null) {
// Wrap class loaders.
JarClassLoader bootLoader = (JarClassLoader)AccessController.doPrivileged(
new PrivilegedAction() {
public Object run() {
return new JarClassLoader(WRAP_DIR);
}
}
);
setProperties(bootLoader);
bootLoader.load(null);
// Read the "Wrap-Class-Loader" property from the wraploader jar file.
// This is the class to use as a wrapping class-loader.
InputStream is = Boot.class.getResourceAsStream(WRAP_JAR);
if (is != null) {
JarInputStream jis = new JarInputStream(is);
String wrapLoader = jis.getManifest().getMainAttributes().getValue(WRAP_CLASS_LOADER);
jis.close();
if (wrapLoader == null) {
WARNING(url + " did not contain a " + WRAP_CLASS_LOADER + " attribute, unable to load wrapping classloader");
} else {
INFO("using " + wrapLoader);
Class jarLoaderClass = bootLoader.loadClass(wrapLoader);
Constructor ctor = jarLoaderClass.getConstructor(new Class[]{ClassLoader.class});
setClassLoader((JarClassLoader)ctor.newInstance(new Object[]{bootLoader}));
}
}
} else {
INFO("using JarClassLoader");
setClassLoader((JarClassLoader)AccessController.doPrivileged(
new PrivilegedAction() {
public Object run() {
return new JarClassLoader(Boot.class.getClassLoader());
}
}
));
}
setProperties(loader);
mainClass = loader.load(mainClass);
if (mainClass == null && !loader.isExpanding())
throw new Exception(getMyJarName() + " main class was not found (fix: add main/main.jar with a Main-Class manifest attribute, or specify -D" + P_MAIN_CLASS + "=<your.class.name>), 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);
Method main = cls.getMethod("main", new Class[]{String[].class});
main.invoke(null, new Object[]{args});
}
}
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);
}
if (getProperty(P_INFO)) {
jarloader.setInfo(true);
}
}
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(PROPERTY_PREFIX + "jarname");
if (myJarPath == null) {
try {
// Hack to obtain the name of this jar file.
String jarname = System.getProperty(JarClassLoader.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<jars.length; i++) {
jarname = jars[i];
// Allow for URL based paths, as well as file-based paths. File
InputStream is = null;
try {
is = new URL(jarname).openStream();
} catch (MalformedURLException mux) {
// Try a local file.
try {
is = new FileInputStream(jarname);
} catch (IOException iox) {
// Ignore...
continue;
}
}
JarEntry entry = findJarEntry(new JarInputStream(is), Boot.class.getName().replace('.', '/') + ".class");
if (entry != null) {
myJarPath = jarname;
break;
}
}
} catch (Exception x) {
x.printStackTrace();
WARNING("jar=" + myJarPath + " loaded from " + JarClassLoader.JAVA_CLASS_PATH + " (" + System.getProperty(JarClassLoader.JAVA_CLASS_PATH) + ")");
}
}
// Normalize those annoying DOS backslashes.
myJarPath = myJarPath.replace('\\', '/');
return myJarPath;
}
public static JarEntry findJarEntry(JarInputStream jis, String name) throws IOException {
JarEntry entry; ;
while ((entry = jis.getNextJarEntry()) != null) {
if (entry.getName().equals(name)) {
return entry;
}
}
return null;
}
public static int firstWidth(String[] table) {
int width = 0;
for (int i=0; i<table.length; i+=2) {
if (table[i].length() > width) width = table[i].length();
}
return width;
}
public static String pad(String indent, String string, int width) {
StringBuffer buf = new StringBuffer();
buf.append(indent);
buf.append(string);
for (int i=0; i<width-string.length(); i++) {
buf.append(" ");
}
return buf.toString();
}
public static String wrap(String indent, String string, int width) {
String padding = pad(indent, "", width);
string = string.replaceAll("\n", "\n" + padding);
return string;
}
public static void processArgs(String args[]) throws Exception {
// Check for arguments which matter to us. Process them, but pass them through to the
// application too. (TODO: maybe make this passthrough optional).
Set arguments = new HashSet(Arrays.asList(args));
if (arguments.contains(HELP)) {
int width = firstWidth(HELP_ARGUMENTS);
// Width of first column
System.out.println("One-Jar uses the following command-line arguments");
for (int i=0; i<HELP_ARGUMENTS.length; i++) {
System.out.print(pad(" ", HELP_ARGUMENTS[i++], width+1));
System.out.println(wrap(" ", HELP_ARGUMENTS[i], width+1));
}
System.out.println();
width = firstWidth(HELP_PROPERTIES);
System.out.println("One-Jar uses the following VM properties (-D<property>=<true|false|string>)");
for (int i=0; i<HELP_PROPERTIES.length; i++) {
System.out.print(pad(" ", HELP_PROPERTIES[i++], width+1));
System.out.println(wrap(" ", HELP_PROPERTIES[i], width+1));
}
System.out.println();
System.exit(0);
} else if (arguments.contains(VERSION)) {
InputStream is = Boot.class.getResourceAsStream("/.version");
if (is != null) {
BufferedReader br = new BufferedReader(new InputStreamReader(is));
String version = br.readLine();
br.close();
System.out.println("One-JAR version " + version);
} else {
System.out.println("Unable to determine One-JAR version (missing /.version resource in One-JAR archive)");
}
System.exit(0);
}
}
}