package org.hotswap.agent.util.classloader; import org.hotswap.agent.logging.AgentLogger; import org.hotswap.agent.watch.WatchFileEvent; import org.hotswap.agent.watch.WatchEventListener; import org.hotswap.agent.watch.Watcher; import java.io.IOException; import java.io.InputStream; import java.lang.reflect.Method; import java.net.*; import java.util.*; /** * Special URL classloader to get only changed resources from URL. * * Use this classloader to support watchResources property. * * This classloader checks if the resource was modified after application startup and in that case * delegates getResource()/getResources() to custom URL classloader. Otherwise returns null or resource * from paren classloader (depending on searchParent property). * * @author Jiri Bubnik */ public class WatchResourcesClassLoader extends URLClassLoader { private static AgentLogger LOGGER = AgentLogger.getLogger(WatchResourcesClassLoader.class); /** * URLs of changed resources. Use this set to check if the resource was changed and hence should * be returned by this classloader. */ Set<URL> changedUrls = new HashSet<URL>(); /** * Watch for requested resource in parent classloader in case it is not found by this classloader? * Note that there is child first precedence anyway. */ boolean searchParent = true; public void setSearchParent(boolean searchParent) { this.searchParent = searchParent; } /** * URL classloader configured to get resources only from exact set of URL's (no parent delegation) */ ClassLoader watchResourcesClassLoader; public WatchResourcesClassLoader() { this(false); } public WatchResourcesClassLoader(boolean searchParent) { super(new URL[]{}, searchParent ? WatchResourcesClassLoader.class.getClassLoader() : null); this.searchParent = searchParent; } public WatchResourcesClassLoader(ClassLoader classLoader) { super(new URL[] {}, classLoader); this.searchParent = false; } /** * Configure new instance with urls and watcher service. * * @param extraPath the URLs from which to load resources */ public void initExtraPath(URL[] extraPath) { for (URL url : extraPath) addURL(url); } /** * Configure new instance with urls and watcher service. * * @param watchResources the URLs from which to load resources * @param watcher watcher service to register watch events */ public void initWatchResources(URL[] watchResources, Watcher watcher) { // create classloader to serve resources only from watchResources URL's this.watchResourcesClassLoader = new UrlOnlyClassLoader(watchResources); // register watch resources - on change event each modified resource will be added to changedUrls. for (URL resource : watchResources) { try { URI uri = resource.toURI(); LOGGER.debug("Watching directory '{}' for changes.", uri); watcher.addEventListener(this, uri, new WatchEventListener() { @Override public void onEvent(WatchFileEvent event) { try { if (event.isFile() || event.isDirectory()) { changedUrls.add(event.getURI().toURL()); LOGGER.trace("File '{}' changed and will be returned instead of original classloader equivalent.", event.getURI().toURL()); } } catch (MalformedURLException e) { LOGGER.error("Unexpected - cannot convert URI {} to URL.", e, event.getURI()); } } }); } catch (URISyntaxException e) { LOGGER.warning("Unable to convert watchResources URL '{}' to URI. URL is skipped.", e, resource); } } } /** * Check if the resource was changed after this classloader instantiaton. * * @param url full URL of the file * @return true if was changed after instantiation */ public boolean isResourceChanged(URL url) { return changedUrls.contains(url); } /** * Returns URL only if the resource is found in changedURL and was actually changed after * instantiation of this classloader. */ @Override public URL getResource(String name) { if (watchResourcesClassLoader != null) { URL resource = watchResourcesClassLoader.getResource(name); if (resource != null && isResourceChanged(resource)) { LOGGER.trace("watchResources - using changed resource {}", name); return resource; } } // child first (extra classpath) URL resource = findResource(name); if (resource != null) return resource; // without parent do not call super (ignore even bootstrapResources) if (searchParent) return super.getResource(name); else return null; } @Override public InputStream getResourceAsStream(String name) { URL url = getResource(name); try { return url != null ? url.openStream() : null; } catch (IOException e) { } return null; } /** * Returns only a single instance of the changed resource. * There are conflicting requirements for other resources inclusion. This class * should "hide" the original resource, hence it should not be included in the resoult. * On the other hand, there may be resource with the same name in other JAR which * should be included and now is hidden (for example multiple persistence.xml). * Maybe a new property to influence this behaviour? */ @Override public Enumeration<URL> getResources(String name) throws IOException { if (watchResourcesClassLoader != null) { URL resource = watchResourcesClassLoader.getResource(name); if (resource != null && isResourceChanged(resource)) { LOGGER.trace("watchResources - using changed resource {}", name); Vector<URL> res = new Vector<URL>(); res.add(resource); return res.elements(); } } // if extraClasspath contains at least one element, return only extraClasspath if (findResources(name).hasMoreElements()) return findResources(name); return super.getResources(name); } /** * Support for classpath builder on Tomcat. */ public String getClasspath() { ClassLoader parent = getParent(); if (parent == null) return null; try { Method m = parent.getClass().getMethod("getClasspath", new Class[] {}); if( m==null ) return null; Object o = m.invoke( parent, new Object[] {} ); if( o instanceof String ) return (String)o; return null; } catch( Exception ex ) { LOGGER.debug("getClasspath not supported on parent classloader."); } return null; } /** * Helper classloader to get resources from list of urls only. */ public static class UrlOnlyClassLoader extends URLClassLoader { public UrlOnlyClassLoader(URL[] urls) { super(urls); } // do not use parent resource (may introduce infinite loop) @Override public URL getResource(String name) { return findResource(name); } }; }