synchronized void unloadPlugin( String canonicalName )
    {
        Log.debug( "Unloading plugin '{}'...", canonicalName );

        failureToLoadCount.remove( canonicalName );
        lastLoadWarnings.remove(canonicalName);

        Plugin plugin = pluginsLoaded.get( canonicalName );
        if ( plugin != null )
        {
            // See if any child plugins are defined.
            if ( parentPluginMap.containsKey( plugin ) )
            {
                String[] childPlugins = parentPluginMap.get( plugin ).toArray(new String[0]);
                for ( String childPlugin : childPlugins )
                {
                    Log.debug( "Unloading child plugin: '{}'.", childPlugin );
                    childPluginMap.remove( pluginsLoaded.get( childPlugin ) );
                    unloadPlugin( childPlugin );
                }
                parentPluginMap.remove( plugin );
            }

            Path webXML = pluginDirectory.resolve( canonicalName ).resolve( "web" ).resolve( "WEB-INF" ).resolve( "web.xml" );
            if ( Files.exists( webXML ) )
            {
                AdminConsole.removeModel( canonicalName );
                PluginServlet.unregisterServlets( webXML.toFile() );
            }
            Path customWebXML = pluginDirectory.resolve( canonicalName ).resolve( "web" ).resolve( "WEB-INF" ).resolve( "web-custom.xml" );
            if ( Files.exists( customWebXML ) )
            {
                PluginServlet.unregisterServlets( customWebXML.toFile() );
            }

            // Wrap destroying the plugin in a try/catch block. Otherwise, an exception raised
            // in the destroy plugin process will disrupt the whole unloading process. It's still
            // 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
            // resources.
            try
            {
                plugin.destroyPlugin();
                Log.debug( "Destroyed plugin '{}'.", canonicalName );
            }
            catch ( Exception e )
            {
                Log.error( "An exception occurred while unloading plugin '{}':", canonicalName, e );
            }
        }

        // Remove references to the plugin so it can be unloaded from memory
        // 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
        // and in a subsequent refresh it will appear if failed to be removed
        pluginsLoaded.remove( canonicalName );
        final String pluginName = getMetadata(canonicalName).getName();
        Log.info("Removing all System Properties for the plugin '{}'", pluginName);
        SystemProperty.removePropertiesForPlugin(pluginName);
        Path pluginFile = pluginDirs.remove( canonicalName );
        PluginClassLoader pluginLoader = classloaders.remove( plugin );
        PluginMetadata metadata = pluginMetadata.remove( canonicalName );

        // try to close the cached jar files from the plugin class loader
        if ( pluginLoader != null )
        {
            pluginLoader.unloadJarFiles();

            try {
                pluginLoader.close(); // OF-2348
            } catch (IOException e) {
                Log.warn( "Closing plugin loader failed for '{}':", canonicalName , e);
            }
        }
        else
        {
            Log.warn( "No plugin loader found for '{}'.", canonicalName );
        }

        // 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
        // plugin are still in memory.
        Path dir = pluginDirectory.resolve( canonicalName );
        // Give the plugin 2 seconds to unload.
        try
        {
            Thread.sleep( 2000 );
            // Ask the system to clean up references.
            System.gc();
            int count = 0;
            while ( !deleteDir( dir ) && count++ < 5 )
            {
                Log.warn( "Error unloading plugin '{}'. Will attempt again momentarily.", canonicalName );
                Thread.sleep( 8000 );
                // Ask the system to clean up references.
                System.gc();
            }
        }
        catch ( InterruptedException e )
        {
            Log.debug( "Stopped waiting for plugin '{}' to be fully unloaded.", canonicalName, e );
        }

        if ( plugin != null && Files.notExists( dir ) )
        {
            // Unregister plugin caches
            PluginCacheRegistry.getInstance().unregisterCaches( canonicalName );

            // See if this is a child plugin. If it is, we should unload
            // the parent plugin as well.
            if ( childPluginMap.containsKey( plugin ) )
            {
                String parentPluginName = childPluginMap.get( plugin );
                Plugin parentPlugin = pluginsLoaded.get( parentPluginName );
                List<String> childrenPlugins = parentPluginMap.get( parentPlugin );

                childrenPlugins.remove( canonicalName );
                childPluginMap.remove( plugin );

                // 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,
                // it's proper if the parent "gets informed first" about the dying child when the
                // child is the one being killed first.
                if ( parentPlugin instanceof PluginListener )
                {
                    PluginListener listener;
                    listener = (PluginListener) parentPlugin;
                    listener.pluginDestroyed( canonicalName, plugin );
                }
                unloadPlugin( parentPluginName );
            }
            firePluginDestroyedEvent( canonicalName, plugin );
            Log.info( "Successfully unloaded plugin '{}'.", canonicalName );
        }
        else if ( plugin != null )
        {
            //FIXME this make no sense, while state of plugin is undetermined, (Destroy is partly executed, files are/are not removed)
            Log.info( "Restore references since we failed to remove the plugin '{}'.", canonicalName );
            pluginsLoaded.put( canonicalName, plugin );
            pluginDirs.put( canonicalName, pluginFile );
            classloaders.put( plugin, pluginLoader );
            pluginMetadata.put( canonicalName, metadata );
        }
    }
Unloads a plugin. The {@link Plugin#destroyPlugin()} method will be called and then any resources will be
released. The name should be the canonical name of the plugin (based on the plugin directory name) and not the
human readable name as given by the plugin meta-data.

This method only removes 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  time the plugin monitor process
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.

@param canonicalName the canonical name of the plugin to unload.
xmppserver/src/main/java/org/jivesoftware/openfire/container/PluginManager.java