Java tutorial
/* * Copyright (c) 2009, 2010, 2011, 2012, 2013, B3log Team * * 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.b3log.latke.plugin; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.lang.reflect.Method; import java.net.URL; import java.net.URLClassLoader; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Properties; import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; import org.apache.commons.io.FileUtils; import org.apache.commons.lang.StringUtils; import org.b3log.latke.Latkes; import org.b3log.latke.RuntimeEnv; import org.b3log.latke.cache.Cache; import org.b3log.latke.cache.CacheFactory; import org.b3log.latke.cache.local.memory.LruMemoryCache; import org.b3log.latke.event.AbstractEventListener; import org.b3log.latke.event.Event; import org.b3log.latke.event.EventException; import org.b3log.latke.event.EventManager; import org.b3log.latke.model.Plugin; import org.b3log.latke.servlet.AbstractServletListener; import org.b3log.latke.util.Stopwatchs; import org.b3log.latke.util.Strings; import org.json.JSONException; import org.json.JSONObject; /** * Plugin loader. * * @author <a href="mailto:DL88250@gmail.com">Liang Ding</a> * @version 1.0.1.0, Dec 3, 2011 */ public final class PluginManager { /** * Logger. */ private static final Logger LOGGER = Logger.getLogger(PluginManager.class.getName()); /** * Type of loaded event. */ public static final String PLUGIN_LOADED_EVENT = "pluginLoadedEvt"; /** * Plugin Cache. */ private static final String PLUGIN_CACHE_NAME = "pluginCache"; /** * Plugins cache. * * <p> * Caches plugins with the key "plugins" and its value is the real holder, * a map: * <"hosting view name", plugins> * </p> */ @SuppressWarnings("unchecked") private final Cache<String, HashMap<String, HashSet<AbstractPlugin>>> pluginCache; /** * Plugin root directory. */ public static final String PLUGIN_ROOT = AbstractServletListener.getWebRoot() + Plugin.PLUGINS; /** * Plugin class loaders. */ private Set<ClassLoader> classLoaders = new HashSet<ClassLoader>(); /** * Updates the specified plugin. * * @param plugin the specified plugin */ public void update(final AbstractPlugin plugin) { final String rendererId = plugin.getRendererId(); HashMap<String, HashSet<AbstractPlugin>> holder = pluginCache.get(PLUGIN_CACHE_NAME); if (null == holder) { LOGGER.info("Plugin cache miss, reload"); load(); holder = pluginCache.get(PLUGIN_CACHE_NAME); if (null == holder) { throw new IllegalStateException("Plugin cache state error!"); } } final HashSet<AbstractPlugin> set = holder.get(rendererId); // Refresh set.remove(plugin); set.add(plugin); plugin.changeStatus(); pluginCache.put(PLUGIN_CACHE_NAME, holder); } /** * Gets all plugins. * * @return all plugins, returns an empty list if not found */ public List<AbstractPlugin> getPlugins() { Map<String, HashSet<AbstractPlugin>> holder = pluginCache.get(PLUGIN_CACHE_NAME); if (null == holder) { LOGGER.info("Plugin cache miss, reload"); load(); holder = pluginCache.get(PLUGIN_CACHE_NAME); if (null == holder) { throw new IllegalStateException("Plugin cache state error!"); } } final List<AbstractPlugin> ret = new ArrayList<AbstractPlugin>(); for (final Map.Entry<String, HashSet<AbstractPlugin>> entry : holder.entrySet()) { ret.addAll(entry.getValue()); } return ret; } /** * Gets a plugin by the specified view name. * * @param viewName the specified view name * @return a plugin, returns an empty list if not found */ public Set<AbstractPlugin> getPlugins(final String viewName) { Map<String, HashSet<AbstractPlugin>> holder = pluginCache.get(PLUGIN_CACHE_NAME); if (null == holder) { LOGGER.info("Plugin cache miss, reload"); load(); holder = pluginCache.get(PLUGIN_CACHE_NAME); if (null == holder) { throw new IllegalStateException("Plugin cache state error!"); } } final Set<AbstractPlugin> ret = holder.get(viewName); if (null == ret) { return Collections.emptySet(); } return ret; } /** * Loads plugins from directory {@literal webRoot/plugins/}. */ public void load() { Stopwatchs.start("Load Plugins"); classLoaders.clear(); final File[] pluginsDirs = new File(PLUGIN_ROOT).listFiles(); final List<AbstractPlugin> plugins = new ArrayList<AbstractPlugin>(); HashMap<String, HashSet<AbstractPlugin>> holder = pluginCache.get(PLUGIN_CACHE_NAME); if (null == holder) { LOGGER.info("Creates an empty plugin holder"); holder = new HashMap<String, HashSet<AbstractPlugin>>(); } if (pluginsDirs != null) { for (int i = 0; i < pluginsDirs.length; i++) { final File pluginDir = pluginsDirs[i]; if (pluginDir.isDirectory() && !pluginDir.isHidden() && !pluginDir.getName().startsWith(".")) { try { LOGGER.log(Level.INFO, "Loading plugin under directory[{0}]", pluginDir.getName()); final AbstractPlugin plugin = load(pluginDir, holder); if (plugin != null) { plugins.add(plugin); } } catch (final Exception e) { LOGGER.log(Level.WARNING, "Load plugin under directory[" + pluginDir.getName() + "] failed", e); } } else { LOGGER.log(Level.WARNING, "It[{0}] is not a directory under " + "directory plugins, ignored", pluginDir.getName()); } } } try { EventManager.getInstance() .fireEventSynchronously(new Event<List<AbstractPlugin>>(PLUGIN_LOADED_EVENT, plugins)); } catch (final EventException e) { throw new RuntimeException("Plugin load error", e); } pluginCache.put(PLUGIN_CACHE_NAME, holder); Stopwatchs.end(); } /** * Loads a plugin by the specified plugin directory and put it into the * specified holder. * * @param pluginDir the specified plugin directory * @param holder the specified holder * @return loaded plugin * @throws Exception exception */ private AbstractPlugin load(final File pluginDir, final HashMap<String, HashSet<AbstractPlugin>> holder) throws Exception { final Properties props = new Properties(); props.load(new FileInputStream(pluginDir.getPath() + File.separator + "plugin.properties")); final File defaultClassesFileDir = new File(pluginDir.getPath() + File.separator + "classes"); final URL defaultClassesFileDirURL = defaultClassesFileDir.toURI().toURL(); final String webRoot = StringUtils.substringBeforeLast(AbstractServletListener.getWebRoot(), File.separator); final String classesFileDirPath = webRoot + props.getProperty("classesDirPath"); final File classesFileDir = new File(classesFileDirPath); final URL classesFileDirURL = classesFileDir.toURI().toURL(); final URLClassLoader classLoader = new URLClassLoader( new URL[] { defaultClassesFileDirURL, classesFileDirURL }, PluginManager.class.getClassLoader()); classLoaders.add(classLoader); String pluginClassName = props.getProperty(Plugin.PLUGIN_CLASS); if (StringUtils.isBlank(pluginClassName)) { pluginClassName = NotInteractivePlugin.class.getName(); } final String rendererId = props.getProperty(Plugin.PLUGIN_RENDERER_ID); if (StringUtils.isBlank(rendererId)) { LOGGER.log(Level.WARNING, "no renderer defined by this plugin[" + pluginDir.getName() + "]this plugin will be ignore!"); return null; } final Class<?> pluginClass = classLoader.loadClass(pluginClassName); LOGGER.log(Level.FINEST, "Loading plugin class[name={0}]", pluginClassName); final AbstractPlugin ret = (AbstractPlugin) pluginClass.newInstance(); ret.setRendererId(rendererId); setPluginProps(pluginDir, ret, props); registerEventListeners(props, classLoader, ret); register(ret, holder); ret.changeStatus(); return ret; } /** * Registers the specified plugin into the specified holder. * * @param plugin the specified plugin * @param holder the specified holder */ private void register(final AbstractPlugin plugin, final HashMap<String, HashSet<AbstractPlugin>> holder) { final String rendererId = plugin.getRendererId(); /** * the rendererId support multiple,using ';' to split. * and using Map to match the plugin is not flexible, a regular expression match pattern may be needed in futrue. */ final String[] redererIds = rendererId.split(";"); for (String rid : redererIds) { HashSet<AbstractPlugin> set = holder.get(rid); if (null == set) { set = new HashSet<AbstractPlugin>(); holder.put(rid, set); } set.add(plugin); } LOGGER.log(Level.FINER, "Registered plugin[name={0}, version={1}] for rendererId[name={2}], [{3}] plugins totally", new Object[] { plugin.getName(), plugin.getVersion(), rendererId, holder.size() }); } /** * Sets the specified plugin's properties from the specified properties file * under the specified plugin directory. * * @param pluginDir the specified plugin directory * @param plugin the specified plugin * @param props the specified properties file * @throws Exception exception */ private static void setPluginProps(final File pluginDir, final AbstractPlugin plugin, final Properties props) throws Exception { final String author = props.getProperty(Plugin.PLUGIN_AUTHOR); final String name = props.getProperty(Plugin.PLUGIN_NAME); final String version = props.getProperty(Plugin.PLUGIN_VERSION); final String types = props.getProperty(Plugin.PLUGIN_TYPES); LOGGER.log(Level.FINEST, "Plugin[name={0}, author={1}, version={2}, types={3}]", new Object[] { name, author, version, types }); plugin.setAuthor(author); plugin.setName(name); plugin.setId(name + "_" + version); plugin.setVersion(version); plugin.setDir(pluginDir); plugin.readLangs(); // try to find the setting config.json final File settingFile = new File(pluginDir.getPath() + File.separator + "config.json"); if (settingFile.exists()) { try { final String config = FileUtils.readFileToString(settingFile); final JSONObject jsonObject = new JSONObject(config); plugin.setSetting(jsonObject); } catch (final IOException ie) { LOGGER.log(Level.SEVERE, "reading the config of the plugin[" + name + "] failed", ie); } catch (final JSONException e) { LOGGER.log(Level.SEVERE, "convert the config of the plugin[" + name + "] to json failed", e); } } final String[] typeArray = types.split(","); for (int i = 0; i < typeArray.length; i++) { final PluginType type = PluginType.valueOf(typeArray[i]); plugin.addType(type); } } /** * Registers event listeners with the specified plugin properties, class * loader and plugin. * * <p> * <b>Note</b>: If the specified plugin has some event listeners, each * of these listener MUST implement a static method named * {@code getInstance} to obtain an instance of this listener. See * <a href="http://en.wikipedia.org/wiki/Singleton_pattern"> * Singleton Pattern</a> for more details. * </p> * * @param props the specified plugin properties * @param classLoader the specified class loader * @param plugin the specified plugin * @throws Exception exception */ private static void registerEventListeners(final Properties props, final URLClassLoader classLoader, final AbstractPlugin plugin) throws Exception { final String eventListenerClasses = props.getProperty(Plugin.PLUGIN_EVENT_LISTENER_CLASSES); final String[] eventListenerClassArray = eventListenerClasses.split(","); for (int i = 0; i < eventListenerClassArray.length; i++) { final String eventListenerClassName = eventListenerClassArray[i]; if (Strings.isEmptyOrNull(eventListenerClassName)) { LOGGER.log(Level.INFO, "No event listener to load for plugin[name={0}]", plugin.getName()); return; } LOGGER.log(Level.FINER, "Loading event listener[className={0}]", eventListenerClassName); final Class<?> eventListenerClass = classLoader.loadClass(eventListenerClassName); final Method getInstance = eventListenerClass.getMethod("getInstance"); final AbstractEventListener<?> eventListener = (AbstractEventListener) getInstance .invoke(eventListenerClass); final EventManager eventManager = EventManager.getInstance(); eventManager.registerListener(eventListener); LOGGER.log(Level.FINER, "Registered event listener[class={0}, eventType={1}] for plugin[name={2}]", new Object[] { eventListener.getClass(), eventListener.getEventType(), plugin.getName() }); } } /** * Gets the plugin class loaders. * * @return plugin class loaders */ public Set<ClassLoader> getClassLoaders() { return Collections.unmodifiableSet(classLoaders); } /** * Gets the {@link PluginManager} singleton. * * @return a plugin manager singleton */ public static PluginManager getInstance() { return PluginManagerSingletonHolder.SINGLETON; } /** * Plugin manager singleton holder. * * @author <a href="mailto:DL88250@gmail.com">Liang Ding</a> * @version 1.0.0.0, Jul 23, 2011 */ private static final class PluginManagerSingletonHolder { /** * Singleton. */ private static final PluginManager SINGLETON = new PluginManager(); /** * Private default constructor. */ private PluginManagerSingletonHolder() { } } /** * Private default constructor. */ @SuppressWarnings("unchecked") private PluginManager() { if (RuntimeEnv.BAE == Latkes.getRuntimeEnv()) { pluginCache = new LruMemoryCache<String, HashMap<String, HashSet<AbstractPlugin>>>(); } else { pluginCache = (Cache<String, HashMap<String, HashSet<AbstractPlugin>>>) CacheFactory .getCache(PLUGIN_CACHE_NAME); } } }