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