/******************************************************************************* * Copyright (c) 2008, 2015 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.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 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 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; } 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.registeredAsParallel = CLOSEABLE_REGISTERED_AS_PARALLEL && this.getClass() == CloseableURLClassLoader.class; 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 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 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); 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$ } } } /* (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); if (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()]); } private final Map 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; } }