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