MQTTSuite
Loading...
Searching...
No Matches
JsonMappingReader.cpp
Go to the documentation of this file.
1/*
2 * MQTTSuite - A lightweight MQTT Integration System
3 * Copyright (C) Volker Christian <me@vchrist.at>
4 * 2022, 2023, 2024, 2025, 2026
5 * Tobias Pfeil
6 * 2025, 2026
7 *
8 * This program is free software: you can redistribute it and/or modify it
9 * under the terms of the GNU General Public License as published by the Free
10 * Software Foundation, either version 3 of the License, or (at your option)
11 * any later version.
12 *
13 * This program is distributed in the hope that it will be useful, but WITHOUT
14 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
15 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
16 * more details.
17 *
18 * You should have received a copy of the GNU General Public License along
19 * with this program. If not, see <https://www.gnu.org/licenses/>.
20 */
21
22/*
23 * MIT License
24 *
25 * Permission is hereby granted, free of charge, to any person obtaining a copy
26 * of this software and associated documentation files (the "Software"), to deal
27 * in the Software without restriction, including without limitation the rights
28 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
29 * copies of the Software, and to permit persons to whom the Software is
30 * furnished to do so, subject to the following conditions:
31 *
32 * The above copyright notice and this permission notice shall be included in
33 * all copies or substantial portions of the Software.
34 *
35 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
36 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
37 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
38 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
39 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
40 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
41 * THE SOFTWARE.
42 */
43
45
46#include "MqttMapper.h"
47
48#ifndef DOXYGEN_SHOULD_SKIP_THIS
49
50// #include "nlohmann/json-schema.hpp"
51
52#include <algorithm>
53#include <chrono>
54#include <compare>
55#include <ctime>
56#include <exception>
57#include <filesystem>
58#include <fstream>
59#include <iomanip>
60#include <log/Logger.h>
61#include <map>
62#include <sstream>
63#include <stdexcept>
64
65#endif
66
67namespace mqtt::lib {
68
69 namespace fs = std::filesystem;
70
71 std::string JsonMappingReader::getDraftPath(const std::string& mapFilePath) {
72 return mapFilePath + ".draft";
73 }
74
75 void JsonMappingReader::saveDraft(const std::string& mapFilePath, const nlohmann::json& content) {
76 std::ofstream out(getDraftPath(mapFilePath), std::ios::trunc);
77 if (!out) {
78 throw std::runtime_error("Cannot open draft file for writing: " + getDraftPath(mapFilePath));
79 }
80 out << content.dump(2) << std::endl;
81 }
82
83 nlohmann::json JsonMappingReader::readDraftOrActive(const std::string& mapFilePath) {
84 std::string draftPath = getDraftPath(mapFilePath);
85 if (fs::exists(draftPath)) {
86 std::ifstream f(draftPath);
87 if (f) {
88 nlohmann::json j;
89 f >> j;
90 return j;
91 }
92 }
93 // Fallback to active file
94 std::ifstream f(mapFilePath);
95 if (!f)
96 throw std::runtime_error("Cannot open mapping file: " + mapFilePath);
97 nlohmann::json j;
98 f >> j;
99 return j;
100 }
101
102 nlohmann::json JsonMappingReader::deployDraft(const std::string& mapFilePath) {
103 std::string draftPath = getDraftPath(mapFilePath);
104 if (!fs::exists(draftPath))
105 return nlohmann::json();
106
107 nlohmann::json j;
108
109 // 1. Inject creation timestamp into draft
110 try {
111 std::ifstream f(draftPath);
112 f >> j;
113 f.close();
114
115 auto now = std::chrono::system_clock::now();
116 std::time_t now_c = std::chrono::system_clock::to_time_t(now);
117 std::stringstream ss;
118 ss << std::put_time(std::gmtime(&now_c), "%Y-%m-%dT%H:%M:%SZ");
119
120 if (!j.contains("meta"))
121 j["meta"] = nlohmann::json::object();
122 j["meta"]["created"] = ss.str();
123 j["meta"]["version"] = std::to_string(std::chrono::duration_cast<std::chrono::seconds>(now.time_since_epoch()).count());
124
125 std::ofstream out(draftPath, std::ios::trunc);
126 out << j.dump(2);
127 out.close();
128 } catch (const std::exception& e) {
129 VLOG(1) << "Failed to inject metadata into draft: " << e.what();
130 }
131
132 // 2. Backup current active file
133 if (fs::exists(mapFilePath)) {
134 fs::path versionDir = fs::path(mapFilePath).parent_path() / "versions";
135 if (!fs::exists(versionDir)) {
136 fs::create_directories(versionDir);
137 }
138
139 auto now = std::chrono::system_clock::now();
140 auto timestamp = std::chrono::duration_cast<std::chrono::seconds>(now.time_since_epoch()).count();
141 std::string filename = fs::path(mapFilePath).filename().string();
142 std::string backupPath = versionDir / (filename + "." + std::to_string(timestamp));
143
144 fs::copy_file(mapFilePath, backupPath, fs::copy_options::overwrite_existing);
145
146 // 3. Prune old versions (Keep last 50)
147 try {
148 std::vector<fs::path> versions;
149 for (const auto& entry : fs::directory_iterator(versionDir)) {
150 if (entry.path().filename().string().starts_with(filename + ".")) {
151 versions.push_back(entry.path());
152 }
153 }
154 if (versions.size() > 50) {
155 std::sort(versions.begin(), versions.end(), [](const fs::path& a, const fs::path& b) {
156 return fs::last_write_time(a) < fs::last_write_time(b); // Oldest first
157 });
158 for (size_t i = 0; i < versions.size() - 50; ++i) {
159 fs::remove(versions[i]);
160 }
161 }
162 } catch (...) {
163 }
164 }
165
166 // 4. Erase draft file
167 fs::remove(draftPath);
168
169 return j;
170 }
171
172 void JsonMappingReader::discardDraft(const std::string& mapFilePath) {
173 std::string draftPath = getDraftPath(mapFilePath);
174 if (fs::exists(draftPath)) {
175 fs::remove(draftPath);
176 }
177 }
178
179 std::vector<JsonMappingReader::VersionEntry> JsonMappingReader::getHistory(const std::string& mapFilePath) {
180 std::vector<VersionEntry> history;
181 fs::path versionDir = fs::path(mapFilePath).parent_path() / "versions";
182 std::string baseName = fs::path(mapFilePath).filename().string();
183
184 if (!fs::exists(versionDir))
185 return history;
186
187 for (const auto& entry : fs::directory_iterator(versionDir)) {
188 if (entry.path().filename().string().starts_with(baseName + ".")) {
189 VersionEntry v;
190 v.filename = entry.path().string();
191 // Extract ID (timestamp) from filename extension
192 v.id = entry.path().extension().string().substr(1);
193
194 // Peek inside JSON to get the comment
195 try {
196 std::ifstream f(v.filename);
197 nlohmann::json j;
198 f >> j;
199 if (j.contains("meta")) {
200 if (j["meta"].contains("comment"))
201 v.comment = j["meta"]["comment"];
202 if (j["meta"].contains("created"))
203 v.date = j["meta"]["created"];
204 }
205 } catch (...) {
206 }
207
208 // Fallback date if not in meta
209 if (v.date.empty()) {
210 try {
211 long long ts = std::stoll(v.id);
212 std::time_t t = static_cast<std::time_t>(ts);
213 std::stringstream ss;
214 ss << std::put_time(std::gmtime(&t), "%Y-%m-%dT%H:%M:%SZ");
215 v.date = ss.str();
216 } catch (...) {
217 v.date = "Unknown";
218 }
219 }
220
221 history.push_back(v);
222 }
223 }
224 // Sort by ID (descending)
225 std::sort(history.begin(), history.end(), [](const VersionEntry& a, const VersionEntry& b) {
226 // String comparison of timestamps works if they are same length, but better to be safe
227 try {
228 return std::stoll(a.id) > std::stoll(b.id);
229 } catch (...) {
230 return a.id > b.id;
231 }
232 });
233 return history;
234 }
235
236 nlohmann::json JsonMappingReader::rollbackTo(const std::string& mapFilePath, const std::string& versionId) {
237 nlohmann::json j;
238
239 fs::path versionDir = fs::path(mapFilePath).parent_path() / "versions";
240 std::string baseName = fs::path(mapFilePath).filename().string();
241 fs::path backupPath = versionDir / (baseName + "." + versionId);
242
243 if (!fs::exists(backupPath)) {
244 throw std::runtime_error("Version not found: " + versionId);
245 }
246
247 // Validate before rollback
248 try {
249 std::ifstream f(backupPath);
250 f >> j;
252 } catch (const std::exception& e) {
253 throw std::runtime_error(std::string("Cannot rollback: Version is invalid against current schema: ") + e.what());
254 }
255
256 // Overwrite active file
257 fs::copy_file(backupPath, mapFilePath, fs::copy_options::overwrite_existing);
258
259 // Delete any existing draft to avoid confusion
260 discardDraft(mapFilePath);
261
262 return j;
263 }
264
265} // namespace mqtt::lib
static nlohmann::json deployDraft(const std::string &mapFilePath)
static nlohmann::json rollbackTo(const std::string &mapFilePath, const std::string &versionId)
static void discardDraft(const std::string &mapFilePath)
static std::vector< VersionEntry > getHistory(const std::string &mapFilePath)
static nlohmann::json readDraftOrActive(const std::string &mapFilePath)
static std::string getDraftPath(const std::string &mapFilePath)
static void saveDraft(const std::string &mapFilePath, const nlohmann::json &content)
static const nlohmann::json validate(const nlohmann::json &json)