org.jtalks.jcommune.plugin.api.PluginLoader.java Source code

Java tutorial

Introduction

Here is the source code for org.jtalks.jcommune.plugin.api.PluginLoader.java

Source

/**
 * Copyright (C) 2011  JTalks.org Team
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 */
package org.jtalks.jcommune.plugin.api;

import org.apache.commons.lang.Validate;
import org.jtalks.common.service.exceptions.NotFoundException;
import org.jtalks.jcommune.model.dao.PluginConfigurationDao;
import org.jtalks.jcommune.model.entity.PluginConfiguration;
import org.jtalks.jcommune.plugin.api.exceptions.UnexpectedErrorException;
import org.jtalks.jcommune.plugin.api.filters.PluginFilter;
import org.jtalks.jcommune.plugin.api.filters.TypeFilter;
import org.jtalks.jcommune.plugin.api.core.Plugin;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.net.URLClassLoader;
import java.nio.file.*;
import java.util.ArrayList;
import java.util.List;
import java.util.ServiceLoader;

import static java.nio.file.StandardWatchEventKinds.ENTRY_CREATE;
import static java.nio.file.StandardWatchEventKinds.ENTRY_DELETE;
import static java.nio.file.StandardWatchEventKinds.ENTRY_MODIFY;

/**
 * Load plugins from path and save configuration for them.
 * Also load plugin for class name.
 *
 * @author Anuar_Nurmakanov
 * @author Evgeny Naumenko
 */
public class PluginLoader {
    private static final Logger LOGGER = LoggerFactory.getLogger(PluginLoader.class);

    private URLClassLoader classLoader;
    private WatchKey watchKey;
    private String folder;
    private List<Plugin> plugins;
    private WatchService watchService;
    private PluginConfigurationDao pluginConfigurationDao;

    /**
     * Constructs an instance for loading plugins from passed path to plugins directory.
     *
     * @param pluginsFolderPath      a path to a folder that contains plugins
     * @param pluginConfigurationDao to load and save configuration for loaded plugins
     * @throws java.io.IOException when it's impossible to start tracking changes in plugins folder
     */
    public PluginLoader(String pluginsFolderPath, PluginConfigurationDao pluginConfigurationDao)
            throws IOException {
        this.pluginConfigurationDao = pluginConfigurationDao;
        Validate.notEmpty(pluginsFolderPath);
        this.folder = this.resolveUserHome(pluginsFolderPath);
        Path path = Paths.get(folder);
        watchService = FileSystems.getDefault().newWatchService();
        watchKey = path.register(watchService, ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY);
    }

    private String resolveUserHome(String path) {
        if (path.contains("~")) {
            String home = System.getProperty("user.home");
            return path.replace("~", home);
        } else {
            return path;
        }
    }

    /**
     * Will be called by container after bean creation.
     */
    public void init() {
        this.initPluginList();
    }

    /**
     * Reloads plugin by calling {@link #getPlugins(org.jtalks.jcommune.plugin.api.filters.PluginFilter...)} method
     *
     * @param filters determines which plugins will be reloaded
     *
     * @see org.jtalks.jcommune.plugin.api.filters.TypeFilter
     * @see org.jtalks.jcommune.plugin.api.filters.NameFilter
     * @see org.jtalks.jcommune.plugin.api.filters.StateFilter
     */
    public void reloadPlugins(PluginFilter... filters) {
        getPlugins(filters);
    }

    /**
     * Returns actual list of plugins available. Client code should not cache the plugin
     * references and always use this method to obtain a plugin reference as needed.
     * Violation of this simple rule may cause memory leaks.
     *
     * @return list of plugins available at the moment
     */
    public synchronized List<Plugin> getPlugins(PluginFilter... filters) {
        this.synchronizePluginList();
        List<Plugin> filtered = new ArrayList<>(plugins.size());
        loadConfigurationFor(plugins);
        plugins: for (Plugin plugin : plugins) {
            for (PluginFilter filter : filters) {
                if (!filter.accept(plugin)) {
                    continue plugins;
                }
            }
            filtered.add(plugin);
        }
        LOGGER.trace("JCommune forum has {} plugins now.", filtered.size());
        return filtered;
    }

    private void synchronizePluginList() {
        List events = watchKey.pollEvents();
        if (!events.isEmpty()) {
            watchKey.reset();
            try {
                classLoader.close();
            } catch (IOException e) {
                LOGGER.error("Failed to close plugin class loader", e);
            }
            this.initPluginList();
        }
    }

    private synchronized void initPluginList() {
        classLoader = new PluginClassLoader(folder);
        ServiceLoader<Plugin> pluginLoader = ServiceLoader.load(Plugin.class, classLoader);
        List<Plugin> plugins = new ArrayList<>();
        for (Plugin plugin : pluginLoader) {
            plugins.add(plugin);
        }
        this.plugins = plugins;
    }

    /**
     * Get plugin by class name.
     *
     * @param cl class name
     * @return plugin
     */
    public Plugin getPluginByClassName(Class<? extends Plugin> cl) {
        PluginFilter pluginFilter = new TypeFilter(cl);
        List<Plugin> plugins = getPlugins(pluginFilter);
        return !plugins.isEmpty() ? plugins.get(0) : null;
    }

    private void loadConfigurationFor(List<Plugin> plugins) {
        for (Plugin plugin : plugins) {
            String name = plugin.getName();
            PluginConfiguration configuration;
            try {
                configuration = pluginConfigurationDao.get(name);
                if (configuration.getProperties().isEmpty()) {
                    // Wee can't use #setProperties method. It will lead to exception
                    configuration.getProperties().addAll(plugin.getDefaultConfiguration());
                }
            } catch (NotFoundException e) {
                configuration = new PluginConfiguration(name, false, plugin.getDefaultConfiguration());
                pluginConfigurationDao.saveOrUpdate(configuration);
            }

            try {
                plugin.configure(configuration);
            } catch (UnexpectedErrorException e) {
                LOGGER.error("Can't configure plugin during loading. Plugin name = " + plugin.getName());
            }
        }
    }

    /**
     * Will be called by container to release resource before bean destroying.
     */
    public void destroy() {
        try {
            classLoader.close();
            watchService.close();
        } catch (IOException e1) {
            LOGGER.error("Failed to close plugin class loader", e1);
        }
    }

}