text: /**
   * Copyright © 2020 IBM Corporation
   *
   * 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.
   */
  #pragma once
  
  #include "sdbusplus.hpp"
  
  #include <fmt/format.h>
  
  #include <nlohmann/json.hpp>
  #include <phosphor-logging/log.hpp>
  #include <sdbusplus/bus.hpp>
  #include <sdeventplus/source/signal.hpp>
  
  #include <filesystem>
  #include <fstream>
  
  namespace phosphor::fan
  {
  
  namespace fs = std::filesystem;
  using json = nlohmann::json;
  using namespace phosphor::logging;
  
  constexpr auto confOverridePath = "/etc/phosphor-fan-presence";
  constexpr auto confBasePath = "/usr/share/phosphor-fan-presence";
  constexpr auto confCompatServ = "xyz.openbmc_project.EntityManager";
  constexpr auto confCompatIntf =
      "xyz.openbmc_project.Configuration.IBMCompatibleSystem";
  constexpr auto confCompatProp = "Names";
  
  /**
   * @class NoConfigFound - A no JSON configuration found exception
   *
   * A no JSON configuration found exception that is used to denote that a JSON
   * configuration has not been found yet.
   */
  class NoConfigFound : public std::runtime_error
  {
    public:
      NoConfigFound() = delete;
      NoConfigFound(const NoConfigFound&) = delete;
      NoConfigFound(NoConfigFound&&) = delete;
      NoConfigFound& operator=(const NoConfigFound&) = delete;
      NoConfigFound& operator=(NoConfigFound&&) = delete;
      ~NoConfigFound() = default;
  
      /**
       * @brief No JSON configuration found exception object
       *
       * When attempting to find the JSON configuration file(s), a NoConfigFound
       * exception can be thrown to denote that at that time finding/loading the
       * JSON configuration file(s) for a fan application failed. Details on what
       * application and JSON configuration file that failed to be found will be
       * logged resulting in the application being terminated.
       *
       * @param[in] details - Additional details
       */
      NoConfigFound(const std::string& appName, const std::string& fileName) :
          std::runtime_error(fmt::format("JSON configuration not found [Could "
                                         "not find fan {} conf file {}]",
                                         appName, fileName)
                                 .c_str())
      {}
  };
  
  class JsonConfig
  {
    public:
      /**
       * @brief Get the object paths with the compatible interface
       *
       * Retrieve all the object paths implementing the compatible interface for
       * configuration file loading.
       */
      static auto& getCompatObjPaths() __attribute__((pure))
      {
          static auto paths = util::SDBusPlus::getSubTreePathsRaw(
              util::SDBusPlus::getBus(), "/", confCompatIntf, 0);
          return paths;
      }
  
      /**
       * @brief Constructor
       *
       * Attempts to set the list of compatible values from the compatible
       * interface and call the fan app's function to load its config file(s). If
       * the compatible interface is not found, it subscribes to the
       * interfacesAdded signal for that interface on the compatible service
       * defined above.
       *
       * @param[in] func - Fan app function to call to load its config file(s)
       */
      JsonConfig(std::function<void()> func) : _loadFunc(func)
      {
          std::vector<std::string> compatObjPaths;
  
          _match = std::make_unique<sdbusplus::bus::match_t>(
              util::SDBusPlus::getBus(),
              sdbusplus::bus::match::rules::interfacesAdded() +
                  sdbusplus::bus::match::rules::sender(confCompatServ),
              std::bind(&JsonConfig::compatIntfAdded, this,
                        std::placeholders::_1));
  
          try
          {
              compatObjPaths = getCompatObjPaths();
          }
          catch (const util::DBusMethodError&)
          {
              // Compatible interface does not exist on any dbus object yet
          }
  
          if (!compatObjPaths.empty())
          {
              for (auto& path : compatObjPaths)
              {
                  try
                  {
                      // Retrieve json config compatible relative path
                      // locations (last one found will be what's used if more
                      // than one dbus object implementing the comptaible
                      // interface exists).
                      _confCompatValues =
                          util::SDBusPlus::getProperty<std::vector<std::string>>(
                              util::SDBusPlus::getBus(), path, confCompatIntf,
                              confCompatProp);
                  }
                  catch (const util::DBusError&)
                  {
                      // Compatible property unavailable on this dbus object
                      // path's compatible interface, ignore
                  }
              }
              _loadFunc();
          }
          else
          {
              // Check if required config(s) are found not needing the
              // compatible interface, otherwise this is intended to catch the
              // exception thrown by the getConfFile function when the
              // required config file was not found. This would then result in
              // waiting for the compatible interfacesAdded signal
              try
              {
                  _loadFunc();
              }
              catch (const NoConfigFound&)
              {
                  // Wait for compatible interfacesAdded signal
              }
          }
      }
  
      /**
       * @brief InterfacesAdded callback function for the compatible interface.
       *
       * @param[in] msg - The D-Bus message contents
       *
       * If the compatible interface is found, it uses the compatible property on
       * the interface to set the list of compatible values to be used when
       * attempting to get a configuration file. Once the list of compatible
       * values has been updated, it calls the load function.
       */
      void compatIntfAdded(sdbusplus::message::message& msg)
      {
          sdbusplus::message::object_path op;
          std::map<std::string,
                   std::map<std::string, std::variant<std::vector<std::string>>>>
              intfProps;
  
          msg.read(op, intfProps);
  
          if (intfProps.find(confCompatIntf) == intfProps.end())
          {
              return;
          }
  
          const auto& props = intfProps.at(confCompatIntf);
          // Only one dbus object with the compatible interface is used at a time
          _confCompatValues =
              std::get<std::vector<std::string>>(props.at(confCompatProp));
          _loadFunc();
      }
  
      /**
       * Get the json configuration file. The first location found to contain
       * the json config file for the given fan application is used from the
       * following locations in order.
       * 1.) From the confOverridePath location
       * 2.) From the default confBasePath location
       * 3.) From config file found using an entry from a list obtained from an
       * interface's property as a relative path extension on the base path where:
       *     interface = Interface set in confCompatIntf with the property
       *     property = Property set in confCompatProp containing a list of
       *                subdirectories in priority order to find a config
       *
       * @brief Get the configuration file to be used
       *
       * @param[in] bus - The dbus bus object
       * @param[in] appName - The phosphor-fan-presence application name
       * @param[in] fileName - Application's configuration file's name
       * @param[in] isOptional - Config file is optional, default to 'false'
       *
       * @return filesystem path
       *     The filesystem path to the configuration file to use
       */
      static const fs::path getConfFile(sdbusplus::bus::bus& bus,
                                        const std::string& appName,
                                        const std::string& fileName,
                                        bool isOptional = false)
      {
          // Check override location
          fs::path confFile = fs::path{confOverridePath} / appName / fileName;
          if (fs::exists(confFile))
          {
              return confFile;
          }
  
          // If the default file is there, use it
          confFile = fs::path{confBasePath} / appName / fileName;
          if (fs::exists(confFile))
          {
              return confFile;
          }
  
          // Look for a config file at each entry relative to the base
          // path and use the first one found
          auto it = std::find_if(
              _confCompatValues.begin(), _confCompatValues.end(),
              [&confFile, &appName, &fileName](const auto& value) {
                  confFile = fs::path{confBasePath} / appName / value / fileName;
                  return fs::exists(confFile);
              });
          if (it == _confCompatValues.end())
          {
              confFile.clear();
          }
  
          if (confFile.empty() && !isOptional)
          {
              throw NoConfigFound(appName, fileName);
          }
  
          return confFile;
      }
  
      /**
       * @brief Load the JSON config file
       *
       * @param[in] confFile - File system path of the configuration file to load
       *
       * @return Parsed JSON object
       *     The parsed JSON configuration file object
       */
      static const json load(const fs::path& confFile)
      {
          std::ifstream file;
          json jsonConf;
  
          if (!confFile.empty() && fs::exists(confFile))
          {
              log<level::INFO>(
                  fmt::format("Loading configuration from {}", confFile.string())
                      .c_str());
              file.open(confFile);
              try
              {
                  // Enable ignoring `//` or `/* */` comments
                  jsonConf = json::parse(file, nullptr, true, true);
              }
              catch (const std::exception& e)
              {
                  log<level::ERR>(
                      fmt::format(
                          "Failed to parse JSON config file: {}, error: {}",
                          confFile.string(), e.what())
                          .c_str());
                  throw std::runtime_error(
                      fmt::format(
                          "Failed to parse JSON config file: {}, error: {}",
                          confFile.string(), e.what())
                          .c_str());
              }
          }
          else
          {
              log<level::ERR>(fmt::format("Unable to open JSON config file: {}",
                                          confFile.string())
                                  .c_str());
              throw std::runtime_error(
                  fmt::format("Unable to open JSON config file: {}",
                              confFile.string())
                      .c_str());
          }
  
          return jsonConf;
      }
  
      /**
       * @brief Return the compatible values property
       *
       * @return const std::vector<std::string>& - The values
       */
      static const std::vector<std::string>& getCompatValues()
      {
          return _confCompatValues;
      }
  
    private:
      /* Load function to call for a fan app to load its config file(s). */
      std::function<void()> _loadFunc;
  
      /**
       * @brief The interfacesAdded match that is used to wait
       *        for the IBMCompatibleSystem interface to show up.
       */
      std::unique_ptr<sdbusplus::bus::match_t> _match;
  
      /**
       * @brief List of compatible values from the compatible interface
       *
       * Only supports a single instance of the compatible interface on a dbus
       * object. If more than one dbus object exists with the compatible
       * interface, the last one found will be the list of compatible values used.
       */
      inline static std::vector<std::string> _confCompatValues;
  };
  
  } // namespace phosphor::fan
  