blob: f29efbf4ba405919a7bfdb88c58bfb1d1d423d7c [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2008, 2009 IBM Corporation and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* IBM Corporation - initial API and implementation
*******************************************************************************/
/**
* The java.net.URLClassLoader class allows one to load resources from arbitrary URLs and in particular is optimized to handle
* "jar" URLs. Unfortunately for jar files this optimization ends up holding the file open which ultimately prevents the file from
* being deleted or update until the VM is shutdown.
*
* The CloseableURLClassLoader is meant to replace the URLClassLoader and provides an additional method to allow one to "close" any
* resources left open. In the current version the CloseableURLClassLoader will only ensure the closing of jar file resources. The
* jar handling behavior in this class will also provides a construct to allow one to turn off jar file verification in performance
* sensitive situations where the verification us not necessary.
*
* also see https://bugs.eclipse.org/bugs/show_bug.cgi?id=190279
*/
package org.eclipse.equinox.servletbridge;
import java.io.*;
import java.net.*;
import java.security.*;
import java.util.*;
import java.util.jar.*;
import java.util.jar.Attributes.Name;
public class CloseableURLClassLoader extends URLClassLoader {
static final String DOT_CLASS = ".class"; //$NON-NLS-1$
static final String BANG_SLASH = "!/"; //$NON-NLS-1$
static final String JAR = "jar"; //$NON-NLS-1$
// @GuardedBy("loaders")
final ArrayList loaders = new ArrayList(); // package private to avoid synthetic access.
// @GuardedBy("loaders")
private final ArrayList loaderURLs = new ArrayList(); // note: protected by loaders
// @GuardedBy("loaders")
boolean closed = false; // note: protected by loaders, package private to avoid synthetic access.
private final AccessControlContext context;
private final boolean verifyJars;
private static class CloseableJarURLConnection extends JarURLConnection {
private final JarFile jarFile;
// @GuardedBy("this")
private JarEntry entry;
public CloseableJarURLConnection(URL url, JarFile jarFile) throws MalformedURLException {
super(url);
this.jarFile = jarFile;
}
public void connect() throws IOException {
internalGetEntry();
}
private synchronized JarEntry internalGetEntry() throws IOException {
if (entry != null)
return entry;
entry = jarFile.getJarEntry(getEntryName());
if (entry == null)
throw new FileNotFoundException(getEntryName());
return entry;
}
public InputStream getInputStream() throws IOException {
return jarFile.getInputStream(internalGetEntry());
}
/**
* @throws IOException
* Documented to avoid warning
*/
public JarFile getJarFile() throws IOException {
return jarFile;
}
public JarEntry getJarEntry() throws IOException {
return internalGetEntry();
}
}
private static class CloseableJarURLStreamHandler extends URLStreamHandler {
private final JarFile jarFile;
public CloseableJarURLStreamHandler(JarFile jarFile) {
this.jarFile = jarFile;
}
protected URLConnection openConnection(URL u) throws IOException {
return new CloseableJarURLConnection(u, jarFile);
}
protected void parseURL(URL u, String spec, int start, int limit) {
setURL(u, JAR, null, 0, null, null, spec.substring(start, limit), null, null);
}
}
private static class CloseableJarFileLoader {
private final JarFile jarFile;
private final Manifest manifest;
private final CloseableJarURLStreamHandler jarURLStreamHandler;
private final String jarFileURLPrefixString;
public CloseableJarFileLoader(File file, boolean verify) throws IOException {
this.jarFile = new JarFile(file, verify);
this.manifest = jarFile.getManifest();
this.jarURLStreamHandler = new CloseableJarURLStreamHandler(jarFile);
this.jarFileURLPrefixString = file.toURL().toString() + BANG_SLASH;
}
public URL getURL(String name) {
if (jarFile.getEntry(name) != null)
try {
return new URL(JAR, null, -1, jarFileURLPrefixString + name, jarURLStreamHandler);
} catch (MalformedURLException e) {
// ignore
}
return null;
}
public Manifest getManifest() {
return manifest;
}
public void close() {
try {
jarFile.close();
} catch (IOException e) {
// ignore
}
}
}
/**
* @param urls the array of URLs to use for loading resources
* @see URLClassLoader
*/
public CloseableURLClassLoader(URL[] urls) {
this(urls, ClassLoader.getSystemClassLoader(), true);
}
/**
* @param urls the URLs from which to load classes and resources
* @param parent the parent class loader used for delegation
* @see URLClassLoader
*/
public CloseableURLClassLoader(URL[] urls, ClassLoader parent) {
this(excludeFileJarURLS(urls), parent, true);
}
/**
* @param urls the URLs from which to load classes and resources
* @param parent the parent class loader used for delegation
* @param verifyJars flag to determine if jar file verification should be performed
* @see URLClassLoader
*/
public CloseableURLClassLoader(URL[] urls, ClassLoader parent, boolean verifyJars) {
super(excludeFileJarURLS(urls), parent);
this.context = AccessController.getContext();
this.verifyJars = verifyJars;
for (int i = 0; i < urls.length; i++) {
if (isFileJarURL(urls[i])) {
loaderURLs.add(urls[i]);
safeAddLoader(urls[i]);
}
}
}
// @GuardedBy("loaders")
private void safeAddLoader(URL url) {
String path = url.getPath();
File file = new File(path);
if (file.exists()) {
try {
loaders.add(new CloseableJarFileLoader(file, verifyJars));
} catch (IOException e) {
// ignore
}
}
}
private static URL[] excludeFileJarURLS(URL[] urls) {
ArrayList urlList = new ArrayList();
for (int i = 0; i < urls.length; i++) {
if (!isFileJarURL(urls[i]))
urlList.add(urls[i]);
}
return (URL[]) urlList.toArray(new URL[urlList.size()]);
}
private static boolean isFileJarURL(URL url) {
if (!url.getProtocol().equals("file")) //$NON-NLS-1$
return false;
String path = url.getPath();
if (path != null && path.endsWith("/")) //$NON-NLS-1$
return false;
return true;
}
/* (non-Javadoc)
* @see java.net.URLClassLoader#findClass(java.lang.String)
*/
protected Class findClass(final String name) throws ClassNotFoundException {
try {
Class clazz = (Class) AccessController.doPrivileged(new PrivilegedExceptionAction() {
public Object run() throws ClassNotFoundException {
String resourcePath = name.replace('.', '/') + DOT_CLASS;
CloseableJarFileLoader loader = null;
URL resourceURL = null;
synchronized (loaders) {
if (closed)
return null;
for (Iterator iterator = loaders.iterator(); iterator.hasNext();) {
loader = (CloseableJarFileLoader) iterator.next();
resourceURL = loader.getURL(resourcePath);
if (resourceURL != null)
break;
}
}
if (resourceURL != null) {
try {
return defineClass(name, resourceURL, loader.getManifest());
} catch (IOException e) {
throw new ClassNotFoundException(name, e);
}
}
return null;
}
}, context);
if (clazz != null)
return clazz;
} catch (PrivilegedActionException e) {
throw (ClassNotFoundException) e.getException();
}
return super.findClass(name);
}
// package private to avoid synthetic access.
Class defineClass(String name, URL resourceURL, Manifest manifest) throws IOException {
JarURLConnection connection = (JarURLConnection) resourceURL.openConnection();
int lastDot = name.lastIndexOf('.');
if (lastDot != -1) {
String packageName = name.substring(0, lastDot + 1);
Package pkg = getPackage(packageName);
if (pkg != null) {
checkForSealedPackage(pkg, packageName, manifest, connection.getJarFileURL());
} else {
definePackage(packageName, manifest, connection.getJarFileURL());
}
}
JarEntry entry = connection.getJarEntry();
byte[] bytes = new byte[(int) entry.getSize()];
DataInputStream is = null;
try {
is = new DataInputStream(connection.getInputStream());
is.readFully(bytes, 0, bytes.length);
CodeSource cs = new CodeSource(connection.getJarFileURL(), entry.getCertificates());
return defineClass(name, bytes, 0, bytes.length, cs);
} finally {
if (is != null)
try {
is.close();
} catch (IOException e) {
// ignore
}
}
}
private void checkForSealedPackage(Package pkg, String packageName, Manifest manifest, URL jarFileURL) {
if (pkg.isSealed() && !pkg.isSealed(jarFileURL))
throw new SecurityException("The package '" + packageName + "' was previously loaded and is already sealed."); //$NON-NLS-1$ //$NON-NLS-2$
String entryPath = packageName.replace('.', '/') + "/"; //$NON-NLS-1$
Attributes entryAttributes = manifest.getAttributes(entryPath);
String sealed = null;
if (entryAttributes != null)
sealed = entryAttributes.getValue(Name.SEALED);
if (sealed == null) {
Attributes mainAttributes = manifest.getMainAttributes();
if (mainAttributes != null)
sealed = mainAttributes.getValue(Name.SEALED);
}
if (Boolean.valueOf(sealed).booleanValue())
throw new SecurityException("The package '" + packageName + "' was previously loaded unsealed. Cannot seal package."); //$NON-NLS-1$ //$NON-NLS-2$
}
/* (non-Javadoc)
* @see java.net.URLClassLoader#findResource(java.lang.String)
*/
public URL findResource(final String name) {
URL url = (URL) AccessController.doPrivileged(new PrivilegedAction() {
public Object run() {
synchronized (loaders) {
if (closed)
return null;
for (Iterator iterator = loaders.iterator(); iterator.hasNext();) {
CloseableJarFileLoader loader = (CloseableJarFileLoader) iterator.next();
URL resourceURL = loader.getURL(name);
if (resourceURL != null)
return resourceURL;
}
}
return null;
}
}, context);
if (url != null)
return url;
return super.findResource(name);
}
/* (non-Javadoc)
* @see java.net.URLClassLoader#findResources(java.lang.String)
*/
public Enumeration findResources(final String name) throws IOException {
final List resources = new ArrayList();
AccessController.doPrivileged(new PrivilegedAction() {
public Object run() {
synchronized (loaders) {
if (closed)
return null;
for (Iterator iterator = loaders.iterator(); iterator.hasNext();) {
CloseableJarFileLoader loader = (CloseableJarFileLoader) iterator.next();
URL resourceURL = loader.getURL(name);
if (resourceURL != null)
resources.add(resourceURL);
}
}
return null;
}
}, context);
Enumeration e = super.findResources(name);
while (e.hasMoreElements())
resources.add(e.nextElement());
return Collections.enumeration(resources);
}
/**
* The "close" method is called when the class loader is no longer needed and we should close any open resources.
* In particular this method will close the jar files associated with this class loader.
*/
public void close() {
synchronized (loaders) {
if (closed)
return;
for (Iterator iterator = loaders.iterator(); iterator.hasNext();) {
CloseableJarFileLoader loader = (CloseableJarFileLoader) iterator.next();
loader.close();
}
closed = true;
}
}
/* (non-Javadoc)
* @see java.net.URLClassLoader#addURL(java.net.URL)
*/
protected void addURL(URL url) {
synchronized (loaders) {
if (isFileJarURL(url)) {
if (closed)
throw new IllegalStateException("Cannot add url. CloseableURLClassLoader is closed."); //$NON-NLS-1$
loaderURLs.add(url);
safeAddLoader(url);
return;
}
}
super.addURL(url);
}
/* (non-Javadoc)
* @see java.net.URLClassLoader#getURLs()
*/
public URL[] getURLs() {
List result = new ArrayList();
synchronized (loaders) {
result.addAll(loaderURLs);
}
result.addAll(Arrays.asList(super.getURLs()));
return (URL[]) result.toArray(new URL[result.size()]);
}
}