/*
* freenet - JarClassLoader.java Copyright © 2007 David Roden
*
* This program is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License as published by the Free Software
* Foundation; either version 2 of the License, or (at your option) any later
* version.
*
* 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., 59 Temple
* Place - Suite 330, Boston, MA 02111-1307, USA.
*/
package freenet.support;
import java.io.ByteArrayOutputStream;
import java.io.Closeable;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.jar.Attributes;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.Manifest;
import java.util.jar.Attributes.Name;
import java.util.zip.ZipEntry;
import freenet.support.io.FileUtil;
/**
* Class loader that loads classes from a JAR file. The JAR file gets copied
* to a temporary location; requests for classes and resources from this class
* loader are then satisfied from this local copy.
*
* @author <a href="mailto:dr@ina-germany.de">David Roden</a>
* @version $Id$
*/
public class JarClassLoader extends ClassLoader implements Closeable {
private static volatile boolean logMINOR;
static {
Logger.registerClass(JarClassLoader.class);
}
/** The temporary jar file. */
private JarFile tempJarFile;
/**
* Constructs a new jar class loader that loads classes from the jar file
* with the given name in the local file system.
*
* @param fileName
* The name of the jar file
* @throws IOException
* if an I/O error occurs
*/
public JarClassLoader(String fileName) throws IOException {
this(new File(fileName));
}
/**
* Constructs a new jar class loader that loads classes from the specified
* URL.
*
* @param fileUrl
* The URL to load the jar file from
* @param length
* The length of the jar file if known, <code>-1</code>
* otherwise
* @throws IOException
* if an I/O error occurs
*/
public JarClassLoader(URL fileUrl, long length) throws IOException {
copyFileToTemp(fileUrl.openStream(), length);
}
/**
* Constructs a new jar class loader that loads classes from the specified
* file.
*
* @param file
* The file to load classes from
* @throws IOException
* if an I/O error occurs
*/
public JarClassLoader(File file) throws IOException {
tempJarFile = new JarFile(file);
}
/**
* Copies the contents of the input stream (which are supposed to be the
* contents of a jar file) to a temporary location.
*
* @param inputStream
* The input stream to read from
* @param length
* The length of the stream if known, <code>-1</code> if the
* length is not known
* @throws IOException
* if an I/O error occurs
*/
private void copyFileToTemp(InputStream inputStream, long length) throws IOException {
File tempFile = File.createTempFile("jar-", ".tmp");
FileOutputStream fileOutputStream = new FileOutputStream(tempFile);
FileUtil.copy(inputStream, fileOutputStream, length);
fileOutputStream.close();
tempFile.deleteOnExit();
tempJarFile = new JarFile(tempFile);
}
/**
* {@inheritDoc}
* <p>
* This method searches the temporary copy of the jar file for an entry
* that is specified by the given class name.
*
* @see java.lang.ClassLoader#findClass(java.lang.String)
*/
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
String pathName = transformName(name);
JarEntry jarEntry = tempJarFile.getJarEntry(pathName);
if (jarEntry != null) {
long size = jarEntry.getSize();
InputStream jarEntryInputStream = tempJarFile.getInputStream(jarEntry);
ByteArrayOutputStream classBytesOutputStream = new ByteArrayOutputStream((int) size);
FileUtil.copy(jarEntryInputStream, classBytesOutputStream, size);
classBytesOutputStream.close();
jarEntryInputStream.close();
byte[] classBytes = classBytesOutputStream.toByteArray();
definePackage(name);
Class<?> clazz = defineClass(name, classBytes, 0, classBytes.length);
return clazz;
}
throw new ClassNotFoundException("could not find jar entry for class " + name);
} catch (IOException e) {
throw new ClassNotFoundException(e.getMessage(), e);
}
}
/**
* {@inheritDoc}
* <p>
* Finds the resource within this jar only. If it isn't found within the jar, getResourceAsStream()
* will look elsewhere.
*/
@Override
protected URL findResource(String name) {
/* FIXME compatibility code. remove when all plugins are fixed. */
if (name.startsWith("/")) {
name = name.substring(1);
}
try {
if(tempJarFile.getJarEntry(name)==null) {
return null;
}
return new URL("jar:" + new File(tempJarFile.getName()).toURI().toURL() + "!/" + name);
} catch (MalformedURLException e) {
}
return null;
}
/**
* {@inheritDoc}
* <p>
* If the resource is found in this jar, opens the stream using ZipEntry's,
* so when tempJarFile is closed, so are all the streams, hence we can delete
* the jar on Windows.
*
* @see java.lang.ClassLoader#getResourceAsStream(java.lang.String)
*/
@Override
public InputStream getResourceAsStream(String name) {
if(logMINOR) Logger.minor(this, "Requested resource: " + name, new Exception("debug"));
URL url = getResource(name);
if (url == null)
return null;
if(logMINOR) Logger.minor(this, "Found resource at URL: " + url);
// If the resource is not from our jar, return it as normal
URL localUrl = findResource(name);
if (localUrl == null || !url.toString().equals(localUrl.toString()))
try {
return url.openStream();
} catch (IOException e) {
return null;
}
// If the resource is from our jar, open InputStream explicitly from the jar
// so that we can close() all opened streams later and let the jar file
// to be deleted on Windows
/* FIXME compatibility code. remove when all plugins are fixed. */
if (name.startsWith("/")) {
name = name.substring(1);
}
ZipEntry entry = tempJarFile.getEntry(name);
try {
return entry != null ? tempJarFile.getInputStream(entry) : null;
} catch (IOException e) {
return null;
}
}
/**
* Transforms the class name into a file name that can be used to locate
* an entry in the jar file.
*
* @param name
* The name of the class
* @return The path name of the entry in the jar file
*/
private String transformName(String name) {
return name.replace('.', '/') + ".class";
}
protected Package definePackage(String name) throws IllegalArgumentException {
Package pkg = null;
int i = name.lastIndexOf('.');
if (i != -1) {
String pkgname = name.substring(0, i);
pkg = getPackage(pkgname);
if (pkg == null) {
try {
Manifest man = tempJarFile.getManifest();
if(man == null) throw new IOException();
pkg = definePackage(pkgname, man);
} catch (IOException e) {
pkg = definePackage(pkgname, null, null, null, null, null, null, null);
}
}
}
return pkg;
}
protected Package definePackage(String name, Manifest man) throws IllegalArgumentException {
String path = name.replace('.', '/').concat("/");
String specTitle = null, specVersion = null, specVendor = null;
String implTitle = null, implVersion = null, implVendor = null;
String sealed = null;
URL sealBase = null;
Attributes attr = man.getAttributes(path);
if (attr != null) {
specTitle = attr.getValue(Name.SPECIFICATION_TITLE);
specVersion = attr.getValue(Name.SPECIFICATION_VERSION);
specVendor = attr.getValue(Name.SPECIFICATION_VENDOR);
implTitle = attr.getValue(Name.IMPLEMENTATION_TITLE);
implVersion = attr.getValue(Name.IMPLEMENTATION_VERSION);
implVendor = attr.getValue(Name.IMPLEMENTATION_VENDOR);
sealed = attr.getValue(Name.SEALED);
}
attr = man.getMainAttributes();
if (attr != null) {
if (specTitle == null) {
specTitle = attr.getValue(Name.SPECIFICATION_TITLE);
}
if (specVersion == null) {
specVersion = attr.getValue(Name.SPECIFICATION_VERSION);
}
if (specVendor == null) {
specVendor = attr.getValue(Name.SPECIFICATION_VENDOR);
}
if (implTitle == null) {
implTitle = attr.getValue(Name.IMPLEMENTATION_TITLE);
}
if (implVersion == null) {
implVersion = attr.getValue(Name.IMPLEMENTATION_VERSION);
}
if (implVendor == null) {
implVendor = attr.getValue(Name.IMPLEMENTATION_VENDOR);
}
if (sealed == null) {
sealed = attr.getValue(Name.SEALED);
}
}
return definePackage(name, specTitle, specVersion, specVendor, implTitle, implVersion, implVendor, sealBase);
}
@Override
public void close() throws IOException {
tempJarFile.close();
}
}