/** * $RCSfile: ,v $ * $Revision: $ * $Date: $ * * Copyright (C) 2004-2011 Jive Software. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.jivesoftware.spark; import java.awt.EventQueue; import java.io.File; import java.io.FileOutputStream; import java.io.FilenameFilter; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.net.MalformedURLException; import java.net.URL; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Enumeration; import java.util.List; import java.util.Locale; import java.util.PropertyResourceBundle; import java.util.ResourceBundle; import java.util.StringTokenizer; import java.util.concurrent.CopyOnWriteArrayList; import java.util.jar.JarEntry; import java.util.jar.JarFile; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; import javax.swing.JPanel; import org.dom4j.Document; import org.dom4j.DocumentException; import org.dom4j.Element; import org.dom4j.Node; import org.dom4j.io.SAXReader; import org.jivesoftware.MainWindowListener; import org.jivesoftware.Spark; import org.jivesoftware.resource.Default; import org.jivesoftware.spark.PluginRes.ResourceType; import org.jivesoftware.spark.component.tabbedPane.SparkTabbedPane; import org.jivesoftware.spark.plugin.Plugin; import org.jivesoftware.spark.plugin.PluginClassLoader; import org.jivesoftware.spark.plugin.PluginDependency; import org.jivesoftware.spark.plugin.PublicPlugin; import org.jivesoftware.spark.util.StringUtils; import org.jivesoftware.spark.util.URLFileSystem; import org.jivesoftware.spark.util.log.Log; import org.jivesoftware.sparkimpl.settings.JiveInfo; import org.jivesoftware.sparkimpl.settings.local.SettingsManager; /** * This manager is responsible for the loading of all Plugins and Workspaces within Spark environment. * * @author Derek DeMoro */ public class PluginManager implements MainWindowListener { private final List<Plugin> plugins = new ArrayList<Plugin>(); private final List<PublicPlugin> publicPlugins = new CopyOnWriteArrayList<PublicPlugin>(); private static PluginManager singleton; private static final Object LOCK = new Object(); /** * The root Plugins Directory. */ public static File PLUGINS_DIRECTORY = new File(Spark.getBinDirectory().getParent(), "plugins").getAbsoluteFile(); private Plugin pluginClass; private PluginClassLoader classLoader; private Collection<String> _blacklistPlugins; /** * Returns the singleton instance of <CODE>PluginManager</CODE>, * creating it if necessary. * <p/> * * @return the singleton instance of <Code>PluginManager</CODE> */ public static PluginManager getInstance() { // Synchronize on LOCK to ensure that we don't end up creating // two singletons. synchronized (LOCK) { if (null == singleton) { PluginManager controller = new PluginManager(); singleton = controller; return controller; } } return singleton; } private PluginManager() { try { PLUGINS_DIRECTORY = new File(Spark.getBinDirectory().getParentFile(), "plugins").getCanonicalFile(); } catch (IOException e) { Log.error(e); } // Do not use deployable plugins if not installed. if (System.getProperty("plugin") == null) { movePlugins(); } // Create the extension directory if one does not exist. if (!PLUGINS_DIRECTORY.exists()) { PLUGINS_DIRECTORY.mkdirs(); } _blacklistPlugins = Default.getPluginBlacklist(); } private void movePlugins() { // Current Plugin directory File newPlugins = new File(Spark.getLogDirectory().getParentFile(), "plugins").getAbsoluteFile(); newPlugins.mkdirs(); deleteOldPlugins(newPlugins); File[] files = PLUGINS_DIRECTORY.listFiles(); if (files != null) { final int no = files.length; for (int i = 0; i < no; i++) { File file = files[i]; if (file.isFile()) { // Copy over File newFile = new File(newPlugins, file.getName()); if (newFile.lastModified() >= file.lastModified()) { continue; } try { URLFileSystem.copy(file.toURI().toURL(), newFile); } catch (IOException e) { Log.error(e); } } } } PLUGINS_DIRECTORY = newPlugins; } /** * Deletes Plugins in pathtosearch that have a different md5-hash than * its correspondant in install\spark\plugins\ * @param pathtosearch */ public void deleteOldPlugins(File pathtosearch) { String installPath = Spark.getBinDirectory().getParentFile() + File.separator + "plugins" + File.separator; List<File> installerFiles = Arrays.asList(new File(installPath) .listFiles()); File[] oldFiles = pathtosearch.listFiles(); if (oldFiles != null) { for (File file : oldFiles) { if (file.isDirectory()) { File jarFile = new File(pathtosearch, file.getName() + ".jar"); if (!jarFile.exists()) { uninstall(file); } else { try { File f = new File(installPath + jarFile.getName()); if (installerFiles.contains(f)) { String oldfile = StringUtils.getMD5Checksum(jarFile.getAbsolutePath()); String newfile = StringUtils.getMD5Checksum(f.getAbsolutePath()); Log.debug(f.getAbsolutePath() + " " + jarFile.getAbsolutePath()); Log.debug(newfile + " " + oldfile + " equal:" + oldfile.equals(newfile)); if (!oldfile.equals(newfile)) { Log.debug("deleting: "+ file.getAbsolutePath() + "," + jarFile.getAbsolutePath()); uninstall(file); jarFile.delete(); } } } catch (Exception e) { Log.error("No such file", e); } } } } } } /** * Loads all {@link Plugin} from the agent plugins.xml and extension lib. */ public void loadPlugins() { // Delete all old plugins File[] oldFiles = PLUGINS_DIRECTORY.listFiles(); if (oldFiles != null) { for (File file : oldFiles) { if (file.isDirectory()) { // Check to see if it has an associated .jar File jarFile = new File(PLUGINS_DIRECTORY, file.getName() + ".jar"); if (!jarFile.exists()) { uninstall(file); } } } } updateClasspath(); // At the moment, the plug list is hardcode internally until I begin // using external property files. All depends on deployment. final URL url = getClass().getClassLoader().getResource("META-INF/plugins.xml"); try { InputStreamReader reader = new InputStreamReader(url.openStream()); loadInternalPlugins(reader); } catch (IOException e) { Log.error("Could not load plugins.xml file."); } // Load extension plugins loadPublicPlugins(); // For development purposes, load the plugin specified by -Dplugin=... String plugin = System.getProperty("plugin"); if (plugin != null) { final StringTokenizer st = new StringTokenizer(plugin, ",", false); while (st.hasMoreTokens()) { String token = st.nextToken(); File pluginXML = new File(token); loadPublicPlugin(pluginXML.getParentFile()); } } loadPluginResources(); } private boolean hasDependencies(File pluginFile) { SAXReader saxReader = new SAXReader(); Document pluginXML = null; try { pluginXML = saxReader.read(pluginFile); List<? extends Node> dependencies = pluginXML.selectNodes("plugin/depends/plugin"); return dependencies != null && dependencies.size() > 0 ? true : false; } catch (DocumentException e) { Log.error(e); return false; } } /** * Loads public plugins. * * @param pluginDir the directory of the expanded public plugin. * @return the new Plugin model for the Public Plugin. */ private Plugin loadPublicPlugin(File pluginDir) { File pluginFile = new File(pluginDir, "plugin.xml"); SAXReader saxReader = new SAXReader(); Document pluginXML = null; try { pluginXML = saxReader.read(pluginFile); } catch (DocumentException e) { Log.error(e); } Plugin pluginClass = null; List<? extends Node> plugins = pluginXML.selectNodes("/plugin"); for (Node plugin1 : plugins) { PublicPlugin publicPlugin = new PublicPlugin(); String clazz = null; String name; String minVersion; try { name = plugin1.selectSingleNode("name").getText(); clazz = plugin1.selectSingleNode("class").getText(); try { String lower = name.replaceAll("[^0-9a-zA-Z]","").toLowerCase(); // Dont load the plugin if its on the Blacklist if(_blacklistPlugins.contains(lower) || _blacklistPlugins.contains(clazz) || SettingsManager.getLocalPreferences().getDeactivatedPlugins().contains(name)) { return null; } } catch (Exception e) { // Whatever^^ return null; } // Check for minimum Spark version try { minVersion = plugin1.selectSingleNode("minSparkVersion").getText(); String buildNumber = JiveInfo.getVersion(); boolean ok = buildNumber.compareTo(minVersion) >= 0; if (!ok) { return null; } } catch (Exception e) { Log.error("Unable to load plugin " + name + " due to missing <minSparkVersion>-Tag in plugin.xml."); return null; } // Check for minimum Java version try { String javaversion = plugin1.selectSingleNode("java").getText().replaceAll("[^0-9]", ""); javaversion = javaversion == null? "0" : javaversion; int jv = Integer.parseInt(attachMissingZero(javaversion)); String myversion = System.getProperty("java.version").replaceAll("[^0-9]", ""); int mv = Integer.parseInt(attachMissingZero(myversion)); boolean ok = (mv >= jv); if (!ok) { Log.error("Unable to load plugin " + name + " due to old JavaVersion.\nIt Requires "+plugin1.selectSingleNode("java").getText()+ " you have "+ System.getProperty("java.version")); return null; } } catch (NullPointerException e) { Log.warning("Plugin "+name+" has no <java>-Tag, consider getting a newer Version"); } // set dependencies try { List<? extends Node> dependencies = plugin1.selectNodes("depends/plugin"); for (Node depend1 : dependencies) { Element depend = (Element) depend1; PluginDependency dependency = new PluginDependency(); dependency.setVersion(depend.selectSingleNode("version").getText()); dependency.setName(depend.selectSingleNode("name").getText()); publicPlugin.addDependency(dependency); } } catch (Exception e) { e.printStackTrace(); } // Do operating system check. boolean operatingSystemOK = isOperatingSystemOK(plugin1); if (!operatingSystemOK) { return null; } publicPlugin.setPluginClass(clazz); publicPlugin.setName(name); try { String version = plugin1.selectSingleNode("version").getText(); publicPlugin.setVersion(version); String author = plugin1.selectSingleNode("author").getText(); publicPlugin.setAuthor(author); String email = plugin1.selectSingleNode("email").getText(); publicPlugin.setEmail(email); String description = plugin1.selectSingleNode("description").getText(); publicPlugin.setDescription(description); String homePage = plugin1.selectSingleNode("homePage").getText(); publicPlugin.setHomePage(homePage); } catch (Exception e) { Log.debug("We can ignore these."); } try { pluginClass = (Plugin) getParentClassLoader().loadClass(clazz).newInstance(); Log.debug(name + " has been loaded."); publicPlugin.setPluginDir(pluginDir); publicPlugins.add(publicPlugin); registerPlugin(pluginClass); } catch (Throwable e) { Log.error("Unable to load plugin " + clazz + ".", e); } } catch (Exception ex) { Log.error("Unable to load plugin " + clazz + ".", ex); } } return pluginClass; } private String attachMissingZero(String value) { while(value.length()<5) { value = value+"0"; } return value; } /** * Loads an internal plugin. * * @param reader the inputstreamreader for an internal plugin. */ private void loadInternalPlugins(InputStreamReader reader) { SAXReader saxReader = new SAXReader(); Document pluginXML = null; try { pluginXML = saxReader.read(reader); } catch (DocumentException e) { Log.error(e); } List<? extends Node> plugins = pluginXML.selectNodes("/plugins/plugin"); for (final Object plugin1 : plugins) { EventQueue.invokeLater(new Runnable() { public void run() { String clazz = null; String name; try { Element plugin = (Element) plugin1; name = plugin.selectSingleNode("name").getText(); clazz = plugin.selectSingleNode("class").getText(); Plugin pluginClass = (Plugin) Class.forName(clazz).newInstance(); Log.debug(name + " has been loaded. Internal plugin."); registerPlugin(pluginClass); } catch (Throwable ex) { Log.error("Unable to load plugin " + clazz + ".", ex); } } }); } } private void updateClasspath() { try { classLoader = new PluginClassLoader(getParentClassLoader(), PLUGINS_DIRECTORY); PluginRes.setClassLoader(classLoader); } catch (MalformedURLException e) { Log.error("Error updating classpath.", e); } Thread.currentThread().setContextClassLoader(classLoader); } private void loadPluginResources(String resourceName, ResourceType type) { try { PropertyResourceBundle prbPlugin = (PropertyResourceBundle) ResourceBundle.getBundle(resourceName, Locale.getDefault(), classLoader); for (String key : prbPlugin.keySet()) { PluginRes.putRes(key, prbPlugin.getString(key), type); } } catch (Exception ex) { Log.debug(resourceName + "is not overwritten in plugin "); } } /** * Loads property resources from spark.properties, default.properties, spark_i18n.properties (properly localized) * located in plugin jar, if any * In case the plugin contains preferences.properties, plugin specific defaults will be loaded instead of spark defaults for preferences * This method is called right after all plugins are loaded, specifically after plugins class * loader is initialized and plugins jars are loaded in classpath */ private void loadPluginResources() { loadPluginResources("spark", ResourceType.SPARK); loadPluginResources("default", ResourceType.DEFAULT); loadPluginResources("preferences", ResourceType.PREFERENCES); loadPluginResources("spark_i18n", ResourceType.I18N); } /** * Returns the plugin classloader. * * @return the plugin classloader. */ public ClassLoader getPluginClassLoader() { return classLoader; } /** * Registers a plugin. * * @param plugin the plugin to register. */ public void registerPlugin(Plugin plugin) { plugins.add(plugin); } /** * Removes a plugin from the plugin list. * * @param plugin the plugin to remove. */ public void removePlugin(Plugin plugin) { plugins.remove(plugin); } /** * Returns a Collection of Plugins. * * @return a Collection of Plugins. */ public Collection<Plugin> getPlugins() { return plugins; } /** * Returns the instance of the plugin class initialized during startup. * * @param communicatorPlugin the plugin to find. * @return the instance of the plugin. */ public Plugin getPlugin(Class<? extends Plugin> communicatorPlugin) { for (Object o : getPlugins()) { Plugin plugin = (Plugin) o; if (plugin.getClass() == communicatorPlugin) { return plugin; } } return null; } /** * Loads and initalizes all Plugins. * * @see Plugin */ public void initializePlugins() { try { int j = 0; boolean dependsfound = false; // Dependency check for (int i = 0; i< publicPlugins.size(); i++) { // if dependencies are available, check these if((publicPlugins.get(i)).getDependency().size()>0) { List<PluginDependency> dependencies = (publicPlugins.get(i)).getDependency(); // go trough all dependencies for( PluginDependency dependency : dependencies) { j = 0; dependsfound = false; // look for the specific plugin for(PublicPlugin plugin1 :publicPlugins) { if(plugin1.getName()!= null && plugin1.getName().equals(dependency.getName())) { // if the version is compatible then reorder if(dependency.compareVersion(plugin1.getVersion())){ dependsfound = true; // when depended Plugin hadn't been installed yet if(j>i){ // find the position of plugins-List because it has more entries int counter = 0, x = 0, z = 0; for(Plugin plug : plugins) { // find the position of the aim-object if(plug.getClass().toString().substring(6).equals(publicPlugins.get(j).getPluginClass())) { x = counter; } // find the change-position else if(plug.getClass().toString().substring(6).equals(publicPlugins.get(i).getPluginClass())) { z = counter; } counter ++; } // change the order publicPlugins.add(i, publicPlugins.get(j)); publicPlugins.remove(j+1); plugins.add(z, plugins.get(x)); plugins.remove(x+1); // start again, to check the other dependencies i--; } } // else don't load the plugin and show an error else { Log.error("Depended Plugin " + dependency.getName() + " hasn't the right version (" + dependency.getVersion() + "<>" + plugin1.getVersion()); } break; } j++; } // if the depended Plugin wasn't found, then show error if(!dependsfound) { Log.error("Depended Plugin " + dependency.getName() + " is missing for the Plugin " + (publicPlugins.get(i)).getName()); // find the posiion of plugins-List because it has more entries int counter = 0; for(Plugin plug : plugins) { // find the delete-position if(plug.getClass().toString().substring(6).equals(publicPlugins.get(i).getPluginClass())) { break; } counter ++; } // delete the Plugin, because the depended Plugin is missing publicPlugins.remove(i); plugins.remove(counter); i--; break; } } } } EventQueue.invokeLater(new Runnable() { public void run() { for (Plugin plugin1 : plugins) { long start = System.currentTimeMillis(); Log.debug("Trying to initialize " + plugin1); try { plugin1.initialize(); } catch (Throwable e) { Log.error(e); } long end = System.currentTimeMillis(); Log.debug("Took " + (end - start) + " ms. to load " + plugin1); } } }); } catch (Exception e) { e.printStackTrace(); } } public void shutdown() { for (Plugin plugin1 : plugins) { try { plugin1.shutdown(); } catch (Exception e) { Log.warning("Exception on shutdown of plugin.", e); } } } public void mainWindowActivated() { } public void mainWindowDeactivated() { } /** * Locates the best class loader based on context (see class description). * * @return The best parent classloader to use */ private ClassLoader getParentClassLoader() { ClassLoader parent = Thread.currentThread().getContextClassLoader(); if (parent == null) { parent = this.getClass().getClassLoader(); if (parent == null) { parent = ClassLoader.getSystemClassLoader(); } } return parent; } /** * Expands all plugin packs (.jar files located in the plugin dir with plugin.xml). */ private void expandNewPlugins() { File[] jars = PLUGINS_DIRECTORY.listFiles(new FilenameFilter() { public boolean accept(File dir, String name) { boolean accept = false; String smallName = name.toLowerCase(); if (smallName.endsWith(".jar")) { accept = true; } return accept; } }); // Do nothing if no jar or zip files were found if (jars == null) { return; } for (File jar : jars) { if (jar.isFile()) { URL url = null; try { url = jar.toURI().toURL(); } catch (MalformedURLException e) { Log.error(e); } String name = URLFileSystem.getName(url); File directory = new File(PLUGINS_DIRECTORY, name); if (directory.exists() && directory.isDirectory()) { // Check to see if directory contains the plugin.xml file. // If not, delete directory. File pluginXML = new File(directory, "plugin.xml"); if (pluginXML.exists()) { if (pluginXML.lastModified() < jar.lastModified()) { uninstall(directory); unzipPlugin(jar, directory); } continue; } uninstall(directory); } else { // Unzip contents into directory unzipPlugin(jar, directory); } } } } private void loadPublicPlugins() { // First, expand all plugins that have yet to be expanded. expandNewPlugins(); File[] files = PLUGINS_DIRECTORY.listFiles(new FilenameFilter() { public boolean accept(File dir, String name) { return dir.isDirectory(); } }); // Do nothing if no jar or zip files were found if (files == null) { return; } //Make sure to load first the plugins with no dependencies //If a plugin with dependencies gets loaded before one of dependencies, //class not found exception may be thrown if a dependency class is used during plugin creation List<File> dependencies = new ArrayList<File>(); List<File> nodependencies = new ArrayList<File>(); for (File file : files) { File pluginXML = new File(file, "plugin.xml"); if (pluginXML.exists()) { if (hasDependencies(pluginXML)) { dependencies.add(file); } else { nodependencies.add(file); } } } try { for (File file : nodependencies) { loadPlugin(classLoader, file); } for(File file : dependencies) { loadPlugin(classLoader, file); } } catch (Throwable e) { Log.error("Unable to load dirs", e); } } private void loadPlugin(PluginClassLoader classLoader, File file) throws MalformedURLException { classLoader.addPlugin(file); loadPublicPlugin(file); } /** * Adds and installs a new plugin into Spark. * * @param plugin the plugin to install. * @throws Exception thrown if there was a problem loading the plugin. */ public void addPlugin(PublicPlugin plugin) throws Exception { expandNewPlugins(); URL url = new URL(plugin.getDownloadURL()); String name = URLFileSystem.getName(url); File pluginDownload = new File(PluginManager.PLUGINS_DIRECTORY, name); ((PluginClassLoader) getParentClassLoader()).addPlugin(pluginDownload); pluginClass = loadPublicPlugin(pluginDownload); try { EventQueue.invokeAndWait(new Runnable() { @Override public void run() { Log.debug("Trying to initialize " + pluginClass); pluginClass.initialize(); } }); } catch (Exception e) { Log.error(e); } } /** * Unzips a plugin from a JAR file into a directory. If the JAR file * isn't a plugin, this method will do nothing. * * @param file the JAR file * @param dir the directory to extract the plugin to. */ private void unzipPlugin(File file, File dir) { try { ZipFile zipFile = new JarFile(file); // Ensure that this JAR is a plugin. if (zipFile.getEntry("plugin.xml") == null) { return; } dir.mkdir(); for (Enumeration<? extends ZipEntry> e = zipFile.entries(); e.hasMoreElements();) { JarEntry entry = (JarEntry)e.nextElement(); File entryFile = new File(dir, entry.getName()); // Ignore any manifest.mf entries. if (entry.getName().toLowerCase().endsWith("manifest.mf")) { continue; } if (!entry.isDirectory()) { entryFile.getParentFile().mkdirs(); FileOutputStream out = new FileOutputStream(entryFile); InputStream zin = zipFile.getInputStream(entry); byte[] b = new byte[512]; int len; while ((len = zin.read(b)) != -1) { out.write(b, 0, len); } out.flush(); out.close(); zin.close(); } } zipFile.close(); } catch (Throwable e) { Log.error("Error unzipping plugin", e); } } /** * Returns a collection of all public plugins. * * @return the collection of public plugins. */ public List<PublicPlugin> getPublicPlugins() { return publicPlugins; } private void uninstall(File pluginDir) { File[] files = pluginDir.listFiles(); for (File f : files) { if (f.isFile()) { f.delete(); } } File libDir = new File(pluginDir, "lib"); File[] libs = libDir.listFiles(); if (libs != null) { for (File f : libs) { f.delete(); } } libDir.delete(); pluginDir.delete(); } /** * Removes and uninstall a plugin from Spark. * * @param plugin the plugin to uninstall. */ public void removePublicPlugin(PublicPlugin plugin) { for (PublicPlugin publicPlugin : getPublicPlugins()) { if (plugin.getName().equals(publicPlugin.getName())) { publicPlugins.remove(plugin); } } } /** * Returns true if the specified plugin is installed. * * @param plugin the <code>PublicPlugin</code> plugin to check. * @return true if installed. */ public boolean isInstalled(PublicPlugin plugin) { for (PublicPlugin publicPlugin : getPublicPlugins()) { if (plugin.getName().equals(publicPlugin.getName())) { return true; } } return false; } /** * Checks the plugin for required operating system. * * @param plugin the Plugin element to check. * @return true if the operating system is ok for the plugin to run on. */ private boolean isOperatingSystemOK(Node plugin) { // Check for operating systems try { final Element osElement = (Element)plugin.selectSingleNode("os"); if (osElement != null) { String operatingSystem = osElement.getText(); boolean ok = false; final String currentOS = JiveInfo.getOS().toLowerCase(); // Iterate through comma delimited string StringTokenizer tkn = new StringTokenizer(operatingSystem, ","); while (tkn.hasMoreTokens()) { String os = tkn.nextToken().toLowerCase(); if (currentOS.contains(os) || currentOS.equalsIgnoreCase(os)) { ok = true; } } if (!ok) { Log.debug("Unable to load plugin " + plugin.selectSingleNode("name").getText() + " due to invalid operating system. Required OS = " + operatingSystem); return false; } } } catch (Exception e) { Log.error(e); } return true; } }