/**
* 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);
}
}
}