OF-1147: Refactoring of Plugin Management

- Plugin Monitor moved to different class
- Methods that operate on plugin.xml files moved to utilility class
  (and made to work even when plugin is not yet loaded)
- Plugin monitoring now loads plugins more efficiently by:
-- Observing parent/child hierarchy (allowing for one-shot load)
-- allowing for concurrent plugin loading
- Dev plugins are now processed as if they were ordinary plugins
- Plugin canoncical naming is now used throughout the code (fixes
  case sensitivity bug)
parent cc3da295
...@@ -79,6 +79,9 @@ ...@@ -79,6 +79,9 @@
<logger name="org.jivesoftware.openfire.container.PluginManager"> <logger name="org.jivesoftware.openfire.container.PluginManager">
<appender-ref ref="console"/> <appender-ref ref="console"/>
</logger> </logger>
<logger name="org.jivesoftware.openfire.container.PluginMonitor">
<appender-ref ref="console"/>
</logger>
<!-- OF-506: Jetty INFO messages are generally not useful. Ignore them by default. --> <!-- OF-506: Jetty INFO messages are generally not useful. Ignore them by default. -->
<logger name="org.eclipse.jetty"> <logger name="org.eclipse.jetty">
......
/*
* Copyright 2016 IgniteRealtime.org
*
* 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.openfire.container;
/**
* An enumeration for license agreement types.
*/
public enum License
{
/**
* Distributed using a commercial license.
*/
commercial,
/**
* Distributed using the GNU Public License (GPL).
*/
gpl,
/**
* Distributed using the Apache license.
*/
apache,
/**
* For internal use at an organization only and is not re-distributed.
*/
internal,
/**
* Distributed under another license agreement not covered by one of the other choices. The license agreement
* should be detailed in a Readme or License file that accompanies the code.
*/
other
}
...@@ -36,22 +36,10 @@ import java.io.BufferedInputStream; ...@@ -36,22 +36,10 @@ import java.io.BufferedInputStream;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.nio.file.DirectoryStream; import java.nio.file.*;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.StandardCopyOption;
import java.nio.file.attribute.BasicFileAttributes; import java.nio.file.attribute.BasicFileAttributes;
import java.util.*; import java.util.*;
import java.util.concurrent.CopyOnWriteArraySet; import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.zip.ZipFile;
/** /**
* Loads and manages plugins. The <tt>plugins</tt> directory is monitored for any * Loads and manages plugins. The <tt>plugins</tt> directory is monitored for any
...@@ -65,87 +53,81 @@ import java.util.zip.ZipFile; ...@@ -65,87 +53,81 @@ import java.util.zip.ZipFile;
* @see Plugin * @see Plugin
* @see org.jivesoftware.openfire.XMPPServer#getPluginManager() * @see org.jivesoftware.openfire.XMPPServer#getPluginManager()
*/ */
public class PluginManager { public class PluginManager
{
private static final Logger Log = LoggerFactory.getLogger(PluginManager.class); private static final Logger Log = LoggerFactory.getLogger( PluginManager.class );
private Path pluginDirectory; private final Path pluginDirectory;
private Map<String, Plugin> plugins; private final Map<String, Plugin> plugins = new TreeMap<>( String.CASE_INSENSITIVE_ORDER );
private Map<Plugin, PluginClassLoader> classloaders; private final Map<Plugin, PluginClassLoader> classloaders = new HashMap<>();
private Map<Plugin, Path> pluginDirs; private final Map<Plugin, Path> pluginDirs = new HashMap<>();
/** private final Map<Plugin, PluginDevEnvironment> pluginDevelopment = new HashMap<>();
* Keep track of plugin names and their unzipped files. This list is updated when plugin private final Map<Plugin, List<String>> parentPluginMap = new HashMap<>();
* is exploded and not when is loaded. private final Map<Plugin, String> childPluginMap = new HashMap<>();
*/ private final Set<PluginListener> pluginListeners = new CopyOnWriteArraySet<>();
private Map<String, Path> pluginFiles; private final Set<PluginManagerListener> pluginManagerListeners = new CopyOnWriteArraySet<>();
private ScheduledExecutorService executor = null;
private Map<Plugin, PluginDevEnvironment> pluginDevelopment; private final PluginMonitor pluginMonitor;
private Map<Plugin, List<String>> parentPluginMap; private boolean executed = false;
private Map<Plugin, String> childPluginMap;
private Set<String> devPlugins;
private PluginMonitor pluginMonitor;
private Set<PluginListener> pluginListeners = new CopyOnWriteArraySet<>();
private Set<PluginManagerListener> pluginManagerListeners = new CopyOnWriteArraySet<>();
/** /**
* Constructs a new plugin manager. * Constructs a new plugin manager.
* *
* @param pluginDir the plugin directory. * @param pluginDir the directory containing all Openfire plugins, typically OPENFIRE_HOME/plugins/
*/ */
public PluginManager(File pluginDir) { public PluginManager( File pluginDir )
{
this.pluginDirectory = pluginDir.toPath(); this.pluginDirectory = pluginDir.toPath();
plugins = new TreeMap<>( String.CASE_INSENSITIVE_ORDER ); pluginMonitor = new PluginMonitor( this );
pluginDirs = new HashMap<>();
pluginFiles = new TreeMap<>( String.CASE_INSENSITIVE_ORDER );
classloaders = new HashMap<>();
pluginDevelopment = new HashMap<>();
parentPluginMap = new HashMap<>();
childPluginMap = new HashMap<>();
devPlugins = new TreeSet<>( String.CASE_INSENSITIVE_ORDER );
pluginMonitor = new PluginMonitor();
} }
/** /**
* Starts plugins and the plugin monitoring service. * Starts plugins and the plugin monitoring service.
*/ */
public void start() { public synchronized void start()
executor = new ScheduledThreadPoolExecutor(1); {
// See if we're in development mode. If so, check for new plugins once every 5 seconds. pluginMonitor.start();
// Otherwise, default to every 20 seconds.
if (Boolean.getBoolean("developmentMode")) {
executor.scheduleWithFixedDelay(pluginMonitor, 0, 5, TimeUnit.SECONDS);
}
else {
executor.scheduleWithFixedDelay(pluginMonitor, 0, 20, TimeUnit.SECONDS);
}
} }
/** /**
* Shuts down all running plugins. * Shuts down all running plugins.
*/ */
public void shutdown() { public synchronized void shutdown()
Log.info("Shutting down. Unloading all installed plugins..."); {
Log.info( "Shutting down. Unloading all installed plugins..." );
// Stop the plugin monitoring service. // Stop the plugin monitoring service.
if (executor != null) { pluginMonitor.stop();
executor.shutdown();
}
// Shutdown all installed plugins. // Shutdown all installed plugins.
for (Map.Entry<String, Plugin> plugin : plugins.entrySet()) { for ( Map.Entry<String, Plugin> plugin : plugins.entrySet() )
try { {
try
{
plugin.getValue().destroyPlugin(); plugin.getValue().destroyPlugin();
Log.info("Unloaded plugin '{}'.", plugin.getKey()); Log.info( "Unloaded plugin '{}'.", plugin.getKey() );
} }
catch (Exception e) { catch ( Exception e )
Log.error("An exception occurred while trying to unload plugin '{}':", plugin.getKey(), e); {
Log.error( "An exception occurred while trying to unload plugin '{}':", plugin.getKey(), e );
} }
} }
plugins.clear(); plugins.clear();
pluginDirs.clear(); pluginDirs.clear();
pluginFiles.clear();
classloaders.clear(); classloaders.clear();
pluginDevelopment.clear(); pluginDevelopment.clear();
childPluginMap.clear(); childPluginMap.clear();
pluginMonitor = null; }
/**
* Returns the directory that contains all plugins. This typically is OPENFIRE_HOME/plugins.
*
* @return The directory that contains all plugins.
*/
public Path getPluginsDirectory()
{
return pluginDirectory;
} }
/** /**
...@@ -155,34 +137,40 @@ public class PluginManager { ...@@ -155,34 +137,40 @@ public class PluginManager {
* @param pluginFilename the filename of the plugin to create or update. * @param pluginFilename the filename of the plugin to create or update.
* @return true if the plugin was successfully installed or updated. * @return true if the plugin was successfully installed or updated.
*/ */
public boolean installPlugin(InputStream in, String pluginFilename) { public boolean installPlugin( InputStream in, String pluginFilename )
if ( pluginFilename == null || pluginFilename.isEmpty()) { {
Log.error("Error installing plugin: pluginFilename was null or empty."); if ( pluginFilename == null || pluginFilename.isEmpty() )
{
Log.error( "Error installing plugin: pluginFilename was null or empty." );
return false; return false;
} }
if (in == null) { if ( in == null )
Log.error("Error installing plugin '{}': Input stream was null.", pluginFilename); {
Log.error( "Error installing plugin '{}': Input stream was null.", pluginFilename );
return false; return false;
} }
try { try
{
// If pluginFilename is a path instead of a simple file name, we only want the file name // If pluginFilename is a path instead of a simple file name, we only want the file name
int index = pluginFilename.lastIndexOf(File.separator); int index = pluginFilename.lastIndexOf( File.separator );
if (index != -1) { if ( index != -1 )
pluginFilename = pluginFilename.substring(index+1); {
pluginFilename = pluginFilename.substring( index + 1 );
} }
// Absolute path to the plugin file // Absolute path to the plugin file
Path absolutePath = pluginDirectory.resolve(pluginFilename); Path absolutePath = pluginDirectory.resolve( pluginFilename );
Path partFile = pluginDirectory.resolve(pluginFilename + ".part"); Path partFile = pluginDirectory.resolve( pluginFilename + ".part" );
// Save input stream contents to a temp file // Save input stream contents to a temp file
Files.copy(in, partFile, StandardCopyOption.REPLACE_EXISTING); Files.copy( in, partFile, StandardCopyOption.REPLACE_EXISTING );
// Rename temp file to .jar // Rename temp file to .jar
Files.move(partFile, absolutePath, StandardCopyOption.REPLACE_EXISTING); Files.move( partFile, absolutePath, StandardCopyOption.REPLACE_EXISTING );
// Ask the plugin monitor to update the plugin immediately. // Ask the plugin monitor to update the plugin immediately.
pluginMonitor.run(); pluginMonitor.runNow( true );
} }
catch (IOException e) { catch ( IOException e )
Log.error("An exception occurred while installing new version of plugin '{}':", pluginFilename, e); {
Log.error( "An exception occurred while installing new version of plugin '{}':", pluginFilename, e );
return false; return false;
} }
return true; return true;
...@@ -194,8 +182,9 @@ public class PluginManager { ...@@ -194,8 +182,9 @@ public class PluginManager {
* @param pluginFilename the filename of the plugin to create or update. * @param pluginFilename the filename of the plugin to create or update.
* @return true if the specified filename, that belongs to a plugin, exists. * @return true if the specified filename, that belongs to a plugin, exists.
*/ */
public boolean isPluginDownloaded(String pluginFilename) { public boolean isPluginDownloaded( String pluginFilename )
return Files.exists(pluginDirectory.resolve(pluginFilename)); {
return Files.exists( pluginDirectory.resolve( pluginFilename ) );
} }
/** /**
...@@ -203,8 +192,9 @@ public class PluginManager { ...@@ -203,8 +192,9 @@ public class PluginManager {
* *
* @return a Collection of all installed plugins. * @return a Collection of all installed plugins.
*/ */
public Collection<Plugin> getPlugins() { public Collection<Plugin> getPlugins()
return Collections.unmodifiableCollection(Arrays.asList( plugins.values().toArray( new Plugin[ plugins.size() ]) )); {
return Collections.unmodifiableCollection( Arrays.asList( plugins.values().toArray( new Plugin[ plugins.size() ] ) ) );
} }
/** /**
...@@ -215,28 +205,29 @@ public class PluginManager { ...@@ -215,28 +205,29 @@ public class PluginManager {
* @param name the name of the plugin. * @param name the name of the plugin.
* @return the plugin. * @return the plugin.
*/ */
public Plugin getPlugin(String name) { public Plugin getPlugin( String name )
return plugins.get(name); {
return plugins.get( name );
} }
/** /**
* Returns the plugin's directory. * @deprecated Use #getPluginPath() instead.
*
* @param plugin the plugin.
* @return the plugin's directory.
*/ */
public File getPluginDirectory(Plugin plugin) { @Deprecated
return pluginDirs.get(plugin).toFile(); public File getPluginDirectory( Plugin plugin )
{
return getPluginPath( plugin ).toFile();
} }
/** /**
* Returns the JAR or WAR file that created the plugin. * Returns the plugin's directory.
* *
* @param name the name of the plugin. * @param plugin the plugin.
* @return the plugin JAR or WAR file. * @return the plugin's directory.
*/ */
public File getPluginFile(String name) { public Path getPluginPath( Plugin plugin )
return pluginFiles.get(name).toFile(); {
return pluginDirs.get( plugin );
} }
/** /**
...@@ -244,333 +235,377 @@ public class PluginManager { ...@@ -244,333 +235,377 @@ public class PluginManager {
* that available plugins have been loaded nor that plugins to be added in the future are already * that available plugins have been loaded nor that plugins to be added in the future are already
* loaded. :)<p> * loaded. :)<p>
* *
* TODO Current version does not consider child plugins that may be loaded in a second attempt. It either
* TODO consider plugins that were found but failed to be loaded due to some error.
*
* @return true if at least one attempt to load plugins has been done. * @return true if at least one attempt to load plugins has been done.
*/ */
public boolean isExecuted() { public boolean isExecuted()
return pluginMonitor.executed; {
return executed;
} }
/** /**
* Loads a plug-in module into the container. Loading consists of the * Loads a plugin.
* following steps:<ul>
* <p/>
* <li>Add all jars in the <tt>lib</tt> dir (if it exists) to the class loader</li>
* <li>Add all files in <tt>classes</tt> dir (if it exists) to the class loader</li>
* <li>Locate and load <tt>module.xml</tt> into the context</li>
* <li>For each jive.module entry, load the given class as a module and start it</li>
* <p/>
* </ul>
* *
* @param pluginDir the plugin directory. * @param pluginDir the plugin directory.
*/ */
private void loadPlugin(Path pluginDir) { void loadPlugin( Path pluginDir )
{
// Only load the admin plugin during setup mode. // Only load the admin plugin during setup mode.
if (XMPPServer.getInstance().isSetupMode() && !(pluginDir.getFileName().toString().equals("admin"))) { final String pluginName = pluginDir.getFileName().toString();
if ( XMPPServer.getInstance().isSetupMode() && !( pluginName.equals( "admin" ) ) )
{
return; return;
} }
String pluginName = pluginDir.getFileName().toString();
Log.debug("Loading plugin '{}'...", pluginName); Log.debug( "Loading plugin '{}'...", pluginName );
Plugin plugin; try
try { {
Path pluginConfig = pluginDir.resolve("plugin.xml"); final Path pluginConfig = pluginDir.resolve( "plugin.xml" );
if (Files.exists(pluginConfig)) { if ( !Files.exists( pluginConfig ) )
SAXReader saxReader = new SAXReader(); {
saxReader.setEncoding("UTF-8"); Log.warn( "Plugin '{}' could not be loaded: no plugin.xml file found.", pluginName );
Document pluginXML = saxReader.read(pluginConfig.toFile());
// See if the plugin specifies a version of Openfire
// required to run.
Element minServerVersion = (Element)pluginXML.selectSingleNode("/plugin/minServerVersion");
if (minServerVersion != null) {
Version requiredVersion = new Version(minServerVersion.getTextTrim());
Version currentVersion = XMPPServer.getInstance().getServerInfo().getVersion();
if (requiredVersion.isNewerThan(currentVersion)) {
Log.warn("Ignoring plugin '{}': requires server version {}. Current server version is {}.", pluginName, requiredVersion, currentVersion);
return; return;
} }
}
PluginClassLoader pluginLoader;
// Check to see if this is a child plugin of another plugin. If it is, we
// re-use the parent plugin's class loader so that the plugins can interact.
Element parentPluginNode = (Element)pluginXML.selectSingleNode("/plugin/parentPlugin");
String webRootKey = pluginName + ".webRoot"; final SAXReader saxReader = new SAXReader();
String classesDirKey = pluginName + ".classes"; saxReader.setEncoding( "UTF-8" );
String webRoot = System.getProperty(webRootKey); final Document pluginXML = saxReader.read( pluginConfig.toFile() );
String classesDir = System.getProperty(classesDirKey);
if (webRoot != null) { // See if the plugin specifies a version of Openfire required to run.
final Path compilationClassesDir = pluginDir.resolve("classes"); final Element minServerVersion = (Element) pluginXML.selectSingleNode( "/plugin/minServerVersion" );
if (Files.notExists(compilationClassesDir)) { if ( minServerVersion != null )
Files.createDirectory(compilationClassesDir); {
final Version requiredVersion = new Version( minServerVersion.getTextTrim() );
final Version currentVersion = XMPPServer.getInstance().getServerInfo().getVersion();
if ( requiredVersion.isNewerThan( currentVersion ) )
{
Log.warn( "Ignoring plugin '{}': requires server version {}. Current server version is {}.", pluginName, requiredVersion, currentVersion );
return;
} }
compilationClassesDir.toFile().deleteOnExit();
} }
if (parentPluginNode != null) { // Properties to be used to load external resources. When set, plugin is considered to run in DEV mode.
String parentPlugin = parentPluginNode.getTextTrim(); final String devModeClassesDir = System.getProperty( pluginName + ".classes" );
// See if the parent is already loaded. final String devModewebRoot = System.getProperty( pluginName + ".webRoot" );
if (plugins.containsKey(parentPlugin)) { final boolean devMode = devModewebRoot != null || devModeClassesDir != null;
pluginLoader = classloaders.get(getPlugin(parentPlugin)); final PluginDevEnvironment dev = ( devMode ? configurePluginDevEnvironment( pluginDir, devModeClassesDir, devModewebRoot ) : null );
pluginLoader.addDirectory(pluginDir.toFile(), classesDir != null);
// Initialize the plugin class loader, which is either a new instance, or a the loader from a parent plugin.
final PluginClassLoader pluginLoader;
// Check to see if this is a child plugin of another plugin. If it is, we re-use the parent plugin's class
// loader so that the plugins can interact.
String parentPluginName = null;
Plugin parentPlugin = null;
final Element parentPluginNode = (Element) pluginXML.selectSingleNode( "/plugin/parentPlugin" );
if ( parentPluginNode != null )
{
// The name of the parent plugin as specified in plugin.xml might have incorrect casing. Lookup the correct name.
for ( final Map.Entry<String, Plugin> entry : plugins.entrySet() )
{
if ( entry.getKey().equalsIgnoreCase( parentPluginNode.getTextTrim() ) )
{
parentPluginName = entry.getKey();
parentPlugin = entry.getValue();
break;
} }
else {
// See if the parent plugin exists but just hasn't been loaded yet.
final Set<String> plugins = new TreeSet<>( String.CASE_INSENSITIVE_ORDER );
plugins.addAll( Arrays.asList( pluginDir.getParent().toFile().list() ) );
if ( plugins.contains( parentPlugin + ".jar" ) || plugins.contains( parentPlugin + ".war" ) ) {
// Silently return. The child plugin will get loaded up on the next
// plugin load run after the parent.
return;
} else {
Log.warn("Ignoring plugin '{}': parent plugin '{}' not present.", pluginName, parentPlugin);
return;
} }
// See if the parent is loaded.
if ( parentPlugin == null )
{
Log.info( "Unable to load plugin '{}': parent plugin '{}' has not been loaded.", pluginName, parentPluginNode.getTextTrim() );
return;
} }
pluginLoader = classloaders.get( parentPlugin );
} }
else
{
// This is not a child plugin, so create a new class loader. // This is not a child plugin, so create a new class loader.
else {
pluginLoader = new PluginClassLoader(); pluginLoader = new PluginClassLoader();
pluginLoader.addDirectory(pluginDir.toFile(), classesDir != null);
} }
// Check to see if development mode is turned on for the plugin. If it is, // Add the plugin sources to the classloaded.
// configure dev mode. pluginLoader.addDirectory( pluginDir.toFile(), devMode );
PluginDevEnvironment dev = null;
if (webRoot != null || classesDir != null) {
dev = new PluginDevEnvironment();
Log.info("Plugin '{}' is running in development mode.", pluginName);
if (webRoot != null) {
Path webRootDir = Paths.get(webRoot);
if (Files.notExists(webRootDir)) {
// Ok, let's try it relative from this plugin dir?
webRootDir = pluginDir.resolve(webRoot);
}
if (Files.exists(webRootDir)) { // When running in DEV mode, add optional other sources too.
dev.setWebRoot(webRootDir.toFile()); if ( dev != null && dev.getClassesDir() != null )
} {
pluginLoader.addURLFile( dev.getClassesDir().toURI().toURL() );
} }
if (classesDir != null) { // Instantiate the plugin!
Path classes = Paths.get(classesDir); final String className = pluginXML.selectSingleNode( "/plugin/class" ).getText().trim();
if (Files.notExists(classes)) { final Plugin plugin = (Plugin) pluginLoader.loadClass( className ).newInstance();
// ok, let's try it relative from this plugin dir?
classes = pluginDir.resolve(classesDir);
}
if (Files.exists(classes)) { // Bookkeeping!
dev.setClassesDir(classes.toFile()); classloaders.put( plugin, pluginLoader );
pluginLoader.addURLFile(classes.toUri().toURL()); plugins.put( pluginName, plugin );
} pluginDirs.put( plugin, pluginDir );
} if ( dev != null )
{
pluginDevelopment.put( plugin, dev );
} }
String className = pluginXML.selectSingleNode("/plugin/class").getText().trim();
plugin = (Plugin)pluginLoader.loadClass(className).newInstance();
if (parentPluginNode != null) {
String parentPlugin = parentPluginNode.getTextTrim();
// See if the parent is already loaded.
if (plugins.containsKey(parentPlugin)) {
pluginLoader = classloaders.get(getPlugin(parentPlugin));
classloaders.put(plugin, pluginLoader);
}
}
plugins.put(pluginName, plugin);
pluginDirs.put(plugin, pluginDir);
// If this is a child plugin, register it as such. // If this is a child plugin, register it as such.
if (parentPluginNode != null) { if ( parentPlugin != null )
String parentPlugin = parentPluginNode.getTextTrim(); {
// The name of the parent plugin as specified in plugin.xml might have incorrect casing. Lookup the correct name. List<String> childrenPlugins = parentPluginMap.get( parentPlugin );
for (Map.Entry<String, Plugin> entry : plugins.entrySet() ) { if ( childrenPlugins == null )
if ( entry.getKey().equalsIgnoreCase( parentPlugin ) ) { {
parentPlugin = entry.getKey();
break;
}
}
List<String> childrenPlugins = parentPluginMap.get(plugins.get(parentPlugin));
if (childrenPlugins == null) {
childrenPlugins = new ArrayList<>(); childrenPlugins = new ArrayList<>();
parentPluginMap.put(plugins.get(parentPlugin), childrenPlugins); parentPluginMap.put( parentPlugin, childrenPlugins );
} }
childrenPlugins.add(pluginName); childrenPlugins.add( pluginName );
// Also register child to parent relationship. // Also register child to parent relationship.
childPluginMap.put(plugin, parentPlugin); childPluginMap.put( plugin, parentPluginName );
}
else {
// Only register the class loader in the case of this not being
// a child plugin.
classloaders.put(plugin, pluginLoader);
} }
// Check the plugin's database schema (if it requires one). // Check the plugin's database schema (if it requires one).
if (!DbConnectionManager.getSchemaManager().checkPluginSchema(plugin)) { if ( !DbConnectionManager.getSchemaManager().checkPluginSchema( plugin ) )
{
// The schema was not there and auto-upgrade failed. // The schema was not there and auto-upgrade failed.
Log.error("Error while loading plugin '{}': {}", pluginName, LocaleUtils.getLocalizedString("upgrade.database.failure")); Log.error( "Error while loading plugin '{}': {}", pluginName, LocaleUtils.getLocalizedString( "upgrade.database.failure" ) );
} }
// Load any JSP's defined by the plugin. // Load any JSP's defined by the plugin.
Path webXML = pluginDir.resolve("web").resolve("WEB-INF").resolve("web.xml"); final Path webXML = pluginDir.resolve( "web" ).resolve( "WEB-INF" ).resolve( "web.xml" );
if (Files.exists(webXML)) { if ( Files.exists( webXML ) )
PluginServlet.registerServlets(this, plugin, webXML.toFile()); {
} PluginServlet.registerServlets( this, plugin, webXML.toFile() );
// Load any custom-defined servlets.
Path customWebXML = pluginDir.resolve("web").resolve("WEB-INF").resolve("web-custom.xml");
if (Files.exists(customWebXML)) {
PluginServlet.registerServlets(this, plugin, customWebXML.toFile());
} }
if (dev != null) { // Load any custom-defined servlets.
pluginDevelopment.put(plugin, dev); final Path customWebXML = pluginDir.resolve( "web" ).resolve( "WEB-INF" ).resolve( "web-custom.xml" );
if ( Files.exists( customWebXML ) )
{
PluginServlet.registerServlets( this, plugin, customWebXML.toFile() );
} }
// Configure caches of the plugin // Configure caches of the plugin
configureCaches(pluginDir, pluginName); configureCaches( pluginDir, pluginName );
// Init the plugin. // Initialze the plugin.
ClassLoader oldLoader = Thread.currentThread().getContextClassLoader(); final ClassLoader oldLoader = Thread.currentThread().getContextClassLoader();
Thread.currentThread().setContextClassLoader(pluginLoader); Thread.currentThread().setContextClassLoader( pluginLoader );
plugin.initializePlugin(this, pluginDir.toFile()); plugin.initializePlugin( this, pluginDir.toFile() );
Log.debug("Initialized plugin '{}'.", pluginName); Log.debug( "Initialized plugin '{}'.", pluginName );
Thread.currentThread().setContextClassLoader(oldLoader); Thread.currentThread().setContextClassLoader( oldLoader );
// If there a <adminconsole> section defined, register it. // If there a <adminconsole> section defined, register it.
Element adminElement = (Element)pluginXML.selectSingleNode("/plugin/adminconsole"); final Element adminElement = (Element) pluginXML.selectSingleNode( "/plugin/adminconsole" );
if (adminElement != null) { if ( adminElement != null )
Element appName = (Element)adminElement.selectSingleNode( {
"/plugin/adminconsole/global/appname"); final Element appName = (Element) adminElement.selectSingleNode( "/plugin/adminconsole/global/appname" );
if (appName != null) { if ( appName != null )
{
// Set the plugin name so that the proper i18n String can be loaded. // Set the plugin name so that the proper i18n String can be loaded.
appName.addAttribute("plugin", pluginName); appName.addAttribute( "plugin", pluginName );
} }
// If global images are specified, override their URL. // If global images are specified, override their URL.
Element imageEl = (Element)adminElement.selectSingleNode( Element imageEl = (Element) adminElement.selectSingleNode( "/plugin/adminconsole/global/logo-image" );
"/plugin/adminconsole/global/logo-image"); if ( imageEl != null )
if (imageEl != null) { {
imageEl.setText("plugins/" + pluginName + "/" + imageEl.getText()); imageEl.setText( "plugins/" + pluginName + "/" + imageEl.getText() );
// Set the plugin name so that the proper i18n String can be loaded. imageEl.addAttribute( "plugin", pluginName ); // Set the plugin name so that the proper i18n String can be loaded.
imageEl.addAttribute("plugin", pluginName);
} }
imageEl = (Element)adminElement.selectSingleNode("/plugin/adminconsole/global/login-image"); imageEl = (Element) adminElement.selectSingleNode( "/plugin/adminconsole/global/login-image" );
if (imageEl != null) { if ( imageEl != null )
imageEl.setText("plugins/" + pluginName + "/" + imageEl.getText()); {
// Set the plugin name so that the proper i18n String can be loaded. imageEl.setText( "plugins/" + pluginName + "/" + imageEl.getText() );
imageEl.addAttribute("plugin", pluginName); imageEl.addAttribute( "plugin", pluginName ); // Set the plugin name so that the proper i18n String can be loaded.
} }
// Modify all the URL's in the XML so that they are passed through
// the plugin servlet correctly. // Modify all the URL's in the XML so that they are passed through the plugin servlet correctly.
List urls = adminElement.selectNodes("//@url"); final List urls = adminElement.selectNodes( "//@url" );
for (Object url : urls) { for ( final Object url : urls )
Attribute attr = (Attribute)url; {
attr.setValue("plugins/" + pluginName + "/" + attr.getValue()); final Attribute attr = (Attribute) url;
attr.setValue( "plugins/" + pluginName + "/" + attr.getValue() );
} }
// In order to internationalize the names and descriptions in the model,
// we add a "plugin" attribute to each tab, sidebar, and item so that // In order to internationalize the names and descriptions in the model, we add a "plugin" attribute to
// the the renderer knows where to load the i18n Strings from. // each tab, sidebar, and item so that the the renderer knows where to load the i18n Strings from.
String[] elementNames = new String [] { "tab", "sidebar", "item" }; final String[] elementNames = new String[]{ "tab", "sidebar", "item" };
for (String elementName : elementNames) { for ( final String elementName : elementNames )
List values = adminElement.selectNodes("//" + elementName); {
for (Object value : values) { final List values = adminElement.selectNodes( "//" + elementName );
Element element = (Element) value; for ( final Object value : values )
// Make sure there's a name or description. Otherwise, no need to {
// override i18n settings. final Element element = (Element) value;
if (element.attribute("name") != null || // Make sure there's a name or description. Otherwise, no need to i18n settings.
element.attribute("value") != null) { if ( element.attribute( "name" ) != null || element.attribute( "value" ) != null )
element.addAttribute("plugin", pluginName); {
element.addAttribute( "plugin", pluginName );
} }
} }
} }
AdminConsole.addModel(pluginName, adminElement); AdminConsole.addModel( pluginName, adminElement );
} }
firePluginCreatedEvent(pluginName, plugin); firePluginCreatedEvent( pluginName, plugin );
Log.info( "Successfully loaded plugin '{}'.", pluginName );
} }
else { catch ( Throwable e )
Log.warn("Plugin '{}' could not be loaded: no plugin.xml file found.", pluginName); {
Log.error( "An exception occurred while loading plugin '{}':", pluginName, e );
} }
} }
catch (Throwable e) {
Log.error("An exception occurred while loading plugin '{}':", pluginName, e); private PluginDevEnvironment configurePluginDevEnvironment( final Path pluginDir, String classesDir, String webRoot ) throws IOException
{
final String pluginName = pluginDir.getFileName().toString();
final Path compilationClassesDir = pluginDir.resolve( "classes" );
if ( Files.notExists( compilationClassesDir ) )
{
Files.createDirectory( compilationClassesDir );
} }
Log.info( "Successfully loaded plugin '{}'.", pluginName ); compilationClassesDir.toFile().deleteOnExit();
final PluginDevEnvironment dev = new PluginDevEnvironment();
Log.info( "Plugin '{}' is running in development mode.", pluginName );
if ( webRoot != null )
{
Path webRootDir = Paths.get( webRoot );
if ( Files.notExists( webRootDir ) )
{
// Ok, let's try it relative from this plugin dir?
webRootDir = pluginDir.resolve( webRoot );
} }
private void configureCaches(Path pluginDir, String pluginName) { if ( Files.exists( webRootDir ) )
Path cacheConfig = pluginDir.resolve("cache-config.xml"); {
if (Files.exists(cacheConfig)) { dev.setWebRoot( webRootDir.toFile() );
PluginCacheConfigurator configurator = new PluginCacheConfigurator(); }
try { }
configurator.setInputStream(new BufferedInputStream(Files.newInputStream(cacheConfig)));
configurator.configure(pluginName); if ( classesDir != null )
{
Path classes = Paths.get( classesDir );
if ( Files.notExists( classes ) )
{
// ok, let's try it relative from this plugin dir?
classes = pluginDir.resolve( classesDir );
} }
catch (Exception e) {
Log.error("An exception occurred while trying to configure caches for plugin '{}':", pluginName, e); if ( Files.exists( classes ) )
{
dev.setClassesDir( classes.toFile() );
} }
} }
return dev;
} }
private void firePluginCreatedEvent(String name, Plugin plugin) { private void configureCaches( Path pluginDir, String pluginName )
for(PluginListener listener : pluginListeners) { {
listener.pluginCreated(name, plugin); Path cacheConfig = pluginDir.resolve( "cache-config.xml" );
if ( Files.exists( cacheConfig ) )
{
PluginCacheConfigurator configurator = new PluginCacheConfigurator();
try
{
configurator.setInputStream( new BufferedInputStream( Files.newInputStream( cacheConfig ) ) );
configurator.configure( pluginName );
}
catch ( Exception e )
{
Log.error( "An exception occurred while trying to configure caches for plugin '{}':", pluginName, e );
}
} }
} }
private void firePluginsMonitored() { /**
for(PluginManagerListener listener : pluginManagerListeners) { * Delete a plugin, which removes the plugin.jar/war file after which the plugin is unloaded.
listener.pluginsMonitored(); */
public void deletePlugin( final String pluginName )
{
Log.debug( "Deleting plugin '{}'...", pluginName );
try ( final DirectoryStream<Path> ds = Files.newDirectoryStream( getPluginsDirectory(), new DirectoryStream.Filter<Path>()
{
@Override
public boolean accept( final Path path ) throws IOException
{
if ( Files.isDirectory( path ) )
{
return false;
}
final String fileName = path.getFileName().toString().toLowerCase();
return ( fileName.equals( pluginName + ".jar" ) || fileName.equals( pluginName + ".war" ) );
}
} ) )
{
for ( final Path pluginFile : ds )
{
try
{
Files.delete( pluginFile );
pluginMonitor.runNow( true ); // trigger unload by running the monitor (which is more thread-safe than calling unloadPlugin directly).
}
catch ( IOException ex )
{
Log.warn( "Unable to delete plugin '{}', as the plugin jar/war file cannot be deleted. File path: {}", pluginName, pluginFile, ex );
}
}
}
catch ( Throwable e )
{
Log.error( "An unexpected exception occurred while deleting plugin '{}'.", pluginName, e );
} }
} }
/** /**
* Unloads a plugin. The {@link Plugin#destroyPlugin()} method will be called and then * Unloads a plugin. The {@link Plugin#destroyPlugin()} method will be called and then any resources will be
* any resources will be released. The name should be the name of the plugin directory * released. The name should be the canonical name of the plugin (based on the plugin directory name) and not the
* and not the name as given by the plugin meta-data. This method only removes * human readable name as given by the plugin meta-data.
* the plugin but does not delete the plugin JAR file. Therefore, if the plugin JAR *
* still exists after this method is called, the plugin will be started again the next * This method only removes the plugin but does not delete the plugin JAR file. Therefore, if the plugin JAR still
* time the plugin monitor process runs. This is useful for "restarting" plugins. * exists after this method is called, the plugin will be started again the next time the plugin monitor process
* <p> * runs. This is useful for "restarting" plugins. To completely remove the plugin, use {@link #deletePlugin(String)}
* instead.
*
* This method is called automatically when a plugin's JAR file is deleted. * This method is called automatically when a plugin's JAR file is deleted.
* </p>
* *
* @param pluginName the name of the plugin to unload. * @param pluginName the name of the plugin to unload.
*/ */
public void unloadPlugin(String pluginName) { public void unloadPlugin( String pluginName )
Log.debug("Unloading plugin '{}'...",pluginName); {
Log.debug( "Unloading plugin '{}'...", pluginName );
Plugin plugin = plugins.get(pluginName); Plugin plugin = plugins.get( pluginName );
if (plugin != null) { if ( plugin != null )
{
// Remove from dev mode if it exists. // Remove from dev mode if it exists.
pluginDevelopment.remove(plugin); pluginDevelopment.remove( plugin );
// See if any child plugins are defined. // See if any child plugins are defined.
if (parentPluginMap.containsKey(plugin)) { if ( parentPluginMap.containsKey( plugin ) )
{
String[] childPlugins = String[] childPlugins =
parentPluginMap.get(plugin).toArray(new String[parentPluginMap.get(plugin).size()]); parentPluginMap.get( plugin ).toArray( new String[ parentPluginMap.get( plugin ).size() ] );
parentPluginMap.remove(plugin); parentPluginMap.remove( plugin );
for (String childPlugin : childPlugins) { for ( String childPlugin : childPlugins )
Log.debug("Unloading child plugin: '{}'.", childPlugin); {
childPluginMap.remove(plugins.get(childPlugin)); Log.debug( "Unloading child plugin: '{}'.", childPlugin );
unloadPlugin(childPlugin); childPluginMap.remove( plugins.get( childPlugin ) );
unloadPlugin( childPlugin );
} }
} }
Path webXML = pluginDirectory.resolve(pluginName).resolve("web").resolve("WEB-INF").resolve("web.xml"); Path webXML = pluginDirectory.resolve( pluginName ).resolve( "web" ).resolve( "WEB-INF" ).resolve( "web.xml" );
if (Files.exists(webXML)) { if ( Files.exists( webXML ) )
AdminConsole.removeModel(pluginName); {
PluginServlet.unregisterServlets(webXML.toFile()); AdminConsole.removeModel( pluginName );
PluginServlet.unregisterServlets( webXML.toFile() );
} }
Path customWebXML = pluginDirectory.resolve(pluginName).resolve("web").resolve("WEB-INF").resolve("web-custom.xml"); Path customWebXML = pluginDirectory.resolve( pluginName ).resolve( "web" ).resolve( "WEB-INF" ).resolve( "web-custom.xml" );
if (Files.exists(customWebXML)) { if ( Files.exists( customWebXML ) )
PluginServlet.unregisterServlets(customWebXML.toFile()); {
PluginServlet.unregisterServlets( customWebXML.toFile() );
} }
// Wrap destroying the plugin in a try/catch block. Otherwise, an exception raised // Wrap destroying the plugin in a try/catch block. Otherwise, an exception raised
...@@ -578,12 +613,14 @@ public class PluginManager { ...@@ -578,12 +613,14 @@ public class PluginManager {
// possible that classloader destruction won't work in the case that destroying the plugin // possible that classloader destruction won't work in the case that destroying the plugin
// fails. In that case, Openfire may need to be restarted to fully cleanup the plugin // fails. In that case, Openfire may need to be restarted to fully cleanup the plugin
// resources. // resources.
try { try
{
plugin.destroyPlugin(); plugin.destroyPlugin();
Log.debug( "Destroyed plugin '{}'.", pluginName ); Log.debug( "Destroyed plugin '{}'.", pluginName );
} }
catch (Exception e) { catch ( Exception e )
Log.error( "An exception occurred while unloading plugin '{}':", pluginName, e); {
Log.error( "An exception occurred while unloading plugin '{}':", pluginName, e );
} }
} }
...@@ -591,76 +628,81 @@ public class PluginManager { ...@@ -591,76 +628,81 @@ public class PluginManager {
// If plugin still fails to be removed then we will add references back // If plugin still fails to be removed then we will add references back
// Anyway, for a few seconds admins may not see the plugin in the admin console // Anyway, for a few seconds admins may not see the plugin in the admin console
// and in a subsequent refresh it will appear if failed to be removed // and in a subsequent refresh it will appear if failed to be removed
plugins.remove(pluginName); plugins.remove( pluginName );
Path pluginFile = pluginDirs.remove(plugin); Path pluginFile = pluginDirs.remove( plugin );
PluginClassLoader pluginLoader = classloaders.remove(plugin); PluginClassLoader pluginLoader = classloaders.remove( plugin );
// try to close the cached jar files from the plugin class loader // try to close the cached jar files from the plugin class loader
if (pluginLoader != null) { if ( pluginLoader != null )
{
pluginLoader.unloadJarFiles(); pluginLoader.unloadJarFiles();
} else { }
Log.warn("No plugin loader found for '{}'.",pluginName); else
{
Log.warn( "No plugin loader found for '{}'.", pluginName );
} }
// Try to remove the folder where the plugin was exploded. If this works then // Try to remove the folder where the plugin was exploded. If this works then
// the plugin was successfully removed. Otherwise, some objects created by the // the plugin was successfully removed. Otherwise, some objects created by the
// plugin are still in memory. // plugin are still in memory.
Path dir = pluginDirectory.resolve(pluginName); Path dir = pluginDirectory.resolve( pluginName );
// Give the plugin 2 seconds to unload. // Give the plugin 2 seconds to unload.
try { try
Thread.sleep(2000); {
Thread.sleep( 2000 );
// Ask the system to clean up references. // Ask the system to clean up references.
System.gc(); System.gc();
int count = 0; int count = 0;
while (!deleteDir(dir) && count++ < 5) { while ( !deleteDir( dir ) && count++ < 5 )
Log.warn("Error unloading plugin '{}'. Will attempt again momentarily.", pluginName); {
Log.warn( "Error unloading plugin '{}'. Will attempt again momentarily.", pluginName );
Thread.sleep( 8000 ); Thread.sleep( 8000 );
// Ask the system to clean up references. // Ask the system to clean up references.
System.gc(); System.gc();
} }
} catch (InterruptedException e) { }
catch ( InterruptedException e )
{
Log.debug( "Stopped waiting for plugin '{}' to be fully unloaded.", pluginName, e ); Log.debug( "Stopped waiting for plugin '{}' to be fully unloaded.", pluginName, e );
} }
if (plugin != null && Files.notExists(dir)) { if ( plugin != null && Files.notExists( dir ) )
{
// Unregister plugin caches // Unregister plugin caches
PluginCacheRegistry.getInstance().unregisterCaches(pluginName); PluginCacheRegistry.getInstance().unregisterCaches( pluginName );
// See if this is a child plugin. If it is, we should unload // See if this is a child plugin. If it is, we should unload
// the parent plugin as well. // the parent plugin as well.
if (childPluginMap.containsKey(plugin)) { if ( childPluginMap.containsKey( plugin ) )
String parentPluginName = childPluginMap.get(plugin); {
Plugin parentPlugin = plugins.get(parentPluginName); String parentPluginName = childPluginMap.get( plugin );
List<String> childrenPlugins = parentPluginMap.get(parentPlugin); Plugin parentPlugin = plugins.get( parentPluginName );
List<String> childrenPlugins = parentPluginMap.get( parentPlugin );
childrenPlugins.remove(pluginName); childrenPlugins.remove( pluginName );
childPluginMap.remove(plugin); childPluginMap.remove( plugin );
// When the parent plugin implements PluginListener, its pluginDestroyed() method // When the parent plugin implements PluginListener, its pluginDestroyed() method
// isn't called if it dies first before its child. Athough the parent will die anyway, // isn't called if it dies first before its child. Athough the parent will die anyway,
// it's proper if the parent "gets informed first" about the dying child when the // it's proper if the parent "gets informed first" about the dying child when the
// child is the one being killed first. // child is the one being killed first.
if (parentPlugin instanceof PluginListener) { if ( parentPlugin instanceof PluginListener )
{
PluginListener listener; PluginListener listener;
listener = (PluginListener) parentPlugin; listener = (PluginListener) parentPlugin;
listener.pluginDestroyed(pluginName, plugin); listener.pluginDestroyed( pluginName, plugin );
} }
unloadPlugin(parentPluginName); unloadPlugin( parentPluginName );
} }
firePluginDestroyedEvent(pluginName, plugin); firePluginDestroyedEvent( pluginName, plugin );
Log.info("Successfully unloaded plugin '{}'.", pluginName); Log.info( "Successfully unloaded plugin '{}'.", pluginName );
} }
else if (plugin != null) { else if ( plugin != null )
Log.info("Restore references since we failed to remove the plugin '{}'.", pluginName); {
plugins.put(pluginName, plugin); Log.info( "Restore references since we failed to remove the plugin '{}'.", pluginName );
pluginDirs.put(plugin, pluginFile); plugins.put( pluginName, plugin );
classloaders.put(plugin, pluginLoader); pluginDirs.put( plugin, pluginFile );
} classloaders.put( plugin, pluginLoader );
}
private void firePluginDestroyedEvent(String name, Plugin plugin) {
for (PluginListener listener : pluginListeners) {
listener.pluginDestroyed(name, plugin);
} }
} }
...@@ -674,10 +716,11 @@ public class PluginManager { ...@@ -674,10 +716,11 @@ public class PluginManager {
* @throws IllegalAccessException if not allowed to access the class. * @throws IllegalAccessException if not allowed to access the class.
* @throws InstantiationException if the class could not be created. * @throws InstantiationException if the class could not be created.
*/ */
public Class loadClass(Plugin plugin, String className) throws ClassNotFoundException, public Class loadClass( Plugin plugin, String className ) throws ClassNotFoundException,
IllegalAccessException, InstantiationException { IllegalAccessException, InstantiationException
PluginClassLoader loader = classloaders.get(plugin); {
return loader.loadClass(className); PluginClassLoader loader = classloaders.get( plugin );
return loader.loadClass( className );
} }
/** /**
...@@ -688,130 +731,81 @@ public class PluginManager { ...@@ -688,130 +731,81 @@ public class PluginManager {
* @return the plugin dev environment, or <tt>null</tt> if development * @return the plugin dev environment, or <tt>null</tt> if development
* mode is not enabled for the plugin. * mode is not enabled for the plugin.
*/ */
public PluginDevEnvironment getDevEnvironment(Plugin plugin) { public PluginDevEnvironment getDevEnvironment( Plugin plugin )
return pluginDevelopment.get(plugin); {
return pluginDevelopment.get( plugin );
} }
/** /**
* Returns the name of a plugin. The value is retrieved from the plugin.xml file * @deprecated Moved to {@link PluginMetadataHelper#getName(Plugin)}.
* of the plugin. If the value could not be found, <tt>null</tt> will be returned.
* Note that this value is distinct from the name of the plugin directory.
*
* @param plugin the plugin.
* @return the plugin's name.
*/ */
public String getName(Plugin plugin) { @Deprecated
String name = getElementValue(plugin, "/plugin/name"); public String getName( Plugin plugin )
String pluginName = pluginDirs.get(plugin).getFileName().toString(); {
if (name != null) { return PluginMetadataHelper.getName( plugin );
return AdminConsole.getAdminText(name, pluginName);
}
else {
return pluginName;
}
} }
/** /**
* Returns the description of a plugin. The value is retrieved from the plugin.xml file * @deprecated Moved to {@link PluginMetadataHelper#getDescription(Plugin)}.
* of the plugin. If the value could not be found, <tt>null</tt> will be returned.
*
* @param plugin the plugin.
* @return the plugin's description.
*/ */
public String getDescription(Plugin plugin) { @Deprecated
String pluginName = pluginDirs.get(plugin).getFileName().toString(); public String getDescription( Plugin plugin )
return AdminConsole.getAdminText(getElementValue(plugin, "/plugin/description"), pluginName); {
return PluginMetadataHelper.getDescription( plugin );
} }
/** /**
* Returns the author of a plugin. The value is retrieved from the plugin.xml file * @deprecated Moved to {@link PluginMetadataHelper#getAuthor(Plugin)}.
* of the plugin. If the value could not be found, <tt>null</tt> will be returned.
*
* @param plugin the plugin.
* @return the plugin's author.
*/ */
public String getAuthor(Plugin plugin) { @Deprecated
return getElementValue(plugin, "/plugin/author"); public String getAuthor( Plugin plugin )
{
return PluginMetadataHelper.getAuthor( plugin );
} }
/** /**
* Returns the version of a plugin. The value is retrieved from the plugin.xml file * @deprecated Moved to {@link PluginMetadataHelper#getVersion(Plugin)}.
* of the plugin. If the value could not be found, <tt>null</tt> will be returned.
*
* @param plugin the plugin.
* @return the plugin's version.
*/ */
public String getVersion(Plugin plugin) { @Deprecated
return getElementValue(plugin, "/plugin/version"); public String getVersion( Plugin plugin )
{
return PluginMetadataHelper.getVersion( plugin );
} }
/** /**
* Returns the minimum server version this plugin can run within. The value is retrieved from the plugin.xml file * @deprecated Moved to {@link PluginMetadataHelper#getMinServerVersion(Plugin)}.
* of the plugin. If the value could not be found, <tt>null</tt> will be returned.
*
* @param plugin the plugin.
* @return the plugin's version.
*/ */
public String getMinServerVersion(Plugin plugin) { @Deprecated
return getElementValue(plugin, "/plugin/minServerVersion"); public String getMinServerVersion( Plugin plugin )
{
return PluginMetadataHelper.getMinServerVersion( plugin );
} }
/** /**
* Returns the database schema key of a plugin, if it exists. The value is retrieved * @deprecated Moved to {@link PluginMetadataHelper#getDatabaseKey(Plugin)}.
* from the plugin.xml file of the plugin. If the value could not be found, <tt>null</tt>
* will be returned.
*
* @param plugin the plugin.
* @return the plugin's database schema key or <tt>null</tt> if it doesn't exist.
*/ */
public String getDatabaseKey(Plugin plugin) { @Deprecated
return getElementValue(plugin, "/plugin/databaseKey"); public String getDatabaseKey( Plugin plugin )
{
return PluginMetadataHelper.getDatabaseKey( plugin );
} }
/** /**
* Returns the database schema version of a plugin, if it exists. The value is retrieved * @deprecated Moved to {@link PluginMetadataHelper#getDatabaseVersion(Plugin)}.
* from the plugin.xml file of the plugin. If the value could not be found, <tt>-1</tt>
* will be returned.
*
* @param plugin the plugin.
* @return the plugin's database schema version or <tt>-1</tt> if it doesn't exist.
*/ */
public int getDatabaseVersion(Plugin plugin) { @Deprecated
String versionString = getElementValue(plugin, "/plugin/databaseVersion"); public int getDatabaseVersion( Plugin plugin )
if (versionString != null) { {
try { return PluginMetadataHelper.getDatabaseVersion( plugin );
return Integer.parseInt(versionString.trim());
}
catch (NumberFormatException nfe) {
Log.error("Unable to parse the database version for plugin '{}'.", getName( plugin ), nfe);
}
}
return -1;
} }
/** /**
* Returns the license agreement type that the plugin is governed by. The value * @deprecated Moved to {@link PluginMetadataHelper#getLicense(Plugin)}.
* is retrieved from the plugin.xml file of the plugin. If the value could not be
* found, {@link License#other} is returned.
*
* @param plugin the plugin.
* @return the plugin's license agreement.
*/ */
public License getLicense(Plugin plugin) { @Deprecated
String licenseString = getElementValue(plugin, "/plugin/licenseType"); public License getLicense( Plugin plugin )
if (licenseString != null) { {
try { return PluginMetadataHelper.getLicense( plugin );
// Attempt to load the get the license type. We lower-case and
// trim the license type to give plugin author's a break. If the
// license type is not recognized, we'll log the error and default
// to "other".
return License.valueOf(licenseString.toLowerCase().trim());
}
catch (IllegalArgumentException iae) {
Log.error("Unrecognized license type '{}' for plugin '{}'.", licenseString.toLowerCase().trim(), getName( plugin ), iae);
}
}
return License.other;
} }
/** /**
...@@ -820,350 +814,181 @@ public class PluginManager { ...@@ -820,350 +814,181 @@ public class PluginManager {
* @param plugin the plugin. * @param plugin the plugin.
* @return the classloader of the plugin. * @return the classloader of the plugin.
*/ */
public PluginClassLoader getPluginClassloader(Plugin plugin) { public PluginClassLoader getPluginClassloader( Plugin plugin )
return classloaders.get(plugin); {
} return classloaders.get( plugin );
/**
* Returns the value of an element selected via an xpath expression from
* a Plugin's plugin.xml file.
*
* @param plugin the plugin.
* @param xpath the xpath expression.
* @return the value of the element selected by the xpath expression.
*/
private String getElementValue(Plugin plugin, String xpath) {
Path pluginDir = pluginDirs.get(plugin);
if (pluginDir == null) {
return null;
}
try {
Path pluginConfig = pluginDir.resolve("plugin.xml");
if (Files.exists(pluginConfig)) {
SAXReader saxReader = new SAXReader();
saxReader.setEncoding("UTF-8");
Document pluginXML = saxReader.read(pluginConfig.toFile());
Element element = (Element)pluginXML.selectSingleNode(xpath);
if (element != null) {
return element.getTextTrim();
}
}
}
catch (Exception e) {
Log.error("Unable to get element value '{}' from plugin.xml of plugin '{}':", xpath, getName(plugin), e);
}
return null;
}
/**
* An enumberation for plugin license agreement types.
*/
@SuppressWarnings({"UnnecessarySemicolon"}) // Support for QDox Parser
public enum License {
/**
* The plugin is distributed using a commercial license.
*/
commercial,
/**
* The plugin is distributed using the GNU Public License (GPL).
*/
gpl,
/**
* The plugin is distributed using the Apache license.
*/
apache,
/**
* The plugin is for internal use at an organization only and is not re-distributed.
*/
internal,
/**
* The plugin is distributed under another license agreement not covered by
* one of the other choices. The license agreement should be detailed in the
* plugin Readme.
*/
other;
} }
/**
* A service that monitors the plugin directory for plugins. It periodically
* checks for new plugin JAR files and extracts them if they haven't already
* been extracted. Then, any new plugin directories are loaded.
*/
private class PluginMonitor implements Runnable {
/**
* Tracks if the monitor is currently running.
*/
private boolean running = false;
/**
* True if the monitor has been executed at least once. After the first iteration in {@link #run}
* this variable will always be true.
* */
private boolean executed = false;
/** /**
* True when it's the first time the plugin monitor process runs. This is helpful for * Deletes a directory.
* bootstrapping purposes. *
* @param dir the directory to delete.
* @return true if the directory was deleted.
*/ */
private boolean firstRun = true; static boolean deleteDir( Path dir )
{
try
{
if ( Files.isDirectory( dir ) )
{
Files.walkFileTree( dir, new SimpleFileVisitor<Path>()
{
@Override @Override
public void run() { public FileVisitResult visitFile( Path file, BasicFileAttributes attrs ) throws IOException
// If the task is already running, return. {
synchronized (this) { try
if (running) { {
return; Files.deleteIfExists( file );
} }
running = true; catch ( IOException e )
} {
try { Log.debug( "Plugin removal: could not delete: {}", file );
running = true; throw e;
// Look for extra plugin directories specified as a system property.
String pluginDirs = System.getProperty("pluginDirs");
if (pluginDirs != null) {
StringTokenizer st = new StringTokenizer(pluginDirs, ", ");
while (st.hasMoreTokens()) {
String dir = st.nextToken();
if (!devPlugins.contains(dir)) {
loadPlugin(Paths.get(dir));
devPlugins.add(dir);
}
}
}
// Turn the list of JAR/WAR files into a set so that we can do lookups.
Set<String> jarSet = new HashSet<>();
try (DirectoryStream<Path> directoryStream = Files.newDirectoryStream(pluginDirectory, new DirectoryStream.Filter<Path>() {
@Override
public boolean accept(Path pathname) throws IOException {
String fileName = pathname.getFileName().toString().toLowerCase();
return (fileName.endsWith(".jar") || fileName.endsWith(".war"));
}
})) {
for (Path jarFile : directoryStream) {
jarSet.add(jarFile.getFileName().toString().toLowerCase());
String pluginName = jarFile.getFileName().toString().substring(0,
jarFile.getFileName().toString().length() - 4).toLowerCase();
// See if the JAR has already been exploded.
Path dir = pluginDirectory.resolve(pluginName);
// Store the JAR/WAR file that created the plugin folder
pluginFiles.put(pluginName, jarFile);
// If the JAR hasn't been exploded, do so.
if (Files.notExists(dir)) {
unzipPlugin(pluginName, jarFile, dir);
}
// See if the JAR is newer than the directory. If so, the plugin
// needs to be unloaded and then reloaded.
else if (Files.getLastModifiedTime(jarFile).toMillis() > Files.getLastModifiedTime(dir).toMillis()) {
// If this is the first time that the monitor process is running, then
// plugins won't be loaded yet. Therefore, just delete the directory.
if (firstRun) {
int count = 0;
// Attempt to delete the folder for up to 5 seconds.
while (!deleteDir(dir) && count++ < 5) {
Thread.sleep(1000);
}
}
else {
unloadPlugin(pluginName);
}
// If the delete operation was a success, unzip the plugin.
if (Files.notExists(dir)) {
unzipPlugin(pluginName, jarFile, dir);
}
}
} }
return FileVisitResult.CONTINUE;
} }
List<Path> dirs = new ArrayList<>();
try (DirectoryStream<Path> directoryStream = Files.newDirectoryStream(pluginDirectory, new DirectoryStream.Filter<Path>() {
@Override @Override
public boolean accept(Path pathname) throws IOException { public FileVisitResult postVisitDirectory( Path dir, IOException exc ) throws IOException
return Files.isDirectory(pathname); {
} try
})) { {
for (Path path : directoryStream) { Files.deleteIfExists( dir );
dirs.add(path); }
} catch ( IOException e )
} {
Log.debug( "Plugin removal: could not delete: {}", dir );
// Sort the list of directories so that the "admin" plugin is always throw e;
// first in the list.
Collections.sort(dirs, new Comparator<Path>() {
public int compare(Path file1, Path file2) {
if (file1.getFileName().toString().equals("admin")) {
return -1;
} else if (file2.getFileName().toString().equals("admin")) {
return 1;
} else {
return file1.compareTo(file2);
}
}
});
// See if any currently running plugins need to be unloaded
// due to the JAR file being deleted (ignore admin plugin).
// Build a list of plugins to delete first so that the plugins
// keyset isn't modified as we're iterating through it.
List<String> toDelete = new ArrayList<>();
for (Path pluginDir : dirs) {
String pluginName = pluginDir.getFileName().toString();
if (pluginName.equals("admin")) {
continue;
}
if (!jarSet.contains(pluginName + ".jar")) {
if (!jarSet.contains(pluginName + ".war")) {
Log.info( "Plugin '{}' was removed from the file system.", pluginName);
toDelete.add(pluginName);
} }
return FileVisitResult.CONTINUE;
} }
} );
} }
for (String pluginName : toDelete) { return Files.notExists( dir ) || Files.deleteIfExists( dir );
unloadPlugin(pluginName);
} }
catch ( IOException e )
// Load all plugins that need to be loaded. {
boolean somethingChanged = false; return Files.notExists( dir );
for (Path dirFile : dirs) {
// If the plugin hasn't already been started, start it.
if (Files.exists(dirFile) && !plugins.containsKey(dirFile.getFileName().toString())) {
somethingChanged = true;
loadPlugin(dirFile);
} }
} }
if ( somethingChanged ) { /**
Log.info( "Finished processing all plugins." ); * Registers a PluginListener, which will now start receiving events regarding plugin creation and destruction.
*
* When the listener was already registered, this method will have no effect.
*
* @param listener the listener to be notified (cannot be null).
*/
public void addPluginListener( PluginListener listener )
{
pluginListeners.add( listener );
} }
// Set that at least one iteration was done. That means that "all available" plugins /**
// have been loaded by now. * Deregisters a PluginListener, which will no longer receive events.
if (!XMPPServer.getInstance().isSetupMode()) { *
executed = true; * When the listener was never added, this method will have no effect.
*
* @param listener the listener to be removed (cannot be null).
*/
public void removePluginListener( PluginListener listener )
{
pluginListeners.remove( listener );
} }
// Trigger event that plugins have been monitored /**
* Registers a PluginManagerListener, which will now start receiving events regarding plugin management.
*
* @param listener the listener to be notified (cannot be null).
*/
public void addPluginManagerListener( PluginManagerListener listener )
{
pluginManagerListeners.add( listener );
if ( isExecuted() )
{
firePluginsMonitored(); firePluginsMonitored();
} }
catch (Throwable e) {
Log.error("An unexpected exception occurred:", e);
}
// Finished running task.
synchronized (this) {
running = false;
}
// Process finished, so set firstRun to false (setting it multiple times doesn't hurt).
firstRun = false;
} }
/** /**
* Unzips a plugin from a JAR file into a directory. If the JAR file * Deregisters a PluginManagerListener, which will no longer receive events.
* isn't a plugin, this method will do nothing. *
* When the listener was never added, this method will have no effect.
* *
* @param pluginName the name of the plugin. * @param listener the listener to be notified (cannot be null).
* @param file the JAR file
* @param dir the directory to extract the plugin to.
*/ */
private void unzipPlugin(String pluginName, Path file, Path dir) { public void removePluginManagerListener( PluginManagerListener listener )
try (ZipFile zipFile = new JarFile(file.toFile())) { {
// Ensure that this JAR is a plugin. pluginManagerListeners.remove( listener );
if (zipFile.getEntry("plugin.xml") == null) {
return;
}
Files.createDirectory(dir);
// Set the date of the JAR file to the newly created folder
Files.setLastModifiedTime(dir, Files.getLastModifiedTime(file));
Log.debug("Extracting plugin '{}'...", pluginName);
for (Enumeration e = zipFile.entries(); e.hasMoreElements();) {
JarEntry entry = (JarEntry)e.nextElement();
Path entryFile = dir.resolve(entry.getName());
// Ignore any manifest.mf entries.
if (entry.getName().toLowerCase().endsWith("manifest.mf")) {
continue;
}
if (!entry.isDirectory()) {
Files.createDirectories(entryFile.getParent());
try (InputStream zin = zipFile.getInputStream(entry)) {
Files.copy(zin, entryFile, StandardCopyOption.REPLACE_EXISTING);
} }
/**
* Notifies all registered PluginListener instances that a new plugin was created.
*
* @param name The name of the plugin
* @param plugin the plugin.
*/
void firePluginCreatedEvent( String name, Plugin plugin )
{
for ( final PluginListener listener : pluginListeners )
{
try
{
listener.pluginCreated( name, plugin );
} }
} catch ( Exception ex )
Log.debug("Successfully extracted plugin '{}'.", pluginName); {
} Log.warn( "An exception was thrown when one of the pluginManagerListeners was notified of a 'created' event for plugin '{}'!", name, ex );
catch (Exception e) {
Log.error("An exception occurred while trying to extract plugin '{}':", pluginName, e);
} }
} }
} }
/** /**
* Deletes a directory. * Notifies all registered PluginListener instances that a plugin was destroyed.
* *
* @param dir the directory to delete. * @param name The name of the plugin
* @return true if the directory was deleted. * @param plugin the plugin.
*/ */
private boolean deleteDir(Path dir) { void firePluginDestroyedEvent( String name, Plugin plugin )
try { {
if (Files.isDirectory(dir)) { for ( final PluginListener listener : pluginListeners )
Files.walkFileTree(dir, new SimpleFileVisitor<Path>() { {
@Override try
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { {
try { listener.pluginDestroyed( name, plugin );
Files.deleteIfExists(file);
} catch (IOException e) {
Log.debug("Plugin removal: could not delete: {}", file);
throw e;
} }
return FileVisitResult.CONTINUE; catch ( Exception ex )
{
Log.warn( "An exception was thrown when one of the pluginManagerListeners was notified of a 'destroyed' event for plugin '{}'!", name, ex );
} }
@Override
public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
try {
Files.deleteIfExists(dir);
} catch (IOException e) {
Log.debug("Plugin removal: could not delete: {}", dir);
throw e;
}
return FileVisitResult.CONTINUE;
}
});
}
boolean deleted = Files.notExists(dir) || Files.deleteIfExists(dir);
if (deleted) {
// Remove the JAR/WAR file that created the plugin folder
pluginFiles.remove(dir.getFileName().toString());
}
return deleted;
} catch (IOException e) {
return Files.notExists(dir);
} }
} }
public void addPluginListener(PluginListener listener) { /**
pluginListeners.add(listener); * Notifies all registered PluginManagerListener instances that the service monitoring for plugin changes completed a
* periodic check.
*/
void firePluginsMonitored()
{
// Set that at least one iteration was done. That means that "all available" plugins
// have been loaded by now.
if ( !XMPPServer.getInstance().isSetupMode() )
{
executed = true;
} }
public void removePluginListener(PluginListener listener) { for ( final PluginManagerListener listener : pluginManagerListeners )
pluginListeners.remove(listener); {
try
{
listener.pluginsMonitored();
} }
catch ( Exception ex )
public void addPluginManagerListener(PluginManagerListener listener) { {
pluginManagerListeners.add(listener); Log.warn( "An exception was thrown when one of the pluginManagerListeners was notified of a 'monitored' event!", ex );
if (isExecuted()) {
firePluginsMonitored();
} }
} }
public void removePluginManagerListener(PluginManagerListener listener) {
pluginManagerListeners.remove(listener);
} }
} }
\ No newline at end of file
/*
* Copyright 2016 IgniteRealtime.org
*
* 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.openfire.container;
import org.dom4j.Document;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;
import org.jivesoftware.admin.AdminConsole;
import org.jivesoftware.openfire.XMPPServer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Path;
/**
* Various helper methods to retrieve plugin metadat from plugin.xml files.
*
* @author Guus der Kinderen, guus.der.kinderen@gmail.com
*/
public class PluginMetadataHelper
{
private static final Logger Log = LoggerFactory.getLogger( PluginMetadataHelper.class );
/**
* Returns the name of the directory of the parent for this plugin. The value is retrieved from the plugin.xml file
* of the plugin (which is casted down to lower-case). If the value could not be found, <tt>null</tt> will be returned.
*
* @param pluginDir the path of the plugin directory.
* @return the parent plugin's directory name
*/
public static String getParentPlugin( Path pluginDir )
{
final String name = getElementValue( pluginDir, "/plugin/parentPlugin" );
if ( name != null && !name.isEmpty() )
{
return name.toLowerCase();
}
return null;
}
/**
* Returns the canonical name for the plugin, derived from the plugin directory name.
*
* Note that this value can be different from the 'human readable' plugin name, as returned by {@link #getName(Path)}.
*
* Note that this method will return data only for plugins that have successfully been installed. To obtain data
* from plugin (directories) that have not (yet) been installed, refer to the overloaded method that takes a Path
* argument.
*
* @param plugin The plugin (cannot be null)
* @return the plugin's canonical name.
*/
public static String getCanonicalName( Plugin plugin )
{
return getCanonicalName( XMPPServer.getInstance().getPluginManager().getPluginPath( plugin ) );
}
/**
* Returns the canonical name for the plugin, derived from the plugin directory name.
*
* Note that this value can be different from the 'human readable' plugin name, as returned by {@link #getName(Path)}.
*
* @param pluginDir the path of the plugin directory.
* @return the plugin's canonical name.
*/
public static String getCanonicalName( Path pluginDir )
{
return pluginDir.getFileName().toString().toLowerCase();
}
/**
* Returns the name of a plugin. The value is retrieved from the plugin.xml file of the plugin. If the value could not
* be found, <tt>null</tt> will be returned. Note that this value is a 'human readable' name, which can be distinct
* from the name of the plugin directory as returned by {@link #getCanonicalName(Path)}.
*
* Note that this method will return data only for plugins that have successfully been installed. To obtain data
* from plugin (directories) that have not (yet) been installed, refer to the overloaded method that takes a Path
* argument.
*
* @param plugin The plugin (cannot be null)
* @return the plugin's human-readable name.
*/
public static String getName( Plugin plugin )
{
return getName( XMPPServer.getInstance().getPluginManager().getPluginPath( plugin ) );
}
/**
* Returns the name of a plugin. The value is retrieved from the plugin.xml file of the plugin. If the value could not
* be found, <tt>null</tt> will be returned. Note that this value is a 'human readable' name, which can be distinct
* from the name of the plugin directory as returned by {@link #getCanonicalName(Path)}.
*
* @param pluginDir the path of the plugin directory.
* @return the plugin's human-readable name.
*/
public static String getName( Path pluginDir )
{
final String name = getElementValue( pluginDir, "/plugin/name" );
final String pluginName = getCanonicalName( pluginDir );
if ( name != null )
{
return AdminConsole.getAdminText( name, pluginName );
}
else
{
return pluginName;
}
}
/**
* Returns the description of a plugin. The value is retrieved from the plugin.xml file of the plugin. If the value
* could not be found, <tt>null</tt> will be returned.
*
* Note that this method will return data only for plugins that have successfully been installed. To obtain data
* from plugin (directories) that have not (yet) been installed, refer to the overloaded method that takes a Path
* argument.
*
* @param plugin The plugin (cannot be null)
* @return the plugin's description.
*/
public static String getDescription( Plugin plugin )
{
return getDescription( XMPPServer.getInstance().getPluginManager().getPluginPath( plugin ) );
}
/**
* Returns the description of a plugin. The value is retrieved from the plugin.xml file of the plugin. If the value
* could not be found, <tt>null</tt> will be returned.
*
* @param pluginDir the path of the plugin directory.
* @return the plugin's description.
*/
public static String getDescription( Path pluginDir )
{
final String name = getCanonicalName( pluginDir );
final String description = getElementValue( pluginDir, "/plugin/description" );
return AdminConsole.getAdminText( description, name );
}
/**
* Returns the author of a plugin. The value is retrieved from the plugin.xml file of the plugin. If the value could
* not be found, <tt>null</tt> will be returned.
*
* Note that this method will return data only for plugins that have successfully been installed. To obtain data
* from plugin (directories) that have not (yet) been installed, refer to the overloaded method that takes a Path
* argument.
*
* @param plugin The plugin (cannot be null)
* @return the plugin's author.
*/
public static String getAuthor( Plugin plugin )
{
return getAuthor( XMPPServer.getInstance().getPluginManager().getPluginPath( plugin ) );
}
/**
* Returns the author of a plugin. The value is retrieved from the plugin.xml file of the plugin. If the value could
* not be found, <tt>null</tt> will be returned.
*
* @param pluginDir the path of the plugin directory.
* @return the plugin's author.
*/
public static String getAuthor( Path pluginDir )
{
return getElementValue( pluginDir, "/plugin/author" );
}
/**
* Returns the version of a plugin. The value is retrieved from the plugin.xml file of the plugin. If the value
* could not be found, <tt>null</tt> will be returned.
*
* Note that this method will return data only for plugins that have successfully been installed. To obtain data
* from plugin (directories) that have not (yet) been installed, refer to the overloaded method that takes a Path
* argument.
*
* @param plugin The plugin (cannot be null)
* @return the plugin's version.
*/
public static String getVersion( Plugin plugin )
{
return getVersion( XMPPServer.getInstance().getPluginManager().getPluginPath( plugin ) );
}
/**
* Returns the version of a plugin. The value is retrieved from the plugin.xml file of the plugin. If the value
* could not be found, <tt>null</tt> will be returned.
*
* @param pluginDir the path of the plugin directory.
* @return the plugin's version.
*/
public static String getVersion( Path pluginDir )
{
return getElementValue( pluginDir, "/plugin/version" );
}
/**
* Returns the minimum server version this plugin can run within. The value is retrieved from the plugin.xml file
* of the plugin. If the value could not be found, <tt>null</tt> will be returned.
*
* Note that this method will return data only for plugins that have successfully been installed. To obtain data
* from plugin (directories) that have not (yet) been installed, refer to the overloaded method that takes a Path
* argument.
*
* @param plugin The plugin (cannot be null)
* @return the plugin's minimum server version.
*/
public static String getMinServerVersion( Plugin plugin )
{
return getMinServerVersion( XMPPServer.getInstance().getPluginManager().getPluginPath( plugin ) );
}
/**
* Returns the minimum server version this plugin can run within. The value is retrieved from the plugin.xml file
* of the plugin. If the value could not be found, <tt>null</tt> will be returned.
*
* @param pluginDir the path of the plugin directory.
* @return the plugin's minimum server version.
*/
public static String getMinServerVersion( Path pluginDir )
{
return getElementValue( pluginDir, "/plugin/minServerVersion" );
}
/**
* Returns the database schema key of a plugin, if it exists. The value is retrieved from the plugin.xml file of the
* plugin. If the value could not be found, <tt>null</tt> will be returned.
*
* Note that this method will return data only for plugins that have successfully been installed. To obtain data
* from plugin (directories) that have not (yet) been installed, refer to the overloaded method that takes a Path
* argument.
*
* @param plugin The plugin (cannot be null)
* @return the plugin's database schema key or <tt>null</tt> if it doesn't exist.
*/
public static String getDatabaseKey( Plugin plugin )
{
return getDatabaseKey( XMPPServer.getInstance().getPluginManager().getPluginPath( plugin ) );
}
/**
* Returns the database schema key of a plugin, if it exists. The value is retrieved from the plugin.xml file of the
* plugin. If the value could not be found, <tt>null</tt> will be returned.
*
* @param pluginDir the path of the plugin directory.
* @return the plugin's database schema key or <tt>null</tt> if it doesn't exist.
*/
public static String getDatabaseKey( Path pluginDir )
{
return getElementValue( pluginDir, "/plugin/databaseKey" );
}
/**
* Returns the database schema version of a plugin, if it exists. The value is retrieved from the plugin.xml file of
* the plugin. If the value could not be found, <tt>-1</tt> will be returned.
*
* Note that this method will return data only for plugins that have successfully been installed. To obtain data
* from plugin (directories) that have not (yet) been installed, refer to the overloaded method that takes a Path
* argument.
*
* @param plugin The plugin (cannot be null)
* @return the plugin's database schema version or <tt>-1</tt> if it doesn't exist.
*/
public static int getDatabaseVersion( Plugin plugin )
{
return getDatabaseVersion( XMPPServer.getInstance().getPluginManager().getPluginPath( plugin ) );
}
/**
* Returns the database schema version of a plugin, if it exists. The value is retrieved from the plugin.xml file of
* the plugin. If the value could not be found, <tt>-1</tt> will be returned.
*
* @param pluginDir the path of the plugin directory.
* @return the plugin's database schema version or <tt>-1</tt> if it doesn't exist.
*/
public static int getDatabaseVersion( Path pluginDir )
{
String versionString = getElementValue( pluginDir, "/plugin/databaseVersion" );
if ( versionString != null )
{
try
{
return Integer.parseInt( versionString.trim() );
}
catch ( NumberFormatException nfe )
{
Log.error( "Unable to parse the database version for plugin '{}'.", getCanonicalName( pluginDir ), nfe );
}
}
return -1;
}
/**
* Returns the license agreement type that the plugin is governed by. The value is retrieved from the plugin.xml
* file of the plugin. If the value could not be found, {@link License#other} is returned.
*
* Note that this method will return data only for plugins that have successfully been installed. To obtain data
* from plugin (directories) that have not (yet) been installed, refer to the overloaded method that takes a Path
* argument.
*
* @param plugin The plugin (cannot be null)
* @return the plugin's license agreement.
*/
public static License getLicense( Plugin plugin )
{
return getLicense( XMPPServer.getInstance().getPluginManager().getPluginPath( plugin ) );
}
/**
* Returns the license agreement type that the plugin is governed by. The value is retrieved from the plugin.xml
* file of the plugin. If the value could not be found, {@link License#other} is returned.
*
* @param pluginDir the path of the plugin directory.
* @return the plugin's license agreement.
*/
public static License getLicense( Path pluginDir )
{
String licenseString = getElementValue( pluginDir, "/plugin/licenseType" );
if ( licenseString != null )
{
try
{
// Attempt to load the get the license type. We lower-case and trim the license type to give plugin
// author's a break. If the license type is not recognized, we'll log the error and default to "other".
return License.valueOf( licenseString.toLowerCase().trim() );
}
catch ( IllegalArgumentException iae )
{
Log.error( "Unrecognized license type '{}' for plugin '{}'.", licenseString.toLowerCase().trim(), getCanonicalName( pluginDir ), iae );
}
}
return License.other;
}
/**
* Returns the value of an element selected via an xpath expression from
* a Plugin's plugin.xml file.
*
* @param pluginDir the path of the plugin directory.
* @param xpath the xpath expression.
* @return the value of the element selected by the xpath expression.
*/
static String getElementValue( Path pluginDir, String xpath )
{
if ( pluginDir == null )
{
return null;
}
try
{
final Path pluginConfig = pluginDir.resolve( "plugin.xml" );
if ( Files.exists( pluginConfig ) )
{
final SAXReader saxReader = new SAXReader();
saxReader.setEncoding( "UTF-8" );
final Document pluginXML = saxReader.read( pluginConfig.toFile() );
final Element element = (Element) pluginXML.selectSingleNode( xpath );
if ( element != null )
{
return element.getTextTrim();
}
}
}
catch ( Exception e )
{
Log.error( "Unable to get element value '{}' from plugin.xml of plugin in '{}':", xpath, pluginDir, e );
}
return null;
}
}
/*
* Copyright 2016 IgniteRealtime.org
*
* 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.openfire.container;
import org.jivesoftware.openfire.XMPPServer;
import org.jivesoftware.util.JiveGlobals;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.*;
import java.util.*;
import java.util.concurrent.*;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.zip.ZipFile;
/**
* A service that monitors the plugin directory for plugins. It periodically checks for new plugin JAR files and
* extracts them if they haven't already been extracted. Then, any new plugin directories are loaded, using the
* PluginManager.
*
* @author Guus der Kinderen, guus.der.kinderen@gmail.com
*/
public class PluginMonitor
{
private static final Logger Log = LoggerFactory.getLogger( PluginMonitor.class );
private final PluginManager pluginManager;
private ScheduledExecutorService executor;
public PluginMonitor( final PluginManager pluginManager )
{
this.pluginManager = pluginManager;
}
/**
* Start periodically checking the plugin directory.
*/
public void start()
{
if ( executor != null )
{
executor.shutdown();
}
executor = new ScheduledThreadPoolExecutor( 1 );
// See if we're in development mode. If so, check for new plugins once every 5 seconds Otherwise, default to every 20 seconds.
if ( Boolean.getBoolean( "developmentMode" ) )
{
executor.scheduleWithFixedDelay( new MonitorTask(), 0, 5, TimeUnit.SECONDS );
}
else
{
executor.scheduleWithFixedDelay( new MonitorTask(), 0, 20, TimeUnit.SECONDS );
}
}
/**
* Stop periodically checking the plugin directory.
*/
public void stop()
{
if ( executor != null )
{
executor.shutdown();
}
}
/**
* Immediately run a check of the plugin directory.
*/
public void runNow( boolean blockUntilDone )
{
final Future<?> future = executor.submit( new MonitorTask() );
if ( blockUntilDone )
{
try
{
future.get();
}
catch ( Exception e )
{
Log.warn( "An exception occurred while waiting for a check of the plugin directory to complete.", e );
}
}
}
private class MonitorTask implements Runnable
{
@Override
public void run()
{
// Prevent two tasks from running in parallel by using the plugin monitor itself as a mutex.
synchronized ( PluginMonitor.this )
{
try
{
// The directory that contains all plugins.
final Path pluginsDirectory = pluginManager.getPluginsDirectory();
if ( !Files.isDirectory( pluginsDirectory ) || !Files.isReadable( pluginsDirectory ) )
{
Log.error( "Unable to process plugins. The plugins directory does not exist (or is no directory): {}", pluginsDirectory );
return;
}
// Turn the list of JAR/WAR files into a set so that we can do lookups.
final Set<String> jarSet = new HashSet<>();
// Explode all plugin files that have not yet been exploded (or need to be re-exploded).
try ( final DirectoryStream<Path> ds = Files.newDirectoryStream( pluginsDirectory, new DirectoryStream.Filter<Path>()
{
@Override
public boolean accept( final Path path ) throws IOException
{
if ( Files.isDirectory( path ) )
{
return false;
}
final String fileName = path.getFileName().toString().toLowerCase();
return ( fileName.endsWith( ".jar" ) || fileName.endsWith( ".war" ) );
}
} ) )
{
for ( final Path jarFile : ds )
{
final String fileName = jarFile.getFileName().toString();
final String pluginName = fileName.substring( 0, fileName.length() - 4 ).toLowerCase(); // strip extension.
jarSet.add( pluginName );
// See if the JAR has already been exploded.
final Path dir = pluginsDirectory.resolve( pluginName );
// See if the JAR is newer than the directory. If so, the plugin needs to be unloaded and then reloaded.
if ( Files.exists( dir ) && Files.getLastModifiedTime( jarFile ).toMillis() > Files.getLastModifiedTime( dir ).toMillis() )
{
// If this is the first time that the monitor process is running, then plugins won't be loaded yet. Therefore, just delete the directory.
if ( pluginManager.isExecuted() )
{
int count = 0;
// Attempt to delete the folder for up to 5 seconds.
while ( !PluginManager.deleteDir( dir ) && count++ < 5 )
{
Thread.sleep( 1000 );
}
}
else
{
// Not the first time? Properly unload the plugin.
pluginManager.unloadPlugin( pluginName );
}
}
// If the JAR needs to be exploded, do so.
if ( Files.notExists( dir ) )
{
unzipPlugin( pluginName, jarFile, dir );
}
}
}
// See if any currently running plugins need to be unloaded due to the JAR file being deleted. Note
// that unloading a parent plugin might cause more than one plugin to disappear. Don't reuse the
// directory stream afterwards!
try ( final DirectoryStream<Path> ds = Files.newDirectoryStream( pluginsDirectory, new DirectoryStream.Filter<Path>()
{
@Override
public boolean accept( final Path path ) throws IOException
{
if ( !Files.isDirectory( path ) )
{
return false;
}
final String pluginName = PluginMetadataHelper.getCanonicalName( path );
return !pluginName.equals( "admin" ) && !jarSet.contains( pluginName );
}
} ) )
{
for ( final Path path : ds )
{
final String pluginName = PluginMetadataHelper.getCanonicalName( path );
Log.info( "Plugin '{}' was removed from the file system.", pluginName );
pluginManager.unloadPlugin( pluginName );
}
}
// Load all plugins that need to be loaded. Make sure that the admin plugin is loaded first (as that
// should be available as soon as possible), followed by all other plugins. Ensure that parent plugins
// are loaded before their children.
try ( final DirectoryStream<Path> ds = Files.newDirectoryStream( pluginsDirectory, new DirectoryStream.Filter<Path>()
{
@Override
public boolean accept( final Path path ) throws IOException
{
return Files.isDirectory( path );
}
} ) )
{
// Look for extra plugin directories specified as a system property.
final Set<Path> devPlugins = new HashSet<>();
final String devPluginDirs = System.getProperty( "pluginDirs" );
if ( devPluginDirs != null )
{
final StringTokenizer st = new StringTokenizer( devPluginDirs, "," );
while ( st.hasMoreTokens() )
{
try
{
final String devPluginDir = st.nextToken().trim();
final Path devPluginPath = Paths.get( devPluginDir );
if ( Files.exists( devPluginPath ) && Files.isDirectory( devPluginPath ) )
{
devPlugins.add( devPluginPath );
}
else
{
Log.error( "Unable to load a dev plugin as its path (as supplied in the 'pluginDirs' system property) does not exist, or is not a directory. Offending path: [{}] (parsed from raw value [{}])", devPluginPath, devPluginDir );
}
}
catch ( InvalidPathException ex )
{
Log.error( "Unable to load a dev plugin as an invalid path was added to the 'pluginDirs' system property.", ex );
}
}
}
// Sort the list of directories so that the "admin" plugin is always first in the list, and 'parent'
// plugins always precede their children.
final Deque<List<Path>> dirs = sortPluginDirs( ds, devPlugins );
// Hierarchy processing could be parallel.
final Collection<Callable<Integer>> parallelProcesses = new ArrayList<>();
for ( final List<Path> hierarchy : dirs )
{
parallelProcesses.add( new Callable<Integer>()
{
@Override
public Integer call() throws Exception
{
int loaded = 0;
for ( final Path path : hierarchy )
{
// If the plugin hasn't already been started, start it.
final String pluginName = PluginMetadataHelper.getCanonicalName( path );
if ( pluginManager.getPlugin( pluginName ) == null )
{
pluginManager.loadPlugin( path );
loaded++;
}
}
return loaded;
}
} );
}
// Hierarchies could be processed in parallel. This is likely to be beneficial during the first
// execution of this monitor, as during later executions, most plugins will likely already be loaded.
final int parallelProcessMax = JiveGlobals.getIntProperty( "plugins.loading.max-parallel", 4 );
final int parallelProcessCount = ( pluginManager.isExecuted() ? 1 : parallelProcessMax );
final ExecutorService executorService = Executors.newFixedThreadPool( parallelProcessCount );
try
{
// Blocks until ready
final List<Future<Integer>> futures = executorService.invokeAll( parallelProcesses );
// Unless nothing happened, report that we're done loading plugins.
int pluginsLoaded = 0;
for ( Future<Integer> future : futures )
{
pluginsLoaded += future.get();
}
if ( pluginsLoaded > 0 && !XMPPServer.getInstance().isSetupMode() )
{
Log.info( "Finished processing all plugins." );
}
}
finally
{
executorService.shutdown();
}
// Trigger event that plugins have been monitored
pluginManager.firePluginsMonitored();
}
}
catch ( Throwable e )
{
Log.error( "An unexpected exception occurred:", 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 pluginName the name of the plugin.
* @param file the JAR file
* @param dir the directory to extract the plugin to.
*/
private void unzipPlugin( String pluginName, Path file, Path dir )
{
try ( ZipFile zipFile = new JarFile( file.toFile() ) )
{
// Ensure that this JAR is a plugin.
if ( zipFile.getEntry( "plugin.xml" ) == null )
{
return;
}
Files.createDirectory( dir );
// Set the date of the JAR file to the newly created folder
Files.setLastModifiedTime( dir, Files.getLastModifiedTime( file ) );
Log.debug( "Extracting plugin '{}'...", pluginName );
for ( Enumeration e = zipFile.entries(); e.hasMoreElements(); )
{
JarEntry entry = (JarEntry) e.nextElement();
Path entryFile = dir.resolve( entry.getName() );
// Ignore any manifest.mf entries.
if ( entry.getName().toLowerCase().endsWith( "manifest.mf" ) )
{
continue;
}
if ( !entry.isDirectory() )
{
Files.createDirectories( entryFile.getParent() );
try ( InputStream zin = zipFile.getInputStream( entry ) )
{
Files.copy( zin, entryFile, StandardCopyOption.REPLACE_EXISTING );
}
}
}
Log.debug( "Successfully extracted plugin '{}'.", pluginName );
}
catch ( Exception e )
{
Log.error( "An exception occurred while trying to extract plugin '{}':", pluginName, e );
}
}
/**
* Returns all plugin directories, in a deque of lists with these characteristics:
* <ol>
* <li>Every list is a hierarchy of parent/child plugins (or is a list of one element).</li>
* <li>Every list is ordered to ensure that all parent plugins have a lower index than their children.</li>
* <li>The first element of every list will be a plugin that has no 'parent' plugin.</li>
* <li>the first element of the first list will be the 'admin' plugin.</li>
* </ol>
*
* Plugins within the provided argument that refer to non-existing parent plugins will not be part of the returned
* collection.
*
* @param dirs Collections of paths that refer every plugin directory (but not the corresponding .jar/.war files).
* @return An ordered collection of paths.
*/
@SafeVarargs
private final Deque<List<Path>> sortPluginDirs( Iterable<Path>... dirs )
{
// Map all plugins to they parent plugin (lower-cased), using a null key for parent-less plugins;
final Map<String, Set<Path>> byParent = new HashMap<>();
for ( final Iterable<Path> iterable : dirs )
{
for ( final Path dir : iterable )
{
final String parent = PluginMetadataHelper.getParentPlugin( dir );
if ( !byParent.containsKey( parent ) )
{
byParent.put( parent, new HashSet<Path>() );
}
byParent.get( parent ).add( dir );
}
}
// Transform the map into a tree structure (where the root node is a placeholder without data).
final Node root = new Node();
populateTree( root, byParent );
// byParent should be consumed. Remaining entries are depending on a non-existing parent.
for ( Map.Entry<String, Set<Path>> entry : byParent.entrySet() )
{
if ( !entry.getValue().isEmpty() )
{
for ( final Path path : entry.getValue() )
{
final String name = PluginMetadataHelper.getCanonicalName( path );
Log.warn( "Unable to load plugin '{}' as its defined parent plugin '{}' is not installed.", name, entry.getKey() );
}
}
}
// Return a deque of lists, where each list is parent-child chain of plugins (the parents preceding its children).
final Deque<List<Path>> result = new ArrayDeque<>();
for ( final Node noParentPlugin : root.children )
{
final List<Path> hierarchy = new ArrayList<>();
walkTree( noParentPlugin, hierarchy );
// The admin plugin should go first
if ( noParentPlugin.getName().equals( "admin" ) )
{
result.addFirst( hierarchy );
}
else
{
result.addLast( hierarchy );
}
}
return result;
}
private void populateTree( final Node parent, Map<String, Set<Path>> byParent )
{
final String parentName = parent.path == null ? null : PluginMetadataHelper.getCanonicalName( parent.path );
final Set<Path> children = byParent.remove( parentName );
if ( children != null )
{
for ( final Path child : children )
{
final Node node = new Node();
node.path = child;
if ( !parent.children.add( node ) )
{
Log.warn( "Detected plugin duplicates for name: '{}'. Only one plugin will be loaded.", node.getName() );
}
// recurse to find further children.
populateTree( node, byParent );
}
}
}
private void walkTree( final Node node, List<Path> result )
{
result.add( node.path );
if ( node.children != null )
{
for ( Node child : node.children )
{
walkTree( child, result );
}
}
}
class Node
{
Path path;
SortedSet<Node> children = new TreeSet<>( new Comparator<Node>()
{
@Override
public int compare( Node o1, Node o2 )
{
return o1.getName().compareToIgnoreCase( o2.getName() );
}
} );
String getName()
{
return PluginMetadataHelper.getCanonicalName( path );
}
}
}
}
...@@ -85,14 +85,7 @@ ...@@ -85,14 +85,7 @@
} }
if (csrf_check && deletePlugin != null) { if (csrf_check && deletePlugin != null) {
File pluginDir = pluginManager.getPluginDirectory(pluginManager.getPlugin(deletePlugin)); pluginManager.deletePlugin( deletePlugin );
File pluginJar = new File(pluginDir.getParent(), pluginDir.getName() + ".jar");
// Also try the .war extension.
if (!pluginJar.exists()) {
pluginJar = new File(pluginDir.getParent(), pluginDir.getName() + ".war");
}
pluginJar.delete();
pluginManager.unloadPlugin(pluginDir.getName());
// Log the event // Log the event
webManager.logEvent("deleted plugin "+deletePlugin, null); webManager.logEvent("deleted plugin "+deletePlugin, null);
response.sendRedirect("plugin-admin.jsp?deletesuccess=true"); response.sendRedirect("plugin-admin.jsp?deletesuccess=true");
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment