cleanup: move all dbus interface functions in own files and namespace

Improving maintainability by 'separation of concern'.

Change-Id: I2797813c44ca0d70d03c8115035adc2c5efae4fe
Signed-off-by: Christopher Meis <christopher.meis@9elements.com>
diff --git a/src/dbus_interface.cpp b/src/dbus_interface.cpp
new file mode 100644
index 0000000..8c975bb
--- /dev/null
+++ b/src/dbus_interface.cpp
@@ -0,0 +1,395 @@
+#include "dbus_interface.hpp"
+
+#include "perform_probe.hpp"
+#include "utils.hpp"
+
+#include <boost/algorithm/string/case_conv.hpp>
+#include <boost/container/flat_map.hpp>
+
+#include <regex>
+#include <string>
+#include <vector>
+
+using JsonVariantType =
+    std::variant<std::vector<std::string>, std::vector<double>, std::string,
+                 int64_t, uint64_t, double, int32_t, uint32_t, int16_t,
+                 uint16_t, uint8_t, bool>;
+
+namespace dbus_interface
+{
+
+const std::regex illegalDbusPathRegex("[^A-Za-z0-9_.]");
+const std::regex illegalDbusMemberRegex("[^A-Za-z0-9_]");
+
+// NOLINTBEGIN(cppcoreguidelines-avoid-non-const-global-variables)
+// store reference to all interfaces so we can destroy them later
+boost::container::flat_map<
+    std::string, std::vector<std::weak_ptr<sdbusplus::asio::dbus_interface>>>
+    inventory;
+// NOLINTEND(cppcoreguidelines-avoid-non-const-global-variables)
+
+void tryIfaceInitialize(std::shared_ptr<sdbusplus::asio::dbus_interface>& iface)
+{
+    try
+    {
+        iface->initialize();
+    }
+    catch (std::exception& e)
+    {
+        std::cerr << "Unable to initialize dbus interface : " << e.what()
+                  << "\n"
+                  << "object Path : " << iface->get_object_path() << "\n"
+                  << "interface name : " << iface->get_interface_name() << "\n";
+    }
+}
+
+std::shared_ptr<sdbusplus::asio::dbus_interface> createInterface(
+    sdbusplus::asio::object_server& objServer, const std::string& path,
+    const std::string& interface, const std::string& parent, bool checkNull)
+{
+    // on first add we have no reason to check for null before add, as there
+    // won't be any. For dynamically added interfaces, we check for null so that
+    // a constant delete/add will not create a memory leak
+
+    auto ptr = objServer.add_interface(path, interface);
+    auto& dataVector = inventory[parent];
+    if (checkNull)
+    {
+        auto it = std::find_if(dataVector.begin(), dataVector.end(),
+                               [](const auto& p) { return p.expired(); });
+        if (it != dataVector.end())
+        {
+            *it = ptr;
+            return ptr;
+        }
+    }
+    dataVector.emplace_back(ptr);
+    return ptr;
+}
+
+void createDeleteObjectMethod(
+    const std::string& jsonPointerPath,
+    const std::shared_ptr<sdbusplus::asio::dbus_interface>& iface,
+    sdbusplus::asio::object_server& objServer,
+    nlohmann::json& systemConfiguration)
+{
+    std::weak_ptr<sdbusplus::asio::dbus_interface> interface = iface;
+    iface->register_method(
+        "Delete", [&objServer, &systemConfiguration, interface,
+                   jsonPointerPath{std::string(jsonPointerPath)}]() {
+            std::shared_ptr<sdbusplus::asio::dbus_interface> dbusInterface =
+                interface.lock();
+            if (!dbusInterface)
+            {
+                // this technically can't happen as the pointer is pointing to
+                // us
+                throw DBusInternalError();
+            }
+            nlohmann::json::json_pointer ptr(jsonPointerPath);
+            systemConfiguration[ptr] = nullptr;
+
+            // todo(james): dig through sdbusplus to find out why we can't
+            // delete it in a method call
+            boost::asio::post(io, [&objServer, dbusInterface]() mutable {
+                objServer.remove_interface(dbusInterface);
+            });
+
+            if (!configuration::writeJsonFiles(systemConfiguration))
+            {
+                std::cerr << "error setting json file\n";
+                throw DBusInternalError();
+            }
+        });
+}
+
+// adds simple json types to interface's properties
+void populateInterfaceFromJson(
+    nlohmann::json& systemConfiguration, const std::string& jsonPointerPath,
+    std::shared_ptr<sdbusplus::asio::dbus_interface>& iface,
+    nlohmann::json& dict, sdbusplus::asio::object_server& objServer,
+    sdbusplus::asio::PropertyPermission permission)
+{
+    for (const auto& [key, value] : dict.items())
+    {
+        auto type = value.type();
+        bool array = false;
+        if (value.type() == nlohmann::json::value_t::array)
+        {
+            array = true;
+            if (value.empty())
+            {
+                continue;
+            }
+            type = value[0].type();
+            bool isLegal = true;
+            for (const auto& arrayItem : value)
+            {
+                if (arrayItem.type() != type)
+                {
+                    isLegal = false;
+                    break;
+                }
+            }
+            if (!isLegal)
+            {
+                std::cerr << "dbus format error" << value << "\n";
+                continue;
+            }
+        }
+        if (type == nlohmann::json::value_t::object)
+        {
+            continue; // handled elsewhere
+        }
+
+        std::string path = jsonPointerPath;
+        path.append("/").append(key);
+        if (permission == sdbusplus::asio::PropertyPermission::readWrite)
+        {
+            // all setable numbers are doubles as it is difficult to always
+            // create a configuration file with all whole numbers as decimals
+            // i.e. 1.0
+            if (array)
+            {
+                if (value[0].is_number())
+                {
+                    type = nlohmann::json::value_t::number_float;
+                }
+            }
+            else if (value.is_number())
+            {
+                type = nlohmann::json::value_t::number_float;
+            }
+        }
+
+        switch (type)
+        {
+            case (nlohmann::json::value_t::boolean):
+            {
+                if (array)
+                {
+                    // todo: array of bool isn't detected correctly by
+                    // sdbusplus, change it to numbers
+                    addArrayToDbus<uint64_t>(key, value, iface.get(),
+                                             permission, systemConfiguration,
+                                             path);
+                }
+
+                else
+                {
+                    addProperty(key, value.get<bool>(), iface.get(),
+                                systemConfiguration, path, permission);
+                }
+                break;
+            }
+            case (nlohmann::json::value_t::number_integer):
+            {
+                if (array)
+                {
+                    addArrayToDbus<int64_t>(key, value, iface.get(), permission,
+                                            systemConfiguration, path);
+                }
+                else
+                {
+                    addProperty(key, value.get<int64_t>(), iface.get(),
+                                systemConfiguration, path,
+                                sdbusplus::asio::PropertyPermission::readOnly);
+                }
+                break;
+            }
+            case (nlohmann::json::value_t::number_unsigned):
+            {
+                if (array)
+                {
+                    addArrayToDbus<uint64_t>(key, value, iface.get(),
+                                             permission, systemConfiguration,
+                                             path);
+                }
+                else
+                {
+                    addProperty(key, value.get<uint64_t>(), iface.get(),
+                                systemConfiguration, path,
+                                sdbusplus::asio::PropertyPermission::readOnly);
+                }
+                break;
+            }
+            case (nlohmann::json::value_t::number_float):
+            {
+                if (array)
+                {
+                    addArrayToDbus<double>(key, value, iface.get(), permission,
+                                           systemConfiguration, path);
+                }
+
+                else
+                {
+                    addProperty(key, value.get<double>(), iface.get(),
+                                systemConfiguration, path, permission);
+                }
+                break;
+            }
+            case (nlohmann::json::value_t::string):
+            {
+                if (array)
+                {
+                    addArrayToDbus<std::string>(key, value, iface.get(),
+                                                permission, systemConfiguration,
+                                                path);
+                }
+                else
+                {
+                    addProperty(key, value.get<std::string>(), iface.get(),
+                                systemConfiguration, path, permission);
+                }
+                break;
+            }
+            default:
+            {
+                std::cerr << "Unexpected json type in system configuration "
+                          << key << ": " << value.type_name() << "\n";
+                break;
+            }
+        }
+    }
+    if (permission == sdbusplus::asio::PropertyPermission::readWrite)
+    {
+        createDeleteObjectMethod(jsonPointerPath, iface, objServer,
+                                 systemConfiguration);
+    }
+    tryIfaceInitialize(iface);
+}
+
+void createAddObjectMethod(
+    const std::string& jsonPointerPath, const std::string& path,
+    nlohmann::json& systemConfiguration,
+    sdbusplus::asio::object_server& objServer, const std::string& board)
+{
+    std::shared_ptr<sdbusplus::asio::dbus_interface> iface = createInterface(
+        objServer, path, "xyz.openbmc_project.AddObject", board);
+
+    iface->register_method(
+        "AddObject",
+        [&systemConfiguration, &objServer,
+         jsonPointerPath{std::string(jsonPointerPath)}, path{std::string(path)},
+         board](const boost::container::flat_map<std::string, JsonVariantType>&
+                    data) {
+            nlohmann::json::json_pointer ptr(jsonPointerPath);
+            nlohmann::json& base = systemConfiguration[ptr];
+            auto findExposes = base.find("Exposes");
+
+            if (findExposes == base.end())
+            {
+                throw std::invalid_argument("Entity must have children.");
+            }
+
+            // this will throw invalid-argument to sdbusplus if invalid json
+            nlohmann::json newData{};
+            for (const auto& item : data)
+            {
+                nlohmann::json& newJson = newData[item.first];
+                std::visit(
+                    [&newJson](auto&& val) {
+                        newJson = std::forward<decltype(val)>(val);
+                    },
+                    item.second);
+            }
+
+            auto findName = newData.find("Name");
+            auto findType = newData.find("Type");
+            if (findName == newData.end() || findType == newData.end())
+            {
+                throw std::invalid_argument("AddObject missing Name or Type");
+            }
+            const std::string* type = findType->get_ptr<const std::string*>();
+            const std::string* name = findName->get_ptr<const std::string*>();
+            if (type == nullptr || name == nullptr)
+            {
+                throw std::invalid_argument("Type and Name must be a string.");
+            }
+
+            bool foundNull = false;
+            size_t lastIndex = 0;
+            // we add in the "exposes"
+            for (const auto& expose : *findExposes)
+            {
+                if (expose.is_null())
+                {
+                    foundNull = true;
+                    continue;
+                }
+
+                if (expose["Name"] == *name && expose["Type"] == *type)
+                {
+                    throw std::invalid_argument(
+                        "Field already in JSON, not adding");
+                }
+
+                if (foundNull)
+                {
+                    continue;
+                }
+
+                lastIndex++;
+            }
+
+            std::ifstream schemaFile(
+                std::string(configuration::schemaDirectory) + "/" +
+                boost::to_lower_copy(*type) + ".json");
+            // todo(james) we might want to also make a list of 'can add'
+            // interfaces but for now I think the assumption if there is a
+            // schema avaliable that it is allowed to update is fine
+            if (!schemaFile.good())
+            {
+                throw std::invalid_argument(
+                    "No schema avaliable, cannot validate.");
+            }
+            nlohmann::json schema =
+                nlohmann::json::parse(schemaFile, nullptr, false, true);
+            if (schema.is_discarded())
+            {
+                std::cerr << "Schema not legal" << *type << ".json\n";
+                throw DBusInternalError();
+            }
+            if (!configuration::validateJson(schema, newData))
+            {
+                throw std::invalid_argument("Data does not match schema");
+            }
+            if (foundNull)
+            {
+                findExposes->at(lastIndex) = newData;
+            }
+            else
+            {
+                findExposes->push_back(newData);
+            }
+            if (!configuration::writeJsonFiles(systemConfiguration))
+            {
+                std::cerr << "Error writing json files\n";
+                throw DBusInternalError();
+            }
+            std::string dbusName = *name;
+
+            std::regex_replace(dbusName.begin(), dbusName.begin(),
+                               dbusName.end(), illegalDbusMemberRegex, "_");
+
+            std::shared_ptr<sdbusplus::asio::dbus_interface> interface =
+                createInterface(objServer, path + "/" + dbusName,
+                                "xyz.openbmc_project.Configuration." + *type,
+                                board, true);
+            // permission is read-write, as since we just created it, must be
+            // runtime modifiable
+            populateInterfaceFromJson(
+                systemConfiguration,
+                jsonPointerPath + "/Exposes/" + std::to_string(lastIndex),
+                interface, newData, objServer,
+                sdbusplus::asio::PropertyPermission::readWrite);
+        });
+    tryIfaceInitialize(iface);
+}
+
+std::vector<std::weak_ptr<sdbusplus::asio::dbus_interface>>&
+    getDeviceInterfaces(const nlohmann::json& device)
+{
+    return inventory[device["Name"].get<std::string>()];
+}
+
+} // namespace dbus_interface