swift
Loading...
Searching...
No Matches
valuecache.cpp
1// SPDX-FileCopyrightText: Copyright (C) 2015 swift Project Community / Contributors
2// SPDX-License-Identifier: GPL-3.0-or-later OR LicenseRef-swift-pilot-client-1
3
5
6#include "misc/valuecache.h"
7
8#include <algorithm>
9#include <atomic>
10#include <exception>
11#include <functional>
12#include <limits>
13
14#include <QByteArray>
15#include <QCoreApplication>
16#include <QDBusMetaType>
17#include <QDir>
18#include <QDirIterator>
19#include <QFileInfo>
20#include <QFlags>
21#include <QIODevice>
22#include <QJsonDocument>
23#include <QList>
24#include <QMetaMethod>
25#include <QMutexLocker>
26#include <QStandardPaths>
27#include <QThread>
28#include <Qt>
29
30#include "misc/atomicfile.h"
31#include "misc/identifier.h"
32#include "misc/lockfree.h"
33#include "misc/logcategories.h"
34#include "misc/logmessage.h"
36
37namespace swift::misc
38{
39
40 using private_ns::CValuePage;
41
43 template <typename T>
44 bool isSafeToIncrement(const T &value)
45 {
46 return value < std::numeric_limits<T>::max();
47 }
48
50 std::pair<QString &, std::atomic<bool> &> getCacheRootDirectoryMutable()
51 {
53 static std::atomic<bool> frozen { false };
54 return { dir, frozen };
55 }
56
57 void setMockCacheRootDirectory(const QString &dir)
58 {
59 Q_ASSERT_X(!getCacheRootDirectoryMutable().second, Q_FUNC_INFO, "Too late to call this function");
60 getCacheRootDirectoryMutable().first = dir;
61 }
62
63 const QString &CValueCache::getCacheRootDirectory()
64 {
65 getCacheRootDirectoryMutable().second = true;
66 return getCacheRootDirectoryMutable().first;
67 }
68
70 // CValueCachePacket
72
73 CValueCachePacket::CValueCachePacket(const CVariantMap &values, qint64 timestamp, bool saved, bool valuesChanged)
74 : m_saved(saved), m_valuesChanged(valuesChanged)
75 {
76 for (auto it = values.cbegin(); it != values.cend(); ++it)
77 {
78 implementationOf(*this).insert(CDictionary::cend(), it.key(), std::make_pair(it.value(), timestamp));
79 }
80 }
81
82 void CValueCachePacket::insert(const QString &key, const CVariant &value, qint64 timestamp)
83 {
84 CDictionary::insert(key, std::make_pair(value, timestamp));
85 }
86
87 void CValueCachePacket::insert(const CVariantMap &values, qint64 timestamp)
88 {
89 for (auto it = values.cbegin(); it != values.cend(); ++it)
90 {
91 CDictionary::insert(it.key(), std::make_pair(it.value(), timestamp));
92 }
93 }
94
95 CVariantMap CValueCachePacket::toVariantMap() const
96 {
97 CVariantMap result;
98 for (auto it = cbegin(); it != cend(); ++it)
99 {
100 implementationOf(result).insert(result.cend(), it.key(), it.value());
101 }
102 return result;
103 }
104
105 QMap<QString, qint64> CValueCachePacket::toTimestampMap() const
106 {
108 for (auto it = cbegin(); it != cend(); ++it)
109 {
110 implementationOf(result).insert(result.cend(), it.key(), it.timestamp());
111 }
112 return result;
113 }
114
115 QString CValueCachePacket::toTimestampMapString(const QStringList &keys) const
116 {
117 QStringList result;
118 for (const auto &key : keys)
119 {
120 QString time =
121 contains(key) ?
123 "no timestamp";
124 result.push_back(key + " (" + time + ")");
125 }
126 return result.join(",");
127 }
128
129 void CValueCachePacket::setTimestamps(const QMap<QString, qint64> &times)
130 {
131 for (auto it = times.cbegin(); it != times.cend(); ++it)
132 {
133 if (!contains(it.key()))
134 {
135 continue;
136 }
137 (*this)[it.key()].second = it.value();
138 }
139 }
140
141 CValueCachePacket CValueCachePacket::takeByKey(const QString &key)
142 {
143 auto copy = *this;
144 remove(key);
145 copy.removeByKeyIf([=](const QString &key2) { return key2 != key; });
146 return copy;
147 }
148
149 void CValueCachePacket::registerMetadata()
150 {
151 MetaType::registerMetadata();
152 qDBusRegisterMetaType<value_type>();
153 }
154
156 // CValueCache
158
159 const QStringList &CValueCache::getLogCategories()
160 {
161 static const QStringList cats({ "swift.valuecache", CLogCategories::services() });
162 return cats;
163 }
164
165 CValueCache::CValueCache(int fileSplitDepth, QObject *parent) : QObject(parent), m_fileSplitDepth(fileSplitDepth)
166 {
167 Q_ASSERT_X(fileSplitDepth >= 0, Q_FUNC_INFO, "Negative value not allowed, use 0 for maximum split depth");
168 Q_ASSERT_X(QThread::currentThread() == qApp->thread(), Q_FUNC_INFO, "Cache constructed in wrong thread");
169 }
170
171 struct CValueCache::Element
172 {
173 Element(const QString &key) : m_key(key) {}
174 const QString m_key;
175 CVariant m_value;
176 int m_pendingChanges = 0;
177 bool m_saved = false;
178 std::atomic<qint64> m_timestamp { 0 };
179 };
180
181 CValueCache::Element &CValueCache::getElement(const QString &key)
182 {
183 QMutexLocker lock(&m_mutex);
184 return getElement(key, std::as_const(m_elements).lowerBound(key));
185 }
186
187 CValueCache::Element &CValueCache::getElement(const QString &key, QMap<QString, ElementPtr>::const_iterator pos)
188 {
189 QMutexLocker lock(&m_mutex);
190 if (pos != m_elements.cend() && pos.key() == key)
191 {
192 return **pos;
193 }
194 Q_ASSERT(pos == std::as_const(m_elements).lowerBound(key));
195 return **m_elements.insert(pos, key, ElementPtr(new Element(key)));
196 }
197
198 std::tuple<CVariant, qint64, bool> CValueCache::getValue(const QString &key)
199 {
200 QMutexLocker lock(&m_mutex);
201 const auto &element = getElement(key);
202 return std::make_tuple(element.m_value, element.m_timestamp.load(), element.m_saved);
203 }
204
205 CVariantMap CValueCache::getAllValues(const QString &keyPrefix) const
206 {
207 QMutexLocker lock(&m_mutex);
208 CVariantMap map;
209 for (const auto &element : elementsStartingWith(keyPrefix))
210 {
211 if (!element->m_value.isValid())
212 {
213 continue;
214 }
215 implementationOf(map).insert(map.cend(), element->m_key, element->m_value);
216 }
217 return map;
218 }
219
220 CVariantMap CValueCache::getAllValues(const QStringList &keys) const
221 {
222 QMutexLocker lock(&m_mutex);
223 CVariantMap map;
224 for (const auto &key : keys)
225 {
226 auto it = m_elements.constFind(key);
227 if (it == m_elements.cend())
228 {
229 continue;
230 }
231 if (!(*it)->m_value.isValid())
232 {
233 continue;
234 }
235 map.insert(key, (*it)->m_value);
236 }
237 return map;
238 }
239
240 CValueCachePacket CValueCache::getAllValuesWithTimestamps(const QString &keyPrefix) const
241 {
242 QMutexLocker lock(&m_mutex);
243 CValueCachePacket map;
244 for (const auto &element : elementsStartingWith(keyPrefix))
245 {
246 if (!element->m_value.isValid())
247 {
248 continue;
249 }
250 map.insert(element->m_key, element->m_value, element->m_timestamp);
251 }
252 return map;
253 }
254
255 QStringList CValueCache::getAllUnsavedKeys(const QString &keyPrefix) const
256 {
257 QMutexLocker lock(&m_mutex);
258 QStringList keys;
259 for (const auto &element : elementsStartingWith(keyPrefix))
260 {
261 if (element->m_value.isValid() && !element->m_saved)
262 {
263 keys.push_back(element->m_key);
264 }
265 }
266 return keys;
267 }
268
269 void CValueCache::insertValues(const CValueCachePacket &values)
270 {
271 QMutexLocker lock(&m_mutex);
272 changeValues(values);
273 }
274
275 void CValueCache::changeValues(const CValueCachePacket &values)
276 {
277 QMutexLocker lock(&m_mutex);
278 if (values.isEmpty())
279 {
280 return;
281 }
282 m_elements.detach();
283 auto out = std::as_const(m_elements).lowerBound(values.cbegin().key());
284 auto end = std::as_const(m_elements).upperBound((std::prev(values.cend())).key());
285 for (auto in = values.cbegin(); in != values.cend(); ++in)
286 {
287 while (out != end && out.key() < in.key())
288 {
289 ++out;
290 }
291 auto &element = getElement(in.key(), out);
292
293 if (values.valuesChanged())
294 {
295 Q_ASSERT(isSafeToIncrement(element.m_pendingChanges));
296 element.m_pendingChanges++;
297 element.m_value = in.value();
298 element.m_timestamp = in.timestamp();
299 }
300 element.m_saved = values.isSaved();
301 }
302 if (values.valuesChanged())
303 {
304 emit valuesChanged(values, sender());
305 }
306 emit valuesChangedByLocal(values);
307
308 if (!isSignalConnected(QMetaMethod::fromSignal(&CValueCache::valuesChangedByLocal)))
309 {
310 changeValuesFromRemote(values, CIdentifier());
311 }
312 }
313
314 void CValueCache::changeValuesFromRemote(const CValueCachePacket &values, const CIdentifier &originator)
315 {
316 QMutexLocker lock(&m_mutex);
317 if (values.isEmpty())
318 {
319 return;
320 }
321 if (!values.valuesChanged())
322 {
323 if (values.isSaved())
324 {
325 emit valuesSaveRequested(values);
326 }
327 return;
328 }
329 CValueCachePacket ratifiedChanges(values.isSaved());
330 CValueCachePacket ackedChanges(values.isSaved());
331 m_elements.detach();
332 auto out = std::as_const(m_elements).lowerBound(values.cbegin().key());
333 auto end = std::as_const(m_elements).upperBound((std::prev(values.cend())).key());
334 for (auto in = values.cbegin(); in != values.cend(); ++in)
335 {
336 while (out != end && out.key() < in.key())
337 {
338 ++out;
339 }
340 auto &element = getElement(in.key(), out);
341
342 if (originator.hasApplicationProcessId()) // round trip
343 {
344 element.m_pendingChanges--;
345 Q_ASSERT(element.m_pendingChanges >= 0);
346 ackedChanges.insert(in.key(), in.value(), in.timestamp());
347 }
348 else if (element.m_pendingChanges ==
349 0) // ratify a change only if own change is not pending, to ensure consistency
350 {
351 element.m_value = in.value();
352 element.m_timestamp = in.timestamp();
353 element.m_saved = values.isSaved();
354 ratifiedChanges.insert(in.key(), in.value(), in.timestamp());
355 }
356 }
357 if (!ratifiedChanges.isEmpty())
358 {
359 if (ratifiedChanges.isSaved())
360 {
361 emit valuesSaveRequested(ratifiedChanges);
362 }
363 emit valuesChanged(ratifiedChanges, nullptr);
364 }
365 if (!ackedChanges.isEmpty() && ackedChanges.isSaved())
366 {
367 emit valuesSaveRequested(ackedChanges);
368 }
369 }
370
371 QJsonObject CValueCache::saveToJson(const QString &keyPrefix) const
372 {
373 return getAllValues(keyPrefix).toMemoizedJson();
374 }
375
376 void CValueCache::loadFromJson(const QJsonObject &json)
377 {
378 CVariantMap map;
379 map.convertFromMemoizedJson(json);
380 if (!map.isEmpty())
381 {
382 insertValues({ map, QDateTime::currentMSecsSinceEpoch() });
383 }
384 }
385
386 CStatusMessageList CValueCache::loadFromJsonNoThrow(const QJsonObject &json, const CLogCategoryList &categories,
387 const QString &prefix)
388 {
389 CVariantMap map;
390 auto messages = map.convertFromMemoizedJsonNoThrow(json, categories, prefix);
391 if (!map.isEmpty())
392 {
393 insertValues({ map, QDateTime::currentMSecsSinceEpoch() });
394 }
395 return messages;
396 }
397
398 CStatusMessage CValueCache::saveToFiles(const QString &dir, const QString &keyPrefix)
399 {
400 QMutexLocker lock(&m_mutex);
401 auto values = getAllValues(keyPrefix);
402 auto status = saveToFiles(dir, values);
403 if (status.isSuccess())
404 {
405 markAllAsSaved(keyPrefix);
406 }
407 return status;
408 }
409
410 CStatusMessage CValueCache::saveToFiles(const QString &dir, const QStringList &keys)
411 {
412 QMutexLocker lock(&m_mutex);
413 auto values = getAllValues(keys);
414 auto status = saveToFiles(dir, values);
415 if (status.isSuccess())
416 {
417 markAllAsSaved(keys);
418 }
419 return status;
420 }
421
422 CStatusMessage CValueCache::saveToFiles(const QString &dir, const CVariantMap &values,
423 const QString &keysMessage) const
424 {
426 for (auto it = values.cbegin(); it != values.cend(); ++it)
427 {
428 Q_ASSERT(it.value().isValid());
429 namespaces[it.key().section('/', 0, m_fileSplitDepth - 1)].insert(it.key(), it.value());
430 }
431 if (!QDir::root().mkpath(dir))
432 {
433 return CStatusMessage(this).error(u"Failed to create directory '%1'") << dir;
434 }
435 for (auto it = namespaces.cbegin(); it != namespaces.cend(); ++it)
436 {
437 CAtomicFile file(dir + "/" + it.key() + ".json");
438 if (!QDir::root().mkpath(QFileInfo(file).path()))
439 {
440 return CStatusMessage(this).error(u"Failed to create directory '%1'") << QFileInfo(file).path();
441 }
442 if (!file.open(QFile::ReadWrite | QFile::Text))
443 {
444 return CStatusMessage(this).error(u"Failed to open %1: %2") << file.fileName() << file.errorString();
445 }
446 auto json = QJsonDocument::fromJson(file.readAll());
447 if (json.isArray() || (json.isNull() && !json.isEmpty()))
448 {
449 return CStatusMessage(this).error(u"Invalid JSON format in %1") << file.fileName();
450 }
451 auto object = json.object();
452 json.setObject(it->mergeToMemoizedJson(object));
453
454 if (!file.seek(0) || !file.resize(0) || file.write(json.toJson()) <= 0 || !file.checkedClose())
455 {
456 return CStatusMessage(this).error(u"Failed to write to %1: %2")
457 << file.fileName() << file.errorString();
458 }
459 }
460 return CStatusMessage(this).info(u"Written '%1' to value cache in '%2'")
461 << (keysMessage.isEmpty() ? values.keys().to<QStringList>().join(",") : keysMessage) << dir;
462 }
463
464 CStatusMessage CValueCache::loadFromFiles(const QString &dir)
465 {
466 QMutexLocker lock(&m_mutex);
467 CValueCachePacket values;
468 auto status = loadFromFiles(dir, {}, getAllValues(), values);
469 values.setSaved();
470 insertValues(values);
471 return status;
472 }
473
474 CStatusMessage CValueCache::loadFromFiles(const QString &dir, const QSet<QString> &keys,
475 const CVariantMap &currentValues, CValueCachePacket &o_values,
476 const QString &keysMessage, bool keysOnly) const
477 {
478 if (!QDir(dir).exists())
479 {
480 return CStatusMessage(this).warning(u"No such directory '%1'") << dir;
481 }
482 if (!QDir(dir).isReadable())
483 {
484 return CStatusMessage(this).error(u"Failed to read from directory '%1'") << dir;
485 }
486
487 QMap<QString, QStringList> keysInFiles;
488 for (const auto &key : keys)
489 {
490 keysInFiles[key.section('/', 0, m_fileSplitDepth - 1) + ".json"].push_back(key);
491 }
492 if (keys.isEmpty())
493 {
494 QDirIterator iter(dir, { "*.json" }, QDir::Files, QDirIterator::Subdirectories);
495 while (iter.hasNext())
496 {
497 keysInFiles.insert(QDir(dir).relativeFilePath(iter.next()), {});
498 }
499 }
500 bool ok = true;
501 for (auto it = keysInFiles.cbegin(); it != keysInFiles.cend(); ++it)
502 {
503 QFile file(QDir(dir).absoluteFilePath(it.key()));
504 if (!file.exists())
505 {
506 continue;
507 }
508 if (!file.open(QFile::ReadOnly | QFile::Text))
509 {
510 return CStatusMessage(this).error(u"Failed to open %1: %2") << file.fileName() << file.errorString();
511 }
512 auto json = QJsonDocument::fromJson(file.readAll());
513 if (json.isArray() || (json.isNull() && !json.isEmpty()))
514 {
515 return CStatusMessage(this).error(u"Invalid JSON format in %1") << file.fileName();
516 }
517
518 CVariantMap temp;
519 if (keysOnly)
520 {
521 for (const auto &key : json.object().keys())
522 {
523 temp.insert(key, {});
524 } // clazy:exclude=range-loop
525 }
526 else
527 {
528 const QString messagePrefix = QStringLiteral("Parsing %1").arg(it.key());
529 auto messages = temp.convertFromMemoizedJsonNoThrow(json.object(), it.value(), this, messagePrefix);
530 if (it.value().isEmpty())
531 {
532 messages.push_back(temp.convertFromMemoizedJsonNoThrow(json.object(), this, messagePrefix));
533 }
534 if (!messages.isEmpty())
535 {
536 ok = false;
537 backupFile(file);
538 CLogMessage::preformatted(messages);
539 }
540 }
541 temp.removeDuplicates(currentValues);
542 o_values.insert(temp, QFileInfo(file).lastModified().toMSecsSinceEpoch());
543 }
544 return CStatusMessage(this).info(u"Loaded cache values '%1' from '%2' '%3'")
545 << (keysMessage.isEmpty() ? o_values.keys().to<QStringList>().join(",") : keysMessage) << dir
546 << (ok ? "successfully" : "with errors");
547 }
548
549 void CValueCache::backupFile(QFile &file) const
550 {
551 QDir dir = getCacheRootDirectory();
552 QString relative = "backups/" + dir.relativeFilePath(file.fileName());
553 QString absolute = dir.absoluteFilePath(relative);
554 absolute += "." + QDateTime::currentDateTime().toString(QStringLiteral("yyMMddhhmmss"));
555 if (QFile::exists(absolute))
556 {
557 return;
558 }
559 if (!dir.mkpath(QFileInfo(relative).path()))
560 {
561 CLogMessage(this).error(u"Failed to create %1") << QFileInfo(absolute).path();
562 return;
563 }
564 if (!file.copy(absolute))
565 {
566 CLogMessage(this).error(u"Failed to back up %1: %2") << QFileInfo(file).fileName() << file.errorString();
567 return;
568 }
569 CLogMessage(this).info(u"Backed up %1 to %2") << QFileInfo(file).fileName() << dir.absoluteFilePath("backups");
570 }
571
572 void CValueCache::markAllAsSaved(const QString &keyPrefix)
573 {
574 QMutexLocker lock(&m_mutex);
575 for (const auto &element : elementsStartingWith(keyPrefix))
576 {
577 element->m_saved = true;
578 }
579 }
580
581 void CValueCache::markAllAsSaved(const QStringList &keys)
582 {
583 QMutexLocker lock(&m_mutex);
584 for (const auto &key : keys)
585 {
586 getElement(key).m_saved = true;
587 }
588 }
589
590 QString CValueCache::filenameForKey(const QString &key) const
591 {
592 return key.section('/', 0, m_fileSplitDepth - 1) + ".json";
593 }
594
595 QStringList CValueCache::enumerateFiles(const QString &dir) const
596 {
597 auto values = getAllValues();
598 QSet<QString> files;
599 for (auto it = values.begin(); it != values.end(); ++it)
600 {
601 files.insert(dir + "/" + filenameForKey(it.key()));
602 }
603 return files.values();
604 }
605
606 void CValueCache::clearAllValues(const QString &keyPrefix)
607 {
608 QMutexLocker lock(&m_mutex);
609 auto values = getAllValues(keyPrefix);
610 for (auto it = values.begin(); it != values.end(); ++it)
611 {
612 it.value() = CVariant();
613 }
614 changeValues({ values, 0 });
615 }
616
617 QString CValueCache::getHumanReadableName(const QString &key) const
618 {
619 QMutexLocker lock(&m_mutex);
620 return m_humanReadable.value(key, key);
621 }
622
623 QString CValueCache::getHumanReadableWithKey(const QString &key) const
624 {
625 QMutexLocker lock(&m_mutex);
626 QString hr = m_humanReadable.value(key);
627 return hr.isEmpty() ? key : QStringLiteral("%1 (%2)").arg(hr, key);
628 }
629
630 void CValueCache::setHumanReadableName(const QString &key, const QString &name)
631 {
632 QMutexLocker lock(&m_mutex);
633 if (!m_humanReadable.contains(key))
634 {
635 m_humanReadable.insert(key, name);
636 }
637 }
638
639 CValueCache::BatchGuard CValueCache::batchChanges(QObject *owner)
640 {
641 Q_ASSERT(QThread::currentThread() == owner->thread());
642
643 auto &page = CValuePage::getPageFor(owner, this);
644 page.beginBatch();
645 return page;
646 }
647
648 CValueCache::BatchGuard::~BatchGuard()
649 {
650 if (m_page)
651 {
652 if (std::uncaught_exceptions() > 0)
653 {
654 m_page->abandonBatch();
655 }
656 else
657 {
658 m_page->endBatch();
659 }
660 }
661 }
662
663 void CValueCache::connectPage(CValuePage *page)
664 {
665 connect(page, &CValuePage::valuesWantToCache, this, &CValueCache::changeValues);
666 connect(this, &CValueCache::valuesChanged, page, &CValuePage::setValuesFromCache);
667 }
668
670 // Private :: CValuePage
672
673 const QStringList &CValuePage::getLogCategories() { return CValueCache::getLogCategories(); }
674
675 CValuePage::CValuePage(QObject *parent, CValueCache *cache) : QObject(parent), m_cache(cache)
676 {
677 m_cache->connectPage(this);
678 }
679
680 CValuePage &CValuePage::getPageFor(QObject *parent, CValueCache *cache)
681 {
682 auto pages = parent->findChildren<CValuePage *>("", Qt::FindDirectChildrenOnly);
683 auto it = std::ranges::find_if(pages, [cache](CValuePage *page) { return page->m_cache == cache; });
684 if (it == pages.cend())
685 {
686 return *new CValuePage(parent, cache);
687 }
688 return **it;
689 }
690
691 struct CValuePage::Element
692 {
693 Element(const QString &key, const QString &name, int metaType, const Validator &validator,
694 const CVariant &defaultValue)
695 : m_key(key), m_name(name), m_metaType(metaType), m_validator(validator), m_default(defaultValue)
696 {}
697 const QString m_key;
698 const QString m_name;
699 const QString m_nameWithKey = m_name.isEmpty() ? m_key : QStringLiteral("%1 (%2)").arg(m_name, m_key);
700 LockFree<CVariant> m_value;
701 std::atomic<qint64> m_timestamp { 0 };
702 const int m_metaType = QMetaType::UnknownType;
703 const Validator m_validator;
704 const CVariant m_default;
705 NotifySlot m_notifySlot;
706 int m_pendingChanges = 0;
707 bool m_saved = false;
708 };
709
710 CValuePage::Element &CValuePage::createElement(const QString &keyTemplate, const QString &name, int metaType,
711 const Validator &validator, const CVariant &defaultValue)
712 {
713 if (parent()->objectName().isEmpty() && keyTemplate.contains("%OwnerName%"))
714 {
715 static Element dummy("", "", QMetaType::UnknownType, nullptr, {});
716 return dummy;
717 }
718
719 QString key = keyTemplate;
720 key.replace("%Application%", QFileInfo(QCoreApplication::applicationFilePath()).completeBaseName(),
722 key.replace("%OwnerClass%", QString(parent()->metaObject()->className()).replace("::", "/"),
724 key.replace("%OwnerName%", parent()->objectName(), Qt::CaseInsensitive);
725
726 QString unused;
727 Q_ASSERT_X(!m_elements.contains(key), "CValuePage",
728 "Can't have two CCached in the same object referring to the same value");
729 Q_ASSERT_X(defaultValue.isValid() ? defaultValue.userType() == metaType : true, "CValuePage",
730 "Metatype mismatch for default value");
731 Q_ASSERT_X(defaultValue.isValid() && validator ? validator(defaultValue, unused) : true, "CValuePage",
732 "Validator rejects default value");
733 Q_UNUSED(unused)
734
735 auto &element = *(m_elements[key] = ElementPtr(new Element(key, name, metaType, validator, defaultValue)));
736 std::forward_as_tuple(element.m_value.uniqueWrite(), element.m_timestamp, element.m_saved) =
737 m_cache->getValue(key);
738
739 auto status = validate(element, element.m_value.read(), CStatusMessage::SeverityDebug);
740 if (!status.isEmpty()) // intentionally kept !empty here, debug message supposed to write default value
741 {
742 element.m_value.uniqueWrite() = defaultValue;
743
744 if (status.getSeverity() == CStatusMessage::SeverityDebug)
745 {
746 QMutexLocker lock(&m_cache->m_warnedKeysMutex);
747 if (!m_cache->m_warnedKeys.contains(key))
748 {
749 m_cache->m_warnedKeys.insert(key);
750 CLogMessage::preformatted(status);
751 }
752 }
753 else
754 {
755 CLogMessage::preformatted(status);
756 }
757 }
758
759 return element;
760 }
761
762 void CValuePage::setNotifySlot(Element &element, const NotifySlot &slot) { element.m_notifySlot = slot; }
763
764 bool CValuePage::isInitialized(const Element &element) const { return !element.m_key.isEmpty(); }
765
766 bool CValuePage::isValid(const Element &element, int typeId) const
767 {
768 auto reader = element.m_value.read();
769 return reader->isValid() && reader->userType() == typeId;
770 }
771
772 const CVariant &CValuePage::getValue(const Element &element) const
773 {
774 Q_ASSERT_X(!element.m_key.isEmpty(), Q_FUNC_INFO,
775 "Empty key suggests an attempt to use value before objectName available for %%OwnerName%%");
776 Q_ASSERT(QThread::currentThread() == thread());
777
778 return element.m_value.read();
779 }
780
781 CVariant CValuePage::getValueCopy(const Element &element) const
782 {
783 Q_ASSERT_X(!element.m_key.isEmpty(), Q_FUNC_INFO,
784 "Empty key suggests an attempt to use value before objectName available for %%OwnerName%%");
785 return element.m_value.read(); // NOLINT(modernize-return-braced-init-list)
786 }
787
788 CStatusMessage CValuePage::setValue(Element &element, CVariant value, qint64 timestamp, bool save)
789 {
790 Q_ASSERT_X(!element.m_key.isEmpty(), Q_FUNC_INFO,
791 "Empty key suggests an attempt to use value before objectName available for %%OwnerName%%");
792 Q_ASSERT(QThread::currentThread() == thread());
793
794 if (timestamp == 0)
795 {
797 }
798 if (!value.isValid())
799 {
800 value = element.m_value.read();
801 }
802
803 bool changed = element.m_timestamp != timestamp || element.m_value.read() != value;
804 if (!changed && !save)
805 {
806 return CStatusMessage(this).info(u"Value '%1' not set, same timestamp and value") << element.m_nameWithKey;
807 }
808
809 auto status = validate(element, value, CStatusMessage::SeverityError);
810 if (status.isSuccess())
811 {
812 if (!changed)
813 {
814 element.m_saved = save;
815 emit valuesWantToCache({ { { element.m_key, value } }, 0, save, false });
816 }
817 else if (m_batchMode > 0)
818 {
819 m_batchedValues[element.m_key] = value;
820 }
821 else
822 {
823 Q_ASSERT(isSafeToIncrement(element.m_pendingChanges));
824 element.m_pendingChanges++;
825 element.m_saved = save;
826
827 element.m_timestamp = timestamp;
828 element.m_value.uniqueWrite() = value;
829 emit valuesWantToCache({ { { element.m_key, value } }, timestamp, save });
830 }
831 // All good info
832 status = CStatusMessage(this).info(u"Set value '%1'") << element.m_nameWithKey;
833 }
834 return status;
835 }
836
837 const QString &CValuePage::getKey(const Element &element) const { return element.m_key; }
838
839 qint64 CValuePage::getTimestamp(const Element &element) const
840 {
841 Q_ASSERT_X(!element.m_key.isEmpty(), Q_FUNC_INFO,
842 "Empty key suggests an attempt to use value before objectName available for %%OwnerName%%");
843 return element.m_timestamp;
844 }
845
846 bool CValuePage::isSaved(const Element &element) const { return element.m_saved && !element.m_pendingChanges; }
847
848 bool CValuePage::isSaving(const Element &element) const { return element.m_saved && element.m_pendingChanges; }
849
850 void CValuePage::setValuesFromCache(const CValueCachePacket &values, QObject *changedBy)
851 {
852 Q_ASSERT(QThread::currentThread() == thread());
853 Q_ASSERT_X(values.valuesChanged(), Q_FUNC_INFO, "packet with unchanged values should not reach here");
854
855 CSequence<NotifySlot *> notifySlots;
856
857 forEachIntersection(m_elements, values,
858 [changedBy, this, &notifySlots, &values](const QString &, const ElementPtr &element,
859 CValueCachePacket::const_iterator it) {
860 if (changedBy == this) // round trip
861 {
862 element->m_pendingChanges--;
863 Q_ASSERT(element->m_pendingChanges >= 0);
864 }
865 else if (element->m_pendingChanges ==
866 0) // ratify a change only if own change is not pending, to ensure consistency
867 {
868 auto error = validate(*element, it.value(), CStatusMessage::SeverityError);
869 if (error.isSuccess())
870 {
871 element->m_value.uniqueWrite() = it.value();
872 element->m_timestamp = it.timestamp();
873 element->m_saved = values.isSaved();
874 if (element->m_notifySlot.first &&
875 (!element->m_notifySlot.second || !notifySlots.containsBy([&](auto slot) {
876 return slot->second == element->m_notifySlot.second;
877 })))
878 {
879 notifySlots.push_back(&element->m_notifySlot);
880 }
881 }
882 else
883 {
884 CLogMessage::preformatted(error);
885 }
886 }
887 });
888
889 for (auto slot : notifySlots)
890 {
891 slot->first(parent());
892 }
893 }
894
895 void CValuePage::beginBatch()
896 {
897 Q_ASSERT(QThread::currentThread() == thread());
898
899 Q_ASSERT(isSafeToIncrement(m_batchMode));
900 if (m_batchMode <= 0)
901 {
902 m_batchedValues.clear();
903 }
904 m_batchMode++;
905 }
906
907 void CValuePage::abandonBatch()
908 {
909 Q_ASSERT(QThread::currentThread() == thread());
910
911 Q_ASSERT(m_batchMode >= 0);
912 m_batchMode--;
913 }
914
915 void CValuePage::endBatch()
916 {
917 Q_ASSERT(QThread::currentThread() == thread());
918
919 Q_ASSERT(m_batchMode >= 0);
920 m_batchMode--;
921
922 if (m_batchMode <= 0 && !m_batchedValues.isEmpty())
923 {
924 qint64 timestamp = QDateTime::currentMSecsSinceEpoch();
926 m_elements, m_batchedValues,
927 [timestamp](const QString &, const ElementPtr &element, CVariantMap::const_iterator it) {
928 Q_ASSERT(isSafeToIncrement(element->m_pendingChanges));
929 element->m_pendingChanges++;
930 element->m_value.uniqueWrite() = it.value();
931 element->m_timestamp = timestamp;
932 element->m_saved = false;
933 });
934 emit valuesWantToCache({ m_batchedValues, timestamp });
935 }
936 }
937
938 CStatusMessage CValuePage::validate(const Element &element, const CVariant &value,
939 CStatusMessage::StatusSeverity invalidSeverity) const
940 {
941 QString reason;
942 if (!value.isValid())
943 {
944 return CStatusMessage(this, invalidSeverity, u"Empty cache value %1", true) << element.m_nameWithKey;
945 }
946 if (value.userType() != element.m_metaType)
947 {
948 return CStatusMessage(this).error(u"Expected %1 but got %2 for %3")
949 << QMetaType(element.m_metaType).name() << value.typeName() << element.m_nameWithKey;
950 }
951 if (element.m_validator && !element.m_validator(value, reason))
952 {
953 if (reason.isEmpty())
954 {
955 return CStatusMessage(this).error(u"%1 is not valid for %2")
956 << value.toQString() << element.m_nameWithKey;
957 }
958 return CStatusMessage(this).error(u"%1 (%2 for %3)")
959 << reason << value.toQString() << element.m_nameWithKey;
960 }
961 else
962 {
963 return {};
964 }
965 }
966
967} // namespace swift::misc
968
static const QString & normalizedApplicationDataDirectory()
swift application data directory for one specific installation (a version)
CValueCachePacket(bool saved=false, bool valuesChanged=true)
Constructor.
Definition valuecache.h:76
Map of { QString, CVariant } pairs.
Definition variantmap.h:35
Free functions in swift::misc.
SWIFT_MISC_EXPORT void setMockCacheRootDirectory(const QString &path)
Overwrite the default root directory for cache and settings, for testing purposes.
QString className(const QObject *object)
Class name as from QMetaObject::className with namespace.
void forEachIntersection(const Map1 &map1, const Map2 &map2, F functor)
Call a functor for each {key,value1,value2} triple in the keywise intersection of two maps.
Definition dictionary.h:541
T::const_iterator end(const LockFreeReader< T > &reader)
Non-member begin() and end() for so LockFree containers can be used in ranged for loops.
Definition lockfree.h:264
QMap< Key, Value > & implementationOf(QMap< Key, Value > &dict)
Identity function for API consistency with CDictionary::implementationOf.
Definition dictionary.h:523
QString applicationFilePath()
QDateTime currentDateTime()
qint64 currentMSecsSinceEpoch()
QDateTime fromMSecsSinceEpoch(qint64 msecs)
QString toString(QStringView format) const const
QString absoluteFilePath(const QString &fileName) const const
bool mkpath(const QString &dirPath, std::optional< QFileDevice::Permissions > permissions) const const
QString relativeFilePath(const QString &fileName) const const
QDir root()
bool copy(const QString &fileName, const QString &newName)
bool exists(const QString &fileName)
virtual QString fileName() const const override
QString fileName() const const
QString path() const const
QString errorString() const const
QJsonDocument fromJson(const QByteArray &json, QJsonParseError *error)
bool isEmpty() const const
QStringList keys() const const
QJsonValue value(QLatin1StringView key) const const
void push_back(QList< T >::parameter_type value)
const Key & key() const const
QMap< Key, T >::const_iterator cbegin() const const
QMap< Key, T >::const_iterator cend() const const
QMap< Key, T >::iterator insert(QMap< Key, T >::const_iterator pos, const Key &key, const T &value)
QMetaMethod fromSignal(PointerToMemberFunction signal)
const char * name() const const
QList< T > findChildren(QAnyStringView name, Qt::FindChildOptions options) const const
QThread * thread() const const
QSet< T >::iterator insert(QSet< T >::const_iterator it, const T &value)
QList< T > values() const const
QString arg(Args &&... args) const const
bool contains(QChar ch, Qt::CaseSensitivity cs) const const
QString first(qsizetype n) &&
bool isEmpty() const const
QString & replace(QChar before, QChar after, Qt::CaseSensitivity cs)
QString section(QChar sep, qsizetype start, qsizetype end, QString::SectionFlags flags) const const
QString join(QChar separator) const const
CaseInsensitive
FindDirectChildrenOnly
QFuture< void > map(Iterator begin, Iterator end, MapFunctor &&function)
QFuture< QtFuture::ArgsType< Signal > > connect(Sender *sender, Signal signal)
QThread * currentThread()
QTimeZone utc()
bool validator(int value, QString &)
Is value between 0 - 100?