package org.hotswap.agent.plugin.tomcat; import org.hotswap.agent.annotation.Plugin; import org.hotswap.agent.config.PluginConfiguration; import org.hotswap.agent.config.PluginManager; import org.hotswap.agent.logging.AgentLogger; import org.hotswap.agent.util.PluginManagerInvoker; import org.hotswap.agent.util.ReflectionHelper; import org.hotswap.agent.util.classloader.WatchResourcesClassLoader; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.net.URISyntaxException; import java.net.URL; import java.util.HashMap; import java.util.Map; /** * Catalina servlet container support. * * <p/> * <p>Plugin</p><ul> * </ul> * * @author Jiri Bubnik */ @Plugin(name = "Tomcat", description = "Catalina based servlet containers.", testedVersions = {"7.0.50"}, expectedVersions = {"6x","7x", "8x"}, supportClass={WebappLoaderTransformer.class} ) public class TomcatPlugin { private static AgentLogger LOGGER = AgentLogger.getLogger(TomcatPlugin.class); private static final String TOMCAT_WEBAPP_CLASS_LOADER = "org.apache.catalina.loader.WebappClassLoader"; private static final String TOMCAT_PARALLEL_WEBAPP_CLASS_LOADER = "org.apache.catalina.loader.ParallelWebappClassLoader"; private static final String GLASSFISH_WEBAPP_CLASS_LOADER = "org.glassfish.web.loader.WebappClassLoader"; private static final String WEB_INF_CLASSES = "/WEB-INF/classes/"; // resolved tomcat version (6/7/8). int tomcatMajorVersion = 8; // tomcat associated resource object to a web application classloader static Map<Object, ClassLoader> registeredResourcesMap = new HashMap<Object, ClassLoader>(); /** * Init the plugin during WebappLoader.start lifecycle. This method is invoked before the plugin is initialized. * @param appClassLoader main tomcat classloader for the webapp (in creation process). Only standard WebappClassLoader is supported. * @param resource tomcat resource associated to the classloader. */ public static void init(ClassLoader appClassLoader, Object resource) { String version = resolveTomcatVersion(appClassLoader); int majorVersion = resolveTomcatMajorVersion(version); String classLoaderName = appClassLoader.getClass().getName(); if (classLoaderName.equals(TOMCAT_WEBAPP_CLASS_LOADER) || classLoaderName.equals(TOMCAT_PARALLEL_WEBAPP_CLASS_LOADER) || classLoaderName.equals(GLASSFISH_WEBAPP_CLASS_LOADER)) { registeredResourcesMap.put(resource, appClassLoader); // create plugin configuration in advance to get extraClasspath and watchResources properties PluginConfiguration pluginConfiguration = new PluginConfiguration(appClassLoader); WatchResourcesClassLoader watchResourcesClassLoader = new WatchResourcesClassLoader(false); URL[] extraClasspath = pluginConfiguration.getExtraClasspath(); if (extraClasspath.length > 0) { if (majorVersion > 7) watchResourcesClassLoader.initExtraPath(extraClasspath); else addRepositoriesAtStart(appClassLoader, extraClasspath, false); } URL[] watchResources = pluginConfiguration.getWatchResources(); if (watchResources.length > 0) { if (majorVersion > 7) watchResourcesClassLoader.initWatchResources(watchResources, PluginManager.getInstance().getWatcher()); else addRepositoriesAtStart(appClassLoader, watchResources, true); } // register special repo getExtraRepositories(appClassLoader).put(WEB_INF_CLASSES, watchResourcesClassLoader); URL[] webappDir = pluginConfiguration.getWebappDir(); if (webappDir.length > 0) { for (URL url : webappDir) { LOGGER.debug("Watching 'webappDir' for changes: {}", url); } WatchResourcesClassLoader webappDirClassLoader = new WatchResourcesClassLoader(false); webappDirClassLoader.initExtraPath(webappDir); getExtraRepositories(appClassLoader).put("/", webappDirClassLoader); } } Object plugin = PluginManagerInvoker.callInitializePlugin(TomcatPlugin.class, appClassLoader); if (plugin != null) { ReflectionHelper.invoke(plugin, plugin.getClass(), "init", new Class[]{String.class, ClassLoader.class}, version, appClassLoader); } else { LOGGER.debug("TomcatPlugin is disabled in {}", appClassLoader); } } /** * Init plugin and resolve major version. * * @param version tomcat version string * @param classLoader the class loader */ private void init(String version, ClassLoader appClassLoader ) { if (appClassLoader.getClass().getName().equals(GLASSFISH_WEBAPP_CLASS_LOADER)) { LOGGER.info("Tomcat plugin initialized - GlassFish embedded Tomcat version '{}'", version); } else { LOGGER.info("Tomcat plugin initialized - Tomcat version '{}'", version); } tomcatMajorVersion = resolveTomcatMajorVersion(version); } // for each app classloader map of tomcat repository name to associated watch resource classloader private static Map<ClassLoader, Map<String, ClassLoader>> extraRepositories = new HashMap<ClassLoader, Map<String, ClassLoader>>(); private static void addRepositoriesAtStart(ClassLoader appClassLoader, URL[] newRepositories, boolean watchResources) { String[] currentRepositories = (String[]) ReflectionHelper.get(appClassLoader, "repositories"); String[] repositories = new String[currentRepositories.length + newRepositories.length]; for (int i=0; i < newRepositories.length; i++) { repositories[i] = "extraClasspath:" + newRepositories[i].toString(); } for (int i = 0; i < currentRepositories.length; i++) { repositories[i+newRepositories.length] = currentRepositories[i]; } ReflectionHelper.set(appClassLoader, appClassLoader.getClass(), "repositories", repositories); File[] files = (File[]) ReflectionHelper.get(appClassLoader, "files"); File[] result2 = new File[files.length + newRepositories.length]; for (int i=0; i < newRepositories.length; i++) { try { WatchResourcesClassLoader watchResourcesClassLoader = new WatchResourcesClassLoader(); if (watchResources) { watchResourcesClassLoader.initWatchResources(new URL[]{newRepositories[i]}, PluginManager.getInstance().getWatcher()); } else { watchResourcesClassLoader.initExtraPath(new URL[]{newRepositories[i]}); } getExtraRepositories(appClassLoader).put(repositories[i], watchResourcesClassLoader); result2[i] = new File(newRepositories[i].toURI()); } catch (URISyntaxException e) { e.printStackTrace(); } } for (int i = 0; i < files.length; i++) { result2[i+newRepositories.length] = files[i]; } ReflectionHelper.set(appClassLoader, appClassLoader.getClass(), "files", result2); } private static Map<String, ClassLoader> getExtraRepositories(ClassLoader appClassLoader) { if (!extraRepositories.containsKey(appClassLoader)) { extraRepositories.put(appClassLoader, new HashMap<String, ClassLoader>()); } return extraRepositories.get(appClassLoader); } public static InputStream getExtraResource(Object resource, String name) { URL url = getExtraResource0(resource, name); if (url != null) { try { return url.openStream(); } catch (IOException e) { LOGGER.error("Unable to open stream from URL {}", e, url); } } return null; } public static File getExtraResourceFile(Object resource, String name) { ClassLoader appClassLoader = registeredResourcesMap.get(resource); if (appClassLoader == null) return null; Map<String, ClassLoader> classLoaderExtraRepositories = getExtraRepositories(appClassLoader); if (name.startsWith(WEB_INF_CLASSES) && classLoaderExtraRepositories.containsKey(WEB_INF_CLASSES)) { // strip of leading /WEB-INF/classes/ and search for the resources String resourceName = name.substring(WEB_INF_CLASSES.length()); URL url = classLoaderExtraRepositories.get(WEB_INF_CLASSES).getResource(resourceName); if (url != null) { try { return new File(url.toURI()); } catch (Exception e) { LOGGER.error("Unable to open stream from URL {}", e, url); } } } else if (classLoaderExtraRepositories.containsKey("/")) { URL url = classLoaderExtraRepositories.get("/").getResource(name.substring(1)); // strip off leading "/" if (url != null) { try { return new File(url.toURI()); } catch (Exception e) { LOGGER.error("Unable to open stream from URL {}", e, url); } } } return null; } public static long getExtraResourceLength(Object resource, String name) { URL url = getExtraResource0(resource, name); if (url != null) { try { return new File(url.toURI()).length(); } catch (Exception e) { LOGGER.error("Unable to open file at URL {}", e, url); } } return 0; } private static URL getExtraResource0(Object resource, String name) { ClassLoader appClassLoader = registeredResourcesMap.get(resource); if (appClassLoader == null) return null; for (Map.Entry<String, ClassLoader> repo : getExtraRepositories(appClassLoader).entrySet()) { if (name.startsWith(repo.getKey())) { String resourceName = name.substring(repo.getKey().length()); // return from associated classloader return repo.getValue().getResource(resourceName); } } return null; } /** * Resolve the server version from ServerInfo class. * @param appClassLoader application classloader * @return the server info String */ private static String resolveTomcatVersion(ClassLoader appClassLoader) { try { Class serverInfo = appClassLoader.loadClass("org.apache.catalina.util.ServerInfo"); if (appClassLoader.getClass().getName().equals(GLASSFISH_WEBAPP_CLASS_LOADER)) { return (String) ReflectionHelper.invoke(null, serverInfo, "getPublicServerInfo", new Class[]{}); } return (String) ReflectionHelper.invoke(null, serverInfo, "getServerNumber", new Class[]{}); } catch (Exception e) { LOGGER.debug("Unable to resolve server version", e); return "unknown"; } } /** * Try to resolve version string from the version. */ private static int resolveTomcatMajorVersion(String version) { try { return Integer.valueOf(version.substring(0, 1)); } catch (Exception e) { LOGGER.debug("Unable to resolve server main version from version string {}", e, version); // assume latest known version return 8; } } }