1 #include <rapidjson/prettywriter.h>
2 #include <rapidjson/writer.h>
3
4 #include <fstream>
5 #include <iostream>
6 #include <pajlada/settings/detail/realpath.hpp>
7 #include <pajlada/settings/internal.hpp>
8 #include <pajlada/settings/settingdata.hpp>
9 #include <pajlada/settings/settingmanager.hpp>
10 #include <string>
11
12 using namespace std;
13
14 namespace pajlada {
15 namespace Settings {
16
SettingManager()17 SettingManager::SettingManager()
18 : document(rapidjson::kObjectType)
19 {
20 }
21
~SettingManager()22 SettingManager::~SettingManager()
23 {
24 // XXX(pajlada): Should settings automatically save on exit?
25 // Or on each setting change?
26 // Or only manually?
27 if (this->hasSaveMethodFlag(SaveMethod::SaveOnExit)) {
28 this->save();
29 }
30 }
31
32 void
pp(const string & prefix)33 SettingManager::pp(const string &prefix)
34 {
35 rapidjson::StringBuffer buffer;
36 rapidjson::PrettyWriter<rapidjson::StringBuffer> writer(buffer);
37 this->document.Accept(writer);
38
39 cout << prefix << buffer.GetString() << endl;
40 }
41
42 void
gPP(const string & prefix)43 SettingManager::gPP(const string &prefix)
44 {
45 auto instance = SettingManager::getInstance();
46
47 instance->pp(prefix);
48 }
49
50 string
stringify(const rapidjson::Value & v)51 SettingManager::stringify(const rapidjson::Value &v)
52 {
53 rapidjson::StringBuffer buffer;
54 rapidjson::Writer<rapidjson::StringBuffer> writer(buffer);
55 v.Accept(writer);
56
57 return string(buffer.GetString());
58 }
59
60 rapidjson::Value *
get(const char * path)61 SettingManager::get(const char *path)
62 {
63 auto ptr = rapidjson::Pointer(path);
64
65 if (!ptr.IsValid()) {
66 // For invalid paths, i.e. "988934jksgrhjkh" or "jgkh34gjk" (missing /)
67 return nullptr;
68 }
69
70 return ptr.Get(this->document);
71 }
72
73 bool
set(const char * path,const rapidjson::Value & value,SignalArgs args)74 SettingManager::set(const char *path, const rapidjson::Value &value,
75 SignalArgs args)
76 {
77 if (args.writeToFile) {
78 rapidjson::Pointer(path).Set(this->document, value);
79
80 if (this->hasSaveMethodFlag(SaveMethod::SaveOnSettingChange)) {
81 this->save();
82 }
83 }
84
85 this->notifyUpdate(path, value, std::move(args));
86
87 return true;
88 }
89
90 void
notifyUpdate(const string & path,const rapidjson::Value & value,SignalArgs args)91 SettingManager::notifyUpdate(const string &path, const rapidjson::Value &value,
92 SignalArgs args)
93 {
94 auto setting = this->getSetting(path);
95 if (!setting) {
96 return;
97 }
98
99 setting->notifyUpdate(value, std::move(args));
100 }
101
102 void
notifyLoadedValues()103 SettingManager::notifyLoadedValues()
104 {
105 // Fill in any settings that registered before we called load
106 this->settingsMutex.lock();
107
108 auto loadedSettings = this->settings;
109
110 this->settingsMutex.unlock();
111
112 for (const auto &it : loadedSettings) {
113 auto *v = this->get(it.first.c_str());
114 if (v == nullptr) {
115 continue;
116 }
117
118 // Maybe a "Load" source would make sense?
119 SignalArgs args;
120 args.source = SignalArgs::Source::Setter;
121
122 it.second->notifyUpdate(*v, std::move(args));
123 }
124 }
125
126 rapidjson::SizeType
arraySize(const string & path)127 SettingManager::arraySize(const string &path)
128 {
129 const auto &instance = SettingManager::getInstance();
130
131 auto valuePointer =
132 rapidjson::Pointer(path.c_str()).Get(instance->document);
133 if (valuePointer == nullptr) {
134 return false;
135 }
136
137 rapidjson::Value &value = *valuePointer;
138
139 if (!value.IsArray()) {
140 // Do we need to throw an error here?
141 return 0;
142 }
143
144 return value.Size();
145 }
146
147 // Returns true if the value at the given path is null or if doesn't exist
148 bool
isNull(const string & path)149 SettingManager::isNull(const string &path)
150 {
151 const auto &instance = SettingManager::getInstance();
152
153 return instance->_isNull(path);
154 }
155
156 bool
_isNull(const string & path)157 SettingManager::_isNull(const string &path)
158 {
159 auto valuePointer = rapidjson::Pointer(path.c_str()).Get(this->document);
160 if (valuePointer == nullptr) {
161 return true;
162 }
163
164 return valuePointer->IsNull();
165 }
166
167 void
setNull(const string & path)168 SettingManager::setNull(const string &path)
169 {
170 const auto &instance = SettingManager::getInstance();
171
172 rapidjson::Pointer(path.c_str())
173 .Set(instance->document, rapidjson::Value());
174 }
175
176 bool
removeArrayValue(const string & arrayPath,rapidjson::SizeType index)177 SettingManager::removeArrayValue(const string &arrayPath,
178 rapidjson::SizeType index)
179 {
180 const auto &instance = SettingManager::getInstance();
181
182 instance->clearSettings(arrayPath + "/" + to_string(index) + "/");
183
184 rapidjson::SizeType size = SettingManager::arraySize(arrayPath);
185
186 if (size == 0) {
187 // No values to remove
188 return false;
189 }
190
191 if (index >= size) {
192 // Index out of bounds
193 return false;
194 }
195
196 auto valuePointer =
197 rapidjson::Pointer(arrayPath.c_str()).Get(instance->document);
198 if (valuePointer == nullptr) {
199 return false;
200 }
201
202 rapidjson::Value &array = *valuePointer;
203
204 if (index == size - 1) {
205 // We want to remove the last element
206 array.PopBack();
207 } else {
208 SettingManager::setNull(arrayPath + "/" + to_string(index));
209 }
210
211 instance->clearSettings(arrayPath + "/" + to_string(index) + "/");
212
213 return true;
214 }
215
216 rapidjson::SizeType
cleanArray(const string & arrayPath)217 SettingManager::cleanArray(const string &arrayPath)
218 {
219 rapidjson::SizeType size = SettingManager::arraySize(arrayPath);
220
221 if (size == 0) {
222 // No values to remove
223 return 0;
224 }
225
226 const auto &instance = SettingManager::getInstance();
227
228 rapidjson::SizeType numValuesRemoved = 0;
229
230 for (rapidjson::SizeType i = size - 1; i > 0; --i) {
231 if (instance->_isNull(arrayPath + "/" + to_string(i))) {
232 SettingManager::removeArrayValue(arrayPath, i);
233 ++numValuesRemoved;
234 }
235 }
236
237 return numValuesRemoved;
238 }
239
240 vector<string>
getObjectKeys(const string & objectPath)241 SettingManager::getObjectKeys(const string &objectPath)
242 {
243 auto instance = SettingManager::getInstance();
244
245 vector<string> ret;
246
247 auto root = instance->get(objectPath.c_str());
248
249 if (root == nullptr || !root->IsObject()) {
250 return ret;
251 }
252
253 for (rapidjson::Value::ConstMemberIterator it = root->MemberBegin();
254 it != root->MemberEnd(); ++it) {
255 ret.emplace_back(it->name.GetString());
256 }
257
258 return ret;
259 }
260
261 void
clear()262 SettingManager::clear()
263 {
264 const auto &instance = SettingManager::getInstance();
265
266 // Clear document
267 rapidjson::Value(rapidjson::kObjectType).Swap(instance->document);
268
269 // Clear map of settings
270 lock_guard<mutex> lock(instance->settingsMutex);
271
272 instance->settings.clear();
273 }
274
275 bool
removeSetting(const string & path)276 SettingManager::removeSetting(const string &path)
277 {
278 const auto &instance = SettingManager::getInstance();
279
280 return instance->_removeSetting(path);
281 }
282
283 bool
_removeSetting(const string & path)284 SettingManager::_removeSetting(const string &path)
285 {
286 auto ptr = rapidjson::Pointer(path.c_str());
287
288 lock_guard<mutex> lock(this->settingsMutex);
289
290 this->settings.erase(path);
291
292 string pathWithExtendor;
293 if (path.at(path.length() - 1) == '/') {
294 pathWithExtendor = path;
295 } else {
296 pathWithExtendor = path + '/';
297 }
298
299 auto iter = this->settings.begin();
300 auto endIter = this->settings.end();
301 for (; iter != endIter;) {
302 const auto &p = *iter;
303 if (p.first.compare(0, pathWithExtendor.length(), pathWithExtendor) ==
304 0) {
305 rapidjson::Pointer(p.first.c_str()).Erase(this->document);
306 this->settings.erase(iter++);
307 } else {
308 ++iter;
309 }
310 }
311
312 return ptr.Erase(this->document);
313 }
314
315 void
clearSettings(const string & root)316 SettingManager::clearSettings(const string &root)
317 {
318 lock_guard<mutex> lock(this->settingsMutex);
319
320 vector<string> keysToBeRemoved;
321
322 for (const auto &setting : this->settings) {
323 if (setting.first.compare(0, root.length(), root) == 0) {
324 keysToBeRemoved.push_back(setting.first);
325 }
326 }
327
328 for (const auto &settingKey : keysToBeRemoved) {
329 this->settings.erase(settingKey);
330 }
331 }
332
333 void
setPath(const fs::path & newPath)334 SettingManager::setPath(const fs::path &newPath)
335 {
336 this->filePath = newPath;
337 }
338
339 SettingManager::LoadError
gLoad(const fs::path & path)340 SettingManager::gLoad(const fs::path &path)
341 {
342 const auto &instance = SettingManager::getInstance();
343
344 return instance->load(path);
345 }
346
347 SettingManager::LoadError
gLoadFrom(const fs::path & path)348 SettingManager::gLoadFrom(const fs::path &path)
349 {
350 const auto &instance = SettingManager::getInstance();
351
352 return instance->loadFrom(path);
353 }
354
355 SettingManager::LoadError
load(const fs::path & path)356 SettingManager::load(const fs::path &path)
357 {
358 if (!path.empty()) {
359 this->filePath = path;
360 }
361
362 return this->loadFrom(this->filePath);
363 }
364
365 SettingManager::LoadError
loadFrom(const fs::path & _path)366 SettingManager::loadFrom(const fs::path &_path)
367 {
368 fs_error_code ec;
369
370 auto path = detail::RealPath(_path, ec);
371
372 if (ec) {
373 return LoadError::FileHandleError;
374 }
375
376 // Open file
377 std::ifstream fh(path.c_str(), std::ios::binary | std::ios::in);
378 if (!fh) {
379 // Unable to open file at `path`
380 return LoadError::CannotOpenFile;
381 }
382
383 // Read size of file
384 auto fileSize = fs::file_size(path, ec);
385 if (ec) {
386 return LoadError::FileHandleError;
387 }
388
389 if (fileSize == 0) {
390 // Nothing to load
391 return LoadError::NoError;
392 }
393
394 // Create vector of appropriate size
395 std::vector<char> fileBuffer;
396 fileBuffer.resize(fileSize);
397
398 // Read file data into buffer
399 fh.read(&fileBuffer[0], fileSize);
400
401 // Merge newly parsed config file into our pre-existing document
402 // The pre-existing document might be empty, but we don't know that
403
404 rapidjson::ParseResult ok = this->document.Parse(&fileBuffer[0], fileSize);
405
406 // Make sure the file parsed okay
407 if (!ok) {
408 return LoadError::JSONParseError;
409 }
410
411 // This restricts config files a bit. They NEED to have an object root
412 if (!this->document.IsObject()) {
413 return LoadError::JSONParseError;
414 }
415
416 // Perform deep merge of objects
417 // detail::mergeObjects(document, d, document.GetAllocator());
418
419 this->notifyLoadedValues();
420
421 return LoadError::NoError;
422 }
423
424 bool
gSave(const fs::path & path)425 SettingManager::gSave(const fs::path &path)
426 {
427 const auto &instance = SettingManager::getInstance();
428
429 return instance->save(path);
430 }
431
432 bool
gSaveAs(const fs::path & path)433 SettingManager::gSaveAs(const fs::path &path)
434 {
435 const auto &instance = SettingManager::getInstance();
436
437 return instance->saveAs(path);
438 }
439
440 bool
save(const fs::path & path)441 SettingManager::save(const fs::path &path)
442 {
443 if (!path.empty()) {
444 this->filePath = path;
445 }
446
447 return this->saveAs(this->filePath);
448 }
449
450 bool
saveAs(const fs::path & _path)451 SettingManager::saveAs(const fs::path &_path)
452 {
453 fs_error_code ec;
454 fs::path path = detail::RealPath(_path, ec);
455 if (ec) {
456 return false;
457 }
458 fs::path tmpPath(_path);
459 tmpPath += ".tmp";
460
461 fs::path bkpPath(_path);
462 bkpPath += ".bkp";
463
464 auto res = this->writeTo(tmpPath);
465 if (!res) {
466 return res;
467 }
468
469 if (this->backup.enabled) {
470 fs::path firstBkpPath(bkpPath);
471 firstBkpPath += "-" + std::to_string(1);
472
473 if (this->backup.numSlots > 1) {
474 fs::path topBkpPath(bkpPath);
475 topBkpPath += "-" + std::to_string(this->backup.numSlots);
476 topBkpPath = detail::RealPath(topBkpPath, ec);
477 if (ec) {
478 return false;
479 }
480 // Remove top slot backup
481 fs::remove(topBkpPath, ec);
482
483 // Shift backups one slot up
484 for (uint8_t slotIndex = this->backup.numSlots - 1; slotIndex >= 1;
485 --slotIndex) {
486 fs::path p1(bkpPath);
487 p1 += "-" + std::to_string(slotIndex);
488 p1 = detail::RealPath(p1, ec);
489 if (ec) {
490 return false;
491 }
492 fs::path p2(bkpPath);
493 p2 += "-" + std::to_string(slotIndex + 1);
494 p2 = detail::RealPath(p2, ec);
495 if (ec) {
496 return false;
497 }
498 fs::rename(p1, p2, ec);
499 }
500 }
501
502 // Move current save to first backup slot
503 fs::rename(path, firstBkpPath, ec);
504 }
505
506 fs::rename(tmpPath, path, ec);
507
508 if (ec) {
509 return false;
510 }
511
512 return true;
513 }
514 bool
writeTo(const fs::path & path)515 SettingManager::writeTo(const fs::path &path)
516 {
517 std::ofstream fh(path.c_str(), std::ios::binary | std::ios::out);
518 if (!fh) {
519 // Unable to open file at `path`
520 return false;
521 }
522
523 rapidjson::StringBuffer buffer;
524 rapidjson::PrettyWriter<rapidjson::StringBuffer> writer(buffer);
525 this->document.Accept(writer);
526
527 fh.write(buffer.GetString(), buffer.GetSize());
528
529 return true;
530 }
531
532 void
setBackupEnabled(bool enabled)533 SettingManager::setBackupEnabled(bool enabled)
534 {
535 this->backup.enabled = enabled;
536 }
537
538 void
setBackupSlots(uint8_t numSlots)539 SettingManager::setBackupSlots(uint8_t numSlots)
540 {
541 this->backup.numSlots = numSlots;
542 }
543
544 weak_ptr<SettingData>
getSetting(const string & path,shared_ptr<SettingManager> instance)545 SettingManager::getSetting(const string &path,
546 shared_ptr<SettingManager> instance)
547 {
548 if (!instance) {
549 instance = SettingManager::getInstance();
550 }
551
552 lock_guard<mutex> lock(instance->settingsMutex);
553
554 auto &setting = instance->settings[path];
555
556 if (setting == nullptr) {
557 // No setting has been created with this path
558 setting.reset(new SettingData(path, instance));
559 }
560
561 return static_pointer_cast<SettingData>(setting);
562 }
563
564 shared_ptr<SettingData>
getSetting(const string & path)565 SettingManager::getSetting(const string &path)
566 {
567 lock_guard<mutex> lock(this->settingsMutex);
568
569 auto it = this->settings.find(path);
570
571 if (it == this->settings.end()) {
572 // no setting found at this path
573 return nullptr;
574 }
575
576 return it->second;
577 }
578
579 } // namespace Settings
580 } // namespace pajlada
581