com.jdotsoft.jarloader.JarClassLoader Maven / Gradle / Ivy
/*
* File: JarClassLoader.java
*
* Copyright (C) 2008-2011 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.36 2011/10/21 16:20:08 mg Exp $
*/
package com.jdotsoft.jarloader;
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.io.UnsupportedEncodingException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.net.JarURLConnection;
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.Attributes.Name;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.Manifest;
/**
* This class loader 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.
*
* The top JAR and nested JARs are included in the classpath and searched for
* the class or resource to load. The nested JARs could be located in any
* directories or subdirectories in a parent JAR.
*
* All directories or subdirectories in the top JAR and nested JARs are included
* in the library path and searched for a native library. For example, the
* library "Native.dll" could be in the JAR root directory as "Native.dll" or in
* any directory as "lib/Native.dll" or "abc/xyz/Native.dll".
*
* 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 MyAppLauncher {
public static void main(String[] args) {
JarClassLoader jcl = new JarClassLoader();
try {
jcl.invokeMain("com.mycompany.MyApp", args);
} catch (Throwable e) {
e.printStackTrace();
}
} // main()
} // class MyAppLauncher
*
*
* An application could be started in two different environments:
* 1. Application is started from an exploded JAR with dependent resources
* locations defined in a classpath. Command line to start the application could
* point to the main class e.g. MyApp.main()
or to the
* MyAppLauncher.main()
class (see example above). The application
* behavior in both cases is identical. Application started with
* MyApp.main()
uses system class loader and resources loaded from
* a file system. Application started with MyAppLauncher.main()
* uses JarClassLoader
which transparently passes class loading to
* the system class loader.
*
*
* 2. Application is started from a JAR with dependent JARs and other resources
* inside the main JAR. Application must be started with
* MyAppLauncher.main()
and JarClassLoader
will load
* MyApp.main()
and required resources from the main JAR.
*
*
* Use VM parameters in the command line for logging settings (examples):
*
* -DJarClassLoader.logger=[filename]
for logging into the
* file. The default is console.
* -DJarClassLoader.logger.level=INFO
for logging level. The
* default level is ERROR. See also {@link LogLevel}.
* -DJarClassLoader.logger.area=CLASS,RESOURCE
for logging
* area. The default area is ALL. See also {@link LogArea}. Multiple logging
* areas could be specified with ',' delimiter.
*
*
*
* Known issues: some temporary files created by class loader are not deleted on
* application exit because JVM does not close handles to them. See details in
* {@link #shutdown()}.
*
* 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.class
*
* @version $Revision: 1.36 $
*/
public class JarClassLoader extends ClassLoader {
/** VM parameter key to turn on logging to file or console. */
public static final String KEY_LOGGER = "JarClassLoader.logger";
/**
* VM parameter key to define log level. Valid levels are defined in
* {@link LogLevel}. Default value is {@link LogLevel#OFF}.
*/
public static final String KEY_LOGGER_LEVEL = "JarClassLoader.logger.level";
/**
* VM parameter key to define log area. Valid areas are defined in
* {@link LogArea}. Default value is {@link LogArea#ALL}. Multiple areas
* could be specified with ',' delimiter (no spaces!).
*/
public static final String KEY_LOGGER_AREA = "JarClassLoader.logger.area";
public enum LogLevel {
ERROR, WARN, INFO, DEBUG
}
public enum LogArea {
/** Enable all logging areas. */
ALL,
/** Configuration related logging. Enabled always. */
CONFIG,
/** Enable JAR related logging. */
JAR,
/** Enable class loading related logging. */
CLASS,
/** Enable resource loading related logging. */
RESOURCE,
/** Enable native libraries loading related logging. */
NATIVE
}
/**
* Sub directory name for temporary files.
*
* JarClassLoader extracts all JARs and native libraries into temporary
* files and makes the best attempt to clean these files on exit.
*
* The sub directory is created in the directory defined in a system
* property "java.io.tmpdir". Verify the content of this directory
* periodically and empty it if required. Temporary files could accumulate
* there if application was killed.
*/
public static final String TMP_SUB_DIRECTORY = "JarClassLoader";
private File dirTemp;
private PrintStream logger;
private List lstJarFile;
private Set hsDeleteOnExit;
private Map> hmClass;
private ProtectionDomain pd;
private LogLevel logLevel;
private Set hsLogArea;
private boolean bLogConsole;
/**
* Default constructor. Defines system class loader as a parent class
* loader.
*/
public JarClassLoader() {
this(ClassLoader.getSystemClassLoader());
}
/**
* Constructor.
*
* @param parent
* class loader parent.
*/
public JarClassLoader(ClassLoader parent) {
super(parent);
initLogger();
hmClass = new HashMap>();
lstJarFile = new ArrayList();
hsDeleteOnExit = new HashSet();
// Prepare common for all protocols
String sUrlTopJar = null;
pd = getClass().getProtectionDomain();
CodeSource cs = pd.getCodeSource();
URL urlTopJar = cs.getLocation();
String protocol = urlTopJar.getProtocol();
// Work with different cases:
JarFileInfo jarFileInfo = null;
if ("http".equals(protocol) || "https".equals(protocol)) {
// Protocol 'http' - application launched from WebStart / JNLP
try {
// Convert:
// urlTopJar = "http://.../MyApp.jar" --> connection
// sun.net.www.protocol.http.HttpURLConnection
// to
// urlTopJar = "jar:http://.../MyApp.jar!/" --> connection
// java.net.JarURLConnection
urlTopJar = new URL("jar:" + urlTopJar + "!/");
JarURLConnection jarCon = (JarURLConnection) urlTopJar
.openConnection();
JarFile jarFile = jarCon.getJarFile();
jarFileInfo = new JarFileInfo(jarFile, jarFile.getName(), null,
null);
logInfo(LogArea.JAR,
"Loading from top JAR: '%s' PROTOCOL: '%s'", urlTopJar,
protocol);
} catch (Exception e) {
// ClassCastException, IOException
logError(LogArea.JAR, "Failure to load JNLP JAR: %s %s",
urlTopJar, e.toString());
return;
}
}
if ("file".equals(protocol)) {
// Protocol 'file' - application launched from exploded dir or JAR
// Decoding required for 'space char' in URL:
// URL.getFile() returns "/C:/my%20dir/MyApp.jar" for
// "/C:/my dir/MyApp.jar"
try {
sUrlTopJar = URLDecoder.decode(urlTopJar.getFile(), "UTF-8");
} catch (UnsupportedEncodingException e) {
logError(LogArea.JAR, "Failure to decode URL: %s %s",
urlTopJar, e.toString());
return;
}
File fileJar = new File(sUrlTopJar);
// Application is loaded from directory:
if (fileJar.isDirectory()) {
logInfo(LogArea.JAR, "Loading from exploded directory: %s",
sUrlTopJar);
return; // JarClassLoader completed its job
}
// Application is loaded from a JAR:
try {
// The call "new JarFile(fileJar)" might throw IOException
jarFileInfo = new JarFileInfo(new JarFile(fileJar),
fileJar.getName(), null, null);
logInfo(LogArea.JAR,
"Loading from top JAR: '%s' PROTOCOL: '%s'",
sUrlTopJar, protocol);
} catch (IOException e) {
logError(LogArea.JAR, "Not a JAR: %s %s", sUrlTopJar,
e.toString());
return;
}
}
// FINALLY LOAD TOP JAR:
try {
if (jarFileInfo == null) {
throw new IOException(String.format("Unknown protocol %s",
protocol));
}
loadJar(jarFileInfo);
} catch (IOException e) {
logError(LogArea.JAR, "Not valid URL: %s %s", urlTopJar,
e.toString());
return;
}
checkShading();
Runtime.getRuntime().addShutdownHook(new Thread() {
@Override
public void run() {
shutdown();
}
});
} // JarClassLoader()
// --------------------------------separator--------------------------------
static int ______INIT;
private void initLogger() {
// Logger defaults:
bLogConsole = true;
logger = System.out; // default to console
logLevel = LogLevel.ERROR;
hsLogArea = new HashSet();
hsLogArea.add(LogArea.CONFIG);
// Logger stream console or file:
String sLogger = System.getProperty(KEY_LOGGER);
if (sLogger != null) {
try {
logger = new PrintStream(sLogger);
bLogConsole = false;
} catch (FileNotFoundException e) {
logError(LogArea.CONFIG, "Cannot create log file %s.", sLogger);
}
}
// Logger level:
String sLogLevel = System.getProperty(KEY_LOGGER_LEVEL);
if (sLogLevel != null) {
try {
logLevel = LogLevel.valueOf(sLogLevel);
} catch (Exception e) {
logError(LogArea.CONFIG, "Not valid parameter in %s=%s",
KEY_LOGGER_LEVEL, sLogLevel);
}
}
// Logger area:
String sLogArea = System.getProperty(KEY_LOGGER_AREA);
if (sLogArea != null) {
String[] tokenAll = sLogArea.split(",");
try {
for (String t : tokenAll) {
hsLogArea.add(LogArea.valueOf(t));
}
} catch (Exception e) {
logError(LogArea.CONFIG, "Not valid parameter in %s=%s",
KEY_LOGGER_AREA, sLogArea);
}
}
if ((hsLogArea.size() == 1) && hsLogArea.contains(LogArea.CONFIG)) {
for (LogArea la : LogArea.values()) {
hsLogArea.add(la);
}
}
}
/**
* Using temp files (one per inner JAR/DLL) solves many issues: 1. There are
* no ways to load JAR defined in a JarEntry directly into the JarFile
* object (see also #6 below). 2. Cannot use memory-mapped files because
* they are using nio channels, which are not supported by JarFile ctor. 3.
* JarFile object keeps opened JAR files handlers for fast access. 4. Deep
* resource in a jar-in-jar does not have well defined URL. Making temp file
* with JAR solves this problem. 5. Similar issues with native libraries:
* ClassLoader.findLibrary()
accepts ONLY string with absolute
* path to the file with native library. 6. Option
* "java.protocol.handler.pkgs" does not allow access to nested JARs(?).
*
* @param inf
* JAR entry information.
* @return temporary file object presenting JAR entry.
* @throws JarClassLoaderException
*/
private File createTempFile(JarEntryInfo inf)
throws JarClassLoaderException {
// Temp files directory:
// WinXP: C:/Documents and Settings/username/Local
// Settings/Temp/JarClassLoader
// Unix: /var/tmp/JarClassLoader
if (dirTemp == null) {
File dir = new File(System.getProperty("java.io.tmpdir"),
TMP_SUB_DIRECTORY);
if (!dir.exists()) {
dir.mkdir();
}
chmod777(dir); // Unix - allow temp directory RW access to all
// users.
if (!dir.exists() || !dir.isDirectory()) {
throw new JarClassLoaderException(
"Cannot create temp directory " + dir.getAbsolutePath());
}
dirTemp = dir;
}
File fileTmp = null;
try {
fileTmp = File.createTempFile(inf.getName() + ".", null, dirTemp);
fileTmp.deleteOnExit();
chmod777(fileTmp); // Unix - allow temp file deletion by any user
byte[] a_by = inf.getJarBytes();
BufferedOutputStream os = new BufferedOutputStream(
new FileOutputStream(fileTmp));
os.write(a_by);
os.close();
return fileTmp;
} catch (IOException e) {
throw new JarClassLoaderException(String.format(
"Cannot create temp file '%s' for %s", fileTmp,
inf.jarEntry), e);
}
} // createTempFile()
/**
* Loads specified JAR.
*
* @param jarFileInfo
* @throws IOException
*/
private void loadJar(JarFileInfo jarFileInfo) throws IOException {
lstJarFile.add(jarFileInfo);
try {
Enumeration en = jarFileInfo.jarFile.entries();
final String EXT_JAR = ".jar";
while (en.hasMoreElements()) {
JarEntry je = en.nextElement();
if (je.isDirectory()) {
continue;
}
String s = je.getName().toLowerCase(); // JarEntry name
if (s.lastIndexOf(EXT_JAR) == (s.length() - EXT_JAR.length())) {
JarEntryInfo inf = new JarEntryInfo(jarFileInfo, je);
File fileTemp = createTempFile(inf);
logInfo(LogArea.JAR,
"Loading inner JAR %s from temp file %s",
inf.jarEntry, getFilename4Log(fileTemp));
loadJar(new JarFileInfo(new JarFile(fileTemp),
inf.getName(), jarFileInfo, fileTemp));
}
}
} catch (JarClassLoaderException e) {
throw new RuntimeException("ERROR on loading inner JAR: "
+ e.getMessageAll());
}
} // loadJar()
private JarEntryInfo findJarEntry(String sName) {
for (JarFileInfo jarFileInfo : lstJarFile) {
JarFile jarFile = jarFileInfo.jarFile;
JarEntry jarEntry = jarFile.getJarEntry(sName);
if (jarEntry != null) {
return new JarEntryInfo(jarFileInfo, jarEntry);
}
}
return null;
} // findJarEntry()
private List findJarEntries(String sName) {
List lst = new ArrayList();
for (JarFileInfo jarFileInfo : lstJarFile) {
JarFile jarFile = jarFileInfo.jarFile;
JarEntry jarEntry = jarFile.getJarEntry(sName);
if (jarEntry != null) {
lst.add(new JarEntryInfo(jarFileInfo, jarEntry));
}
}
return lst;
} // findJarEntries()
/**
* Finds native library entry.
*
* @param sLib
* Library name. For example for the library name "Native" the
* Windows returns entry "Native.dll", the Linux returns entry
* "libNative.so", the Mac returns entry "libNative.jnilib".
*
* @return Native library entry.
*/
private JarEntryInfo findJarNativeEntry(String sLib) {
String sName = System.mapLibraryName(sLib);
for (JarFileInfo jarFileInfo : lstJarFile) {
JarFile jarFile = jarFileInfo.jarFile;
Enumeration en = jarFile.entries();
while (en.hasMoreElements()) {
JarEntry je = en.nextElement();
if (je.isDirectory()) {
continue;
}
// Example: sName is "Native.dll"
String sEntry = je.getName(); // "Native.dll" or
// "abc/xyz/Native.dll"
// sName "Native.dll" could be found, for example
// - in the path: abc/Native.dll/xyz/my.dll <-- do not load this
// one!
// - in the partial name: abc/aNative.dll <-- do not load this
// one!
String[] token = sEntry.split("/"); // the last token is library
// name
if ((token.length > 0) && token[token.length - 1].equals(sName)) {
logInfo(LogArea.NATIVE,
"Loading native library '%s' found as '%s' in JAR %s",
sLib, sEntry, jarFileInfo.simpleName);
return new JarEntryInfo(jarFileInfo, je);
}
}
}
return null;
} // findJarNativeEntry()
/**
* Loads class from a JAR and searches for all jar-in-jar.
*
* @param sClassName
* class to load.
* @return Loaded class.
* @throws JarClassLoaderException.
*/
private Class> findJarClass(String sClassName)
throws JarClassLoaderException {
// http://java.sun.com/developer/onlineTraining/Security/Fundamentals
// /magercises/ClassLoader/solution/FileClassLoader.java
Class> c = hmClass.get(sClassName);
if (c != null) {
return c;
}
// Char '/' works for Win32 and Unix.
String sName = sClassName.replace('.', '/') + ".class";
JarEntryInfo inf = findJarEntry(sName);
String jarSimpleName = null;
if (inf != null) {
jarSimpleName = inf.jarFileInfo.simpleName;
definePackage(sClassName, inf);
byte[] a_by = inf.getJarBytes();
try {
c = defineClass(sClassName, a_by, 0, a_by.length, pd);
} catch (ClassFormatError e) {
throw new JarClassLoaderException(null, e);
}
}
if (c == null) {
throw new JarClassLoaderException(sClassName);
}
hmClass.put(sClassName, c);
logInfo(LogArea.CLASS, "Loaded %s by %s from JAR %s", sClassName,
getClass().getName(), jarSimpleName);
return c;
} // findJarClass()
private void checkShading() {
if (logLevel.ordinal() < LogLevel.WARN.ordinal()) {
// Do not waste time if no logging.
return;
}
Map hm = new HashMap();
for (JarFileInfo jarFileInfo : lstJarFile) {
JarFile jarFile = jarFileInfo.jarFile;
Enumeration en = jarFile.entries();
while (en.hasMoreElements()) {
JarEntry je = en.nextElement();
if (je.isDirectory()) {
continue;
}
String sEntry = je.getName(); // "Some.txt" or
// "abc/xyz/Some.txt"
if ("META-INF/MANIFEST.MF".equals(sEntry)) {
continue;
}
JarFileInfo jar = hm.get(sEntry);
if (jar == null) {
hm.put(sEntry, jarFileInfo);
} else {
logWarn(LogArea.JAR, "ENTRY %s IN %s SHADES %s", sEntry,
jar.simpleName, jarFileInfo.simpleName);
}
}
}
}
// --------------------------------separator--------------------------------
static int ______SHUTDOWN;
/**
* Called on shutdown to cleanup temporary files.
*
* JVM does not close handles to native libraries files or JARs with
* resources loaded as getResourceAsStream(). Temp files are not deleted
* even if they are marked deleteOnExit(). They also fail to delete
* explicitly. Workaround is to preserve list with temp files in
* configuration file "[user.home]/.JarClassLoader" and delete them on next
* application run.
*
* See http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4171239 "This
* occurs only on Win32, which does not allow a file to be deleted until all
* streams on it have been closed."
*/
private void shutdown() {
for (JarFileInfo jarFileInfo : lstJarFile) {
try {
jarFileInfo.jarFile.close();
} catch (IOException e) {
// Ignore. In the worst case temp files will accumulate.
}
File file = jarFileInfo.fileDeleteOnExit;
if ((file != null) && !file.delete()) {
hsDeleteOnExit.add(file);
}
}
// Private configuration file with failed to delete temporary files:
// WinXP: C:/Documents and Settings/username/.JarClassLoader
// Unix: /export/home/username/.JarClassLoader
// -or- /home/username/.JarClassLoader
File fileCfg = new File(System.getProperty("user.home")
+ File.separator + ".JarClassLoader");
deleteOldTemp(fileCfg);
persistNewTemp(fileCfg);
} // shutdown()
/**
* Deletes temporary files listed in the file. The method is called on
* shutdown().
*
* @param fileCfg
* file with temporary files list.
*/
private void deleteOldTemp(File fileCfg) {
BufferedReader reader = null;
try {
int count = 0;
reader = new BufferedReader(new FileReader(fileCfg));
String sLine;
while ((sLine = reader.readLine()) != null) {
File file = new File(sLine);
if (!file.exists()) {
continue; // already deleted; from command line?
}
if (file.delete()) {
count++;
} else {
// Cannot delete, will try next time.
hsDeleteOnExit.add(file);
}
}
logDebug(LogArea.CONFIG, "Deleted %d old temp files listed in %s",
count, fileCfg.getAbsolutePath());
} catch (IOException e) {
// Ignore. This file may not exist.
} finally {
if (reader != null) {
try {
reader.close();
} catch (IOException e) {
}
}
}
} // deleteOldTemp()
/**
* Creates file with temporary files list. This list will be used to delete
* temporary files on the next application launch. The method is called from
* shutdown().
*
* @param fileCfg
* file with temporary files list.
*/
private void persistNewTemp(File fileCfg) {
if (hsDeleteOnExit.size() == 0) {
logDebug(LogArea.CONFIG, "No temp file names to persist on exit.");
fileCfg.delete(); // do not pollute disk
return;
}
logDebug(LogArea.CONFIG, "Persisting %d temp file names into %s",
hsDeleteOnExit.size(), fileCfg.getAbsolutePath());
BufferedWriter writer = null;
try {
writer = new BufferedWriter(new FileWriter(fileCfg));
for (File file : hsDeleteOnExit) {
if (!file.delete()) {
String f = file.getCanonicalPath();
writer.write(f);
writer.newLine();
logWarn(LogArea.JAR, "JVM failed to release %s", f);
}
}
} catch (IOException e) {
// Ignore. In the worst case temp files will accumulate.
} finally {
if (writer != null) {
try {
writer.close();
} catch (IOException e) {
}
}
}
} // persistNewTemp()
// --------------------------------separator--------------------------------
static int ______ACCESS;
/**
* Checks how the application was loaded: from JAR or file system.
*
* @return true if application was started from JAR.
*/
public boolean isLaunchedFromJar() {
return (lstJarFile.size() > 0);
} // isLaunchedFromJar()
/**
* Returns the name of the jar file main class, or null if no "Main-Class"
* manifest attributes was defined.
*
* @return Main class declared in JAR's manifest.
*/
public String getManifestMainClass() {
Attributes attr = null;
if (isLaunchedFromJar()) {
try {
// The first element in array is the top level JAR
Manifest m = lstJarFile.get(0).jarFile.getManifest();
attr = m.getMainAttributes();
} catch (IOException e) {
}
}
return (attr == null ? null : attr.getValue(Attributes.Name.MAIN_CLASS));
}
/**
* Invokes main() method on class with provided parameters.
*
* @param sClass
* class name in form "MyClass" for default package or
* "com.abc.MyClass" for class in some package
*
* @param args
* arguments for the main() method or null.
*
* @throws Throwable
* wrapper for many exceptions thrown while
*
* (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);
logInfo(LogArea.CONFIG, "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;
/**
* Class loader 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().
*/
@Override
protected synchronized Class> loadClass(String sClassName,
boolean bResolve) throws ClassNotFoundException {
logDebug(LogArea.CLASS, "LOADING %s (resolve=%b)", sClassName, bResolve);
// Each thread must have THIS class loader set as a context class
// loader.
// This is required to prevent failure finding a class or resource from
// external JAR requested by a common class loaded from rt.jar.
// The best example is external LnF, explained in steps:
// 1. Application requests 'javax.swing.JOptionPane'.
// 2. THIS class loader passes request to system default class loader
// to load the class from rt.jar.
// 3. The class 'javax.swing.JOptionPane' is loaded by system default
// class
// loader.
// 4. The class 'javax.swing.JOptionPane' is requesting
// 'UIDefaults.getUI()'
// for component, which resides in external LnF JAR.
// 5. The class loader which is used to load the requested component is
// current thread context class loader if it is set, otherwise the
// parent
// thread context class loader, or the default system class loader
// for the top level thread. See Thread.getContextClassLoader() JavaDoc.
// 6. The default system class loader is
// - sun.misc.Launcher$AppClassLoader - run from file system or JAR
// - com.sun.jnlp.JNLPClassLoader - run from JNLP
// System class loaders cannot find requested component in external
// JAR and throw exception.
//
// Setting thread context class loader for the top thread in
// invokeMain()
// method is sufficient for most cases, except when thread is created
// by native call, like JNLP.
//
// Setting thread context class loader below must be revisited for
// specific
// conditions.
// See
// http://www.javaworld.com/javaworld/javaqa/2003-06/01-qa-0606-load.html
Thread.currentThread().setContextClassLoader(this);
Class> c = null;
try {
// Step 0. This class is already loaded by system classloader.
if (getClass().getName().equals(sClassName)) {
return JarClassLoader.class;
}
// Step 1. Load from JAR.
if (isLaunchedFromJar()) {
try {
c = findJarClass(sClassName); // Do not simplify! See
// "finally"!
return c;
} catch (JarClassLoaderException e) {
if (e.getCause() == null) {
logDebug(LogArea.CLASS,
"Not found %s in JAR by %s: %s", sClassName,
getClass().getName(), e.getMessage());
} else {
logDebug(LogArea.CLASS,
"Error loading %s in JAR by %s: %s",
sClassName, getClass().getName(), e.getCause());
}
// 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);
logInfo(LogArea.CLASS, "Loaded %s by %s", sClassName, cl
.getClass().getName());
return c;
} catch (ClassNotFoundException e) {
}
// What else?
throw new ClassNotFoundException("Failure to load: " + sClassName);
} finally {
if ((c != null) && bResolve) {
resolveClass(c);
}
}
} // loadClass()
/**
* @see java.lang.ClassLoader#findResource(java.lang.String)
*
* @return A URL object for reading the resource, or null if the resource
* could not be found. Example URL:
* jar:file:C:\...\some.jar!/resources/InnerText.txt
*/
@Override
protected URL findResource(String sName) {
logDebug(LogArea.RESOURCE, "findResource: %s", sName);
if (isLaunchedFromJar()) {
JarEntryInfo inf = findJarEntry(normalizeResourceName(sName));
if (inf != null) {
URL url = inf.getURL();
logInfo(LogArea.RESOURCE, "found resource: %s", url);
return url;
}
logInfo(LogArea.RESOURCE, "not found resource: %s", sName);
return null;
}
return super.findResource(sName);
} // findResource()
/**
* @see java.lang.ClassLoader#findResources(java.lang.String)
*
* @return An enumeration of {@link java.net.URL URL} objects for
* the resources
*/
@Override
public Enumeration findResources(String sName) throws IOException {
logDebug(LogArea.RESOURCE, "getResources: %s", sName);
if (isLaunchedFromJar()) {
List lstJarEntry = findJarEntries(normalizeResourceName(sName));
List lstURL = new ArrayList();
for (JarEntryInfo inf : lstJarEntry) {
URL url = inf.getURL();
if (url != null) {
lstURL.add(url);
}
}
return Collections.enumeration(lstURL);
}
return super.findResources(sName);
} // findResources()
/**
* @see java.lang.ClassLoader#findLibrary(java.lang.String)
*
* @return The absolute path of the native library.
*/
@Override
protected String findLibrary(String sLib) {
logDebug(LogArea.NATIVE, "findLibrary: %s", sLib);
if (isLaunchedFromJar()) {
JarEntryInfo inf = findJarNativeEntry(sLib);
if (inf != null) {
try {
File file = createTempFile(inf);
logDebug(LogArea.NATIVE,
"Loading native library %s from temp file %s",
inf.jarEntry, getFilename4Log(file));
hsDeleteOnExit.add(file);
return file.getAbsolutePath();
} catch (JarClassLoaderException e) {
logInfo(LogArea.NATIVE,
"Failure to load native library %s: %s", sLib,
e.toString());
}
}
return null;
}
return super.findLibrary(sLib);
} // findLibrary()
// --------------------------------separator--------------------------------
static int ______HELPERS;
/**
* The default ClassLoader.defineClass()
does not create
* package for the loaded class and leaves it null. Each package referenced
* by this class loader must be created only once before the
* ClassLoader.defineClass()
call. The base class
* ClassLoader
keeps cache with created packages for reuse.
*
* @param sClassName
* class to load.
* @throws IllegalArgumentException
* If package name duplicates an existing package either in this
* class loader or one of its ancestors.
*/
private void definePackage(String sClassName, JarEntryInfo inf)
throws IllegalArgumentException {
int pos = sClassName.lastIndexOf('.');
String sPackageName = pos > 0 ? sClassName.substring(0, pos) : "";
if (getPackage(sPackageName) == null) {
JarFileInfo jfi = inf.jarFileInfo;
definePackage(sPackageName, jfi.getSpecificationTitle(),
jfi.getSpecificationVersion(),
jfi.getSpecificationVendor(), jfi.getImplementationTitle(),
jfi.getImplementationVersion(),
jfi.getImplementationVendor(), jfi.getSealURL());
}
}
/**
* The system class loader could load resources defined as "com/abc/Foo.txt"
* or "com\abc\Foo.txt". This method converts path with '\' to default '/'
* JAR delimiter.
*
* @param sName
* resource name including path.
* @return normalized resource name.
*/
private String normalizeResourceName(String sName) {
return sName.replace('\\', '/');
}
private void chmod777(File file) {
file.setReadable(true, false);
file.setWritable(true, false);
file.setExecutable(true, false); // Unix: allow content for dir,
// redundant for file
}
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;
}
private void logDebug(LogArea area, String sMsg, Object... obj) {
log(LogLevel.DEBUG, area, sMsg, obj);
}
private void logInfo(LogArea area, String sMsg, Object... obj) {
log(LogLevel.INFO, area, sMsg, obj);
}
private void logWarn(LogArea area, String sMsg, Object... obj) {
log(LogLevel.WARN, area, sMsg, obj);
}
private void logError(LogArea area, String sMsg, Object... obj) {
log(LogLevel.ERROR, area, sMsg, obj);
}
private void log(LogLevel level, LogArea area, String sMsg, Object... obj) {
if (level.ordinal() <= logLevel.ordinal()) {
if (hsLogArea.contains(LogArea.ALL) || hsLogArea.contains(area)) {
logger.printf("JarClassLoader-" + level + ": " + sMsg + "\n",
obj);
}
}
if (!bLogConsole && (level == LogLevel.ERROR)) { // repeat to console
System.out.printf("JarClassLoader-" + level + ": " + sMsg + "\n",
obj);
}
} // log()
/**
* Inner class with JAR file information.
*/
private static class JarFileInfo {
JarFile jarFile; // this is the essence of JarFileInfo wrapper
String simpleName; // accumulated for logging like:
// "topJar!childJar!kidJar"
File fileDeleteOnExit;
Manifest mf; // required for package creation
/**
* @param jarFile
* Never null.
* @param simpleName
* Used for logging. Never null.
* @param jarFileParent
* Used to make simpleName for logging. Null for top level
* JAR.
* @param fileDeleteOnExit
* Used only to delete temporary file on exit. Could be null
* if not required to delete on exit (top level JAR)
*/
JarFileInfo(JarFile jarFile, String simpleName,
JarFileInfo jarFileParent, File fileDeleteOnExit) {
this.simpleName = (jarFileParent == null ? ""
: jarFileParent.simpleName + "!") + simpleName;
this.jarFile = jarFile;
this.fileDeleteOnExit = fileDeleteOnExit;
try {
mf = jarFile.getManifest(); // 'null' if META-INF directory
// is missing
} catch (IOException e) {
// Ignore and create blank manifest
}
if (mf == null) {
mf = new Manifest();
}
}
String getSpecificationTitle() {
return mf.getMainAttributes().getValue(Name.SPECIFICATION_TITLE);
}
String getSpecificationVersion() {
return mf.getMainAttributes().getValue(Name.SPECIFICATION_VERSION);
}
String getSpecificationVendor() {
return mf.getMainAttributes().getValue(Name.SPECIFICATION_VENDOR);
}
String getImplementationTitle() {
return mf.getMainAttributes().getValue(Name.IMPLEMENTATION_TITLE);
}
String getImplementationVersion() {
return mf.getMainAttributes().getValue(Name.IMPLEMENTATION_VERSION);
}
String getImplementationVendor() {
return mf.getMainAttributes().getValue(Name.IMPLEMENTATION_VENDOR);
}
URL getSealURL() {
String seal = mf.getMainAttributes().getValue(Name.SEALED);
if (seal != null) {
try {
return new URL(seal);
} catch (MalformedURLException e) {
// Ignore, will return null
}
}
return null;
}
} // inner class JarFileInfo
/**
* Inner class with JAR entry information. Keeps JAR file and entry object.
*/
private static class JarEntryInfo {
JarFileInfo jarFileInfo;
JarEntry jarEntry;
JarEntryInfo(JarFileInfo jarFileInfo, JarEntry jarEntry) {
this.jarFileInfo = jarFileInfo;
this.jarEntry = jarEntry;
}
URL getURL() { // used in findResource() and findResources()
try {
return new URL("jar:file:" + jarFileInfo.jarFile.getName()
+ "!/" + jarEntry);
} catch (MalformedURLException e) {
return null;
}
}
String getName() { // used in createTempFile() and loadJar()
return jarEntry.getName().replace('/', '_');
}
@Override
public String toString() {
return "JAR: " + jarFileInfo.jarFile.getName() + " ENTRY: "
+ jarEntry;
}
/**
* Read JAR entry and returns byte array of this JAR entry. This is a
* helper method to load JAR entry into temporary file.
*
* @param inf
* JAR entry information object
* @return byte array for the specified JAR entry
* @throws JarClassLoaderException
*/
byte[] getJarBytes() throws JarClassLoaderException {
DataInputStream dis = null;
byte[] a_by = null;
try {
long lSize = jarEntry.getSize();
if ((lSize <= 0) || (lSize >= Integer.MAX_VALUE)) {
throw new JarClassLoaderException("Invalid size " + lSize
+ " for entry " + jarEntry);
}
a_by = new byte[(int) lSize];
InputStream is = jarFileInfo.jarFile.getInputStream(jarEntry);
dis = new DataInputStream(is);
dis.readFully(a_by);
} catch (IOException e) {
throw new JarClassLoaderException(null, e);
} finally {
if (dis != null) {
try {
dis.close();
} catch (IOException e) {
}
}
}
return a_by;
} // getJarBytes()
} // inner class JarEntryInfo
/**
* Inner class to handle JarClassLoader exceptions.
*/
@SuppressWarnings("serial")
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