synchronized boolean loadPlugin( String canonicalName, Path pluginDir )
    {
        final PluginMetadata metadata = PluginMetadata.getInstance( pluginDir );
        pluginMetadata.put( canonicalName, metadata );

        // Only load the admin plugin during setup mode.
        if ( XMPPServer.getInstance().isSetupMode() && !( canonicalName.equals( "admin" ) ) )
        {
            return false;
        }

        final Integer loadFailures = failureToLoadCount.get(canonicalName);
        if (loadFailures != null && loadFailures > JiveGlobals.getIntProperty("plugins.loading.retries", 5))
        {
            Log.debug("The unloaded file for plugin '{}' is silently ignored, as it has failed to load repeatedly.", canonicalName);
            return false;
        }

        // Clean up any warnings that were recorded during a previous attempt to load the plugin.
        lastLoadWarnings.remove(canonicalName);

        Log.debug( "Loading plugin '{}'...", canonicalName );
        try
        {
            final Path pluginConfig = pluginDir.resolve( "plugin.xml" );
            if ( !Files.exists( pluginConfig ) )
            {
                Log.warn( "Plugin '{}' could not be loaded: no plugin.xml file found.", canonicalName );
                failureToLoadCount.put( canonicalName, Integer.MAX_VALUE ); // Don't retry - this cannot be recovered from.
                lastLoadWarnings.put(canonicalName, LocaleUtils.getLocalizedString("plugin.admin.failed.invalidJar"));
                return false;
            }

            final Version currentServerVersion = XMPPServer.getInstance().getServerInfo().getVersion();

            // See if the plugin specifies a minimum version of Openfire required to run.
            if ( metadata.getMinServerVersion() != null )
            {
                // OF-1338: Ignore release status when comparing minimum server version requirement.
                if (metadata.getMinServerVersion().isNewerThan(currentServerVersion.ignoringReleaseStatus())) {
                    Log.warn( "Ignoring plugin '{}': requires server version {}. Current server version is {}.", canonicalName, metadata.getMinServerVersion(), currentServerVersion );
                    failureToLoadCount.put( canonicalName, Integer.MAX_VALUE ); // Don't retry - this cannot be recovered from.
                    lastLoadWarnings.put(canonicalName, LocaleUtils.getLocalizedString("plugin.admin.failed.minserverversion", List.of(metadata.getMinServerVersion().toString())));
                    return false;
                }
            }

            // See if the plugin specifies a maximum version of Openfire required to run.
            if ( metadata.getPriorToServerVersion() != null )
            {
                // OF-1338: Ignore release status when comparing maximum server version requirement.
                final Version compareVersion = new Version( currentServerVersion.getMajor(), currentServerVersion.getMinor(), currentServerVersion.getMicro(), null, -1 );
                if ( !metadata.getPriorToServerVersion().isNewerThan( compareVersion ) )
                {
                    Log.warn( "Ignoring plugin '{}': compatible with server versions up to but excluding {}. Current server version is {}.", canonicalName, metadata.getPriorToServerVersion(), currentServerVersion );
                    failureToLoadCount.put( canonicalName, Integer.MAX_VALUE ); // Don't retry - this cannot be recovered from.
                    lastLoadWarnings.put(canonicalName, LocaleUtils.getLocalizedString("plugin.admin.failed.priortoserverversion", List.of(metadata.getPriorToServerVersion().toString())));
                    return false;
                }
            }

            // See if the plugin specifies a minimum version of Java required to run.
            if ( metadata.getMinJavaVersion() != null )
            {
                final JavaSpecVersion runtimeVersion = new JavaSpecVersion( System.getProperty( "java.specification.version" ) );
                if ( metadata.getMinJavaVersion().isNewerThan( runtimeVersion ) )
                {
                    Log.warn( "Ignoring plugin '{}': requires Java specification version {}. Openfire is currently running in Java {}.", canonicalName, metadata.getMinJavaVersion(), System.getProperty( "java.specification.version" ) );
                    failureToLoadCount.put( canonicalName, Integer.MAX_VALUE ); // Don't retry - this cannot be recovered from.
                    lastLoadWarnings.put(canonicalName, LocaleUtils.getLocalizedString("plugin.admin.failed.minJavaVersion", List.of(metadata.getMinJavaVersion().toString(), runtimeVersion.getVersionString())));
                    return false;
                }
            }

            // 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 String parentCanonicalName = PluginMetadataHelper.getParentPlugin( pluginDir );
            if ( parentCanonicalName != 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 : pluginsLoaded.entrySet() )
                {
                    if ( entry.getKey().equalsIgnoreCase( parentCanonicalName ) )
                    {
                        parentPluginName = entry.getKey();
                        parentPlugin = entry.getValue();
                        break;
                    }
                }

                // See if the parent is loaded.
                if ( parentPlugin == null )
                {
                    Log.info( "Unable to load plugin '{}': parent plugin '{}' has not been loaded.", canonicalName, parentCanonicalName );
                    Integer count = failureToLoadCount.get( canonicalName );
                    if ( count == null ) {
                        count = 0;
                    }
                    failureToLoadCount.put( canonicalName, ++count );
                    lastLoadWarnings.put(canonicalName, LocaleUtils.getLocalizedString("plugin.admin.failed.missingParent", List.of(parentCanonicalName)));
                    return false;
                }
                pluginLoader = classloaders.get( parentPlugin );
            }
            else
            {
                // This is not a child plugin, so create a new class loader.
                pluginLoader = new PluginClassLoader();
            }

            // Add the plugin sources to the classloaded.
            pluginLoader.addDirectory( pluginDir.toFile() );

            // Initialise a logging context, if necessary
            final Path path = pluginDir.resolve("classes/log4j2.xml");
            if (Files.isRegularFile(path)) {
                synchronized (PluginManager.class) {
                    final LoggerContext loggerContext = LogManager.getContext(pluginLoader, false, path.toUri());
                    loggerContext.getLogger("To avoid LOG4J2-1094");
                }
            }

            // Instantiate the plugin!
            final Document pluginXML = SAXReaderUtil.readDocument( pluginConfig.toFile() );

            final String className = pluginXML.selectSingleNode( "/plugin/class" ).getText().trim();
            final Plugin plugin;
            final ClassLoader originalClassLoader = Thread.currentThread().getContextClassLoader();
            try {
                Thread.currentThread().setContextClassLoader(pluginLoader);
                plugin = (Plugin) pluginLoader.loadClass(className).newInstance();
            } finally {
                Thread.currentThread().setContextClassLoader(originalClassLoader);
            }
            // Bookkeeping!
            classloaders.put( plugin, pluginLoader );
            pluginsLoaded.put( canonicalName, plugin );
            pluginDirs.put( canonicalName, pluginDir );

            // If this is a child plugin, register it as such.
            if ( parentPlugin != null )
            {
                List<String> childrenPlugins = parentPluginMap.get( parentPlugin );
                if ( childrenPlugins == null )
                {
                    childrenPlugins = new ArrayList<>();
                    parentPluginMap.put( parentPlugin, childrenPlugins );
                }
                childrenPlugins.add( canonicalName );

                // Also register child to parent relationship.
                childPluginMap.put( plugin, parentPluginName );
            }

            // Check the plugin's database schema (if it requires one).
            if ( !DbConnectionManager.getSchemaManager().checkPluginSchema( plugin ) )
            {
                // The schema was not there and auto-upgrade failed.
                Log.error( "Error while loading plugin '{}': {}", canonicalName, LocaleUtils.getLocalizedString( "upgrade.database.failure" ) );
                lastLoadWarnings.put(canonicalName, LocaleUtils.getLocalizedString("plugin.admin.failed.databaseScript"));
                // Does not prevent the plugin from being loaded, as many database script errors are benign.
            }

            // Load any JSP's defined by the plugin.
            final Path webXML = pluginDir.resolve( "web" ).resolve( "WEB-INF" ).resolve( "web.xml" );
            if ( Files.exists( webXML ) )
            {
                PluginServlet.registerServlets( this, plugin, webXML.toFile() );
            }

            // Load any custom-defined servlets.
            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
            configureCaches( pluginDir, canonicalName );

            // Initialze the plugin.
            final ClassLoader oldLoader = Thread.currentThread().getContextClassLoader();
            Thread.currentThread().setContextClassLoader( pluginLoader );
            plugin.initializePlugin( this, pluginDir.toFile() );
            Log.debug( "Initialized plugin '{}'.", canonicalName );
            Thread.currentThread().setContextClassLoader( oldLoader );

            // If there a <adminconsole> section defined, register it.
            final Element adminElement = (Element) pluginXML.selectSingleNode( "/plugin/adminconsole" );
            if ( adminElement != null )
            {
                final Element appName = (Element) adminElement.selectSingleNode( "/plugin/adminconsole/global/appname" );
                if ( appName != null )
                {
                    // Set the plugin name so that the proper i18n String can be loaded.
                    appName.addAttribute( "plugin", canonicalName );
                }

                // If global images are specified, override their URL.
                Element imageEl = (Element) adminElement.selectSingleNode( "/plugin/adminconsole/global/logo-image" );
                if ( imageEl != null )
                {
                    imageEl.setText( "plugins/" + canonicalName + "/" + imageEl.getText() );
                    imageEl.addAttribute( "plugin", canonicalName ); // Set the plugin name so that the proper i18n String can be loaded.
                }
                imageEl = (Element) adminElement.selectSingleNode( "/plugin/adminconsole/global/login-image" );
                if ( imageEl != null )
                {
                    imageEl.setText( "plugins/" + canonicalName + "/" + imageEl.getText() );
                    imageEl.addAttribute( "plugin", canonicalName ); // 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.
                final List<Node> urls = adminElement.selectNodes( "//@url" );
                for ( final Node url : urls )
                {
                    final Attribute attr = (Attribute) url;
                    attr.setValue( "plugins/" + canonicalName + "/" + 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 the renderer knows where to load the i18n Strings from.
                final String[] elementNames = new String[]{ "tab", "sidebar", "item" };
                for ( final String elementName : elementNames )
                {
                    final List<Node> values = adminElement.selectNodes( "//" + elementName );
                    for ( final Node value : values )
                    {
                        final Element element = (Element) value;
                        // Make sure there's a name or description. Otherwise, no need to i18n settings.
                        if ( element.attribute( "name" ) != null || element.attribute( "value" ) != null )
                        {
                            element.addAttribute( "plugin", canonicalName );
                        }
                    }
                }

                AdminConsole.addModel( canonicalName, adminElement );
            }
            firePluginCreatedEvent( canonicalName, plugin );
            if (metadata.getVersion() != null) {
                Log.info( "Successfully loaded plugin '{}-{}'.", canonicalName, metadata.getVersion());
            } else {
                Log.info( "Successfully loaded plugin '{}'.", canonicalName);
            }

            failureToLoadCount.remove(canonicalName);
            return true;
        }
        catch ( Throwable e )
        {
            Log.error( "An exception occurred while loading plugin '{}':", canonicalName, e );
            Integer count = failureToLoadCount.get( canonicalName );
            if ( count == null ) {
                count = 0;
            }
            failureToLoadCount.put( canonicalName, ++count );
            lastLoadWarnings.put(canonicalName, LocaleUtils.getLocalizedString("plugin.admin.failed.unknown"));
            if (e instanceof InterruptedException) {
                Thread.currentThread().interrupt();
            }
            return false;
        }
    }
Loads a plugin.

@param pluginDir the plugin directory.
xmppserver/src/main/java/org/jivesoftware/openfire/container/PluginManager.java