package org.hotswap.agent.config; import java.io.File; import java.io.IOException; import java.net.MalformedURLException; import java.net.URL; import java.net.URLClassLoader; import java.util.ArrayList; import java.util.Arrays; import java.util.Enumeration; import java.util.List; import java.util.Properties; import java.util.StringTokenizer; import java.util.regex.Pattern; import org.hotswap.agent.HotswapAgent; import org.hotswap.agent.annotation.Plugin; import org.hotswap.agent.logging.AgentLogger; import org.hotswap.agent.util.HotswapProperties; import org.hotswap.agent.util.classloader.HotswapAgentClassLoaderExt; import org.hotswap.agent.util.classloader.URLClassLoaderHelper; /** * Plugin configuration. * <p/> * Single instance exists for each classloader. * * @author Jiri Bubnik */ public class PluginConfiguration { private static AgentLogger LOGGER = AgentLogger.getLogger(PluginConfiguration.class); private static final String PLUGIN_CONFIGURATION = "hotswap-agent.properties"; /** The Constant EXCLUDED_CLASS_LOADERS_KEY. */ private static final String EXCLUDED_CLASS_LOADERS_KEY = "excludedClassLoaderPatterns"; Properties properties = new HotswapProperties(); // if the property is not defined in this classloader, look for parent classloader and it's configuration PluginConfiguration parent; // this configuration adheres to this classloader ClassLoader classLoader; // the hotswap-agent.properties file (or null if not defined for this classloader) URL configurationURL; // is property file defined directly in this classloader? boolean containsPropertyFileDirectly = false; public PluginConfiguration(ClassLoader classLoader) { this(null, classLoader); } public PluginConfiguration(PluginConfiguration parent, ClassLoader classLoader) { this.parent = parent; this.classLoader = classLoader; loadConfigurationFile(); init(); } private void loadConfigurationFile() { try { String externalPropertiesFile = HotswapAgent.getExternalPropertiesFile(); if (externalPropertiesFile != null) { configurationURL = resourceNameToURL(externalPropertiesFile); properties.load(configurationURL.openStream()); return; } } catch (Exception e) { LOGGER.error("Error while loading external properties file " + configurationURL, e); } if (parent == null) { configurationURL = classLoader == null ? ClassLoader.getSystemResource(PLUGIN_CONFIGURATION) : classLoader.getResource(PLUGIN_CONFIGURATION); try { if (configurationURL != null) { containsPropertyFileDirectly = true; properties.load(configurationURL.openStream()); } } catch (Exception e) { LOGGER.error("Error while loading 'hotswap-agent.properties' from base URL " + configurationURL, e); } } else { // search for resources not known by parent classloader (defined in THIS classloader exclusively) // this is necessary in case of parent classloader precedence try { Enumeration<URL> urls = null; if (classLoader != null) { urls = classLoader.getResources(PLUGIN_CONFIGURATION); } if (urls == null) { urls = ClassLoader.getSystemResources(PLUGIN_CONFIGURATION); } while (urls.hasMoreElements()) { URL url = urls.nextElement(); boolean found = false; ClassLoader parentClassLoader = parent.getClassLoader(); Enumeration<URL> parentUrls = parentClassLoader == null ? ClassLoader.getSystemResources(PLUGIN_CONFIGURATION) : parentClassLoader.getResources(PLUGIN_CONFIGURATION); while (parentUrls.hasMoreElements()) { if (url.equals(parentUrls.nextElement())) found = true; } if (!found) { configurationURL = url; break; } } } catch (IOException e) { LOGGER.error("Error while loading 'hotswap-agent.properties' from URL " + configurationURL, e); } if (configurationURL == null) { configurationURL = parent.configurationURL; LOGGER.debug("Classloader does not contain 'hotswap-agent.properties', using parent file '{}'" , parent.configurationURL); } else { LOGGER.debug("Classloader contains 'hotswap-agent.properties' at location '{}'", configurationURL); containsPropertyFileDirectly = true; } try { if (configurationURL != null) properties.load(configurationURL.openStream()); } catch (Exception e) { LOGGER.error("Error while loading 'hotswap-agent.properties' from URL " + configurationURL, e); } } } /** * Initialize the configuration. */ protected void init() { LogConfigurationHelper.configureLog(properties); initPluginPackage(); initExtraClassPath(); initExcludedClassLoaderPatterns(); } private void initPluginPackage() { // only for self property (not parent) if (properties.containsKey("pluginPackages")) { String pluginPackages = properties.getProperty("pluginPackages"); for (String pluginPackage : pluginPackages.split(",")) { PluginManager.getInstance().getPluginRegistry().scanPlugins(getClassLoader(), pluginPackage); } } } private void initExtraClassPath() { URL[] extraClassPath = getExtraClasspath(); if (extraClassPath.length > 0) { if (classLoader instanceof URLClassLoader) { URLClassLoaderHelper.prependClassPath((URLClassLoader) classLoader, extraClassPath); } else if (classLoader instanceof HotswapAgentClassLoaderExt) { ((HotswapAgentClassLoaderExt) classLoader).setExtraClassPath(extraClassPath); } else { LOGGER.debug("Unable to set extraClasspath to {} on classLoader {}. " + "Only URLClassLoader is supported.\n" + "*** extraClasspath configuration property will not be handled on JVM level ***", Arrays.toString(extraClassPath), classLoader); } } } private void initExcludedClassLoaderPatterns() { if (properties != null && properties.containsKey(EXCLUDED_CLASS_LOADERS_KEY)) { List<Pattern> excludedClassLoaderPatterns = new ArrayList<Pattern>(); for (String pattern : properties.getProperty(EXCLUDED_CLASS_LOADERS_KEY).split(",")) { excludedClassLoaderPatterns.add(Pattern.compile(pattern)); } PluginManager.getInstance().getHotswapTransformer() .setExcludedClassLoaderPatterns(excludedClassLoaderPatterns); } } /** * Get configuration property value * * @param property property name * @return the property value or null if not defined */ public String getProperty(String property) { if (properties.containsKey(property)) return properties.getProperty(property); else if (parent != null) return parent.getProperty(property); else return null; } /** * Get configuration property value * * @param property property name * @param defaultValue value to return if property not defined * @return the property value or null if not defined */ public String getProperty(String property, String defaultValue) { String value = getProperty(property); return value != null ? value : defaultValue; } /** * Convenience method to get property as a boolean value using Boolean.valueOf(). * * @param property property name * @return the property value or null if not defined */ public boolean getPropertyBoolean(String property) { if (properties.containsKey(property)) return Boolean.valueOf(properties.getProperty(property)); else if (parent != null) return parent.getPropertyBoolean(property); else return false; } /** * Get extraClasspath property as URL[]. * * @return extraClasspath or empty array (never null) */ public URL[] getExtraClasspath() { return convertToURL(getProperty("extraClasspath")); } /** * Converts watchResources property to URL array. Invalid URLs will be skipped and logged as error. */ public URL[] getWatchResources() { return convertToURL(getProperty("watchResources")); } /** * Return configuration property webappDir as URL. */ public URL[] getWebappDir() { return convertToURL(getProperty("webappDir")); } /** * List of disabled plugin names */ public List<String> getDisabledPlugins() { List<String> ret = new ArrayList<String>(); for (String disabledPlugin : getProperty("disabledPlugins", "").split(",")) { ret.add(disabledPlugin.trim()); } return ret; } /** * Check if the plugin is disabled (in this classloader) */ public boolean isDisabledPlugin(String pluginName) { return HotswapAgent.isPluginDisabled(pluginName) || getDisabledPlugins().contains(pluginName); } /** * Check if the plugin is disabled (in this classloader) */ public boolean isDisabledPlugin(Class<?> pluginClass) { Plugin pluginAnnotation = pluginClass.getAnnotation(Plugin.class); return isDisabledPlugin(pluginAnnotation.name()); } private URL[] convertToURL(String resources) { List<URL> ret = new ArrayList<URL>(); if (resources != null) { StringTokenizer tokenizer = new StringTokenizer(resources, ",;"); while (tokenizer.hasMoreTokens()) { String name = tokenizer.nextToken().trim(); try { ret.add(resourceNameToURL(name)); } catch (Exception e) { LOGGER.error("Invalid configuration value: '{}' is not a valid URL or path and will be skipped.", name, e); } } } return ret.toArray(new URL[ret.size()]); } private static URL resourceNameToURL(String resource) throws Exception { try { // Try to format as a URL? return new URL(resource); } catch (MalformedURLException e) { // try to locate a file if (resource.startsWith("./")) resource = resource.substring(2); File file = new File(resource).getCanonicalFile(); return file.toURI().toURL(); } } /** * Returns classloader associated with this configuration (i.e. it was initiated from). * * @return the classloader */ public ClassLoader getClassLoader() { return classLoader; } /** * Does this classloader contain the property file directly, or is it acquired through parent classloader. * * @return if this contains directly the property file */ public boolean containsPropertyFile() { return containsPropertyFileDirectly; } }