Added PersistentJsonStorageClass

PersistentJsonStorage is used to store persistent information in
json format. This class will be used by ReportManager to save
persistent report configuration.

Tested:
  - Added unit tests for new functionality
  - All other unit tests are passing

Signed-off-by: Krzysztof Grobelny <krzysztof.grobelny@intel.com>
Change-Id: Ib496e6782e849d910fe37c02c355047afda5c79c
diff --git a/meson.build b/meson.build
index c6ef6ea..f373ef7 100644
--- a/meson.build
+++ b/meson.build
@@ -49,15 +49,29 @@
 
 systemd = dependency('systemd')
 
+if cpp.has_header('nlohmann/json.hpp')
+    nlohmann_json = declare_dependency()
+else
+    subproject('nlohmann', required: false)
+    nlohmann_json = declare_dependency(
+        include_directories: [
+            'subprojects/nlohmann/single_include',
+            'subprojects/nlohmann/single_include/nlohmann',
+        ]
+    )
+endif
+
 executable(
     'telemetry',
     [
         'src/main.cpp',
         'src/report.cpp',
         'src/report_manager.cpp',
+        'src/persistent_json_storage.cpp',
     ],
     dependencies: [
         boost,
+        nlohmann_json,
         sdbusplus,
         phosphor_logging,
     ],
@@ -76,3 +90,7 @@
     install: true,
     install_dir: systemd.get_pkgconfig_variable('systemdsystemunitdir'),
 )
