org.tiogasolutions.notify.server.grizzly.JarClassLoader Maven / Gradle / Ivy
/*
* File: JarClassLoader.java
*
* Copyright (C) 2008-2013 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.38 2013/03/29 15:27:33 mg Exp $
*/
package org.tiogasolutions.notify.server.grizzly;
import javax.swing.*;
import java.applet.AppletContext;
import java.applet.AppletStub;
import java.io.*;
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.security.cert.Certificate;
import java.util.*;
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;
public class JarClassLoader extends ClassLoader {
/** VM parameter key to turn on logging to file or console. */
public static final String KEY_LOGGER = "JarClassLoader.logger";
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 LogLevel logLevel;
private Set hsLogArea;
private boolean bLogConsole;
private JApplet applet;
/**
* 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;
ProtectionDomain pdTop = getClass().getProtectionDomain();
CodeSource cs = pdTop.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' or 'https' - application launched from WebStart / JNLP or as Java applet
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, pdTop, null);
logInfo(LogArea.JAR, "Loading from top JAR: '%s' PROTOCOL: '%s'",
urlTopJar, protocol);
} catch (Exception e) {
// ClassCastException, IOException
logError(LogArea.JAR, "Failure to load HTTP 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);
for (String fileName : fileJar.list()) {
logInfo(LogArea.JAR, " %s", fileName);
}
return; // JarClassLoader completed its job
}
// Application is loaded from a JAR:
try {
jarFileInfo = new JarFileInfo(new JarFile(fileJar), fileJar.getName(), null, pdTop, 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); // start recursive JAR loading
} catch (IOException e) {
logError(LogArea.JAR, "Not valid URL: %s %s", urlTopJar, e.toString());
return;
}
checkShading();
Runtime.getRuntime().addShutdownHook(new Thread() {
public void run() {
shutdown();
}
});
} // JarClassLoader()
//--------------------------------separator--------------------------------
static int ______INIT;
private void initLogger() {
// Logger defaults:
bLogConsole = true;
this.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 {
this.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);
}
}
} // initLogger()
/**
* 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();
logInfo(LogArea.JAR, "Created temp file: %s", fileTmp.getAbsolutePath());
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));
// Construct ProtectionDomain for this inner JAR:
URL url = fileTemp.toURI().toURL();
ProtectionDomain pdParent = jarFileInfo.pd;
// 'csParent' is never null: top JAR has it, JCL creates it for child JAR:
CodeSource csParent = pdParent.getCodeSource();
Certificate[] certParent = csParent.getCertificates();
CodeSource csChild = (certParent == null ? new CodeSource(url, csParent.getCodeSigners())
: new CodeSource(url, certParent));
ProtectionDomain pdChild = new ProtectionDomain(csChild,
pdParent.getPermissions(), pdParent.getClassLoader(), pdParent.getPrincipals());
loadJar(new JarFileInfo(
new JarFile(fileTemp), inf.getName(), jarFileInfo, pdChild, 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 on failure
*/
private Class> findJarClass(String sClassName) throws JarClassLoaderException {
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, inf.jarFileInfo.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);
}
}
}
} // checkShading()
//--------------------------------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(Name.MAIN_CLASS));
} // getManifestMainClass()
public void invokeStart(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) {
logInfo(LogArea.CONFIG, "Found main method");
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()
/**
* Call this method to initialize an applet from your launcher class
* MyAppletLauncher.init()
method.
*
* @param sClass class name in form "MyClass" for default package
* or "com.abc.MyClass" for class in some package
*
* @param appletParent parent applet from a launcher.
*
* @throws Throwable wrapper for many exceptions thrown while applet
* instantiation and calling init() method.
*/
public void initApplet(String sClass, final JApplet appletParent) throws Throwable {
Class> clazz = loadClass(sClass);
logInfo(LogArea.CONFIG, "initApplet() --> %s.init(); Loader: %s", sClass, clazz.getClassLoader());
applet = (JApplet)clazz.newInstance();
applet.setStub(new AppletStub() {
@Override
public boolean isActive() {
return appletParent.isActive();
}
@Override
public URL getDocumentBase() {
return appletParent.getDocumentBase();
}
@Override
public URL getCodeBase() {
return appletParent.getCodeBase();
}
@Override
public String getParameter(String name) {
return appletParent.getParameter(name);
}
@Override
public AppletContext getAppletContext() {
return appletParent.getAppletContext();
}
@Override
public void appletResize(int width, int height) {
appletParent.resize(width, height);
}
});
applet.init();
appletParent.setContentPane(applet.getContentPane());
} // initApplet()
/**
* Call this method to start the applet from your launcher class
* MyAppletLauncher.start()
method.
*/
public void startApplet() {
checkApplet();
logInfo(LogArea.CONFIG, "startApplet() --> %s.start()", applet.getClass().getName());
applet.start();
}
/**
* Call this method to stop the applet from your launcher class
* MyAppletLauncher.stop()
method.
*/
public void stopApplet() {
checkApplet();
logInfo(LogArea.CONFIG, "stopApplet() --> %s.stop()", applet.getClass().getName());
applet.stop();
}
/**
* Call this method to destroy the applet from your launcher class
* MyAppletLauncher.destroy()
method.
*/
public void destroyApplet() {
checkApplet();
logInfo(LogArea.CONFIG, "destroyApplet() --> %s.destroy()", applet.getClass().getName());
applet.destroy();
}
//--------------------------------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.
// 6. The system class loader is used to load requested component if
// thread context class loader is not set. 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 invokeStart()
// method is sufficient for most cases. It fails for new threads created
// not from the start thread.
//
// Setting thread context class loader below must be reconsidered
// for specific conditions.
//
// Essential reading:
// - Thread.getContextClassLoader() JavaDoc.
// - 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);
// System classloader does not define ProtectionDomain->CodeSource - null
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 ClassLoader#findResource(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 ClassLoader#findResources(String)
*
* @return An enumeration of {@link 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 ClassLoader#findLibrary(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) {
logError(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 checkApplet() {
if (applet == null) {
throw new IllegalStateException("Applet is not inited. " +
"Please call JarClassLoader.initApplet() first.");
}
}
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 == LogLevel.WARN || level == LogLevel.ERROR) {
// System.err.printf("JarClassLoader-" + level + ": " + sMsg + "\n", obj);
// } else {
// System.out.printf("JarClassLoader-" + level + ": " + sMsg + "\n", 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);
// }
}
/**
* 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
ProtectionDomain pd;
/**
* @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)
* @throws JarClassLoaderException
*/
JarFileInfo(JarFile jarFile, String simpleName, JarFileInfo jarFileParent,
ProtectionDomain pd, File fileDeleteOnExit)
{
this.simpleName = (jarFileParent == null ? "" : jarFileParent.simpleName + "!") + simpleName;
this.jarFile = jarFile;
this.pd = pd;
this.fileDeleteOnExit = fileDeleteOnExit;
try {
this.mf = jarFile.getManifest(); // 'null' if META-INF directory is missing
} catch (IOException e) {
// Ignore and create blank manifest
}
if (this.mf == null) {
this.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.
*
* @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;
}
} // 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