/*
* 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.30 2011/02/04 20:12:19 mg Exp $
*/
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.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 debugging:
* -DJarClassLoader.logger=[filename]
for logging into the file or
* -DJarClassLoader.logger=console
for logging into the console.
*
*
* 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.30 $
*/
public class Bootstrap extends ClassLoader {
/** VM parameter key to turn on debug logging to file or console. */
public static final String KEY_LOGGER = "JarClassLoader.logger";
public static final String CONSOLE = "console";
private PrintStream logger;
protected List lstJarFile;
private Set hsNativeFile;
private Map> hmClass;
private ProtectionDomain pd;
private static final Name BOOTSTRAP_MAIN_CLASS = new Name(
"Bootstrap-MainClass");
private static final Name BOOTSTRAP_CLASSPATH = new Name(
"Bootstrap-Classpath");
public static void main(String[] args) {
Bootstrap main = new Bootstrap();
try {
main.invokeMain(args);
} catch (Throwable e) {
e.printStackTrace();
}
}
public void invokeMain(String[] args) throws Throwable {
String realMainClass = getManifestBootstrapMainClass();
if (realMainClass == null) {
System.out.println("main not found in manifest entry : "
+ BOOTSTRAP_MAIN_CLASS);
System.exit(1);
}
invokeMain(realMainClass, args);
}
private String getManifestBootstrapMainClass() {
Attributes attr = null;
if (isLaunchedFromJar()) {
try {
// The first element in array is the top level JAR
Manifest m = lstJarFile.get(0).getManifest();
attr = m.getMainAttributes();
} catch (IOException e) {
}
}
return (attr == null ? null : attr.getValue(BOOTSTRAP_MAIN_CLASS));
}
private String[] getManifestBoostrapClassPath() {
try {
Attributes attr = null;
// The first element in array is the top level JAR
Manifest m = lstJarFile.get(0).getManifest();
attr = m.getMainAttributes();
String value = attr.getValue(BOOTSTRAP_CLASSPATH);
String[] classPath = value.split(";");
return classPath;
} catch (Exception e) {
return new String[] { "META-INF/lib", "WEB-INF/lib" };
}
}
/**
* Default constructor. Defines system class loader as a parent class
* loader.
*/
public Bootstrap() {
this(ClassLoader.getSystemClassLoader());
}
/**
* Constructor.
*
* @param parent
* class loader parent.
*/
public Bootstrap(ClassLoader parent) {
super(parent);
String sLogger = System.getProperty(KEY_LOGGER);
if (sLogger != null) {
if (sLogger.equals(CONSOLE)) {
this.logger = System.out;
} else {
try {
this.logger = new PrintStream(sLogger);
} catch (FileNotFoundException e) {
throw new RuntimeException(
"JarClassLoader: cannot create log file: " + e);
}
}
}
hmClass = new HashMap>();
lstJarFile = new ArrayList();
hsNativeFile = new HashSet();
String sUrlTopJAR = null;
try {
pd = getClass().getProtectionDomain();
CodeSource cs = pd.getCodeSource();
URL urlTopJAR = cs.getLocation();
// URL.getFile() returns "/C:/my%20dir/MyApp.jar"
sUrlTopJAR = URLDecoder.decode(urlTopJAR.getFile(), "UTF-8");
log("Loading from top JAR: %s", sUrlTopJAR);
loadJar(new JarFile(sUrlTopJAR)); // throws if not JAR
} catch (IOException e) {
// Expected exception: loading NOT from JAR.
log("Not a JAR: %s %s", sUrlTopJAR, e.toString());
return;
}
// Runtime.getRuntime().addShutdownHook(new Thread() {
// public void run() {
// log("shutdown hook started");
// shutdown();
// }
// });
} // JarClassLoader()
// --------------------------------separator--------------------------------
static int ______INIT;
/**
* 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. 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. 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 Exception
*/
private File createTempFile(Map inf) throws Exception {
byte[] a_by = FIgetJarBytes(inf);
try {
File file = File.createTempFile(FIgetName(inf) + ".", null);
file.deleteOnExit();
BufferedOutputStream os = new BufferedOutputStream(
new FileOutputStream(file));
os.write(a_by);
os.close();
return file;
} catch (IOException e) {
throw new Exception("Cannot create temp file for "
+ inf.get("jarEntry"), e);
}
} // createTempFile()
private boolean jarEntryStartWith(String[] classpaths, JarEntry je) {
String name = je.getName();
for (String classpath : classpaths) {
if (name.startsWith(classpath)) {
return true;
}
}
return false;
}
/**
* Loads specified JAR
*
* @param jarFile
* JAR file
*/
private void loadJar(JarFile jarFile) {
lstJarFile.add(jarFile);
try {
Enumeration en = jarFile.entries();
final String EXT_JAR = ".jar";
String[] classpaths = getManifestBoostrapClassPath();
while (en.hasMoreElements()) {
JarEntry je = en.nextElement();
if (je.isDirectory()
|| !jarEntryStartWith(classpaths, je)) {
continue;
}
// System.out.println("load :" + je.getName());
String s = je.getName().toLowerCase(); // JarEntry name
if (s.lastIndexOf(EXT_JAR) == s.length() - EXT_JAR.length()) {
Map inf = FIbuildJarFileInfo(jarFile, je);
File file = createTempFile(inf);
log("Loading inner JAR %s from temp file %s",
inf.get("jarEntry"), getFilename4Log(file));
try {
loadJar(new JarFile(file));
} catch (IOException e) {
throw new Exception("Cannot load inner JAR "
+ inf.get("jarEntry"), e);
}
}
}
} catch (Exception e) {
throw new RuntimeException("ERROR on loading InnerJAR", e);
}
} // loadJar()
private Map findJarEntry(String sName) {
for (JarFile jarFile : lstJarFile) {
JarEntry jarEntry = jarFile.getJarEntry(sName);
if (jarEntry != null) {
return FIbuildJarFileInfo(jarFile, jarEntry);
}
}
return null;
} // findJarEntry()
private List