/*
* Copyright (c) 2004-2010, P. Simon Tuffs (simon@simontuffs.com)
* All rights reserved.
*
* See the full license at http://one-jar.sourceforge.net/one-jar-license.html
* This license is also included in the distributions of this software
* under doc/one-jar-license.txt
*/
/**
* Many thanks to the following for their contributions to One-Jar:
*
* Contributor: Christopher Ottley <xknight@users.sourceforge.net>
* Contributor: Thijs Sujiten (www.semantica.nl)
* Contributor: Gerold Friedmann
*/
package com.simontuffs.onejar;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.net.URLConnection;
import java.net.URLStreamHandler;
import java.security.AccessController;
import java.security.CodeSource;
import java.security.PrivilegedAction;
import java.security.ProtectionDomain;
import java.security.cert.Certificate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.jar.Attributes;
import java.util.jar.JarEntry;
import java.util.jar.JarInputStream;
import java.util.jar.Manifest;
import java.util.jar.Attributes.Name;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Loads classes from pre-defined locations inside the jar file containing this
* class. Classes will be loaded from jar files contained in the following
* locations within the main jar file (on the classpath of the application
* actually, which when running with the "java -jar" command works out to be
* the same thing).
* <ul>
* <li>
* /lib Used to contain library jars.
* </li>
* <li>
* /main Used to contain a default main jar.
* </li>
* </ul>
* @author simon@simontuffs.com (<a href="http://www.simontuffs.com">http://www.simontuffs.com</a>)
*/
public class JarClassLoader extends ClassLoader implements IProperties {
public final static String PROPERTY_PREFIX = "one-jar.";
public final static String P_INFO = PROPERTY_PREFIX + "info";
public final static String P_VERBOSE = PROPERTY_PREFIX + "verbose";
public final static String P_JARNAMES = PROPERTY_PREFIX + "jar.names";
public final static String P_RECORD = PROPERTY_PREFIX + "record";
// System properties.
public final static String P_EXPAND_DIR = JarClassLoader.PROPERTY_PREFIX + "expand.dir";
public final static String P_PATH_SEPARATOR = "|";
public final static String P_ONE_JAR_CLASS_PATH = JarClassLoader.PROPERTY_PREFIX + "class.path";
public final static String MANIFEST = "META-INF/MANIFEST.MF";
public final static String LIB_PREFIX = "lib/";
public final static String BINLIB_PREFIX = "binlib/";
public final static String MAIN_PREFIX = "main/";
public final static String RECORDING = "recording";
public final static String TMP = "tmp";
public final static String UNPACK = "unpack";
public final static String EXPAND = "One-Jar-Expand";
public final static String EXPAND_DIR = "One-Jar-Expand-Dir";
public final static String SHOW_EXPAND = "One-Jar-Show-Expand";
public final static String CONFIRM_EXPAND = "One-Jar-Confirm-Expand";
public final static String CLASS = ".class";
public final static String NL = System.getProperty("line.separator");
public final static String JAVA_PROTOCOL_HANDLER = "java.protocol.handler.pkgs";
protected String name;
protected boolean noExpand, expanded;
protected ClassLoader externalClassLoader;
protected String oneJarPath;
public String getOneJarPath() {
return oneJarPath;
}
public void setOneJarPath(String oneJarPath) throws MalformedURLException {
URL url;
try {
url = new URL(oneJarPath);
} catch (MalformedURLException x) {
url = new URL("file:" + oneJarPath);
}
System.out.println("oneJarPath=" + url);
this.oneJarPath = url.toString();
}
static {
// Add our 'onejar:' protocol handler, but leave open the
// possibility of a subsequent class taking over the
// factory. TODO: (how reasonable is this?)
String handlerPackage = System.getProperty(JAVA_PROTOCOL_HANDLER);
if (handlerPackage == null) handlerPackage = "";
if (handlerPackage.length() > 0) handlerPackage = "|" + handlerPackage;
handlerPackage = "com.simontuffs" + handlerPackage;
System.setProperty(JAVA_PROTOCOL_HANDLER, handlerPackage);
}
protected String PREFIX() {
return "JarClassLoader: ";
}
protected String NAME() {
return (name != null? "'" + name + "' ": "");
}
protected void VERBOSE(String message) {
if (verbose) System.out.println(PREFIX() + NAME() + message);
}
protected void WARNING(String message) {
System.err.println(PREFIX() + "Warning: " + NAME() + message);
}
protected void INFO(String message) {
if (info) System.out.println(PREFIX() + "Info: " + NAME() + message);
}
protected void PRINTLN(String message) {
System.out.println(message);
}
protected void PRINT(String message) {
System.out.print(message);
}
// Synchronize for thread safety. This is less important until we
// start to do lazy loading, but it's a good idea anyway.
protected Map byteCode = Collections.synchronizedMap(new HashMap());
protected Map pdCache = Collections.synchronizedMap(new HashMap());
protected Map binLibPath = Collections.synchronizedMap(new HashMap());
protected Set jarNames = Collections.synchronizedSet(new HashSet());
protected boolean record = false, flatten = false, unpackFindResource = false;
protected boolean verbose = false, info = false;
protected String recording = RECORDING;
protected String jarName, mainJar, wrapDir;
protected boolean delegateToParent;
protected static class ByteCode {
public ByteCode(String $name, String $original, ByteArrayOutputStream baos, String $codebase, Manifest $manifest) {
name = $name;
original = $original;
bytes = baos.toByteArray();
codebase = $codebase;
manifest = $manifest;
}
public byte bytes[];
public String name, original, codebase;
public Manifest manifest;
}
/**
* Create a non-delegating but jar-capable classloader for bootstrap
* purposes.
* @param $wrap The directory in the archive from which to load a
* wrapping classloader.
*/
public JarClassLoader(String $wrap) {
wrapDir = $wrap;
delegateToParent = wrapDir == null;
setProperties(this);
init();
}
/**
* The main constructor for the Jar-capable classloader.
* @param $record If true, the JarClassLoader will record all used classes
* into a recording directory (called 'recording' by default)
* The name of each jar file will be used as a directory name
* for the recorded classes.
* @param $flatten Whether to flatten out the recorded classes (i.e. eliminate
* the jar-file name from the recordings).
*
* Example: Given the following layout of the one-jar.jar file
* <pre>
* /
* /META-INF
* | MANIFEST.MF
* /com
* /simontuffs
* /onejar
* Boot.class
* JarClassLoader.class
* /main
* main.jar
* /com
* /main
* Main.class
* /lib
* util.jar
* /com
* /util
* Util.clas
* </pre>
* The recording directory will look like this:
* <ul>
* <li>flatten=false</li>
* <pre>
* /recording
* /main.jar
* /com
* /main
* Main.class
* /util.jar
* /com
* /util
* Util.class
* </pre>
*
* <li>flatten = true</li>
* <pre>
* /recording
* /com
* /main
* Main.class
* /util
* Util.class
*
* </ul>
* Flatten mode is intended for when you want to create a super-jar which can
* be launched directly without using one-jar's launcher. Run your application
* under all possible scenarios to collect the actual classes which are loaded,
* then jar them all up, and point to the main class with a "Main-Class" entry
* in the manifest.
*
*/
public JarClassLoader(ClassLoader parent) {
super(parent);
delegateToParent = true;
setProperties(this);
init();
// System.out.println(PREFIX() + this + " parent=" + parent + " loaded by " + this.getClass().getClassLoader());
}
protected static ThreadLocal current = new ThreadLocal();
/**
* Common initialization code: establishes a classloader for delegation
* to one-jar.class.path resources.
*/
protected void init() {
String classpath = System.getProperty(JarClassLoader.P_ONE_JAR_CLASS_PATH);
if (classpath != null) {
String tokens[] = classpath.split("\\" + JarClassLoader.P_PATH_SEPARATOR);
List list = new ArrayList();
for (int i=0; i<tokens.length; i++) {
String path = tokens[i];
try {
list.add(new URL(path));
} catch (MalformedURLException mux) {
// Try a file:// prefix and an absolute path.
try {
String _path = new File(path).getCanonicalPath();
// URLClassLoader searches in a directory if and only if the path ends with '/':
// toURI() takes care of adding the trailing slash in this case so everything's ok
list.add(new File(_path).toURI().toURL());
} catch (Exception ignore) {
WARNING("Unable to parse external path: " + path + ":- " + ignore);
}
}
}
final URL urls[] = (URL[])list.toArray(new URL[0]);
INFO("external URLs=" + Arrays.asList(urls));
// BUG-2833948
// Delegate back into this classloader, use ThreadLocal to avoid recursion.
externalClassLoader = (URLClassLoader)AccessController.doPrivileged(
new PrivilegedAction() {
public Object run() {
return new URLClassLoader(urls, JarClassLoader.this) {
// Handle recursion for classes, and mutual recursion for resources.
final static String LOAD_CLASS = "loadClass():";
final static String GET_RESOURCE = "getResource():";
final static String FIND_RESOURCE = "findResource():";
// Protect entry points which could lead to recursion. Strangely
// inelegant because you can't proxy a class. Or use closures.
public Class loadClass(String name) throws ClassNotFoundException {
if (reentered(LOAD_CLASS + name)) {
throw new ClassNotFoundException(name);
}
VERBOSE("externalClassLoader.loadClass(" + name + ")");
Object old = current.get();
current.set(LOAD_CLASS + name);
try {
return super.loadClass(name);
} finally {
current.set(old);
}
}
public URL getResource(String name) {
if (reentered(GET_RESOURCE + name))
return null;
VERBOSE("externalClassLoader.getResource(" + name + ")");
Object old = current.get();
current.set(GET_RESOURCE + name);
try {
return super.getResource(name);
} finally {
current.set(old);
}
}
public URL findResource(String name) {
if (reentered(FIND_RESOURCE + name))
return null;
VERBOSE("externalClassLoader.findResource(" + name + ")");
Object old = current.get();
current.set(name);
try {
current.set(FIND_RESOURCE + name);
return super.findResource(name);
} finally {
current.set(old);
}
}
protected boolean reentered(String name) {
// Defend against null name: not sure about semantics there.
Object old = current.get();
return old != null && old.equals(name);
}
};
}
});
}
}
public String load(String mainClass) {
// Hack: if there is a one-jar.jarname property, use it.
return load(mainClass, oneJarPath);
}
public String load(String mainClass, String jarName) {
VERBOSE("load("+mainClass+","+jarName+")");
if (record) {
new File(recording).mkdirs();
}
try {
if (jarName == null) {
jarName = oneJarPath;
}
JarInputStream jis = new JarInputStream(new URL(jarName).openConnection().getInputStream());
Manifest manifest = jis.getManifest();
String expandPaths[] = null;
// TODO: Allow a destination directory (relative or absolute) to
// be specified like this:
// One-Jar-Expand: build=../expanded
String expand = manifest.getMainAttributes().getValue(EXPAND);
String expanddir = System.getProperty(JarClassLoader.P_EXPAND_DIR);
if (expanddir == null) {
expanddir = manifest.getMainAttributes().getValue(EXPAND_DIR);
}
// Default is to expand into temporary directory based on the name of the jar file.
if (expanddir == null) {
String jar = new File(jarName).getName().replaceFirst("\\.[^\\.]*$", "");
expanddir = "${java.io.tmpdir}/" + jar;
}
// Expand system properties.
expanddir = replaceProps(System.getProperties(), expanddir);
// Make a note of this location in the VM system properties in case applications need to know
// where the expanded files are.
System.setProperty(JarClassLoader.P_EXPAND_DIR, expanddir);
boolean shouldExpand = true;
File tmpdir = new File(expanddir);
if (noExpand == false && expand != null) {
expanded = true;
VERBOSE(EXPAND + "=" + expand);
expandPaths = expand.split(",");
boolean getconfirm = Boolean.TRUE.toString().equals(manifest.getMainAttributes().getValue(CONFIRM_EXPAND));
if (getconfirm) {
String answer = getConfirmation(tmpdir);
if (answer == null) answer = "n";
answer = answer.trim().toLowerCase();
if (answer.startsWith("q")) {
PRINTLN("exiting without expansion.");
// Indicate (expected) failure with a non-zero return code.
System.exit(1);
} else if (answer.startsWith("n")) {
shouldExpand = false;
}
}
}
boolean showexpand = Boolean.TRUE.toString().equals(manifest.getMainAttributes().getValue(SHOW_EXPAND));
if (showexpand) {
PRINTLN("Expanding to: " + tmpdir.getAbsolutePath());
}
JarEntry entry;
while ((entry = (JarEntry)jis.getNextEntry()) != null) {
if (entry.isDirectory())
continue;
// The META-INF/MANIFEST.MF file can contain a property which names
// directories in the JAR to be expanded (comma separated). For example:
// One-Jar-Expand: build,tmp,webapps
String $entry = entry.getName();
if (expandPaths != null) {
// TODO: Can't think of a better way to do this right now.
// This code really doesn't need to be optimized anyway.
if (shouldExpand && shouldExpand(expandPaths, $entry)) {
File dest = new File(tmpdir, $entry);
// Override if ZIP file is newer than existing.
if (!dest.exists() || dest.lastModified() < entry.getTime()) {
String msg = "Expanding: " + $entry;
if (showexpand) {
PRINTLN(msg);
} else {
INFO(msg);
}
if (dest.exists()) INFO("Update because lastModified=" + new Date(dest.lastModified()) + ", entry=" + new Date(entry.getTime()));
File parent = dest.getParentFile();
if (parent != null) {
parent.mkdirs();
}
VERBOSE("using jarFile.getInputStream(" + entry + ")");
FileOutputStream os = new FileOutputStream(dest);
copy(jis, os);
os.close();
} else {
String msg = "Up-to-date: " + $entry;
if (showexpand) {
PRINTLN(msg);
} else {
VERBOSE(msg);
}
}
}
}
if (wrapDir != null && $entry.startsWith(wrapDir) || $entry.startsWith(LIB_PREFIX) || $entry.startsWith(MAIN_PREFIX)) {
if (wrapDir != null && !entry.getName().startsWith(wrapDir))
continue;
// Load it!
VERBOSE("caching " + $entry);
VERBOSE("using jarFile.getInputStream(" + entry + ")");
{
// Note: loadByteCode consumes the input stream, so make sure its scope
// does not extend beyond here.
loadByteCode(jis, $entry, null);
}
// Do we need to look for a main class?
if ($entry.startsWith(MAIN_PREFIX)) {
if (mainClass == null) {
JarInputStream mis = new JarInputStream(jis);
Manifest m = mis.getManifest();
// Is this a jar file with a manifest?
if (m != null) {
mainClass = mis.getManifest().getMainAttributes().getValue(Attributes.Name.MAIN_CLASS);
mainJar = $entry;
}
} else if (mainJar != null) {
WARNING("A main class is defined in multiple jar files inside " + MAIN_PREFIX + mainJar + " and " + $entry);
WARNING("The main class " + mainClass + " from " + mainJar + " will be used");
}
}
} else if (wrapDir == null && $entry.startsWith(UNPACK)) {
// Unpack into a temporary directory which is on the classpath of
// the application classloader. Badly designed code which relies on the
// application classloader can be made to work in this way.
InputStream is = this.getClass().getResourceAsStream("/" + $entry);
if (is == null) throw new IOException($entry);
// Make a sentinel.
File dir = new File(TMP);
File sentinel = new File(dir, $entry.replace('/', '.'));
if (!sentinel.exists()) {
INFO("unpacking " + $entry + " into " + dir.getCanonicalPath());
loadByteCode(is, $entry, TMP);
sentinel.getParentFile().mkdirs();
sentinel.createNewFile();
}
} else if ($entry.endsWith(CLASS)) {
// A plain vanilla class file rooted at the top of the jar file.
loadBytes(entry, jis, "/", null, manifest);
VERBOSE("One-Jar class: " + jarName + "!/" + entry.getName());
} else {
// A resource?
loadBytes(entry, jis, "/", null, manifest);
VERBOSE("One-Jar resource: " + jarName + "!/" + entry.getName());
}
}
// If mainClass is still not defined, return null. The caller is then responsible
// for determining a main class.
} catch (IOException iox) {
System.err.println("Unable to load resource: " + iox);
iox.printStackTrace(System.err);
}
return mainClass;
}
public static String replaceProps(Map replace, String string) {
Pattern pat = Pattern.compile("\\$\\{([^\\}]*)");
Matcher mat = pat.matcher(string);
boolean found = mat.find();
Map props = new HashMap();
while (found) {
String prop = mat.group(1);
props.put(prop, replace.get(prop));
found = mat.find();
}
Iterator iter = props.entrySet().iterator();
while (iter.hasNext()) {
Map.Entry entry = (Map.Entry)iter.next();
string = string.replace("${" + entry.getKey() + "}", (String)entry.getValue());
}
return string;
}
public static boolean shouldExpand(String expandPaths[], String name) {
for (int i=0; i<expandPaths.length; i++) {
if (name.startsWith(expandPaths[i])) return true;
}
return false;
}
protected void loadByteCode(InputStream is, String jar, String tmp) throws IOException {
JarInputStream jis = new JarInputStream(is);
JarEntry entry = null;
// TODO: implement lazy loading of bytecode.
Manifest manifest = jis.getManifest();
if (manifest == null) {
WARNING("Null manifest from input stream associated with: " + jar);
}
while ((entry = jis.getNextJarEntry()) != null) {
// if (entry.isDirectory()) continue;
loadBytes(entry, jis, jar, tmp, manifest);
}
// Add in a fake manifest entry.
if (manifest != null) {
entry = new JarEntry(JarClassLoader.MANIFEST);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
manifest.write(baos);
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
loadBytes(entry, bais, jar, tmp, manifest);
}
}
protected void loadBytes(JarEntry entry, InputStream is, String jar, String tmp, Manifest man) throws IOException {
String entryName = entry.getName();
int index = entryName.lastIndexOf('.');
String type = entryName.substring(index+1);
// agattung: patch (for one-jar 0.95)
// add package handling to avoid NullPointer exceptions
// after calls to getPackage method of this ClassLoader
int index2 = entryName.lastIndexOf('/', index-1);
if (entryName.endsWith(CLASS) && index2 > -1) {
String packageName = entryName.substring(0, index2).replace('/', '.');
if (getPackage(packageName) == null) {
// Defend against null manifest.
if (man != null) {
definePackage(packageName, man, urlFactory.getCodeBase(jar));
} else {
definePackage(packageName, null, null, null, null, null, null, null);
}
}
}
// end patch
// Because we are doing stream processing, we don't know what
// the size of the entries is. So we store them dynamically.
ByteArrayOutputStream baos = new ByteArrayOutputStream();
copy(is, baos);
if (tmp != null) {
// Unpack into a temporary working directory which is on the classpath.
File file = new File(tmp, entry.getName());
file.getParentFile().mkdirs();
FileOutputStream fos = new FileOutputStream(file);
fos.write(baos.toByteArray());
fos.close();
} else {
// If entry is a class, check to see that it hasn't been defined
// already. Class names must be unique within a classloader because
// they are cached inside the VM until the classloader is released.
if (type.equals("class")) {
if (alreadyCached(entryName, jar, baos)) return;
byteCode.put(entryName, new ByteCode(entryName, entry.getName(), baos, jar, man));
VERBOSE("cached bytes for class " + entryName);
} else {
// Another kind of resource. Cache this by name, and also prefixed
// by the jar name. Don't duplicate the bytes. This allows us
// to map resource lookups to either jar-local, or globally defined.
String localname = jar + "/" + entryName;
byteCode.put(localname, new ByteCode(localname, entry.getName(), baos, jar, man));
// Keep a set of jar names so we can do multiple-resource lookup by name
// as in findResources().
jarNames.add(jar);
VERBOSE("cached bytes for local name " + localname);
// Only keep the first non-local entry: this is like classpath where the first
// to define wins.
if (alreadyCached(entryName, jar, baos)) return;
byteCode.put(entryName, new ByteCode(entryName, entry.getName(), baos, jar, man));
VERBOSE("cached bytes for entry name " + entryName);
}
}
}
/**
* Override to ensure that this classloader is the thread context classloader
* when used to load a class. Avoids subtle, nasty problems.
*
*/
public Class loadClass(String name, boolean resolve) throws ClassNotFoundException {
// 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.
AccessController.doPrivileged(new PrivilegedAction() {
public Object run() {
Thread.currentThread().setContextClassLoader(JarClassLoader.this);
return null;
}
});
return super.loadClass(name, resolve);
}
/**
* Locate the named class in a jar-file, contained inside the
* jar file which was used to load <u>this</u> class.
*/
protected Class findClass(String name) throws ClassNotFoundException {
// Delegate to external paths first
Class cls = null;
if (externalClassLoader != null) {
try {
return externalClassLoader.loadClass(name);
} catch (ClassNotFoundException cnfx) {
// continue...
}
}
// Make sure not to load duplicate classes.
cls = findLoadedClass(name);
if (cls != null) return cls;
// Look up the class in the byte codes.
// Translate path?
VERBOSE("findClass(" + name + ")");
String cache = name.replace('.', '/') + CLASS;
ByteCode bytecode = (ByteCode)byteCode.get(cache);
if (bytecode != null) {
VERBOSE("found " + name + " in codebase '" + bytecode.codebase + "'");
if (record) {
record(bytecode);
}
// Use a protectionDomain to associate the codebase with the
// class.
ProtectionDomain pd = (ProtectionDomain)pdCache.get(bytecode.codebase);
if (pd == null) {
try {
URL url = urlFactory.getCodeBase(bytecode.codebase);
CodeSource source = new CodeSource(url, (Certificate[])null);
pd = new ProtectionDomain(source, null, this, null);
pdCache.put(bytecode.codebase, pd);
} catch (MalformedURLException mux) {
throw new ClassNotFoundException(name, mux);
}
}
// Do it the simple way.
byte bytes[] = bytecode.bytes;
int i = name.lastIndexOf('.');
if (i != -1) {
String pkgname = name.substring(0, i);
// Check if package already loaded.
Package pkg = getPackage(pkgname);
Manifest man = bytecode.manifest;
if (pkg != null) {
// Package found, so check package sealing.
if (pkg.isSealed()) {
// Verify that code source URL is the same.
if (!pkg.isSealed(pd.getCodeSource().getLocation())) {
throw new SecurityException("sealing violation: package " + pkgname + " is sealed");
}
} else {
// Make sure we are not attempting to seal the package
// at this code source URL.
if ((man != null) && isSealed(pkgname, man)) {
throw new SecurityException("sealing violation: can't seal package " + pkgname + ": already loaded");
}
}
} else {
if (man != null) {
definePackage(pkgname, man, pd.getCodeSource().getLocation());
} else {
definePackage(pkgname, null, null, null, null, null, null, null);
}
}
}
return defineClass(name, bytes, pd);
}
VERBOSE(name + " not found");
throw new ClassNotFoundException(name);
}
private boolean isSealed(String name, Manifest man) {
String path = name.concat("/");
Attributes attr = man.getAttributes(path);
String sealed = null;
if (attr != null) {
sealed = attr.getValue(Name.SEALED);
}
if (sealed == null) {
if ((attr = man.getMainAttributes()) != null) {
sealed = attr.getValue(Name.SEALED);
}
}
return "true".equalsIgnoreCase(sealed);
}
/**
* Defines a new package by name in this ClassLoader. The attributes
* contained in the specified Manifest will be used to obtain package
* version and sealing information. For sealed packages, the additional URL
* specifies the code source URL from which the package was loaded.
*
* @param name
* the package name
* @param man
* the Manifest containing package version and sealing
* information
* @param url
* the code source url for the package, or null if none
* @exception IllegalArgumentException
* if the package name duplicates an existing package either
* in this class loader or one of its ancestors
* @return the newly defined Package object
*/
protected Package definePackage(String name, Manifest man, URL url) throws IllegalArgumentException {
String path = name.concat("/");
String specTitle = null, specVersion = null, specVendor = null;
String implTitle = null, implVersion = null, implVendor = null;
String sealed = null;
URL sealBase = null;
Attributes attr = man.getAttributes(path);
if (attr != null) {
specTitle = attr.getValue(Name.SPECIFICATION_TITLE);
specVersion = attr.getValue(Name.SPECIFICATION_VERSION);
specVendor = attr.getValue(Name.SPECIFICATION_VENDOR);
implTitle = attr.getValue(Name.IMPLEMENTATION_TITLE);
implVersion = attr.getValue(Name.IMPLEMENTATION_VERSION);
implVendor = attr.getValue(Name.IMPLEMENTATION_VENDOR);
sealed = attr.getValue(Name.SEALED);
}
attr = man.getMainAttributes();
if (attr != null) {
if (specTitle == null) {
specTitle = attr.getValue(Name.SPECIFICATION_TITLE);
}
if (specVersion == null) {
specVersion = attr.getValue(Name.SPECIFICATION_VERSION);
}
if (specVendor == null) {
specVendor = attr.getValue(Name.SPECIFICATION_VENDOR);
}
if (implTitle == null) {
implTitle = attr.getValue(Name.IMPLEMENTATION_TITLE);
}
if (implVersion == null) {
implVersion = attr.getValue(Name.IMPLEMENTATION_VERSION);
}
if (implVendor == null) {
implVendor = attr.getValue(Name.IMPLEMENTATION_VENDOR);
}
if (sealed == null) {
sealed = attr.getValue(Name.SEALED);
}
}
if (sealed != null) {
boolean isSealed = Boolean.parseBoolean(sealed);
if (isSealed) {
sealBase = url;
}
}
return definePackage(name, specTitle, specVersion, specVendor, implTitle, implVersion, implVendor, sealBase);
}
protected Class defineClass(String name, byte[] bytes, ProtectionDomain pd) throws ClassFormatError {
// Simple, non wrapped class definition.
VERBOSE("defineClass("+name+")");
return defineClass(name, bytes, 0, bytes.length, pd);
}
protected void record(ByteCode bytecode) {
String fileName = bytecode.original;
// Write out into the record directory.
File dir = new File(recording, flatten? "": bytecode.codebase);
File file = new File(dir, fileName);
if (!file.exists()) {
file.getParentFile().mkdirs();
VERBOSE("" + file);
try {
FileOutputStream fos = new FileOutputStream(file);
fos.write(bytecode.bytes);
fos.close();
} catch (IOException iox) {
System.err.println(PREFIX() + "unable to record " + file + ": " + iox);
}
}
}
/**
* Make a path canonical, removing . and ..
*/
protected String canon(String path) {
path = path.replaceAll("/\\./", "/");
String canon = path;
String next = canon;
do {
next = canon;
canon = canon.replaceFirst("([^/]*/\\.\\./)", "");
} while (!next.equals(canon));
return canon;
}
/**
* Overriden to return resources from the appropriate codebase.
* There are basically two ways this method will be called: most commonly
* it will be called through the class of an object which wishes to
* load a resource, i.e. this.getClass().getResourceAsStream(). Before
* passing the call to us, java.lang.Class mangles the name. It
* converts a file path such as foo/bar/Class.class into a name like foo.bar.Class,
* and it strips leading '/' characters e.g. converting '/foo' to 'foo'.
* All of which is a nuisance, since we wish to do a lookup on the original
* name of the resource as present in the One-Jar jar files.
* The other way is more direct, i.e. this.getClass().getClassLoader().getResourceAsStream().
* Then we get the name unmangled, and can deal with it directly.
*
* The problem is this: if one resource is called /foo/bar/data, and another
* resource is called /foo.bar.data, both will have the same mangled name,
* namely 'foo.bar.data' and only one of them will be visible. Perhaps the
* best way to deal with this is to store the lookup names in mangled form, and
* simply issue warnings if collisions occur. This is not very satisfactory,
* but is consistent with the somewhat limiting design of the resource name mapping
* strategy in Java today.
*/
public InputStream getByteStream(String resource) {
VERBOSE("getByteStream(" + resource + ")");
InputStream result = null;
if (externalClassLoader != null) {
result = externalClassLoader.getResourceAsStream(resource);
}
if (result == null) {
// Delegate to parent classloader first.
ClassLoader parent = getParent();
if (parent != null) {
result = parent.getResourceAsStream(resource);
}
}
if (result == null) {
// Make resource canonical (remove ., .., etc).
resource = canon(resource);
// Look up resolving first. This allows jar-local
// resolution to take place.
ByteCode bytecode = (ByteCode)byteCode.get(resolve(resource));
if (bytecode == null) {
// Try again with an unresolved name.
bytecode = (ByteCode)byteCode.get(resource);
}
if (bytecode != null) result = new ByteArrayInputStream(bytecode.bytes);
}
// Contributed by SourceForge "ffrog_8" (with thanks, Pierce. T. Wetter III).
// Handles JPA loading from jars.
if (result == null) {
if (jarNames.contains(resource)) {
// resource wanted is an actual jar
INFO("loading resource file directly" + resource);
result = super.getResourceAsStream(resource);
}
}
// Special case: if we are a wrapping classloader, look up to our
// parent codebase. Logic is that the boot JarLoader will have
// delegateToParent = false, the wrapping classloader will have
// delegateToParent = true;
if (result == null && delegateToParent) {
// http://code.google.com/p/onejar-maven-plugin/issues/detail?id=16
ClassLoader parentClassLoader = getParent();
// JarClassLoader cannot satisfy requests for actual jar files themselves so it must delegate to it's
// parent. However, the "parent" is not always a JarClassLoader.
if (parentClassLoader instanceof JarClassLoader) {
result = ((JarClassLoader)parentClassLoader).getByteStream(resource);
} else {
result = parentClassLoader.getResourceAsStream(resource);
}
}
VERBOSE("getByteStream(" + resource + ") -> " + result);
return result;
}
/**
* Resolve a resource name. Look first in jar-relative, then in global scope.
* @param resource
* @return
*/
protected String resolve(String $resource) {
if ($resource.startsWith("/")) $resource = $resource.substring(1);
String resource = null;
String caller = getCaller();
ByteCode callerCode = (ByteCode)byteCode.get(caller);
if (callerCode != null) {
// Jar-local first, then global.
String tmp = callerCode.codebase + "/" + $resource;
if (byteCode.get(tmp) != null) {
resource = tmp;
}
}
if (resource == null) {
// One last try.
if (byteCode.get($resource) == null) {
resource = null;
} else {
resource = $resource;
}
}
VERBOSE("resource " + $resource + " resolved to " + resource + (callerCode != null? " in codebase " + callerCode.codebase: " (unknown codebase)"));
return resource;
}
protected boolean alreadyCached(String name, String jar, ByteArrayOutputStream baos) {
// TODO: check resource map to see how we will map requests for this
// resource from this jar file. Only a conflict if we are using a
// global map and the resource is defined by more than
// one jar file (default is to map to local jar).
ByteCode existing = (ByteCode)byteCode.get(name);
if (existing != null) {
byte[] bytes = baos.toByteArray();
// If bytecodes are identical, no real problem. Likewise if it's in
// META-INF.
if (!Arrays.equals(existing.bytes, bytes) && !name.startsWith("META-INF")) {
// TODO: this really needs to be a warning, but there needs to be a way
// to shut it down. INFO it for now. Ideally we need to provide a
// logging layer (like commons-logging) to allow logging to be delegated.
if (name.endsWith(".class")) {
// This is probably trouble.
WARNING(existing.name + " in " + jar + " is hidden by " + existing.codebase + " (with different bytecode)");
} else {
INFO(existing.name + " in " + jar + " is hidden by " + existing.codebase + " (with different bytes)");
}
} else {
VERBOSE(existing.name + " in " + jar + " is hidden by " + existing.codebase + " (with same bytecode)");
}
// Speedup GC.
bytes = null;
return true;
}
return false;
}
protected String getCaller() {
// TODO: revisit caller determination.
/*
StackTraceElement[] stack = new Throwable().getStackTrace();
// Search upward until we get to a known class, i.e. one with a non-null
// codebase. Skip anything in the com.simontuffs.onejar package to avoid
// classloader classes.
for (int i=0; i<stack.length; i++) {
String cls = stack[i].getClassName().replace(".","/") + ".class";
INFO("getCaller(): cls=" + cls);
if (byteCode.get(cls) != null) {
String caller = stack[i].getClassName();
if (!caller.startsWith("com.simontuffs.onejar")) {
return cls;
}
}
}
*/
return null;
}
/**
* Sets the name of the used classes recording directory.
*
* @param $recording A value of "" will use the current working directory
* (not recommended). A value of 'null' will use the default directory, which
* is called 'recording' under the launch directory (recommended).
*/
public void setRecording(String $recording) {
recording = $recording;
if (recording == null) recording = RECORDING;
}
public String getRecording() {
return recording;
}
public void setRecord(boolean $record) {
record = $record;
}
public boolean getRecord() {
return record;
}
public void setFlatten(boolean $flatten) {
flatten = $flatten;
}
public boolean isFlatten() {
return flatten;
}
public void setVerbose(boolean $verbose) {
verbose = $verbose;
if (verbose) info = true;
}
public boolean getVerbose() {
return verbose;
}
public void setInfo(boolean $info) {
info = $info;
}
public boolean getInfo() {
return info;
}
protected URLStreamHandler oneJarHandler = new Handler();
// Injectable URL factory.
public static interface IURLFactory {
public URL getURL(String codebase, String resource) throws MalformedURLException;
public URL getCodeBase(String jar) throws MalformedURLException;
}
// Resolve URL from codebase and resource. Allow URL factory to be specified by
// user of JarClassLoader.
/**
* FileURLFactory generates URL's which are resolved relative to the filesystem.
* These are compatible with frameworks like Spring, but require knowledge of the
* location of the one-jar file via oneJarPath.
*/
public static class FileURLFactory implements IURLFactory {
JarClassLoader jcl;
public FileURLFactory(JarClassLoader jcl) {
this.jcl = jcl;
}
public URLStreamHandler jarHandler = new URLStreamHandler() {
protected URLConnection openConnection(URL url) throws IOException {
URLConnection connection = new OneJarURLConnection(url);
connection.connect();
return connection;
}
};
// TODO: Unify getURL and getCodeBase, if possible.
public URL getURL(String codebase, String resource) throws MalformedURLException {
if (!codebase.equals("/")) {
codebase = codebase + "!/";
} else {
codebase = "";
}
String path = jcl.getOneJarPath() + "!/" + codebase + resource;
URL url = new URL("jar", "", -1, path, jarHandler);
return url;
}
public URL getCodeBase(String jar) throws MalformedURLException {
ProtectionDomain cd = JarClassLoader.class.getProtectionDomain();
URL url = cd.getCodeSource().getLocation();
if (url != null) {
url = new URL("jar", "", -1, url + "!/" + jar, jarHandler);
}
return url;
}
}
/**
* OneJarURLFactory generates URL's which are efficient, using the in-memory bytecode
* to access the resources.
* @author simon
*
*/
public static class OneJarURLFactory implements IURLFactory {
public OneJarURLFactory(JarClassLoader jcl) {
// Argument not used.
}
public URL getURL(String codebase, String resource) throws MalformedURLException {
String base = resource.endsWith(".class")? "": codebase + "/";
URL url = new URL(Handler.PROTOCOL + ":/" + base + resource);
return url;
}
public URL getCodeBase(String jar) throws MalformedURLException {
return new URL(Handler.PROTOCOL + ":" + jar);
}
}
public URL getResource(String name) {
// Delegate to external first.
if (externalClassLoader != null) {
URL url = externalClassLoader.getResource(name);
if (url != null)
return url;
}
return super.getResource(name);
}
protected IURLFactory urlFactory = new FileURLFactory(this);
// Allow override for urlFactory
public void setURLFactory(String urlFactory) throws ClassNotFoundException, IllegalAccessException, InstantiationException, SecurityException, NoSuchMethodException, IllegalArgumentException, InvocationTargetException {
Class factory = loadClass(urlFactory);
try {
// With single JarClassLoader parameter?
Constructor ctor = factory.getConstructor(new Class[]{JarClassLoader.class});
this.urlFactory = (IURLFactory)ctor.newInstance(new Object[]{JarClassLoader.this});
} catch (NoSuchMethodException x) {
// Default constructor?
this.urlFactory = (IURLFactory)loadClass(urlFactory).newInstance();
}
}
public IURLFactory getURLFactory() {
return urlFactory;
}
/* (non-Javadoc)
* @see java.lang.ClassLoader#findResource(java.lang.String)
*/
// TODO: Revisit the issue of protocol handlers for findResource()
// and findResources();
protected URL findResource(String $resource) {
try {
VERBOSE("findResource(\"" + $resource + "\")");
URL url = externalClassLoader!=null ? externalClassLoader.getResource($resource) : null;
if (url != null)
{
INFO("findResource() found in external: \"" + $resource + "\"");
//VERBOSE("findResource(): " + $resource + "=" + url);
return url;
}
// Delegate to parent.
ClassLoader parent = getParent();
if (parent != null) {
url = parent.getResource($resource);
if (url != null) {
return url;
}
}
// Do we have the named resource in our cache? If so, construct a
// 'onejar:' URL so that a later attempt to access the resource
// will be redirected to our Handler class, and thence to this class.
String resource = resolve($resource);
if (resource != null) {
// We know how to handle it.
ByteCode entry = ((ByteCode) byteCode.get(resource));
INFO("findResource() found: \"" + $resource + "\" for caller " + getCaller() + " in codebase " + entry.codebase);
return urlFactory.getURL(entry.codebase, $resource);
}
INFO("findResource(): unable to locate \"" + $resource + "\"");
// If all else fails, return null.
return null;
} catch (MalformedURLException mux) {
WARNING("unable to locate " + $resource + " due to " + mux);
}
return null;
}
protected Enumeration findResources(String name) throws IOException {
INFO("findResources(" + name + ")");
INFO("findResources: looking in " + jarNames);
Iterator iter = jarNames.iterator();
final List resources = new ArrayList();
while (iter.hasNext()) {
String resource = iter.next().toString() + "/" + name;
ByteCode entry = ((ByteCode) byteCode.get(resource));
if (byteCode.containsKey(resource)) {
URL url = urlFactory.getURL(entry.codebase, name);
INFO("findResources(): Adding " + url + " to resources list.");
resources.add(url);
}
}
final Iterator ri = resources.iterator();
return new Enumeration() {
public boolean hasMoreElements() {
return ri.hasNext();
}
public Object nextElement() {
return ri.next();
}
};
}
/**
* Utility to assist with copying InputStream to OutputStream. All
* bytes are copied, but both streams are left open.
* @param in Source of bytes to copy.
* @param out Destination of bytes to copy.
* @throws IOException
*/
protected void copy(InputStream in, OutputStream out) throws IOException {
byte[] buf = new byte[1024];
while (true) {
int len = in.read(buf);
if (len < 0) break;
out.write(buf, 0, len);
}
}
public String toString() {
return super.toString() + (name != null? "(" + name + ")": "");
}
/**
* Returns name of the classloader.
* @return
*/
public String getName() {
return name;
}
/**
* Sets name of the classloader. Default is null.
* @param string
*/
public void setName(String string) {
name = string;
}
public void setExpand(boolean expand) {
noExpand = !expand;
}
public boolean isExpanded() {
return expanded;
}
/**
* Preloader for {@link JarClassLoader#findTheLibrary(String, String)} to allow arch-specific native libraries
*
* @param name the (system specific) name of the requested library
* @author Sebastian Just
*/
protected String findLibrary(String name) {
final String os = System.getProperty("os.name").toLowerCase();
final String arch = System.getProperty("os.arch").toLowerCase();
final String BINLIB_LINUX32_PREFIX = BINLIB_PREFIX + "linux32/";
final String BINLIB_LINUX64_PREFIX = BINLIB_PREFIX + "linux64/";
final String BINLIB_MACOSX_PREFIX = BINLIB_PREFIX + "macosx/";
final String BINLIB_WINDOWS32_PREFIX = BINLIB_PREFIX + "windows32/";
final String BINLIB_WINDOWS64_PREFIX = BINLIB_PREFIX + "windows64/";
String binlib = null;
// Mac
if (os.startsWith("mac os x")) {
//TODO Nood arch detection on mac
binlib = BINLIB_MACOSX_PREFIX;
// Windows
} else if (os.startsWith("windows")) {
if (arch.equals("x86")) {
binlib = BINLIB_WINDOWS32_PREFIX;
} else {
binlib = BINLIB_WINDOWS64_PREFIX;
}
// So it have to be Linux
} else {
if (arch.equals("i386")) {
binlib = BINLIB_LINUX32_PREFIX;
} else {
binlib = BINLIB_LINUX64_PREFIX;
}
}//TODO Need some work for solaris
VERBOSE("Using arch-specific native library path: " + binlib);
String retValue = findTheLibrary(binlib, name);
if (retValue != null) {
VERBOSE("Found in arch-specific directory!");
return retValue;
} else {
VERBOSE("Search in standard native directory!");
return findTheLibrary(BINLIB_PREFIX, name);
}
}
/**
* If the system specific library exists in the JAR, expand it and return the path
* to the expanded library to the caller. Otherwise return null so the caller
* searches the java.library.path for the requested library.
*
*
* @author Christopher Ottley
* @param name the (system specific) name of the requested library
* @param BINLIB_PREFIX the (system specific) folder to search in
* @return the full pathname to the requested library, or null
* @see Runtime#loadLibrary()
* @since 1.2
*/
protected String findTheLibrary(String BINLIB_PREFIX, String name) {
String result = null; // By default, search the java.library.path for it
String resourcePath = BINLIB_PREFIX + System.mapLibraryName(name);
// If it isn't in the map, try to expand to temp and return the full path
// otherwise, remain null so the java.library.path is searched.
// If it has been expanded already and in the map, return the expanded value
if (binLibPath.get(resourcePath) != null) {
result = (String)binLibPath.get(resourcePath);
} else {
// See if it's a resource in the JAR that can be extracted
File tempNativeLib = null;
FileOutputStream os = null;
try {
int lastdot = resourcePath.lastIndexOf('.');
String suffix = null;
if (lastdot >= 0) {
suffix = resourcePath.substring(lastdot);
}
InputStream is = this.getClass().getResourceAsStream("/" + resourcePath);
if ( is != null ) {
tempNativeLib = File.createTempFile(name + "-", suffix);
tempNativeLib.deleteOnExit();
os = new FileOutputStream(tempNativeLib);
copy(is, os);
os.close();
VERBOSE("Stored native library " + name + " at " + tempNativeLib);
result = tempNativeLib.getPath();
binLibPath.put(resourcePath, result);
} else {
// Library is not in the jar
// Return null by default to search the java.library.path
VERBOSE("No native library at " + resourcePath +
"java.library.path will be searched instead.");
}
} catch(Throwable e) {
// Couldn't load the library
// Return null by default to search the java.library.path
WARNING("Unable to load native library: " + e);
}
}
return result;
}
protected String getConfirmation(File location) throws IOException {
String answer = "";
while (answer == null || (!answer.startsWith("n") && !answer.startsWith("y") && !answer.startsWith("q"))) {
promptForConfirm(location);
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
answer = br.readLine();
br.close();
}
return answer;
}
protected void promptForConfirm(File location) {
PRINTLN("Do you want to allow '" + oneJarPath + "' to expand files into the file-system at the following location?");
PRINTLN(" " + location);
PRINT("Answer y(es) to expand files, n(o) to continue without expanding, or q(uit) to exit: ");
}
public void setProperties(IProperties jarloader) {
INFO("setProperties(" + jarloader + ")");
if (JarClassLoader.getProperty(JarClassLoader.P_RECORD)) {
jarloader.setRecord(true);
jarloader.setRecording(System.getProperty(JarClassLoader.P_RECORD));
}
if (JarClassLoader.getProperty(JarClassLoader.P_JARNAMES)) {
jarloader.setRecord(true);
jarloader.setFlatten(false);
}
if (JarClassLoader.getProperty(JarClassLoader.P_VERBOSE)) {
jarloader.setVerbose(true);
jarloader.setInfo(true);
verbose = true;
}
if (JarClassLoader.getProperty(JarClassLoader.P_INFO)) {
jarloader.setInfo(true);
info = true;
}
}
public static boolean getProperty(String key) {
return Boolean.valueOf(System.getProperty(key, "false")).booleanValue();
}
}