/* * File: JarClassLoader.java * * Copyright (C) 2008 JDotSoft. All Rights Reserved. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License version 2 as * published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * See the GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, * MA 02110-1301 USA * * Visit jdotsoft.com for commercial license. * * $Id: JarClassLoader.java,v 1.21 2008/08/19 14:42:21 mg Exp $ */ package com.jdotsoft.jarloader; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.io.BufferedOutputStream; import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.DataInputStream; import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.FileReader; import java.io.FileWriter; import java.io.IOException; import java.io.InputStream; import java.io.PrintStream; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.net.MalformedURLException; import java.net.URL; import java.net.URLDecoder; import java.security.CodeSource; import java.security.ProtectionDomain; import java.util.ArrayList; import java.util.Collections; import java.util.Enumeration; import java.util.HashMap; import java.util.HashSet; 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.JarFile; import java.util.jar.Manifest; import javax.swing.LookAndFeel; import javax.swing.UIDefaults; import javax.swing.UIManager; /** * This classloader loads classes, native libraries and resources from * the top JAR and from JARs inside top JAR. The loading process looks * through JARs hierarchy and allows their tree structure, i.e. nested JARs. *
* This class delegates class loading to the parent class loader and * successfully loads classes, native libraries and resources when it works * not in a JAR environment. *
* Create a Launcher class to use this class loader
* and start its main() method to start your application
* com.mycompany.MyApp
*
*
public class Launcher {
public static void main(String[] args) {
JarClassLoader jcl = new JarClassLoader();
System.out.println("Starting TestMain...");
try {
jcl.invokeMain("com.mycompany.MyApp", args);
} catch (Throwable e) {
e.printStackTrace();
}
} // main()
} // class Launcher
*
* An application could be started from a command line using its main
* class e.g. TestMain.main() or from Launcher.main()
* similar to the the above example. The application behavior in both cases
* is identical if resources are loaded from a file system.
* Starting from Launcher.main() is required only to start
* the application from a JAR file which contains other JARs or native libraries.
*
* Special handling is required for loading external LaF classes.
* Call the method JarClassLoader.loadLookAndFeel(); to preload
* UI classes.
*
* Known issues: temporary files with loaded native libraries are not deleted on * application exit because JVM does not close handles to them. The loader * attempts to delete them on next launch. The list of these temporary files * is preserved in the "[user.home]/.JarClassLoader" file. *
* See also discussion "How load library from jar file?"
* http://discuss.develop.com/archives/wa.exe?A2=ind0302&L=advanced-java&D=0&P=4549
* Unfortunately, the native method java.lang.ClassLoader$NativeLibrary.unload()
* is package accessed in a package accessed inner class.
* Moreover, it's called from finalizer. This does not allow releasing
* the native library handle and delete the temporary library file.
* Option to explore: use JNI function UnregisterNatives(). See also
* native code in ...\jdk\src\share\native\java\lang\ClassLoader.c
*
* @version $Revision: 1.21 $
*/
public class JarClassLoader extends ClassLoader {
/**
* VM parameter to turn on debugging logging to file or console.
* (1) main() method lookup:
* ClassNotFoundException, SecurityException, NoSuchMethodException
* (2) main() method launch:
* IllegalArgumentException, IllegalAccessException (disabled)
* (3) Actual cause of InvocationTargetException
*
* See
* {@link http://java.sun.com/developer/Books/javaprogramming/JAR/api/jarclassloader.html}
* and
* {@link http://java.sun.com/developer/Books/javaprogramming/JAR/api/example-1dot2/JarClassLoader.java}
*/
public void invokeMain(String sClass, String[] args) throws Throwable {
Class> clazz = loadClass(sClass);
log("Launch: %s.main(); Loader: %s", sClass, clazz.getClassLoader());
Method method = clazz.getMethod("main", new Class>[] { String[].class });
boolean bValidModifiers = false;
boolean bValidVoid = false;
if (method != null) {
method.setAccessible(true); // Disable IllegalAccessException
int nModifiers = method.getModifiers(); // main() must be "public static"
bValidModifiers = Modifier.isPublic(nModifiers) &&
Modifier.isStatic(nModifiers);
Class> clazzRet = method.getReturnType(); // main() must be "void"
bValidVoid = (clazzRet == void.class);
}
if (method == null || !bValidModifiers || !bValidVoid) {
throw new NoSuchMethodException(
"The main() method in class \"" + sClass + "\" not found.");
}
// Invoke method.
// Crazy cast "(Object)args" because param is: "Object... args"
try {
method.invoke(null, (Object)args);
} catch (InvocationTargetException e) {
throw e.getTargetException();
}
} // invokeMain()
//--------------------------------separator--------------------------------
static int ______OVERRIDE;
/**
* ClassLoader JavaDoc encourages overriding findClass(String) in derived
* class rather than overriding this method. This does not work for
* loading classes from a JAR. Default implementation of loadClass() is
* able to load a class from a JAR without calling findClass().
* This will "infect" the loaded class with a system class loader.
* The system class loader will be used to load all dependent classes
* and will fail for jar-in-jar classes.
*
* See also:
* http://www.cs.purdue.edu/homes/jv/smc/pubs/liang-oopsla98.pdf
*/
@Override
protected synchronized Class> loadClass(String sClassName, boolean bResolve)
throws ClassNotFoundException
{
log("Loading: %s (resolve=%b)", sClassName, bResolve);
Class> c = null;
try {
// Step 1. Load from JAR.
if (isLaunchedFromJar()) {
try {
c = findJarClass(sClassName);
log("Loaded %s from JAR by %s", sClassName, getClass().getName());
return c;
} catch (JarClassLoaderException e) {
if (e.getCause() == null) {
log("Not found %s in JAR by %s",
e.getMessage(), getClass().getName());
} else {
log("Error %s in JAR by %s", e.getCause(), getClass().getName());
}
// keep looking
}
}
// Step 2. Load by parent (usually system) class loader.
// Call findSystemClass() AFTER attempt to find in a JAR.
// If it called BEFORE it will load class-in-jar using
// SystemClassLoader and "infect" it with SystemClassLoader.
// The SystemClassLoader will be used to load all dependent
// classes. SystemClassLoader will fail to load a class from
// jar-in-jar and to load dll-in-jar.
try {
// No need to call findLoadedClass(sClassName)
// because it's called inside:
ClassLoader cl = getParent();
c = cl.loadClass(sClassName);
log("Loaded %s by %s", sClassName, cl.getClass().getName());
return c;
} catch (ClassNotFoundException e) {
// keep looking
}
// What else?
throw new ClassNotFoundException("Failure to load: " + sClassName);
} finally {
if (c != null && bResolve) {
resolveClass(c);
}
}
} // loadClass()
/**
* @see java.lang.ClassLoader#getResource(java.lang.String)
*/
@Override
public URL getResource(String sName) {
if (isLaunchedFromJar()) {
JarEntryInfo inf = findJarEntry(sName);
return inf == null ? null : inf.getURL();
}
return getParent().getResource(sName);
} // getResource()
/**
* @see java.lang.ClassLoader#getResources(java.lang.String)
*/
@Override
public Enumeration
* Specify -DJarClassLoader.logger=[filename] in the
* command line for logging on into the file or to console if the
* filename is specified as a "console".
*/
public static final String KEY_LOGGER = "JarClassLoader.logger";
public static final String CONSOLE = "console";
private PrintStream logger;
private ListClassLoader.findLibrary() accepts ONLY string with
* absolute path to the file with native library.
*
* @param inf JAR entry information
* @return temporary file object presenting JAR entry
* @throws JarClassLoaderException
*/
private File createTempFile(JarEntryInfo inf)
throws JarClassLoaderException {
byte[] a_by = getJarBytes(inf);
try {
File file = File.createTempFile(inf.getName() + ".", null);
file.deleteOnExit();
BufferedOutputStream os = new BufferedOutputStream(
new FileOutputStream(file));
os.write(a_by);
os.close();
return file;
} catch (IOException e) {
throw new JarClassLoaderException("Cannot create temp file for " + inf.jarEntry, e);
}
} // createTempFile()
/**
* Loads specified JAR
*
* @param jarFile JAR file
*/
private void loadJar(JarFile jarFile) {
lstJarFile.add(jarFile);
try {
Enumeration
* At runtime some JComponent class tries to load LaF UI class.
* The JVM uses the JComponent's class loader, which is system class loader
* to load UI class and fails because LaF classes reside in an external JAR.
* A workaround is to preload LaF classes explicitly.
*
* See details
* https://lists.xcf.berkeley.edu/lists/advanced-java/2001-January/015374.html
* @throws ClassNotFoundException
*/
public static void loadLookAndFeel() throws ClassNotFoundException {
LookAndFeel laf = UIManager.getLookAndFeel();
if (laf == null) {
return; // never null
}
ClassLoader cl = laf.getClass().getClassLoader();
if (cl == null) {
return; // null for system class loader (?)
}
// Does not work: cl.getClass().equals(JarClassLoader.class)
if (cl.getClass().getName().equals(JarClassLoader.class.getName())) {
UIDefaults uidef = UIManager.getDefaults();
Enumeration> en = uidef.keys();
while (en.hasMoreElements()) {
String sClass = (String)en.nextElement();
if (sClass.endsWith("UI")) {
Object obj = uidef.get(sClass);
// If the obj is java.lang.String load the class,
// otherwise it's already loaded java.lang.Class
if (obj instanceof String) {
Class> clazz = cl.loadClass((String)obj);
uidef.put(clazz.getName(), clazz);
}
}
}
}
} // loadLookAndFeel()
private String getFilename4Log(File file) {
if (logger != null) {
try {
// In form "C:\Documents and Settings\..."
return file.getCanonicalPath();
} catch (IOException e) {
// In form "C:\DOCUME~1\..."
return file.getAbsolutePath();
}
}
return null;
} // getFilename4Log()
private void log(String sMsg, Object ... obj) {
if (logger != null) {
logger.printf("JarClassLoader: " + sMsg + "\n", obj);
}
} // log()
/**
* Inner class with JAR entry information. Keeps JAR file and entry object.
*/
private static class JarEntryInfo {
JarFile jarFile;
JarEntry jarEntry;
JarEntryInfo(JarFile jarFile, JarEntry jarEntry) {
this.jarFile = jarFile;
this.jarEntry = jarEntry;
}
URL getURL() {
try {
return new URL("jar:file:" + jarFile.getName() + "!/" + jarEntry);
} catch (MalformedURLException e) {
return null;
}
}
String getName() {
return jarEntry.getName().replace('/', '_');
}
@Override
public String toString() {
return "JAR: " + jarFile.getName() + " ENTRY: " + jarEntry;
}
} // inner class JarEntryInfo
/**
* Inner class to handle specific for the JarClassLoader exceptions
*/
private static class JarClassLoaderException extends Exception {
JarClassLoaderException(String sMsg) {
super(sMsg);
}
JarClassLoaderException(String sMsg, Throwable eCause) {
super(sMsg, eCause);
}
String getMessageAll() {
StringBuilder sb = new StringBuilder();
for (Throwable e = this; e != null; e = e.getCause()) {
if (sb.length() > 0) {
sb.append(" / ");
}
String sMsg = e.getMessage();
if (sMsg == null || sMsg.length() == 0) {
sMsg = e.getClass().getSimpleName();
}
sb.append(sMsg);
}
return sb.toString();
}
} // inner class JarClassLoaderException
} // class JarClassLoader