+
+if get_option('buildtest')
+    subdir('tests')
+endif
diff --git a/meson_options.txt b/meson_options.txt
new file mode 100644
index 0000000..05fc7c1
--- /dev/null
+++ b/meson_options.txt
@@ -0,0 +1 @@
+option('buildtest', type: 'boolean', value: true, description: 'Build tests')
diff --git a/src/interfaces/json_storage.hpp b/src/interfaces/json_storage.hpp
new file mode 100644
index 0000000..badd938
--- /dev/null
+++ b/src/interfaces/json_storage.hpp
@@ -0,0 +1,29 @@
+#pragma once
+
+#include <boost/serialization/strong_typedef.hpp>
+#include <nlohmann/json.hpp>
+
+#include <filesystem>
+#include <optional>
+#include <string>
+
+namespace interfaces
+{
+
+class JsonStorage
+{
+  public:
+    BOOST_STRONG_TYPEDEF(std::filesystem::path, FilePath)
+    BOOST_STRONG_TYPEDEF(std::filesystem::path, DirectoryPath)
+
+    virtual ~JsonStorage() = default;
+
+    virtual void store(const FilePath& subPath, const nlohmann::json& data) = 0;
+    virtual bool remove(const FilePath& subPath) = 0;
+    virtual std::optional<nlohmann::json>
+        load(const FilePath& subPath) const = 0;
+    virtual std::vector<FilePath>
+        list(const DirectoryPath& subDirectory) const = 0;
+};
+
+} // namespace interfaces
diff --git a/src/persistent_json_storage.cpp b/src/persistent_json_storage.cpp
new file mode 100644
index 0000000..8fb3fe9
--- /dev/null
+++ b/src/persistent_json_storage.cpp
@@ -0,0 +1,144 @@
+#include "persistent_json_storage.hpp"
+
+#include <phosphor-logging/log.hpp>
+
+#include <fstream>
+#include <stdexcept>
+
+PersistentJsonStorage::PersistentJsonStorage(const DirectoryPath& directory) :
+    directory(directory)
+{}
+
+void PersistentJsonStorage::store(const FilePath& filePath,
+                                  const nlohmann::json& data)
+{
+    try
+    {
+        const auto path = join(directory, filePath);
+        std::error_code ec;
+
+        phosphor::logging::log<phosphor::logging::level::DEBUG>(
+            "Store to file", phosphor::logging::entry("path=%s", path.c_str()));
+
+        std::filesystem::create_directories(path.parent_path(), ec);
+        if (ec)
+        {
+            throw std::runtime_error(
+                "Unable to create directory for file: " + path.string() +
+                ", ec=" + std::to_string(ec.value()) + ": " + ec.message());
+        }
+
+        std::ofstream file(path);
+        file << data;
+        if (!file)
+        {
+            throw std::runtime_error("Unable to create file: " + path.string());
+        }
+
+        limitPermissions(path.parent_path());
+        limitPermissions(path);
+    }
+    catch (...)
+    {
+        remove(filePath);
+        throw;
+    }
+}
+
+bool PersistentJsonStorage::remove(const FilePath& filePath)
+{
+    const auto path = join(directory, filePath);
+    std::error_code ec;
+
+    auto removed = std::filesystem::remove(path, ec);
+    if (!removed)
+    {
+        phosphor::logging::log<phosphor::logging::level::ERR>(
+            "Unable to remove file",
+            phosphor::logging::entry("path=%s, ec= %lu: %s", path.c_str(),
+                                     ec.value(), ec.message().c_str()));
+        return false;
+    }
+
+    /* removes directory only if it is empty */
+    std::filesystem::remove(path.parent_path(), ec);
+
+    return true;
+}
+
+std::optional<nlohmann::json>
+    PersistentJsonStorage::load(const FilePath& filePath) const
+{
+    const auto path = join(directory, filePath);
+    if (!std::filesystem::exists(path))
+    {
+        return std::nullopt;
+    }
+
+    nlohmann::json result;
+
+    try
+    {
+        std::ifstream file(path);
+        file >> result;
+    }
+    catch (const std::exception& e)
+    {
+        phosphor::logging::log<phosphor::logging::level::ERR>(e.what());
+        return std::nullopt;
+    }
+
+    return result;
+}
+
+std::vector<interfaces::JsonStorage::FilePath>
+    PersistentJsonStorage::list(const DirectoryPath& subDirectory) const
+{
+    auto result = std::vector<FilePath>();
+    const auto path = join(directory, subDirectory);
+
+    if (!std::filesystem::exists(path))
+    {
+        return result;
+    }
+
+    for (const auto& p : std::filesystem::directory_iterator(path))
+    {
+        if (std::filesystem::is_directory(p.path()))
+        {
+            for (auto& item : list(DirectoryPath(p.path())))
+            {
+                result.emplace_back(std::move(item));
+            }
+        }
+        else
+        {
+            const auto item = std::filesystem::relative(
+                p.path().parent_path(), std::filesystem::path{directory});
+
+            if (std::find(result.begin(), result.end(), item) == result.end())
+            {
+                result.emplace_back(item);
+            }
+        }
+    }
+
+    return result;
+}
+
+std::filesystem::path
+    PersistentJsonStorage::join(const std::filesystem::path& left,
+                                const std::filesystem::path& right)
+{
+    return left / right;
+}
+
+void PersistentJsonStorage::limitPermissions(const std::filesystem::path& path)
+{
+    constexpr auto filePerms = std::filesystem::perms::owner_read |
+                               std::filesystem::perms::owner_write;
+    constexpr auto dirPerms = filePerms | std::filesystem::perms::owner_exec;
+    std::filesystem::permissions(
+        path, std::filesystem::is_directory(path) ? dirPerms : filePerms,
+        std::filesystem::perm_options::replace);
+}
diff --git a/src/persistent_json_storage.hpp b/src/persistent_json_storage.hpp
new file mode 100644
index 0000000..4b18aa1
--- /dev/null
+++ b/src/persistent_json_storage.hpp
@@ -0,0 +1,22 @@
+#pragma once
+
+#include "interfaces/json_storage.hpp"
+
+class PersistentJsonStorage : public interfaces::JsonStorage
+{
+  public:
+    explicit PersistentJsonStorage(const DirectoryPath& directory);
+
+    void store(const FilePath& subPath, const nlohmann::json& data) override;
+    bool remove(const FilePath& subPath) override;
+    std::optional<nlohmann::json> load(const FilePath& subPath) const override;
+    std::vector<FilePath>
+        list(const DirectoryPath& subDirectory) const override;
+
+  private:
+    DirectoryPath directory;
+
+    static std::filesystem::path join(const std::filesystem::path&,
+                                      const std::filesystem::path&);
+    static void limitPermissions(const std::filesystem::path& path);
+};
diff --git a/subprojects/googletest.wrap b/subprojects/googletest.wrap
new file mode 100644
index 0000000..56da9ef
--- /dev/null
+++ b/subprojects/googletest.wrap
@@ -0,0 +1,3 @@
+[wrap-git]
+url = https://github.com/google/googletest
+revision = HEAD
diff --git a/subprojects/nlohmann.wrap b/subprojects/nlohmann.wrap
new file mode 100644
index 0000000..48f3c68
--- /dev/null
+++ b/subprojects/nlohmann.wrap
@@ -0,0 +1,3 @@
+[wrap-git]
+revision = db78ac1d7716f56fc9f1b030b715f872f93964e4
+url = https://github.com/nlohmann/json.git
diff --git a/tests/meson.build b/tests/meson.build
new file mode 100644
index 0000000..32baaad
--- /dev/null
+++ b/tests/meson.build
@@ -0,0 +1,40 @@
+gtest_dep = dependency('gtest', main: true, disabler: true, required: false)
+gmock_dep = dependency('gmock', disabler: true, required: false)
+if not gtest_dep.found() or not gmock_dep.found()
+    gtest_proj = import('cmake').subproject('googletest', required: false)
+    if gtest_proj.found()
+        gtest_dep = declare_dependency(
+            dependencies: [
+                dependency('threads'),
+                gtest_proj.dependency('gtest'),
+                gtest_proj.dependency('gtest_main'),
+            ]
+        )
+        gmock_dep = gtest_proj.dependency('gmock')
+  else
+        assert(
+            not get_option('tests').enabled(),
+            'Googletest is required if tests are enabled'
+        )
+  endif
+endif
+
+test(
+    'telemetry-ut',
+    executable(
+        'telemetry-ut',
+        [
+            '../src/persistent_json_storage.cpp',
+            'src/test_persistent_json_storage.cpp',
+        ],
+        dependencies: [
+            boost,
+            gmock_dep,
+            gtest_dep,
+            nlohmann_json,
+            phosphor_logging,
+            sdbusplus,
+        ],
+        include_directories: '../src',
+    )
+)
diff --git a/tests/src/mocks/json_storage.hpp b/tests/src/mocks/json_storage.hpp
new file mode 100644
index 0000000..295bc94
--- /dev/null
+++ b/tests/src/mocks/json_storage.hpp
@@ -0,0 +1,14 @@
+#pragma once
+
+#include "interfaces/json_storage.hpp"
+
+#include <gmock/gmock.h>
+
+class StorageMock : public interfaces::JsonStorage
+{
+  public:
+    MOCK_METHOD2(store, void(const FilePath&, const nlohmann::json&));
+    MOCK_METHOD1(remove, bool(const FilePath&));
+    MOCK_CONST_METHOD1(load, std::optional<nlohmann::json>(const FilePath&));
+    MOCK_CONST_METHOD1(list, std::vector<FilePath>(const DirectoryPath&));
+};
diff --git a/tests/src/test_persistent_json_storage.cpp b/tests/src/test_persistent_json_storage.cpp
new file mode 100644
index 0000000..f8b0557
--- /dev/null
+++ b/tests/src/test_persistent_json_storage.cpp
@@ -0,0 +1,108 @@
+#include "persistent_json_storage.hpp"
+
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+
+using namespace testing;
+
+class TestPersistentJsonStorage : public Test
+{
+  public:
+    using FilePath = interfaces::JsonStorage::FilePath;
+    using DirectoryPath = interfaces::JsonStorage::DirectoryPath;
+
+    static void SetUpTestSuite()
+    {
+        ASSERT_FALSE(std::filesystem::exists(directory));
+    }
+
+    void TearDown() override
+    {
+        if (std::filesystem::exists(directory))
+        {
+            std::filesystem::remove_all(directory);
+        }
+    }
+
+    const FilePath fileName = FilePath("report/1/file.txt");
+
+    static const DirectoryPath directory;
+    PersistentJsonStorage sut{directory};
+};
+
+const interfaces::JsonStorage::DirectoryPath
+    TestPersistentJsonStorage::directory =
+        interfaces::JsonStorage::DirectoryPath(std::tmpnam(nullptr));
+
+TEST_F(TestPersistentJsonStorage, storesJsonData)
+{
+    nlohmann::json data = nlohmann::json::object();
+    data["name"] = "kevin";
+    data["lastname"] = "mc calister";
+
+    sut.store(fileName, data);
+
+    ASSERT_THAT(sut.load(fileName), Eq(data));
+}
+
+TEST_F(TestPersistentJsonStorage, emptyListWhenNoReportsCreated)
+{
+    EXPECT_THAT(sut.list(DirectoryPath("report")), SizeIs(0u));
+}
+
+TEST_F(TestPersistentJsonStorage, listSavedReports)
+{
+    sut.store(FilePath("report/domain-1/name-1/conf-1.json"),
+              nlohmann::json("data-1a"));
+    sut.store(FilePath("report/domain-1/name-2/conf-1.json"),
+              nlohmann::json("data-2a"));
+    sut.store(FilePath("report/domain-1/name-2/conf-2.json"),
+              nlohmann::json("data-2b"));
+    sut.store(FilePath("report/domain-2/name-1/conf-1.json"),
+              nlohmann::json("data-3a"));
+
+    EXPECT_THAT(sut.list(DirectoryPath("report")),
+                UnorderedElementsAre(FilePath("report/domain-1/name-1"),
+                                     FilePath("report/domain-1/name-2"),
+                                     FilePath("report/domain-2/name-1")));
+}
+
+TEST_F(TestPersistentJsonStorage, listSavedReportsWithoutRemovedOnes)
+{
+    sut.store(FilePath("report/domain-1/name-1/conf-1.json"),
+              nlohmann::json("data-1a"));
+    sut.store(FilePath("report/domain-1/name-2/conf-1.json"),
+              nlohmann::json("data-2a"));
+    sut.store(FilePath("report/domain-1/name-2/conf-2.json"),
+              nlohmann::json("data-2b"));
+    sut.store(FilePath("report/domain-2/name-1/conf-1.json"),
+              nlohmann::json("data-3a"));
+    sut.remove(FilePath("report/domain-1/name-1/conf-1.json"));
+    sut.remove(FilePath("report/domain-1/name-2/conf-2.json"));
+
+    EXPECT_THAT(sut.list(DirectoryPath("report")),
+                UnorderedElementsAre(FilePath("report/domain-1/name-2"),
+                                     FilePath("report/domain-2/name-1")));
+}
+
+TEST_F(TestPersistentJsonStorage, removesStoredJson)
+{
+    nlohmann::json data = nlohmann::json::object();
+    data["name"] = "kevin";
+    data["lastname"] = "mc calister";
+
+    sut.store(fileName, data);
+
+    ASSERT_THAT(sut.remove(fileName), Eq(true));
+    ASSERT_THAT(sut.load(fileName), Eq(std::nullopt));
+}
+
+TEST_F(TestPersistentJsonStorage, returnsFalseWhenDeletingNonExistingFile)
+{
+    ASSERT_THAT(sut.remove(fileName), Eq(false));
+}
+
+TEST_F(TestPersistentJsonStorage, returnsNulloptWhenFileDoesntExist)
+{
+    ASSERT_THAT(sut.load(fileName), Eq(std::nullopt));
+}