content: /*
   * Latke - 一款以 JSON 为主的 Java Web 框架
   * Copyright (c) 2009-present, b3log.org
   *
   * Latke is licensed under Mulan PSL v2.
   * You can use this software according to the terms and conditions of the Mulan PSL v2.
   * You may obtain a copy of Mulan PSL v2 at:
   *         http://license.coscl.org.cn/MulanPSL2
   * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
   * See the Mulan PSL v2 for more details.
   */
  package org.b3log.latke.plugin;
  
  import org.apache.commons.io.FileUtils;
  import org.apache.commons.lang3.StringUtils;
  import org.apache.logging.log4j.Level;
  import org.apache.logging.log4j.LogManager;
  import org.apache.logging.log4j.Logger;
  import org.b3log.latke.Latkes;
  import org.b3log.latke.event.Event;
  import org.b3log.latke.event.EventManager;
  import org.b3log.latke.ioc.Inject;
  import org.b3log.latke.ioc.Singleton;
  import org.b3log.latke.model.Plugin;
  import org.b3log.latke.util.Stopwatchs;
  import org.json.JSONException;
  import org.json.JSONObject;
  
  import java.io.File;
  import java.io.FileInputStream;
  import java.io.IOException;
  import java.net.URL;
  import java.net.URLClassLoader;
  import java.nio.charset.Charset;
  import java.util.*;
  import java.util.stream.Collectors;
  
  /**
   * Plugin loader.
   *
   * @author <a href="http://88250.b3log.org">Liang Ding</a>
   * @author <a href="https://ld246.com/member/yanxingangsun">yanxingangsun</a>
   * @version 1.0.3.1, Jul 29, 2019
   */
  @Singleton
  public class PluginManager {
  
      /**
       * Logger.
       */
      private static final Logger LOGGER = LogManager.getLogger(PluginManager.class);
  
      /**
       * Type of loaded event.
       */
      public static final String PLUGIN_LOADED_EVENT = "pluginLoadedEvt";
  
      /**
       * Plugins cache.
       * <p>
       * Caches plugins with the key "plugins" and its value is the real holder, a map: &lt;"hosting view name", plugins&gt;
       * </p>
       */
      private final Map<String, Set<AbstractPlugin>> pluginCache = new HashMap<>();
  
      /**
       * Plugin class loaders.
       */
      private final Set<ClassLoader> classLoaders = new HashSet<>();
  
      /**
       * Event manager.
       */
      @Inject
      private EventManager eventManager;
  
      /**
       * Gets all plugins.
       *
       * @return all plugins, returns an empty list if not found
       */
      public List<AbstractPlugin> getPlugins() {
          if (pluginCache.isEmpty()) {
              LOGGER.info("Plugin cache miss, reload");
  
              load();
          }
  
          return pluginCache.values().stream().flatMap(Set::stream).collect(Collectors.toList());
      }
  
      /**
       * 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) {
          if (pluginCache.isEmpty()) {
              LOGGER.info("Plugin cache miss, reload");
  
              load();
          }
  
          return pluginCache.getOrDefault(viewName, Collections.emptySet());
      }
  
      /**
       * Loads plugins from directory {@literal webRoot/plugins/}.
       */
      public void load() {
          Stopwatchs.start("Load Plugins");
  
          classLoaders.clear();
  
          final List<String> pluginDirPaths = Latkes.listFiles("/plugins");
          final List<AbstractPlugin> plugins = new ArrayList<>();
          for (final String pluginDirPath : pluginDirPaths) {
              try {
                  LOGGER.log(Level.INFO, "Loading plugin under directory [{}]", pluginDirPath);
  
                  final AbstractPlugin plugin = load(pluginDirPath, pluginCache);
                  if (plugin != null) {
                      plugins.add(plugin);
                  }
              } catch (final Exception e) {
                  LOGGER.log(Level.WARN, "Load plugin under directory [" + pluginDirPath + "] failed", e);
              }
          }
  
          eventManager.fireEventSynchronously(new Event<>(PLUGIN_LOADED_EVENT, plugins));
  
          Stopwatchs.end();
      }
  
      /**
       * Loads a plugin by the specified plugin directory and put it into the specified holder.
       *
       * @param pluginDirPath the specified plugin directory
       * @param holder        the specified holder
       * @return loaded plugin
       * @throws Exception exception
       */
      private AbstractPlugin load(final String pluginDirPath, final Map<String, Set<AbstractPlugin>> holder) throws Exception {
          final Properties props = new Properties();
  
          String plugin = StringUtils.substringAfter(pluginDirPath, "/plugins");
          plugin = plugin.replace("/", "");
  
          final File file = Latkes.getFile("/plugins/" + plugin + "/plugin.properties");
          props.load(new FileInputStream(file));
  
          final URL defaultClassesFileDirURL = PluginManager.class.getResource("/plugins/" + plugin + "classes");
          URLClassLoader classLoader;
          if (null != defaultClassesFileDirURL) {
              classLoader = new URLClassLoader(new URL[]{defaultClassesFileDirURL}, PluginManager.class.getClassLoader());
          } else {
              classLoader = new URLClassLoader(new URL[0], 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.WARN, "no renderer defined by this plugin [" + plugin + "]，this plugin will be ignore!");
              return null;
          }
  
          final Class<?> pluginClass = classLoader.loadClass(pluginClassName);
  
          LOGGER.log(Level.TRACE, "Loading plugin class [name={}]", pluginClassName);
          final AbstractPlugin ret = (AbstractPlugin) pluginClass.getDeclaredConstructor().newInstance();
          ret.setRendererId(rendererId);
  
          setPluginProps(plugin, ret, props);
          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 Map<String, Set<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 future.
           */
          Arrays.asList(rendererId.split(";")).forEach(rid ->
                  holder.computeIfAbsent(rid, k -> new HashSet<>()).add(plugin));
  
          LOGGER.log(Level.DEBUG, "Registered plugin [name={}, version={}] for rendererId [name={}], [{}] plugins totally",
                  plugin.getName(), plugin.getVersion(), rendererId, holder.size());
      }
  
      /**
       * Sets the specified plugin's properties from the specified properties file under the specified plugin directory.
       *
       * @param pluginDirName the specified plugin directory
       * @param plugin        the specified plugin
       * @param props         the specified properties file
       */
      static void setPluginProps(final String pluginDirName, final AbstractPlugin plugin, final Properties props) {
          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.TRACE, "Plugin [name={}, author={}, version={}, types={}]", name, author, version, types);
  
          plugin.setAuthor(author);
          plugin.setName(name);
          plugin.setId(name + "_" + version);
          plugin.setVersion(version);
          plugin.setDir(pluginDirName);
          plugin.readLangs();
  
          // try to find the setting config.json
          final File settingFile = Latkes.getFile("/plugins/" + pluginDirName + "/config.json");
  
          if (null != settingFile && settingFile.exists()) {
              try {
                  final String config = FileUtils.readFileToString(settingFile, Charset.defaultCharset());
                  final JSONObject jsonObject = new JSONObject(config);
  
                  plugin.setSetting(jsonObject);
              } catch (final IOException ie) {
                  LOGGER.log(Level.ERROR, "reading the config of the plugin [" + name + "]  failed", ie);
              } catch (final JSONException e) {
                  LOGGER.log(Level.ERROR, "convert the  config of the plugin [" + name + "] to json failed", e);
              }
          }
  
          Arrays.stream(types.split(",")).map(PluginType::valueOf).forEach(plugin::addType);
      }
  
      /**
       * Gets the plugin class loaders.
       *
       * @return plugin class loaders
       */
      public Set<ClassLoader> getClassLoaders() {
          return Collections.unmodifiableSet(classLoaders);
      }
  }
  