package org.async.rmi.server; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.lang.ref.ReferenceQueue; import java.lang.ref.WeakReference; import java.net.MalformedURLException; import java.net.URL; import java.net.URLClassLoader; import java.security.CodeSource; import java.security.PermissionCollection; import java.util.*; import java.util.concurrent.ConcurrentHashMap; import java.util.stream.Collectors; import java.util.stream.Stream; /** * Created by Barak Bar Orion * 11/4/14. */ public class LoaderHandler { @SuppressWarnings("UnusedDeclaration") private static final Logger logger = LoggerFactory.getLogger(LoaderHandler.class); private static String codebaseProperty = null; static { loadCodeBaseProperty(); } private static final HashMap<LoaderKey, LoaderEntry> loaderTable = new HashMap<>(); /** * reference queue for cleared class loader entries */ private static final ReferenceQueue<Loader> refQueue = new ReferenceQueue<>(); public static void loadCodeBaseProperty() { String prop = System.getProperty("java.rmi.server.codebase", null); if (prop != null && prop.trim().length() > 0) { // normalize the string to allow better caching. codebaseProperty = Stream.of(prop.trim().split("\\s+")).map(String::trim).collect(Collectors.joining(" ")); } else { codebaseProperty = null; } } private static URL[] codebaseURLs = null; private static final ConcurrentHashMap<String, URL[]> pathToURLsCache = new ConcurrentHashMap<>(); /** * table of class loaders that use codebase property for annotation */ private static final Map<ClassLoader, Void> codebaseLoaders = Collections.synchronizedMap(new IdentityHashMap<>()); static { for (ClassLoader codebaseLoader = ClassLoader.getSystemClassLoader(); codebaseLoader != null; codebaseLoader = codebaseLoader.getParent()) { codebaseLoaders.put(codebaseLoader, null); } } private LoaderHandler() { } public static String getClassAnnotation(Class<?> cl) { String name = cl.getName(); int nameLength = name.length(); if (nameLength > 0 && name.charAt(0) == '[') { int i = 1; while (nameLength > i && name.charAt(i) == '[') { i++; } if (nameLength > i && name.charAt(i) != 'L') { return null; } } ClassLoader loader = cl.getClassLoader(); if (loader == null || codebaseLoaders.containsKey(loader)) { return codebaseProperty; } String annotation = null; if (loader instanceof Loader) { annotation = ((Loader) loader).getClassAnnotation(); } else if (loader instanceof URLClassLoader) { URL[] urls = ((URLClassLoader) loader).getURLs(); if (urls != null) { annotation = urlsToPath(urls); } } if (annotation != null) { return annotation; } else { return codebaseProperty; } } private static synchronized URL[] getDefaultCodebaseURLs() throws MalformedURLException { if (codebaseURLs == null) { if (codebaseProperty != null) { codebaseURLs = pathToURLs(codebaseProperty); } else { codebaseURLs = new URL[0]; } } return codebaseURLs; } private static URL[] pathToURLs(String path) throws MalformedURLException { URL[] urls = pathToURLsCache.get(path); if (urls == null) { urls = Stream.of(path.split("\\s+")).map(LoaderHandler::toUrl).filter(url -> url != null).toArray(URL[]::new); pathToURLsCache.putIfAbsent(path, urls); } return urls; } private static String urlsToPath(URL[] urls) { if (urls == null || urls.length == 0) { return null; } return Stream.of(urls).map(URL::toExternalForm).collect(Collectors.joining(" ")); } private static URL toUrl(String token) { try { return new URL(token); } catch (Exception ignored) { return null; } } /** * Load a class from a network location (one or more URLs), * but first try to resolve the named class through the given * "default loader". */ public static Class loadClass(String codebase, String name, ClassLoader defaultLoader) throws MalformedURLException, ClassNotFoundException { URL[] urls; if (codebase != null) { urls = pathToURLs(codebase); } else { urls = getDefaultCodebaseURLs(); } if (defaultLoader != null) { try { Class c = Class.forName(name, false, defaultLoader); logger.debug("class {} found via defaultLoader, defined by {}", name, c.getClassLoader()); return c; } catch (ClassNotFoundException ignored) { } } return loadClass(urls, name); } /** * Load a class from the RMI class loader corresponding to the given * codebase URL path in the current execution context. */ private static Class loadClass(URL[] urls, String name) throws ClassNotFoundException { ClassLoader parent = Thread.currentThread().getContextClassLoader(); /* * Get or create the RMI class loader for this codebase URL path * and parent class loader pair. */ Loader loader = lookupLoader(urls, parent); try { Class c = Class.forName(name, false, loader); logger.debug("class {} found via codebase {} , defined by {}", name, Arrays.toString(urls), c.getClassLoader()); return c; } catch (ClassNotFoundException e) { logger.debug("class {} not found via codebase", name); throw e; } } /** * Look up the RMI class loader for the given codebase URL path * and the given parent class loader. A new class loader instance * will be created and returned if no match is found. */ private static Loader lookupLoader(final URL[] urls, final ClassLoader parent) { /* * If the requested codebase URL path is empty, the supplied * parent class loader will be sufficient. */ LoaderEntry entry; Loader loader; synchronized (LoaderHandler.class) { /* * Take this opportunity to remove from the table entries * whose weak references have been cleared. */ while ((entry = (LoaderEntry) refQueue.poll()) != null) { if (!entry.removed) { // ignore entries removed below loaderTable.remove(entry.key); } } /* * Look up the codebase URL path and parent class loader pair * in the table of RMI class loaders. */ LoaderKey key = new LoaderKey(urls, parent); entry = loaderTable.get(key); if (entry == null || (loader = entry.get()) == null) { /* * If entry was in table but it's weak reference was cleared, * remove it from the table and mark it as explicitly cleared, * so that new matching entry that we put in the table will * not be erroneously removed when this entry is processed * from the weak reference queue. */ if (entry != null) { loaderTable.remove(key); entry.removed = true; } /* * A matching loader was not found, so create a new class * loader instance for the requested codebase URL path and * parent class loader. The instance is created within an */ loader = new Loader(urls, parent); /* * Finally, create an entry to hold the new loader with a * weak reference and store it in the table with the key. */ entry = new LoaderEntry(key, loader); loaderTable.put(key, entry); } } return loader; } /** * LoaderKey holds a codebase URL path and parent class loader pair * used to look up RMI class loader instances in its class loader cache. */ private static class LoaderKey { private URL[] urls; private ClassLoader parent; private int hashValue; public LoaderKey(URL[] urls, ClassLoader parent) { this.urls = urls; this.parent = parent; if (parent != null) { hashValue = parent.hashCode(); } for (URL url : urls) { hashValue ^= url.hashCode(); } } public int hashCode() { return hashValue; } public boolean equals(Object obj) { if (obj instanceof LoaderKey) { LoaderKey other = (LoaderKey) obj; if (parent != other.parent) { return false; } if (urls == other.urls) { return true; } if (urls.length != other.urls.length) { return false; } for (int i = 0; i < urls.length; i++) { if (!urls[i].equals(other.urls[i])) { return false; } } return true; } else { return false; } } } /** * LoaderEntry contains a weak reference to an RMIClassLoader. The * weak reference is registered with the private static "refQueue" * queue. The entry contains the codebase URL path and parent class * loader key for the loader so that the mapping can be removed from * the table efficiently when the weak reference is cleared. */ private static class LoaderEntry extends WeakReference<Loader> { public LoaderKey key; /** * set to true if the entry has been removed from the table * because it has been replaced, so it should not be attempted * to be removed again */ public boolean removed = false; public LoaderEntry(LoaderKey key, Loader loader) { super(loader, refQueue); this.key = key; } } /** * Loader is the actual class of the RMI class loaders created * by the RMIClassLoader static methods. */ private static class Loader extends URLClassLoader { private String annotation; private Loader(URL[] urls, ClassLoader parent) { super(urls, parent); /* * Caching the value of class annotation string here assumes * that the protected method addURL() is never called on this * class loader. */ annotation = urlsToPath(urls); } /** * Return the string to be annotated with all classes loaded from * this class loader. */ public String getClassAnnotation() { return annotation; } @SuppressWarnings("ConstantConditions") protected PermissionCollection getPermissions(CodeSource codesource) { return null; } @Override public Class<?> loadClass(String name) throws ClassNotFoundException { return super.loadClass(name); } @Override protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { return super.loadClass(name, resolve); } /** * Return a string representation of this loader (useful for * debugging). */ public String toString() { return super.toString() + "[\"" + annotation + "\"]"; } } }