15#include <QCoreApplication>
16#include <QDBusMetaType>
18#include <QDirIterator>
22#include <QJsonDocument>
25#include <QMutexLocker>
26#include <QStandardPaths>
40 using private_ns::CValuePage;
44 bool isSafeToIncrement(
const T &value)
46 return value < std::numeric_limits<T>::max();
50 std::pair<QString &, std::atomic<bool> &> getCacheRootDirectoryMutable()
53 static std::atomic<bool> frozen {
false };
54 return { dir, frozen };
59 Q_ASSERT_X(!getCacheRootDirectoryMutable().second, Q_FUNC_INFO,
"Too late to call this function");
60 getCacheRootDirectoryMutable().first = dir;
63 const QString &CValueCache::getCacheRootDirectory()
65 getCacheRootDirectoryMutable().second =
true;
66 return getCacheRootDirectoryMutable().
first;
74 : m_saved(saved), m_valuesChanged(valuesChanged)
76 for (
auto it = values.cbegin(); it != values.cend(); ++it)
78 implementationOf(*this).insert(CDictionary::cend(), it.key(), std::make_pair(it.value(), timestamp));
82 void CValueCachePacket::insert(
const QString &key,
const CVariant &value, qint64 timestamp)
84 CDictionary::insert(key, std::make_pair(value, timestamp));
87 void CValueCachePacket::insert(
const CVariantMap &values, qint64 timestamp)
89 for (
auto it = values.cbegin(); it != values.cend(); ++it)
91 CDictionary::insert(it.key(), std::make_pair(it.value(), timestamp));
95 CVariantMap CValueCachePacket::toVariantMap()
const
98 for (
auto it = cbegin(); it != cend(); ++it)
108 for (
auto it = cbegin(); it != cend(); ++it)
118 for (
const auto &key : keys)
124 result.
push_back(key +
" (" + time +
")");
126 return result.
join(
",");
131 for (
auto it = times.
cbegin(); it != times.
cend(); ++it)
133 if (!contains(it.key()))
137 (*this)[it.key()].second = it.value();
141 CValueCachePacket CValueCachePacket::takeByKey(
const QString &key)
145 copy.removeByKeyIf([=](
const QString &key2) {
return key2 != key; });
149 void CValueCachePacket::registerMetadata()
151 MetaType::registerMetadata();
152 qDBusRegisterMetaType<value_type>();
161 static const QStringList cats({
"swift.valuecache", CLogCategories::services() });
165 CValueCache::CValueCache(
int fileSplitDepth,
QObject *parent) :
QObject(parent), m_fileSplitDepth(fileSplitDepth)
167 Q_ASSERT_X(fileSplitDepth >= 0, Q_FUNC_INFO,
"Negative value not allowed, use 0 for maximum split depth");
171 struct CValueCache::Element
173 Element(
const QString &key) : m_key(key) {}
176 int m_pendingChanges = 0;
177 bool m_saved =
false;
178 std::atomic<qint64> m_timestamp { 0 };
181 CValueCache::Element &CValueCache::getElement(
const QString &key)
184 return getElement(key, std::as_const(m_elements).lowerBound(key));
190 if (pos != m_elements.cend() && pos.
key() == key)
194 Q_ASSERT(pos == std::as_const(m_elements).lowerBound(key));
195 return **m_elements.insert(pos, key, ElementPtr(
new Element(key)));
198 std::tuple<CVariant, qint64, bool> CValueCache::getValue(
const QString &key)
201 const auto &element = getElement(key);
202 return std::make_tuple(element.m_value, element.m_timestamp.load(), element.m_saved);
205 CVariantMap CValueCache::getAllValues(
const QString &keyPrefix)
const
209 for (
const auto &element : elementsStartingWith(keyPrefix))
211 if (!element->m_value.isValid())
220 CVariantMap CValueCache::getAllValues(
const QStringList &keys)
const
224 for (
const auto &key : keys)
226 auto it = m_elements.constFind(key);
227 if (it == m_elements.cend())
231 if (!(*it)->m_value.isValid())
235 map.insert(key, (*it)->m_value);
240 CValueCachePacket CValueCache::getAllValuesWithTimestamps(
const QString &keyPrefix)
const
243 CValueCachePacket
map;
244 for (
const auto &element : elementsStartingWith(keyPrefix))
246 if (!element->m_value.isValid())
250 map.insert(element->m_key, element->m_value, element->m_timestamp);
259 for (
const auto &element : elementsStartingWith(keyPrefix))
261 if (element->m_value.isValid() && !element->m_saved)
269 void CValueCache::insertValues(
const CValueCachePacket &values)
272 changeValues(values);
275 void CValueCache::changeValues(
const CValueCachePacket &values)
278 if (values.isEmpty())
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)
287 while (out != end && out.key() < in.key())
291 auto &element = getElement(in.key(), out);
293 if (values.valuesChanged())
295 Q_ASSERT(isSafeToIncrement(element.m_pendingChanges));
296 element.m_pendingChanges++;
297 element.m_value = in.value();
298 element.m_timestamp = in.timestamp();
300 element.m_saved = values.isSaved();
302 if (values.valuesChanged())
304 emit valuesChanged(values, sender());
306 emit valuesChangedByLocal(values);
310 changeValuesFromRemote(values, CIdentifier());
314 void CValueCache::changeValuesFromRemote(
const CValueCachePacket &values,
const CIdentifier &originator)
317 if (values.isEmpty())
321 if (!values.valuesChanged())
323 if (values.isSaved())
325 emit valuesSaveRequested(values);
329 CValueCachePacket ratifiedChanges(values.isSaved());
330 CValueCachePacket ackedChanges(values.isSaved());
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)
336 while (out != end && out.key() < in.key())
340 auto &element = getElement(in.key(), out);
342 if (originator.hasApplicationProcessId())
344 element.m_pendingChanges--;
345 Q_ASSERT(element.m_pendingChanges >= 0);
346 ackedChanges.insert(in.key(), in.value(), in.timestamp());
348 else if (element.m_pendingChanges ==
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());
357 if (!ratifiedChanges.isEmpty())
359 if (ratifiedChanges.isSaved())
361 emit valuesSaveRequested(ratifiedChanges);
363 emit valuesChanged(ratifiedChanges,
nullptr);
365 if (!ackedChanges.isEmpty() && ackedChanges.isSaved())
367 emit valuesSaveRequested(ackedChanges);
373 return getAllValues(keyPrefix).toMemoizedJson();
376 void CValueCache::loadFromJson(
const QJsonObject &json)
379 map.convertFromMemoizedJson(json);
386 CStatusMessageList CValueCache::loadFromJsonNoThrow(
const QJsonObject &json,
const CLogCategoryList &categories,
390 auto messages =
map.convertFromMemoizedJsonNoThrow(json, categories, prefix);
398 CStatusMessage CValueCache::saveToFiles(
const QString &dir,
const QString &keyPrefix)
401 auto values = getAllValues(keyPrefix);
402 auto status = saveToFiles(dir, values);
403 if (status.isSuccess())
405 markAllAsSaved(keyPrefix);
413 auto values = getAllValues(keys);
414 auto status = saveToFiles(dir, values);
415 if (status.isSuccess())
417 markAllAsSaved(keys);
422 CStatusMessage CValueCache::saveToFiles(
const QString &dir,
const CVariantMap &values,
423 const QString &keysMessage)
const
426 for (
auto it = values.cbegin(); it != values.cend(); ++it)
428 Q_ASSERT(it.value().isValid());
429 namespaces[it.key().section(
'/', 0, m_fileSplitDepth - 1)].
insert(it.key(), it.value());
433 return CStatusMessage(
this).error(u
"Failed to create directory '%1'") << dir;
435 for (
auto it = namespaces.
cbegin(); it != namespaces.
cend(); ++it)
437 CAtomicFile file(dir +
"/" + it.key() +
".json");
440 return CStatusMessage(
this).error(u
"Failed to create directory '%1'") <<
QFileInfo(file).
path();
444 return CStatusMessage(
this).error(u
"Failed to open %1: %2") << file.fileName() << file.errorString();
447 if (json.isArray() || (json.isNull() && !json.
isEmpty()))
449 return CStatusMessage(
this).error(u
"Invalid JSON format in %1") << file.fileName();
451 auto object = json.object();
452 json.setObject(it->mergeToMemoizedJson(
object));
454 if (!file.seek(0) || !file.resize(0) || file.write(json.toJson()) <= 0 || !file.checkedClose())
456 return CStatusMessage(
this).error(u
"Failed to write to %1: %2")
457 << file.fileName() << file.errorString();
460 return CStatusMessage(
this).info(u
"Written '%1' to value cache in '%2'")
464 CStatusMessage CValueCache::loadFromFiles(
const QString &dir)
467 CValueCachePacket values;
468 auto status = loadFromFiles(dir, {}, getAllValues(), values);
470 insertValues(values);
475 const CVariantMap ¤tValues, CValueCachePacket &o_values,
476 const QString &keysMessage,
bool keysOnly)
const
478 if (!
QDir(dir).exists())
480 return CStatusMessage(
this).warning(u
"No such directory '%1'") << dir;
482 if (!
QDir(dir).isReadable())
484 return CStatusMessage(
this).error(u
"Failed to read from directory '%1'") << dir;
488 for (
const auto &key : keys)
490 keysInFiles[key.
section(
'/', 0, m_fileSplitDepth - 1) +
".json"].push_back(key);
495 while (iter.hasNext())
497 keysInFiles.
insert(
QDir(dir).relativeFilePath(iter.next()), {});
501 for (
auto it = keysInFiles.
cbegin(); it != keysInFiles.
cend(); ++it)
503 QFile file(
QDir(dir).absoluteFilePath(it.key()));
510 return CStatusMessage(
this).error(u
"Failed to open %1: %2") << file.fileName() << file.errorString();
513 if (json.isArray() || (json.isNull() && !json.
isEmpty()))
515 return CStatusMessage(
this).error(u
"Invalid JSON format in %1") << file.fileName();
521 for (
const auto &key : json.object().
keys())
523 temp.insert(key, {});
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())
532 messages.push_back(temp.convertFromMemoizedJsonNoThrow(json.object(),
this, messagePrefix));
534 if (!messages.isEmpty())
538 CLogMessage::preformatted(messages);
541 temp.removeDuplicates(currentValues);
542 o_values.insert(temp,
QFileInfo(file).lastModified().toMSecsSinceEpoch());
544 return CStatusMessage(
this).info(u
"Loaded cache values '%1' from '%2' '%3'")
546 << (ok ?
"successfully" :
"with errors");
549 void CValueCache::backupFile(
QFile &file)
const
551 QDir dir = getCacheRootDirectory();
561 CLogMessage(
this).error(u
"Failed to create %1") <<
QFileInfo(absolute).
path();
564 if (!file.
copy(absolute))
572 void CValueCache::markAllAsSaved(
const QString &keyPrefix)
575 for (
const auto &element : elementsStartingWith(keyPrefix))
577 element->m_saved =
true;
581 void CValueCache::markAllAsSaved(
const QStringList &keys)
584 for (
const auto &key : keys)
586 getElement(key).m_saved =
true;
592 return key.
section(
'/', 0, m_fileSplitDepth - 1) +
".json";
597 auto values = getAllValues();
599 for (
auto it = values.begin(); it != values.end(); ++it)
601 files.
insert(dir +
"/" + filenameForKey(it.key()));
606 void CValueCache::clearAllValues(
const QString &keyPrefix)
609 auto values = getAllValues(keyPrefix);
610 for (
auto it = values.begin(); it != values.end(); ++it)
612 it.value() = CVariant();
614 changeValues({ values, 0 });
617 QString CValueCache::getHumanReadableName(
const QString &key)
const
620 return m_humanReadable.value(key, key);
623 QString CValueCache::getHumanReadableWithKey(
const QString &key)
const
626 QString hr = m_humanReadable.value(key);
627 return hr.
isEmpty() ? key : QStringLiteral(
"%1 (%2)").
arg(hr, key);
630 void CValueCache::setHumanReadableName(
const QString &key,
const QString &name)
633 if (!m_humanReadable.contains(key))
635 m_humanReadable.insert(key, name);
639 CValueCache::BatchGuard CValueCache::batchChanges(
QObject *owner)
643 auto &page = CValuePage::getPageFor(owner,
this);
648 CValueCache::BatchGuard::~BatchGuard()
652 if (std::uncaught_exceptions() > 0)
654 m_page->abandonBatch();
663 void CValueCache::connectPage(CValuePage *page)
665 connect(page, &CValuePage::valuesWantToCache,
this, &CValueCache::changeValues);
666 connect(
this, &CValueCache::valuesChanged, page, &CValuePage::setValuesFromCache);
673 const QStringList &CValuePage::getLogCategories() {
return CValueCache::getLogCategories(); }
675 CValuePage::CValuePage(
QObject *parent, CValueCache *cache) :
QObject(parent), m_cache(cache)
677 m_cache->connectPage(
this);
680 CValuePage &CValuePage::getPageFor(
QObject *parent, CValueCache *cache)
683 auto it = std::ranges::find_if(pages, [cache](CValuePage *page) {
return page->m_cache == cache; });
684 if (it == pages.cend())
686 return *
new CValuePage(parent, cache);
691 struct CValuePage::Element
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)
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 };
703 const Validator m_validator;
704 const CVariant m_default;
705 NotifySlot m_notifySlot;
706 int m_pendingChanges = 0;
707 bool m_saved =
false;
710 CValuePage::Element &CValuePage::createElement(
const QString &keyTemplate,
const QString &name,
int metaType,
711 const Validator &validator,
const CVariant &defaultValue)
713 if (parent()->objectName().isEmpty() && keyTemplate.
contains(
"%OwnerName%"))
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");
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);
739 auto status = validate(element, element.m_value.read(), CStatusMessage::SeverityDebug);
740 if (!status.isEmpty())
742 element.m_value.uniqueWrite() = defaultValue;
744 if (status.getSeverity() == CStatusMessage::SeverityDebug)
747 if (!m_cache->m_warnedKeys.contains(key))
749 m_cache->m_warnedKeys.insert(key);
750 CLogMessage::preformatted(status);
755 CLogMessage::preformatted(status);
762 void CValuePage::setNotifySlot(Element &element,
const NotifySlot &slot) { element.m_notifySlot = slot; }
764 bool CValuePage::isInitialized(
const Element &element)
const {
return !element.m_key.isEmpty(); }
766 bool CValuePage::isValid(
const Element &element,
int typeId)
const
768 auto reader = element.m_value.read();
769 return reader->isValid() && reader->userType() == typeId;
772 const CVariant &CValuePage::getValue(
const Element &element)
const
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%%");
778 return element.m_value.read();
781 CVariant CValuePage::getValueCopy(
const Element &element)
const
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();
788 CStatusMessage CValuePage::setValue(Element &element, CVariant value, qint64 timestamp,
bool save)
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%%");
798 if (!value.isValid())
800 value = element.m_value.read();
803 bool changed = element.m_timestamp != timestamp || element.m_value.read() != value;
804 if (!changed && !save)
806 return CStatusMessage(
this).info(u
"Value '%1' not set, same timestamp and value") << element.m_nameWithKey;
809 auto status = validate(element, value, CStatusMessage::SeverityError);
810 if (status.isSuccess())
814 element.m_saved = save;
815 emit valuesWantToCache({ { { element.m_key, value } }, 0, save,
false });
817 else if (m_batchMode > 0)
819 m_batchedValues[element.m_key] = value;
823 Q_ASSERT(isSafeToIncrement(element.m_pendingChanges));
824 element.m_pendingChanges++;
825 element.m_saved = save;
827 element.m_timestamp = timestamp;
828 element.m_value.uniqueWrite() = value;
829 emit valuesWantToCache({ { { element.m_key, value } }, timestamp, save });
832 status = CStatusMessage(
this).info(u
"Set value '%1'") << element.m_nameWithKey;
837 const QString &CValuePage::getKey(
const Element &element)
const {
return element.m_key; }
839 qint64 CValuePage::getTimestamp(
const Element &element)
const
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;
846 bool CValuePage::isSaved(
const Element &element)
const {
return element.m_saved && !element.m_pendingChanges; }
848 bool CValuePage::isSaving(
const Element &element)
const {
return element.m_saved && element.m_pendingChanges; }
850 void CValuePage::setValuesFromCache(
const CValueCachePacket &values,
QObject *changedBy)
853 Q_ASSERT_X(values.valuesChanged(), Q_FUNC_INFO,
"packet with unchanged values should not reach here");
855 CSequence<NotifySlot *> notifySlots;
858 [changedBy,
this, ¬ifySlots, &values](
const QString &,
const ElementPtr &element,
859 CValueCachePacket::const_iterator it) {
860 if (changedBy ==
this)
862 element->m_pendingChanges--;
863 Q_ASSERT(element->m_pendingChanges >= 0);
865 else if (element->m_pendingChanges ==
868 auto error = validate(*element, it.value(), CStatusMessage::SeverityError);
869 if (error.isSuccess())
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;
879 notifySlots.push_back(&element->m_notifySlot);
884 CLogMessage::preformatted(error);
889 for (
auto slot : notifySlots)
891 slot->first(parent());
895 void CValuePage::beginBatch()
899 Q_ASSERT(isSafeToIncrement(m_batchMode));
900 if (m_batchMode <= 0)
902 m_batchedValues.clear();
907 void CValuePage::abandonBatch()
911 Q_ASSERT(m_batchMode >= 0);
915 void CValuePage::endBatch()
919 Q_ASSERT(m_batchMode >= 0);
922 if (m_batchMode <= 0 && !m_batchedValues.isEmpty())
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;
934 emit valuesWantToCache({ m_batchedValues, timestamp });
938 CStatusMessage CValuePage::validate(
const Element &element,
const CVariant &value,
939 CStatusMessage::StatusSeverity invalidSeverity)
const
942 if (!value.isValid())
944 return CStatusMessage(
this, invalidSeverity, u
"Empty cache value %1",
true) << element.m_nameWithKey;
946 if (value.userType() != element.m_metaType)
948 return CStatusMessage(
this).error(u
"Expected %1 but got %2 for %3")
949 <<
QMetaType(element.m_metaType).
name() << value.typeName() << element.m_nameWithKey;
951 if (element.m_validator && !element.m_validator(value, reason))
955 return CStatusMessage(
this).error(u
"%1 is not valid for %2")
956 << value.toQString() << element.m_nameWithKey;
958 return CStatusMessage(
this).error(u
"%1 (%2 for %3)")
959 << reason << value.toQString() << element.m_nameWithKey;
static const QString & normalizedApplicationDataDirectory()
swift application data directory for one specific installation (a version)
CValueCachePacket(bool saved=false, bool valuesChanged=true)
Constructor.
Map of { QString, CVariant } pairs.
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.
T::const_iterator end(const LockFreeReader< T > &reader)
Non-member begin() and end() for so LockFree containers can be used in ranged for loops.
QMap< Key, Value > & implementationOf(QMap< Key, Value > &dict)
Identity function for API consistency with CDictionary::implementationOf.
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
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)
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
QFuture< void > map(Iterator begin, Iterator end, MapFunctor &&function)
QFuture< QtFuture::ArgsType< Signal > > connect(Sender *sender, Signal signal)
QThread * currentThread()
bool validator(int value, QString &)
Is value between 0 - 100?