1 /******************************************************************************* 2 * Copyright (c) 2008, 2018 IBM Corporation and others. 3 * 4 * This program and the accompanying materials 5 * are made available under the terms of the Eclipse Public License 2.0 6 * which accompanies this distribution, and is available at 7 * https://www.eclipse.org/legal/epl-2.0/ 8 * 9 * SPDX-License-Identifier: EPL-2.0 10 * 11 * Contributors: 12 * IBM Corporation - initial API and implementation 13 *******************************************************************************/ 14 15 /** 16 * The java.net.URLClassLoader class allows one to load resources from arbitrary URLs and in particular is optimized to handle 17 * "jar" URLs. Unfortunately for jar files this optimization ends up holding the file open which ultimately prevents the file from 18 * being deleted or update until the VM is shutdown. 19 * 20 * The CloseableURLClassLoader is meant to replace the URLClassLoader and provides an additional method to allow one to "close" any 21 * resources left open. In the current version the CloseableURLClassLoader will only ensure the closing of jar file resources. The 22 * jar handling behavior in this class will also provides a construct to allow one to turn off jar file verification in performance 23 * sensitive situations where the verification us not necessary. 24 * 25 * also see https://bugs.eclipse.org/bugs/show_bug.cgi?id=190279 26 */ 27 28 package org.eclipse.equinox.servletbridge; 29 30 import java.io.*; 31 import java.lang.reflect.Method; 32 import java.net.*; 33 import java.security.*; 34 import java.util.*; 35 import java.util.jar.*; 36 import java.util.jar.Attributes.Name; 37 38 public class CloseableURLClassLoader extends URLClassLoader { 39 private static final boolean CLOSEABLE_REGISTERED_AS_PARALLEL; 40 static { 41 boolean registeredAsParallel; 42 try { 43 Method parallelCapableMetod = ClassLoader.class.getDeclaredMethod("registerAsParallelCapable", (Class[]) null); //$NON-NLS-1$ 44 parallelCapableMetod.setAccessible(true); 45 registeredAsParallel = ((Boolean) parallelCapableMetod.invoke(null, (Object[]) null)).booleanValue(); 46 } catch (Throwable e) { 47 // must do everything to avoid failing in clinit 48 registeredAsParallel = true; 49 } 50 CLOSEABLE_REGISTERED_AS_PARALLEL = registeredAsParallel; 51 } 52 static final String DOT_CLASS = ".class"; //$NON-NLS-1$ 53 static final String BANG_SLASH = "!/"; //$NON-NLS-1$ 54 static final String JAR = "jar"; //$NON-NLS-1$ 55 private static final String UNC_PREFIX = "//"; //$NON-NLS-1$ 56 private static final String SCHEME_FILE = "file"; //$NON-NLS-1$ 57 58 // @GuardedBy("loaders") 59 final ArrayList<CloseableJarFileLoader> loaders = new ArrayList<>(); // package private to avoid synthetic access. 60 // @GuardedBy("loaders") 61 private final ArrayList<URL> loaderURLs = new ArrayList<>(); // note: protected by loaders 62 // @GuardedBy("loaders") 63 boolean closed = false; // note: protected by loaders, package private to avoid synthetic access. 64 65 private final AccessControlContext context; 66 private final boolean verifyJars; 67 private final boolean registeredAsParallel; 68 69 private static class CloseableJarURLConnection extends JarURLConnection { 70 private final JarFile jarFile; 71 // @GuardedBy("this") 72 private JarEntry entry; 73 CloseableJarURLConnection(URL url, JarFile jarFile)74 public CloseableJarURLConnection(URL url, JarFile jarFile) throws MalformedURLException { 75 super(url); 76 this.jarFile = jarFile; 77 } 78 79 @Override connect()80 public void connect() throws IOException { 81 internalGetEntry(); 82 } 83 internalGetEntry()84 private synchronized JarEntry internalGetEntry() throws IOException { 85 if (entry != null) 86 return entry; 87 entry = jarFile.getJarEntry(getEntryName()); 88 if (entry == null) 89 throw new FileNotFoundException(getEntryName()); 90 return entry; 91 } 92 93 @Override getInputStream()94 public InputStream getInputStream() throws IOException { 95 return jarFile.getInputStream(internalGetEntry()); 96 } 97 98 /** 99 * @throws IOException 100 * Documented to avoid warning 101 */ 102 @Override getJarFile()103 public JarFile getJarFile() throws IOException { 104 return jarFile; 105 } 106 107 @Override getJarEntry()108 public JarEntry getJarEntry() throws IOException { 109 return internalGetEntry(); 110 } 111 } 112 113 private static class CloseableJarURLStreamHandler extends URLStreamHandler { 114 private final JarFile jarFile; 115 CloseableJarURLStreamHandler(JarFile jarFile)116 public CloseableJarURLStreamHandler(JarFile jarFile) { 117 this.jarFile = jarFile; 118 } 119 120 @Override openConnection(URL u)121 protected URLConnection openConnection(URL u) throws IOException { 122 return new CloseableJarURLConnection(u, jarFile); 123 } 124 125 @Override parseURL(URL u, String spec, int start, int limit)126 protected void parseURL(URL u, String spec, int start, int limit) { 127 setURL(u, JAR, null, 0, null, null, spec.substring(start, limit), null, null); 128 } 129 } 130 131 private static class CloseableJarFileLoader { 132 private final JarFile jarFile; 133 private final Manifest manifest; 134 private final CloseableJarURLStreamHandler jarURLStreamHandler; 135 private final String jarFileURLPrefixString; 136 CloseableJarFileLoader(File file, boolean verify)137 public CloseableJarFileLoader(File file, boolean verify) throws IOException { 138 this.jarFile = new JarFile(file, verify); 139 this.manifest = jarFile.getManifest(); 140 this.jarURLStreamHandler = new CloseableJarURLStreamHandler(jarFile); 141 this.jarFileURLPrefixString = file.toURL().toString() + BANG_SLASH; 142 } 143 getURL(String name)144 public URL getURL(String name) { 145 if (jarFile.getEntry(name) != null) 146 try { 147 return new URL(JAR, null, -1, jarFileURLPrefixString + name, jarURLStreamHandler); 148 } catch (MalformedURLException e) { 149 // ignore 150 } 151 return null; 152 } 153 getManifest()154 public Manifest getManifest() { 155 return manifest; 156 } 157 close()158 public void close() { 159 try { 160 jarFile.close(); 161 } catch (IOException e) { 162 // ignore 163 } 164 } 165 } 166 167 /** 168 * @param urls the array of URLs to use for loading resources 169 * @see URLClassLoader 170 */ CloseableURLClassLoader(URL[] urls)171 public CloseableURLClassLoader(URL[] urls) { 172 this(urls, ClassLoader.getSystemClassLoader(), true); 173 } 174 175 /** 176 * @param urls the URLs from which to load classes and resources 177 * @param parent the parent class loader used for delegation 178 * @see URLClassLoader 179 */ CloseableURLClassLoader(URL[] urls, ClassLoader parent)180 public CloseableURLClassLoader(URL[] urls, ClassLoader parent) { 181 this(excludeFileJarURLS(urls), parent, true); 182 } 183 184 /** 185 * @param urls the URLs from which to load classes and resources 186 * @param parent the parent class loader used for delegation 187 * @param verifyJars flag to determine if jar file verification should be performed 188 * @see URLClassLoader 189 */ CloseableURLClassLoader(URL[] urls, ClassLoader parent, boolean verifyJars)190 public CloseableURLClassLoader(URL[] urls, ClassLoader parent, boolean verifyJars) { 191 super(excludeFileJarURLS(urls), parent); 192 this.registeredAsParallel = CLOSEABLE_REGISTERED_AS_PARALLEL && this.getClass() == CloseableURLClassLoader.class; 193 this.context = AccessController.getContext(); 194 this.verifyJars = verifyJars; 195 for (URL url : urls) { 196 if (isFileJarURL(url)) { 197 loaderURLs.add(url); 198 safeAddLoader(url); 199 } 200 } 201 } 202 203 // @GuardedBy("loaders") safeAddLoader(URL url)204 private boolean safeAddLoader(URL url) { 205 //assume all illegal characters have been properly encoded, so use URI class to unencode 206 try { 207 File file = new File(toURI(url)); 208 if (file.exists()) { 209 try { 210 loaders.add(new CloseableJarFileLoader(file, verifyJars)); 211 return true; 212 } catch (IOException e) { 213 // ignore 214 } 215 } 216 } catch (URISyntaxException e1) { 217 // ignore 218 } 219 220 return false; 221 } 222 toURI(URL url)223 private static URI toURI(URL url) throws URISyntaxException { 224 if (!SCHEME_FILE.equals(url.getProtocol())) { 225 throw new IllegalArgumentException("bad prototcol: " + url.getProtocol()); //$NON-NLS-1$ 226 } 227 //URL behaves differently across platforms so for file: URLs we parse from string form 228 String pathString = url.toExternalForm().substring(5); 229 //ensure there is a leading slash to handle common malformed URLs such as file:c:/tmp 230 if (pathString.indexOf('/') != 0) 231 pathString = '/' + pathString; 232 else if (pathString.startsWith(UNC_PREFIX) && !pathString.startsWith(UNC_PREFIX, 2)) { 233 //URL encodes UNC path with two slashes, but URI uses four (see bug 207103) 234 pathString = ensureUNCPath(pathString); 235 } 236 return new URI(SCHEME_FILE, null, pathString, null); 237 } 238 239 /** 240 * Ensures the given path string starts with exactly four leading slashes. 241 */ ensureUNCPath(String path)242 private static String ensureUNCPath(String path) { 243 int len = path.length(); 244 StringBuilder result = new StringBuilder(len); 245 for (int i = 0; i < 4; i++) { 246 // if we have hit the first non-slash character, add another leading slash 247 if (i >= len || result.length() > 0 || path.charAt(i) != '/') 248 result.append('/'); 249 } 250 result.append(path); 251 return result.toString(); 252 } 253 excludeFileJarURLS(URL[] urls)254 private static URL[] excludeFileJarURLS(URL[] urls) { 255 ArrayList<URL> urlList = new ArrayList<>(); 256 for (URL url : urls) { 257 if (!isFileJarURL(url)) { 258 urlList.add(url); 259 } 260 } 261 return urlList.toArray(new URL[urlList.size()]); 262 } 263 isFileJarURL(URL url)264 private static boolean isFileJarURL(URL url) { 265 if (!url.getProtocol().equals("file")) //$NON-NLS-1$ 266 return false; 267 268 String path = url.getPath(); 269 if (path != null && path.endsWith("/")) //$NON-NLS-1$ 270 return false; 271 272 return true; 273 } 274 275 @Override findClass(final String name)276 protected Class<?> findClass(final String name) throws ClassNotFoundException { 277 try { 278 Class<?> clazz = AccessController.doPrivileged(new PrivilegedExceptionAction<Class<?>>() { 279 @Override 280 public Class<?> run() throws ClassNotFoundException { 281 String resourcePath = name.replace('.', '/') + DOT_CLASS; 282 CloseableJarFileLoader loader = null; 283 URL resourceURL = null; 284 synchronized (loaders) { 285 if (closed) 286 return null; 287 for (Iterator<CloseableJarFileLoader> iterator = loaders.iterator(); iterator.hasNext();) { 288 loader = iterator.next(); 289 resourceURL = loader.getURL(resourcePath); 290 if (resourceURL != null) 291 break; 292 } 293 } 294 if (resourceURL != null) { 295 try { 296 return defineClass(name, resourceURL, loader.getManifest()); 297 } catch (IOException e) { 298 throw new ClassNotFoundException(name, e); 299 } 300 } 301 return null; 302 } 303 }, context); 304 if (clazz != null) 305 return clazz; 306 } catch (PrivilegedActionException e) { 307 throw (ClassNotFoundException) e.getException(); 308 } 309 return super.findClass(name); 310 } 311 312 // package private to avoid synthetic access. defineClass(String name, URL resourceURL, Manifest manifest)313 Class<?> defineClass(String name, URL resourceURL, Manifest manifest) throws IOException { 314 JarURLConnection connection = (JarURLConnection) resourceURL.openConnection(); 315 int lastDot = name.lastIndexOf('.'); 316 if (lastDot != -1) { 317 String packageName = name.substring(0, lastDot); 318 synchronized (pkgLock) { 319 Package pkg = getPackage(packageName); 320 if (pkg != null) { 321 checkForSealedPackage(pkg, packageName, manifest, connection.getJarFileURL()); 322 } else { 323 definePackage(packageName, manifest, connection.getJarFileURL()); 324 } 325 } 326 327 } 328 JarEntry entry = connection.getJarEntry(); 329 byte[] bytes = new byte[(int) entry.getSize()]; 330 DataInputStream is = null; 331 try { 332 is = new DataInputStream(connection.getInputStream()); 333 is.readFully(bytes, 0, bytes.length); 334 CodeSource cs = new CodeSource(connection.getJarFileURL(), entry.getCertificates()); 335 if (isRegisteredAsParallel()) { 336 boolean initialLock = lockClassName(name); 337 try { 338 Class<?> clazz = findLoadedClass(name); 339 if (clazz != null) { 340 return clazz; 341 } 342 return defineClass(name, bytes, 0, bytes.length, cs); 343 } finally { 344 if (initialLock) { 345 unlockClassName(name); 346 } 347 } 348 } 349 return defineClass(name, bytes, 0, bytes.length, cs); 350 } finally { 351 if (is != null) 352 try { 353 is.close(); 354 } catch (IOException e) { 355 // ignore 356 } 357 } 358 } 359 checkForSealedPackage(Package pkg, String packageName, Manifest manifest, URL jarFileURL)360 private void checkForSealedPackage(Package pkg, String packageName, Manifest manifest, URL jarFileURL) { 361 if (pkg.isSealed()) { 362 // previously sealed case 363 if (!pkg.isSealed(jarFileURL)) { 364 // this URL does not seal; ERROR 365 throw new SecurityException("The package '" + packageName + "' was previously loaded and is already sealed."); //$NON-NLS-1$ //$NON-NLS-2$ 366 } 367 } else { 368 // previously unsealed case 369 String entryPath = packageName.replace('.', '/') + "/"; //$NON-NLS-1$ 370 Attributes entryAttributes = manifest.getAttributes(entryPath); 371 String sealed = null; 372 if (entryAttributes != null) 373 sealed = entryAttributes.getValue(Name.SEALED); 374 375 if (sealed == null) { 376 Attributes mainAttributes = manifest.getMainAttributes(); 377 if (mainAttributes != null) 378 sealed = mainAttributes.getValue(Name.SEALED); 379 } 380 if (Boolean.valueOf(sealed).booleanValue()) { 381 // this manifest attempts to seal when package defined previously unsealed; ERROR 382 throw new SecurityException("The package '" + packageName + "' was previously loaded unsealed. Cannot seal package."); //$NON-NLS-1$ //$NON-NLS-2$ 383 } 384 } 385 } 386 387 @Override findResource(final String name)388 public URL findResource(final String name) { 389 URL url = AccessController.doPrivileged(new PrivilegedAction<URL>() { 390 @Override 391 public URL run() { 392 synchronized (loaders) { 393 if (closed) 394 return null; 395 for (CloseableJarFileLoader loader : loaders) { 396 URL resourceURL = loader.getURL(name); 397 if (resourceURL != null) 398 return resourceURL; 399 } 400 } 401 return null; 402 } 403 }, context); 404 if (url != null) 405 return url; 406 return super.findResource(name); 407 } 408 409 @Override findResources(final String name)410 public Enumeration<URL> findResources(final String name) throws IOException { 411 final List<URL> resources = new ArrayList<>(); 412 AccessController.doPrivileged(new PrivilegedAction<Object>() { 413 @Override 414 public Object run() { 415 synchronized (loaders) { 416 if (closed) 417 return null; 418 for (CloseableJarFileLoader loader : loaders) { 419 URL resourceURL = loader.getURL(name); 420 if (resourceURL != null) 421 resources.add(resourceURL); 422 } 423 } 424 return null; 425 } 426 }, context); 427 Enumeration<URL> e = super.findResources(name); 428 while (e.hasMoreElements()) 429 resources.add(e.nextElement()); 430 431 return Collections.enumeration(resources); 432 } 433 434 /** 435 * The "close" method is called when the class loader is no longer needed and we should close any open resources. 436 * In particular this method will close the jar files associated with this class loader. 437 */ 438 @Override close()439 public void close() { 440 synchronized (loaders) { 441 if (closed) 442 return; 443 for (CloseableJarFileLoader loader : loaders) { 444 loader.close(); 445 } 446 closed = true; 447 } 448 } 449 450 @Override addURL(URL url)451 protected void addURL(URL url) { 452 synchronized (loaders) { 453 if (isFileJarURL(url)) { 454 if (closed) 455 throw new IllegalStateException("Cannot add url. CloseableURLClassLoader is closed."); //$NON-NLS-1$ 456 loaderURLs.add(url); 457 if (safeAddLoader(url)) 458 return; 459 } 460 } 461 super.addURL(url); 462 } 463 464 @Override getURLs()465 public URL[] getURLs() { 466 List<URL> result = new ArrayList<>(); 467 synchronized (loaders) { 468 result.addAll(loaderURLs); 469 } 470 result.addAll(Arrays.asList(super.getURLs())); 471 return result.toArray(new URL[result.size()]); 472 } 473 474 private final Map<String, Thread> classNameLocks = new HashMap<>(5); 475 private final Object pkgLock = new Object(); 476 lockClassName(String classname)477 private boolean lockClassName(String classname) { 478 synchronized (classNameLocks) { 479 Object lockingThread = classNameLocks.get(classname); 480 Thread current = Thread.currentThread(); 481 if (lockingThread == current) 482 return false; 483 boolean previousInterruption = Thread.interrupted(); 484 try { 485 while (true) { 486 if (lockingThread == null) { 487 classNameLocks.put(classname, current); 488 return true; 489 } 490 491 classNameLocks.wait(); 492 lockingThread = classNameLocks.get(classname); 493 } 494 } catch (InterruptedException e) { 495 current.interrupt(); 496 throw (LinkageError) new LinkageError(classname).initCause(e); 497 } finally { 498 if (previousInterruption) { 499 current.interrupt(); 500 } 501 } 502 } 503 } 504 unlockClassName(String classname)505 private void unlockClassName(String classname) { 506 synchronized (classNameLocks) { 507 classNameLocks.remove(classname); 508 classNameLocks.notifyAll(); 509 } 510 } 511 isRegisteredAsParallel()512 protected boolean isRegisteredAsParallel() { 513 return registeredAsParallel; 514 } 515 } 516