blob: 30ed331132178978b0aaf35ebee0f15bda9396f3 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2008, 2018 IBM Corporation and others.
*
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* 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.lang.reflect.Method;
import java.net.*;
import java.security.*;
import java.util.*;
import java.util.jar.*;
import java.util.jar.Attributes.Name;
public class CloseableURLClassLoader extends URLClassLoader {
private static final boolean CLOSEABLE_REGISTERED_AS_PARALLEL;
static {
boolean registeredAsParallel;
try {
Method parallelCapableMetod = ClassLoader.class.getDeclaredMethod("registerAsParallelCapable", (Class[]) null); //$NON-NLS-1$
parallelCapableMetod.setAccessible(true);
registeredAsParallel = ((Boolean) parallelCapableMetod.invoke(null, (Object[]) null)).booleanValue();
} catch (Throwable e) {
// must do everything to avoid failing in clinit
registeredAsParallel = true;
}
CLOSEABLE_REGISTERED_AS_PARALLEL = registeredAsParallel;
}
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$
private static final String UNC_PREFIX = "//"; //$NON-NLS-1$
private static final String SCHEME_FILE = "file"; //$NON-NLS-1$
// @GuardedBy("loaders")
final ArrayList<CloseableJarFileLoader> loaders = new ArrayList<>(); // package private to avoid synthetic access.
// @GuardedBy("loaders")
private final ArrayList<URL> 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 final boolean registeredAsParallel;
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;
}
@Override
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;
}
@Override
public InputStream getInputStream() throws IOException {
return jarFile.getInputStream(internalGetEntry());
}
/**
* @throws IOException
* Documented to avoid warning
*/
@Override
public JarFile getJarFile() throws IOException {
return jarFile;
}
@Override
public JarEntry getJarEntry() throws IOException {
return internalGetEntry();
}
}
private static class CloseableJarURLStreamHandler extends URLStreamHandler {
private final JarFile jarFile;
public CloseableJarURLStreamHandler(JarFile jarFile) {
this.jarFile = jarFile;
}
@Override
protected URLConnection openConnection(URL u) throws IOException {
return new CloseableJarURLConnection(u, jarFile);
}
@Override
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.registeredAsParallel = CLOSEABLE_REGISTERED_AS_PARALLEL && this.getClass() == CloseableURLClassLoader.class;
this.context = AccessController.getContext();
this.verifyJars = verifyJars;
for (URL url : urls) {
if (isFileJarURL(url)) {
loaderURLs.add(url);
safeAddLoader(url);
}
}
}
// @GuardedBy("loaders")
private boolean safeAddLoader(URL url) {
//assume all illegal characters have been properly encoded, so use URI class to unencode
try {
File file = new File(toURI(url));
if (file.exists()) {
try {
loaders.add(new CloseableJarFileLoader(file, verifyJars));
return true;
} catch (IOException e) {
// ignore
}
}
} catch (URISyntaxException e1) {
// ignore
}
return false;
}
private static URI toURI(URL url) throws URISyntaxException {
if (!SCHEME_FILE.equals(url.getProtocol())) {
throw new IllegalArgumentException("bad prototcol: " + url.getProtocol()); //$NON-NLS-1$
}
//URL behaves differently across platforms so for file: URLs we parse from string form
String pathString = url.toExternalForm().substring(5);
//ensure there is a leading slash to handle common malformed URLs such as file:c:/tmp
if (pathString.indexOf('/') != 0)
pathString = '/' + pathString;
else if (pathString.startsWith(UNC_PREFIX) && !pathString.startsWith(UNC_PREFIX, 2)) {
//URL encodes UNC path with two slashes, but URI uses four (see bug 207103)
pathString = ensureUNCPath(pathString);
}
return new URI(SCHEME_FILE, null, pathString, null);
}
/**
* Ensures the given path string starts with exactly four leading slashes.
*/
private static String ensureUNCPath(String path) {
int len = path.length();
StringBuffer result = new StringBuffer(len);
for (int i = 0; i < 4; i++) {
// if we have hit the first non-slash character, add another leading slash
if (i >= len || result.length() > 0 || path.charAt(i) != '/')
result.append('/');
}
result.append(path);
return result.toString();
}
private static URL[] excludeFileJarURLS(URL[] urls) {
ArrayList<URL> urlList = new ArrayList<>();
for (URL url : urls) {
if (!isFileJarURL(url)) {
urlList.add(url);
}
}
return 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;
}
@Override
protected Class<?> findClass(final String name) throws ClassNotFoundException {
try {
Class<?> clazz = AccessController.doPrivileged(new PrivilegedExceptionAction<Class<?>>() {
@Override
public Class<?> run() throws ClassNotFoundException {
String resourcePath = name.replace('.', '/') + DOT_CLASS;
CloseableJarFileLoader loader = null;
URL resourceURL = null;
synchronized (loaders) {
if (closed)
return null;
for (Iterator<CloseableJarFileLoader> iterator = loaders.iterator(); iterator.hasNext();) {
loader = 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);
synchronized (pkgLock) {
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());
if (isRegisteredAsParallel()) {
boolean initialLock = lockClassName(name);
try {
Class<?> clazz = findLoadedClass(name);
if (clazz != null) {
return clazz;
}
return defineClass(name, bytes, 0, bytes.length, cs);
} finally {
if (initialLock) {
unlockClassName(name);
}
}
}
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()) {
// previously sealed case
if (!pkg.isSealed(jarFileURL)) {
// this URL does not seal; ERROR
throw new SecurityException("The package '" + packageName + "' was previously loaded and is already sealed."); //$NON-NLS-1$ //$NON-NLS-2$
}
} else {
// previously unsealed case
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()) {
// this manifest attempts to seal when package defined previously unsealed; ERROR
throw new SecurityException("The package '" + packageName + "' was previously loaded unsealed. Cannot seal package."); //$NON-NLS-1$ //$NON-NLS-2$
}
}
}
@Override
public URL findResource(final String name) {
URL url = AccessController.doPrivileged(new PrivilegedAction<URL>() {
@Override
public URL run() {
synchronized (loaders) {
if (closed)
return null;
for (CloseableJarFileLoader loader : loaders) {
URL resourceURL = loader.getURL(name);
if (resourceURL != null)
return resourceURL;
}
}
return null;
}
}, context);
if (url != null)
return url;
return super.findResource(name);
}
@Override
public Enumeration<URL> findResources(final String name) throws IOException {
final List<URL> resources = new ArrayList<>();
AccessController.doPrivileged(new PrivilegedAction<Object>() {
@Override
public Object run() {
synchronized (loaders) {
if (closed)
return null;
for (CloseableJarFileLoader loader : loaders) {
URL resourceURL = loader.getURL(name);
if (resourceURL != null)
resources.add(resourceURL);
}
}
return null;
}
}, context);
Enumeration<URL> 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.
*/
@Override
public void close() {
synchronized (loaders) {
if (closed)
return;
for (CloseableJarFileLoader loader : loaders) {
loader.close();
}
closed = true;
}
}
@Override
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);
if (safeAddLoader(url))
return;
}
}
super.addURL(url);
}
@Override
public URL[] getURLs() {
List<URL> result = new ArrayList<>();
synchronized (loaders) {
result.addAll(loaderURLs);
}
result.addAll(Arrays.asList(super.getURLs()));
return result.toArray(new URL[result.size()]);
}
private final Map<String, Thread> classNameLocks = new HashMap<>(5);
private final Object pkgLock = new Object();
private boolean lockClassName(String classname) {
synchronized (classNameLocks) {
Object lockingThread = classNameLocks.get(classname);
Thread current = Thread.currentThread();
if (lockingThread == current)
return false;
boolean previousInterruption = Thread.interrupted();
try {
while (true) {
if (lockingThread == null) {
classNameLocks.put(classname, current);
return true;
}
classNameLocks.wait();
lockingThread = classNameLocks.get(classname);
}
} catch (InterruptedException e) {
current.interrupt();
throw (LinkageError) new LinkageError(classname).initCause(e);
} finally {
if (previousInterruption) {
current.interrupt();
}
}
}
}
private void unlockClassName(String classname) {
synchronized (classNameLocks) {
classNameLocks.remove(classname);
classNameLocks.notifyAll();
}
}
protected boolean isRegisteredAsParallel() {
return registeredAsParallel;
}
}