swift
databasereader.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 <QByteArray>
7 #include <QFileInfo>
8 #include <QJsonDocument>
9 #include <QJsonObject>
10 #include <QJsonValueRef>
11 #include <QMetaObject>
12 #include <QNetworkReply>
13 #include <QPointer>
14 #include <QReadLocker>
15 #include <QStringBuilder>
16 #include <QUrl>
17 #include <QWriteLocker>
18 
19 #include "core/application.h"
20 #include "core/db/databaseutils.h"
21 #include "core/db/infodatareader.h"
22 #include "core/webdataservices.h"
24 #include "misc/directoryutils.h"
25 #include "misc/logcategories.h"
26 #include "misc/logmessage.h"
29 #include "misc/swiftdirectories.h"
30 #include "misc/verify.h"
31 
32 using namespace swift::misc;
33 using namespace swift::misc::db;
34 using namespace swift::misc::network;
35 using namespace swift::core;
36 using namespace swift::core::data;
37 using namespace swift::core::db;
38 
39 namespace swift::core::db
40 {
41  CDatabaseReader::CDatabaseReader(QObject *owner, const CDatabaseReaderConfigList &config, const QString &name)
42  : CThreadedReader(owner, name), m_config(config)
43  {}
44 
45  void CDatabaseReader::readInBackgroundThread(CEntityFlags::Entity entities, const QDateTime &newerThan)
46  {
47  if (!this->doWorkCheck()) { return; }
48 
49  // we accept cached data
50  Q_ASSERT_X(!entities.testFlag(CEntityFlags::DbInfoObjectEntity), Q_FUNC_INFO, "Read info objects directly");
51  const bool hasDbInfoObjects =
52  this->hasDbInfoObjects(); // no info objects is not necessarily an error, but indicates a) either data not
53  // available (DB down) or b) only caches are used
54  // const bool hasSharedInfoObjects = this->hasSharedInfoObjects();
55  CEntityFlags::Entity allEntities = entities;
56  CEntityFlags::Entity cachedEntities = CEntityFlags::NoEntity;
57  CEntityFlags::Entity dbEntities = CEntityFlags::NoEntity;
58  CEntityFlags::Entity sharedEntities = CEntityFlags::NoEntity;
59  CEntityFlags::Entity currentEntity =
60  CEntityFlags::iterateDbEntities(allEntities); // CEntityFlags::InfoObjectEntity will be ignored
61  while (currentEntity)
62  {
63  const CDatabaseReaderConfig config(this->getConfigForEntity(currentEntity));
64  const QString currentEntityName = CEntityFlags::entitiesToString(currentEntity);
65 
66  // retrieval mode
67  const CDbFlags::DataRetrievalMode rm = config.getRetrievalMode(); // DB reading, cache, shared
68  Q_ASSERT_X(!rm.testFlag(CDbFlags::Unspecified), Q_FUNC_INFO, "Missing retrieval mode");
69  const QString rmString = CDbFlags::flagToString(rm);
70  const CDbFlags::DataRetrievalModeFlag rmDbOrSharedFlag =
71  CDbFlags::modeToModeFlag(rm & CDbFlags::DbReadingOrShared);
72  const QString rmDbOrSharedFlagString = CDbFlags::flagToString(rmDbOrSharedFlag);
73  const bool rmDbReadingOrShared =
74  (rmDbOrSharedFlag == CDbFlags::DbReading || rmDbOrSharedFlag == CDbFlags::Shared);
75  const int currentEntityCount = this->getCacheCount(currentEntity);
76 
77  if (rm.testFlag(CDbFlags::Ignore) || rm.testFlag(CDbFlags::Canceled))
78  {
79  // do not load
80  }
81  else if (rm.testFlag(CDbFlags::Cached))
82  {
83  //
84  // !!! info object comparisons only for: cache + shared or cache + DB data
85  //
86  if (hasDbInfoObjects && rmDbReadingOrShared)
87  {
88  // check mode here for consistency
89  Q_ASSERT_X(!getBaseUrl(rmDbOrSharedFlag).isEmpty(), Q_FUNC_INFO, "Wrong retrieval mode");
90 
91  CUrl oldUrlInfo;
92  CUrl newUrlInfo;
93  const bool changedUrl = this->hasChangedUrl(currentEntity, oldUrlInfo, newUrlInfo);
94  const QDateTime cacheTs(this->getCacheTimestamp(currentEntity));
95  const QDateTime latestEntityTs(this->getLatestEntityTimestampFromDbInfoObjects(currentEntity));
96  const qint64 cacheTimestamp = cacheTs.isValid() ? cacheTs.toMSecsSinceEpoch() : -1;
97  const qint64 latestEntityTimestamp =
98  latestEntityTs.isValid() ? latestEntityTs.toMSecsSinceEpoch() : -1;
99  Q_ASSERT_X(latestEntityTimestamp >= 0, Q_FUNC_INFO, "Missing timestamp");
100  if (!changedUrl && cacheTimestamp >= latestEntityTimestamp && cacheTimestamp >= 0 &&
101  latestEntityTimestamp >= 0)
102  {
103  this->admitCaches(currentEntity);
104  cachedEntities |= currentEntity; // read from cache
105  CLogMessage(this).info(u"Using cache for '%1' (%2, %3)")
106  << currentEntityName << cacheTs.toString() << cacheTimestamp;
107  }
108  else
109  {
110  Q_ASSERT_X(rmDbReadingOrShared, Q_FUNC_INFO, "Wrong retrieval mode");
111  if (rmDbOrSharedFlag == CDbFlags::DbReading) { dbEntities |= currentEntity; }
112  else if (rmDbOrSharedFlag == CDbFlags::Shared) { sharedEntities |= currentEntity; }
113 
114  if (changedUrl)
115  {
116  CLogMessage(this).info(
117  u"Data location for '%1' changed ('%2'->'%3'), will override cache for reading '%4'")
118  << currentEntityName << oldUrlInfo.toQString() << newUrlInfo.toQString()
119  << rmDbOrSharedFlagString;
120  }
121  else
122  {
123  CLogMessage(this).info(u"Cache for '%1' outdated, latest entity (%2, %3), reading '%4'")
124  << currentEntityName << latestEntityTs.toString() << latestEntityTimestamp
125  << rmDbOrSharedFlagString;
126  }
127  }
128  }
129  else
130  {
131  if (!rmDbReadingOrShared)
132  {
133  CLogMessage(this).info(u"No DB or shared reading for '%1', read mode is: '%2'")
134  << currentEntityName << rmString;
135  }
136  if (!hasDbInfoObjects)
137  {
138  CLogMessage(this).info(u"No DB info objects for '%1', read mode is: '%2'")
139  << currentEntityName << rmString;
140  }
141  if (currentEntityCount > 0)
142  {
143  CLogMessage(this).info(u"Cache for '%1' already read, %2 entries")
144  << currentEntityName << currentEntityCount;
145  }
146  else
147  {
148  // no info objects, server down or no shared/db read mode
149  this->admitCaches(currentEntity);
150  if (!rmDbReadingOrShared)
151  {
152  // intentionally we do not want to read from DB/shared
153  CLogMessage(this).info(u"Triggered reading cache for '%1', read mode: %2")
154  << currentEntityName << rmString;
155  }
156  else
157  {
158  // we want to read from DB/shared, but have no info object
159  CLogMessage(this).info(u"No info object for '%1', triggered reading cache, read mode: %2")
160  << currentEntityName << rmString;
161  }
162  }
163  cachedEntities |= currentEntity; // read from cache
164  }
165  }
166  else
167  {
168  // cache ignored
169  Q_ASSERT_X(rmDbReadingOrShared, Q_FUNC_INFO, "Wrong retrieval mode");
170  if (rmDbOrSharedFlag == CDbFlags::DbReading) { dbEntities |= currentEntity; }
171  else if (rmDbOrSharedFlag == CDbFlags::Shared) { sharedEntities |= currentEntity; }
172  }
173  currentEntity = CEntityFlags::iterateDbEntities(allEntities);
174  }
175 
176  // signals for the cached entities
177  if (cachedEntities != CEntityFlags::NoEntity)
178  {
179  this->emitReadSignalPerSingleCachedEntity(cachedEntities, true);
180  }
181 
182  // Real read from DB
183  if (dbEntities != CEntityFlags::NoEntity)
184  {
185  CLogMessage(this).info(u"Start reading DB entities: %1") << CEntityFlags::entitiesToString(dbEntities);
186  this->startReadFromBackendInBackgroundThread(dbEntities, CDbFlags::DbReading, newerThan);
187  }
188 
189  // Real read from shared
190  if (sharedEntities != CEntityFlags::NoEntity)
191  {
192  CLogMessage(this).info(u"Start reading shared entities: %1")
193  << CEntityFlags::entitiesToString(sharedEntities);
194  this->startReadFromBackendInBackgroundThread(dbEntities, CDbFlags::Shared, newerThan);
195  }
196  }
197 
198  CEntityFlags::Entity CDatabaseReader::triggerLoadingDirectlyFromDb(CEntityFlags::Entity entities,
199  const QDateTime &newerThan)
200  {
201  this->startReadFromBackendInBackgroundThread(entities, CDbFlags::DbReading, newerThan);
202  return entities;
203  }
204 
205  CEntityFlags::Entity CDatabaseReader::triggerLoadingDirectlyFromSharedFiles(CEntityFlags::Entity entities,
206  bool checkCacheTsUpfront)
207  {
208  if (entities == CEntityFlags::NoEntity) { return CEntityFlags::NoEntity; }
209  if (checkCacheTsUpfront)
210  {
211  CEntityFlags::Entity newerHeaderEntities = this->getEntitesWithNewerSharedInfoObject(entities);
212  if (newerHeaderEntities != entities)
213  {
214  const CEntityFlags::Entity validInCacheEntities = (entities ^ newerHeaderEntities) & entities;
215  CLogMessage(this).info(u"Reduced '%1' to '%2' before triggering load of shared files (still in cache)")
216  << CEntityFlags::entitiesToString(entities) << CEntityFlags::entitiesToString(newerHeaderEntities);
217 
218  // In case we have difference entities we treat them as they were loaded, they result from still being
219  // in the cache Using timer to first finish this function, then the resulting signal
220  if (validInCacheEntities != CEntityFlags::NoEntity)
221  {
222  QPointer<CDatabaseReader> myself(this);
223  QTimer::singleShot(0, this, [=] {
224  if (!myself) { return; }
225  if (!sApp || sApp->isShuttingDown()) { return; }
226  emit this->dataRead(validInCacheEntities, CEntityFlags::ReadFinished, 0, {});
227  });
228  }
229  if (newerHeaderEntities == CEntityFlags::NoEntity) { return CEntityFlags::NoEntity; }
230  entities = newerHeaderEntities;
231  }
232  }
233  this->startReadFromBackendInBackgroundThread(entities, CDbFlags::Shared);
234  return entities;
235  }
236 
239  const QDateTime &newerThan)
240  {
241  Q_ASSERT_X(mode == CDbFlags::DbReading || mode == CDbFlags::Shared, Q_FUNC_INFO, "Wrong mode");
242 
243  // ps_read is implemented in the derived classes
244  if (entities == CEntityFlags::NoEntity) { return; }
245 
246  QPointer<CDatabaseReader> myself(this);
247  QTimer::singleShot(0, this, [=] {
248  if (!sApp || sApp->isShuttingDown() || !myself) { return; }
249  this->read(entities, mode, newerThan);
250  });
251  }
252 
255  {
256  Q_ASSERT_X(nwReply, Q_FUNC_INFO, "missing reply");
257  JsonDatastoreResponse datastoreResponse;
258  const bool ok = this->setHeaderInfoPart(datastoreResponse, nwReply);
259  if (ok)
260  {
261  const QString dataFileData = nwReply->readAll();
262  nwReply->close(); // close asap
263  datastoreResponse.setStringSize(dataFileData.size());
264  if (dataFileData.isEmpty())
265  {
266  datastoreResponse.setMessage(
267  CStatusMessage(this, CStatusMessage::SeverityError, u"Empty response, no data"));
268  }
269  else { CDatabaseReader::stringToDatastoreResponse(dataFileData, datastoreResponse); }
270  }
271  return datastoreResponse;
272  }
273 
275  {
276  HeaderResponse headerResponse;
277  const bool success = this->setHeaderInfoPart(headerResponse, nwReply);
278  Q_UNUSED(success);
279  return headerResponse;
280  }
281 
283  QNetworkReply *nwReply) const
284  {
285  Q_ASSERT_X(nwReply, Q_FUNC_INFO, "Missing reply");
286  this->threadAssertCheck();
287  if (!this->doWorkCheck())
288  {
289  nwReply->abort();
290  headerResponse.setMessage(
291  CStatusMessage(this, CStatusMessage::SeverityError, u"Terminated data parsing process"));
292  return false; // stop, terminate straight away, ending thread
293  }
294 
295  headerResponse.setValues(nwReply);
296  if (nwReply->error() == QNetworkReply::NoError)
297  {
298  // do not close because of obtaining data
299  return true;
300  }
301  else
302  {
303  // no valid response
304  const QString error(nwReply->errorString());
305  const QString url(nwReply->url().toString());
306  nwReply->abort();
307  headerResponse.setMessage(
308  CStatusMessage(this, CStatusMessage::SeverityError, u"Reading data failed: '" % error % u"' " % url));
309  return false;
310  }
311  }
312 
315  {
316  this->setReplyStatus(nwReply);
318  if (dsr.isSharedFile()) { this->receivedSharedFileHeaderNonClosing(nwReply); }
319  else
320  {
321  if (dsr.isLoadedFromDb())
322  {
323  const bool s = !dsr.hasErrorMessage();
324  emit this->swiftDbDataRead(s);
325  }
326  }
327  return dsr;
328  }
329 
331  {
332  static const CDbInfoList e;
333  if (!sApp->hasWebDataServices()) { return e; }
334  if (!sApp->getWebDataServices()->getDbInfoDataReader()) { return e; }
336  }
337 
339  {
340  static const CDbInfoList e;
341  if (!sApp->hasWebDataServices()) { return e; }
342  if (!sApp->getWebDataServices()->getSharedInfoDataReader()) { return e; }
344  }
345 
347 
349 
350  bool CDatabaseReader::hasSharedFileHeader(const CEntityFlags::Entity entity) const
351  {
352  Q_ASSERT_X(CEntityFlags::isSingleEntity(entity), Q_FUNC_INFO, "need single entity");
353  return m_sharedFileResponses.contains(entity);
354  }
355 
356  bool CDatabaseReader::hasSharedFileHeaders(const CEntityFlags::Entity entities) const
357  {
358  CEntityFlags::Entity myEntities = maskBySupportedEntities(entities);
359  CEntityFlags::Entity currentEntity = CEntityFlags::iterateDbEntities(myEntities);
360  while (currentEntity != CEntityFlags::NoEntity)
361  {
362  if (!hasSharedFileHeader(currentEntity)) { return false; }
363  currentEntity = CEntityFlags::iterateDbEntities(myEntities);
364  }
365  return true;
366  }
367 
368  QDateTime CDatabaseReader::getLatestEntityTimestampFromDbInfoObjects(CEntityFlags::Entity entity) const
369  {
370  Q_ASSERT_X(CEntityFlags::isSingleEntity(entity), Q_FUNC_INFO, "need single entity");
371  static const QDateTime e;
372  const CDbInfoList il(getDbInfoObjects());
373  if (il.isEmpty() || entity == CEntityFlags::NoEntity) { return e; }
374 
375  // for some entities there can be more than one entry because of the
376  // raw tables (see DB view last updates)
377  const CDbInfo info = il.findFirstByEntityOrDefault(entity);
378  if (!info.isValid()) { return e; }
379  return info.getUtcTimestamp();
380  }
381 
382  QDateTime CDatabaseReader::getLatestEntityTimestampFromSharedInfoObjects(CEntityFlags::Entity entity) const
383  {
384  Q_ASSERT_X(CEntityFlags::isSingleEntity(entity), Q_FUNC_INFO, "need single entity");
385  static const QDateTime e;
386  const CDbInfoList il(this->getSharedInfoObjects());
387  if (il.isEmpty() || entity == CEntityFlags::NoEntity) { return e; }
388 
389  const CDbInfo info = il.findFirstByEntityOrDefault(entity);
390  if (!info.isValid()) { return e; }
391  return info.getUtcTimestamp();
392  }
393 
394  QDateTime CDatabaseReader::getLatestSharedFileHeaderTimestamp(CEntityFlags::Entity entity) const
395  {
396  Q_ASSERT_X(CEntityFlags::isSingleEntity(entity), Q_FUNC_INFO, "need single entity");
397  static const QDateTime e;
398  if (!this->hasSharedFileHeader(entity)) { return e; }
399  return m_sharedFileResponses[entity].getLastModifiedTimestamp();
400  }
401  bool CDatabaseReader::isSharedHeaderNewerThanCacheTimestamp(CEntityFlags::Entity entity) const
402  {
403  Q_ASSERT_X(CEntityFlags::isSingleEntity(entity), Q_FUNC_INFO, "need single entity");
404  const QDateTime cacheTs(this->getCacheTimestamp(entity));
405  if (!cacheTs.isValid()) { return true; } // we have no cache ts
406 
407  const QDateTime headerTimestamp(this->getLatestSharedFileHeaderTimestamp(entity));
408  if (!headerTimestamp.isValid()) { return false; }
409  return headerTimestamp > cacheTs;
410  }
411 
412  bool CDatabaseReader::isSharedInfoObjectNewerThanCacheTimestamp(CEntityFlags::Entity entity) const
413  {
414  Q_ASSERT_X(CEntityFlags::isSingleEntity(entity), Q_FUNC_INFO, "need single entity");
415  const QDateTime cacheTs(this->getCacheTimestamp(entity));
416  if (!cacheTs.isValid()) { return true; } // we have no cache ts
417 
418  const QDateTime sharedInfoTimestamp(this->getLatestEntityTimestampFromSharedInfoObjects(entity));
419  if (!sharedInfoTimestamp.isValid()) { return false; }
420  return sharedInfoTimestamp > cacheTs;
421  }
422 
423  CEntityFlags::Entity CDatabaseReader::getEntitesWithNewerHeaderTimestamp(CEntityFlags::Entity entities) const
424  {
425  entities &= CEntityFlags::AllDbEntitiesNoInfoObjects;
426  CEntityFlags::Entity currentEntity = CEntityFlags::iterateDbEntities(entities);
427  CEntityFlags::Entity newerEntities = CEntityFlags::NoEntity;
428  while (currentEntity != CEntityFlags::NoEntity)
429  {
430  if (this->isSharedHeaderNewerThanCacheTimestamp(currentEntity)) { newerEntities |= currentEntity; }
431  currentEntity = CEntityFlags::iterateDbEntities(entities);
432  }
433  return newerEntities;
434  }
435 
436  CEntityFlags::Entity CDatabaseReader::getEntitesWithNewerSharedInfoObject(CEntityFlags::Entity entities) const
437  {
438  entities &= CEntityFlags::AllDbEntitiesNoInfoObjects;
439  CEntityFlags::Entity currentEntity = CEntityFlags::iterateDbEntities(entities);
440  CEntityFlags::Entity newerEntities = CEntityFlags::NoEntity;
441  while (currentEntity != CEntityFlags::NoEntity)
442  {
443  if (this->isSharedInfoObjectNewerThanCacheTimestamp(currentEntity)) { newerEntities |= currentEntity; }
444  currentEntity = CEntityFlags::iterateDbEntities(entities);
445  }
446  return newerEntities;
447  }
448 
450  {
451  return m_config.findFirstOrDefaultForEntity(entity);
452  }
453 
454  CEntityFlags::Entity CDatabaseReader::emitReadSignalPerSingleCachedEntity(CEntityFlags::Entity cachedEntities,
455  bool onlyIfHasData)
456  {
457  if (cachedEntities == CEntityFlags::NoEntity) { return CEntityFlags::NoEntity; }
458  CEntityFlags::Entity emitted = CEntityFlags::NoEntity;
459  CEntityFlags::Entity cachedEntitiesToEmit = cachedEntities;
460  CEntityFlags::Entity currentCachedEntity = CEntityFlags::iterateDbEntities(cachedEntitiesToEmit);
461  while (currentCachedEntity)
462  {
463  const int c = this->getCacheCount(currentCachedEntity);
464  if (!onlyIfHasData || c > 0)
465  {
466  emit this->dataRead(currentCachedEntity, CEntityFlags::ReadFinished, c, {});
467  emitted |= currentCachedEntity;
468  }
469  currentCachedEntity = CEntityFlags::iterateDbEntities(cachedEntitiesToEmit);
470  }
471  return emitted;
472  }
473 
474  void CDatabaseReader::emitAndLogDataRead(CEntityFlags::Entity entity, int number, const JsonDatastoreResponse &res)
475  {
476  // never emit when lock is held, deadlock
477  Q_ASSERT_X(CEntityFlags::isSingleEntity(entity), Q_FUNC_INFO, "Expect single entity");
478  CLogMessage(this).info(u"Read %1 entities of '%2' from '%3' (%4)")
479  << number << CEntityFlags::entitiesToString(entity) << res.getUrlString()
481  emit this->dataRead(entity,
482  res.isRestricted() ? CEntityFlags::ReadFinishedRestricted : CEntityFlags::ReadFinished,
483  number, res.getUrl());
484  }
485 
486  void CDatabaseReader::logNoWorkingUrl(CEntityFlags::Entity entity)
487  {
488  const CStatusMessage msg = CStatusMessage(this, m_severityNoWorkingUrl, u"No working URL for '%1'")
489  << CEntityFlags::entitiesToString(entity);
491  }
492 
494  {
495  if (!sApp || sApp->isShuttingDown()) { return CUrl(); }
496  switch (mode)
497  {
498  case CDbFlags::DbReading: return this->getDbServiceBaseUrl().withAppendedPath("/service");
499  case CDbFlags::SharedInfoOnly:
500  case CDbFlags::Shared: return sApp->getGlobalSetup().getSharedDbDataDirectoryUrl();
501  default: qFatal("Wrong mode"); break;
502  }
503  return CUrl();
504  }
505 
506  bool CDatabaseReader::isChangedUrl(const CUrl &oldUrl, const CUrl &currentUrl)
507  {
508  if (oldUrl.isEmpty()) { return true; }
509  Q_ASSERT_X(!currentUrl.isEmpty(), Q_FUNC_INFO, "No base URL");
510 
511  const QString old(oldUrl.getFullUrl(false));
512  const QString current(currentUrl.getFullUrl(false));
513  return old != current;
514  }
515 
516  void CDatabaseReader::receivedSharedFileHeader(QNetworkReply *nwReplyPtr)
517  {
518  // wrap pointer, make sure any exit cleans up reply
519  // required to use delete later as object is created in a different thread
520  QScopedPointer<QNetworkReply, QScopedPointerDeleteLater> nwReply(nwReplyPtr);
521  if (!this->doWorkCheck()) { return; }
522  this->receivedSharedFileHeaderNonClosing(nwReplyPtr);
523  nwReply->close();
524  }
525 
527  {
528  if (this->isAbandoned()) { return; }
529 
530  const HeaderResponse headerResponse = this->transformReplyIntoHeaderResponse(nwReplyPtr);
531  const QString fileName = nwReplyPtr->url().fileName();
532  const CEntityFlags::Entity entity = CEntityFlags::singleEntityByName(fileName);
533  m_sharedFileResponses[entity] = headerResponse;
534 
535  CLogMessage(this).info(u"Received header for shared file of '%1' from '%2'")
536  << fileName << headerResponse.getUrl().toQString();
537  emit this->sharedFileHeaderRead(entity, fileName, !headerResponse.hasWarningOrAboveMessage());
538  }
539 
541  {
542  QReadLocker rl(&m_statusLock);
543  return m_1stReplyReceived && m_1stReplyStatus == QNetworkReply::NoError;
544  }
545 
546  bool CDatabaseReader::hasReceivedOkReply(QString &message) const
547  {
548  QReadLocker rl(&m_statusLock);
549  message = m_statusMessage;
550  return m_1stReplyReceived && m_1stReplyStatus == QNetworkReply::NoError;
551  }
552 
554  {
555  QReadLocker rl(&m_statusLock);
556  return m_1stReplyReceived;
557  }
558 
560  {
561  return CEntityFlags::entitiesToString(this->getSupportedEntities());
562  }
563 
564  CEntityFlags::Entity CDatabaseReader::maskBySupportedEntities(CEntityFlags::Entity entities) const
565  {
566  return entities & this->getSupportedEntities();
567  }
568 
569  bool CDatabaseReader::supportsAnyOfEntities(CEntityFlags::Entity entities) const
570  {
571  return static_cast<int>(this->maskBySupportedEntities(entities)) > 0;
572  }
573 
574  bool CDatabaseReader::hasCacheTimestampNewerThan(CEntityFlags::Entity entity, const QDateTime &threshold) const
575  {
576  const QDateTime ts = this->getCacheTimestamp(entity);
577  if (!ts.isValid()) { return false; }
578  return ts > threshold;
579  }
580 
581  const QString &CDatabaseReader::getStatusMessage() const { return m_statusMessage; }
582 
584  {
585  return this->initFromLocalResourceFiles(this->getSupportedEntities(), inBackground);
586  }
587 
589  CStatusMessageList CDatabaseReader::initFromLocalResourceFiles(CEntityFlags::Entity entities, bool inBackground)
590  {
591  const bool overrideNewerOnly = true;
592  entities = this->maskBySupportedEntities(entities);
593 
594  if (inBackground || !CThreadUtils::isInThisThread(this))
595  {
597  overrideNewerOnly);
598  return s ? CStatusMessage(this).info(u"Started reading in background from '%1' of entities: '%2'")
599  << CSwiftDirectories::staticDbFilesDirectory() << CEntityFlags::entitiesToString(entities) :
600  CStatusMessage(this).error(u"Starting reading in background from '%1' of entities: '%2' failed")
601  << CSwiftDirectories::staticDbFilesDirectory() << CEntityFlags::entitiesToString(entities);
602  }
603  else
604  {
605  return this->readFromJsonFiles(CSwiftDirectories::staticDbFilesDirectory(), entities, overrideNewerOnly);
606  }
607  }
609 
610  void CDatabaseReader::setReplyStatus(QNetworkReply::NetworkError status, const QString &message)
611  {
612  QWriteLocker wl(&m_statusLock);
613  m_statusMessage = message;
614  m_1stReplyStatus = status;
615  m_1stReplyReceived = true;
616  }
617 
618  void CDatabaseReader::setReplyStatus(QNetworkReply *nwReply)
619  {
620  Q_ASSERT_X(nwReply, Q_FUNC_INFO, "Missing network reply");
621  if (nwReply && nwReply->isFinished())
622  {
623  this->logNetworkReplyReceived(nwReply);
624  this->setReplyStatus(nwReply->error(), nwReply->errorString());
625  }
626  }
627 
628  bool CDatabaseReader::overrideCacheFromFile(bool overrideNewerOnly, const QFileInfo &fileInfo,
629  CEntityFlags::Entity entity, CStatusMessageList &msgs) const
630  {
631  if (!fileInfo.birthTime().isValid()) { return false; }
632  if (!overrideNewerOnly) { return true; }
633 
634  const qint64 fileTs = fileInfo.birthTime().toUTC().toMSecsSinceEpoch();
635  const QDateTime cacheDateTime(this->getCacheTimestamp(entity));
636  if (!cacheDateTime.isValid()) { return true; } // no cache
637  const qint64 cacheTs = cacheDateTime.toUTC().toMSecsSinceEpoch();
638  if (fileTs > cacheTs)
639  {
640  msgs.push_back(CStatusMessage(this).info(u"File '%1' is newer than cache (%2)")
641  << fileInfo.absoluteFilePath() << cacheDateTime.toUTC().toString());
642  return true;
643  }
644  else
645  {
646  msgs.push_back(CStatusMessage(this).info(u"File '%1' is not newer than cache (%2)")
647  << fileInfo.absoluteFilePath() << cacheDateTime.toUTC().toString());
648  return false;
649  }
650  }
651 
652  void CDatabaseReader::logParseMessage(const QString &entity, int size, int msElapsed,
653  const CDatabaseReader::JsonDatastoreResponse &response) const
654  {
655  CLogMessage(this).info(u"Parsed %1 %2 in %3ms, thread %4 | '%5'")
656  << size << entity << msElapsed << CThreadUtils::currentThreadInfo() << response.toQString();
657  }
658 
659  void CDatabaseReader::networkReplyProgress(int logId, qint64 current, qint64 max, const QUrl &url)
660  {
661  CThreadedReader::networkReplyProgress(logId, current, max, url);
662  const QString fileName = url.fileName();
663  const CEntityFlags::Entity entity = CEntityFlags::singleEntityByName(fileName);
664  if (CEntityFlags::isSingleEntity(entity))
665  {
667  m_networkReplyNax, url);
668  }
669  }
670 
671  QString CDatabaseReader::fileNameForMode(CEntityFlags::Entity entity, CDbFlags::DataRetrievalModeFlag mode)
672  {
673  Q_ASSERT_X(CEntityFlags::isSingleEntity(entity), Q_FUNC_INFO, "needs single entity");
674  switch (mode)
675  {
676  case CDbFlags::Shared: return CDbInfo::entityToSharedName(entity);
677  case CDbFlags::SharedInfoOnly: return CDbInfo::sharedInfoFileName();
678  default:
679  case CDbFlags::DbReading: return CDbInfo::entityToServiceName(entity);
680  }
681  }
682 
683  QString CDatabaseReader::dateTimeToDbLatestTs(const QDateTime &ts)
684  {
685  if (!ts.isValid()) { return {}; }
686  return ts.toUTC().toString(Qt::ISODate);
687  }
688 
690  {
691  static const QStringList cats =
694  return cats;
695  }
696 
698  {
699  static const QString p("latestTimestamp");
700  return p;
701  }
702 
703  QString CDatabaseReader::queryLatestTimestamp(const QDateTime &ts)
704  {
705  if (!ts.isValid()) return {};
706  const QString q = parameterLatestTimestamp() % u"=" % dateTimeToDbLatestTs(ts);
707  return q;
708  }
709 
711  {
712  static const CUrl dbUrl(sApp->getGlobalSetup().getDbRootDirectoryUrl());
713  return dbUrl;
714  }
715 
716  void CDatabaseReader::cacheHasChanged(CEntityFlags::Entity entities)
717  {
718  this->emitReadSignalPerSingleCachedEntity(entities, false);
719  }
720 
721  void CDatabaseReader::stringToDatastoreResponse(const QString &jsonContent,
722  JsonDatastoreResponse &datastoreResponse)
723  {
724  const int status = datastoreResponse.getHttpStatusCode();
725  if (jsonContent.isEmpty())
726  {
727  static const QString errorMsg = "Empty JSON string, status: %1, URL: '%2', load time: %3";
728  datastoreResponse.setMessage(
730  errorMsg.arg(status).arg(datastoreResponse.getUrlString(),
731  datastoreResponse.getLoadTimeStringWithStartedHint())));
732  return;
733  }
734 
735  const QJsonDocument jsonResponse = CDatabaseUtils::databaseJsonToQJsonDocument(jsonContent);
736  if (jsonResponse.isEmpty())
737  {
738  if (CNetworkUtils::looksLikePhpErrorMessage(jsonContent))
739  {
740  static const QString errorMsg = "Looks like PHP errror, status %1, URL: '%2', msg: %3";
741  const QString phpErrorMessage = CNetworkUtils::removeHtmlPartsFromPhpErrorMessage(jsonContent);
742  datastoreResponse.setMessage(
744  errorMsg.arg(status).arg(datastoreResponse.getUrlString(), phpErrorMessage)));
745  }
746  else
747  {
748  static const QString errorMsg = "Empty JSON document, URL: '%1', load time: %2";
749  datastoreResponse.setMessage(
751  errorMsg.arg(datastoreResponse.getUrlString(),
752  datastoreResponse.getLoadTimeStringWithStartedHint())));
753  }
754  return;
755  }
756 
757  if (jsonResponse.isArray())
758  {
759  // directly an array, no further info
760  datastoreResponse.setJsonArray(jsonResponse.array());
761  datastoreResponse.setLastModifiedTimestamp(QDateTime::currentDateTimeUtc());
762  }
763  else
764  {
765  const QJsonObject responseObject(jsonResponse.object());
766  datastoreResponse.setJsonArray(responseObject["data"].toArray());
767  const QString ts(responseObject["latest"].toString());
768  datastoreResponse.setLastModifiedTimestamp(ts.isEmpty() ? QDateTime::currentDateTimeUtc() :
769  CDatastoreUtility::parseTimestamp(ts));
770  datastoreResponse.setRestricted(responseObject["restricted"].toBool());
771  }
772  }
773 
775  {
776  return this->getUrl().getHost() == getDbUrl().getHost();
777  }
778 
780  {
781  m_jsonArray = value;
782  m_arraySize = value.size();
783  }
784 
786  {
787  static const QString s("DB: %1 | restricted: %2 | array: %3 | string size: %4 | content: %5");
788  return s.arg(boolToYesNo(this->isLoadedFromDb()), boolToYesNo(this->isRestricted()))
789  .arg(this->getArraySize())
790  .arg(m_stringSize)
791  .arg(this->getContentLengthHeader());
792  }
793 
795  {
796  const QString fn(getUrl().getFileName());
797  return CDbInfo::sharedFileNames().contains(fn, Qt::CaseInsensitive);
798  }
799 
801  {
802  return QStringLiteral("%1ms").arg(getLoadTimeMs());
803  }
804 
806  {
807  if (m_requestStarted < 0) { return this->getLoadTimeString(); }
808  const qint64 diff = QDateTime::currentMSecsSinceEpoch() - m_requestStarted;
809  static const QString s("%1 load time, started %2ms before now");
810  return s.arg(this->getLoadTimeString()).arg(diff);
811  }
812 
813  void CDatabaseReader::HeaderResponse::setValues(const QNetworkReply *nwReply)
814  {
815  Q_ASSERT_X(nwReply, Q_FUNC_INFO, "Need valid reply");
816  this->setUrl(nwReply->url());
817  const QVariant started = nwReply->property("started");
818  if (started.isValid() && started.canConvert<qint64>())
819  {
820  const qint64 now = QDateTime::currentMSecsSinceEpoch();
821  const qint64 start = started.value<qint64>();
822  this->setLoadTimeMs(now - start);
823  m_requestStarted = start;
824  m_responseReceived = now;
825  }
826 
827  const QVariant qvStatusCode = nwReply->attribute(QNetworkRequest::HttpStatusCodeAttribute);
828  if (qvStatusCode.isValid() && qvStatusCode.canConvert<int>()) { m_httpStatusCode = qvStatusCode.toInt(); }
829 
830  const QDateTime lastModified = nwReply->header(QNetworkRequest::LastModifiedHeader).toDateTime();
831  const qulonglong size = nwReply->header(QNetworkRequest::ContentLengthHeader).toULongLong();
832  this->setLastModifiedTimestamp(lastModified);
833  this->setContentLengthHeader(size);
834  }
835 } // namespace swift::core::db
SWIFT_CORE_EXPORT swift::core::CApplication * sApp
Single instance of application object.
Definition: application.cpp:71
data::CGlobalSetup getGlobalSetup() const
Global setup.
bool hasWebDataServices() const
Web data services available?
bool isShuttingDown() const
Is application shutting down?
CWebDataServices * getWebDataServices() const
Get the web data services.
Support for threaded based reading and parsing tasks such as data files via http, or file system and ...
std::atomic_llong m_networkReplyCurrent
current bytes
static const QStringList & getLogCategories()
Log categories.
virtual void networkReplyProgress(int logId, qint64 current, qint64 max, const QUrl &url)
Network request progress.
std::atomic_int m_networkReplyProgress
Progress percentage 0...100.
void threadAssertCheck() const
Make sure everthing runs correctly in own thread.
void logNetworkReplyReceived(QNetworkReply *reply)
Network reply received, mark in m_urlReadLog.
std::atomic_llong m_networkReplyNax
max bytes
bool doWorkCheck() const
Still enabled etc.?
swift::core::db::CInfoDataReader * getDbInfoDataReader() const
DB info data reader.
swift::core::db::CInfoDataReader * getSharedInfoDataReader() const
Shared info data reader.
const swift::misc::network::CUrl & getDbRootDirectoryUrl() const
Root directory of DB.
Definition: globalsetup.h:65
swift::misc::network::CUrl getSharedDbDataDirectoryUrl()
Get shared DB data directory URL.
Definition: globalsetup.cpp:72
Details how to read a certain entity.
swift::misc::db::CDbFlags::DataRetrievalMode getRetrievalMode() const
Supported modes.
Value object encapsulating a list of reader configs.
CDatabaseReaderConfig findFirstOrDefaultForEntity(const swift::misc::network::CEntityFlags::Entity entities) const
Find first one matching given.
Specialized version of threaded reader for DB data.
virtual QDateTime getCacheTimestamp(swift::misc::network::CEntityFlags::Entity entity) const =0
Get cache timestamp.
virtual bool hasChangedUrl(swift::misc::network::CEntityFlags::Entity entity, swift::misc::network::CUrl &oldUrlInfo, swift::misc::network::CUrl &newUrlInfo) const =0
Changed URL, means the cache values have been read from elsewhere.
virtual void admitCaches(swift::misc::network::CEntityFlags::Entity entities)=0
Admit caches for given entities.
QDateTime getLatestSharedFileHeaderTimestamp(swift::misc::network::CEntityFlags::Entity entity) const
Header timestamp (last-modified) for shared file.
void emitAndLogDataRead(swift::misc::network::CEntityFlags::Entity entity, int number, const JsonDatastoreResponse &res)
Emit signal and log when data have been read.
swift::misc::CStatusMessageList initFromLocalResourceFiles(bool inBackground)
Init from local resource file.
swift::misc::network::CEntityFlags::Entity emitReadSignalPerSingleCachedEntity(swift::misc::network::CEntityFlags::Entity cachedEntities, bool onlyIfHasData)
Split into single entity and send dataRead signal.
HeaderResponse transformReplyIntoHeaderResponse(QNetworkReply *nwReply) const
Check if terminated or error, otherwise set header information.
static QString queryLatestTimestamp(const QDateTime &ts)
Latest timestamp query for DB.
QNetworkReply::NetworkError m_1stReplyStatus
Successful connection?
bool isSharedHeaderNewerThanCacheTimestamp(swift::misc::network::CEntityFlags::Entity entity) const
Is the file timestamp newer than cache timestamp?
swift::misc::db::CDbInfoList getSharedInfoObjects() const
Shared info list (latest data timestamps from DB web service)
virtual void cacheHasChanged(swift::misc::network::CEntityFlags::Entity entities)
Cache for given entity has changed.
virtual void networkReplyProgress(int logId, qint64 current, qint64 max, const QUrl &url)
Network request progress.
bool hasSharedInfoObjects() const
Shared info objects available?
swift::misc::CStatusMessage::StatusSeverity m_severityNoWorkingUrl
severity of message if there is no working URL
void sharedFileHeaderRead(swift::misc::network::CEntityFlags::Entity entity, const QString &fileName, bool success)
Header of shared file read.
bool hasReceivedOkReply() const
Has received Ok response from server at least once?
static QString dateTimeToDbLatestTs(const QDateTime &ts)
A newer than value understood by swift DB.
void setReplyStatus(QNetworkReply::NetworkError status, const QString &message="")
Feedback about connection status.
static const QStringList & getLogCategories()
Log categories.
virtual int getCacheCount(swift::misc::network::CEntityFlags::Entity entity) const =0
Cache`s number of entities.
void entityDownloadProgress(swift::misc::network::CEntityFlags::Entity entity, int logId, int progress, qint64 current, qint64 max, const QUrl &url)
Download progress for an entity.
QDateTime getLatestEntityTimestampFromSharedInfoObjects(swift::misc::network::CEntityFlags::Entity entity) const
Obtain latest object timestamp from shared info objects.
QReadWriteLock m_statusLock
Lock.
JsonDatastoreResponse transformReplyIntoDatastoreResponse(QNetworkReply *nwReply) const
Check if terminated or error, otherwise split into array of objects.
bool hasSharedFileHeader(const swift::misc::network::CEntityFlags::Entity entity) const
Header of shared file read (for single entity)?
QDateTime getLatestEntityTimestampFromDbInfoObjects(swift::misc::network::CEntityFlags::Entity entity) const
Obtain latest object timestamp from DB info objects.
void logNoWorkingUrl(swift::misc::network::CEntityFlags::Entity entity)
Log if no working URL exists, using m_noWorkingUrlSeverity.
bool supportsAnyOfEntities(swift::misc::network::CEntityFlags::Entity entities) const
Is any of the given entities supported here by this reader.
QString getSupportedEntitiesAsString() const
Supported entities as string.
void swiftDbDataRead(bool success)
DB have been read.
bool hasDbInfoObjects() const
DB info objects available?
swift::misc::network::CEntityFlags::Entity getEntitesWithNewerHeaderTimestamp(swift::misc::network::CEntityFlags::Entity entities) const
Those entities where the timestamp of the header is newer than the cache timestamp.
swift::misc::network::CEntityFlags::Entity getEntitesWithNewerSharedInfoObject(swift::misc::network::CEntityFlags::Entity entities) const
Those entities where the timestamp of a shared info object is newer than the cache timestamp.
CDatabaseReaderConfig getConfigForEntity(swift::misc::network::CEntityFlags::Entity entity) const
Config for given entity.
QMap< swift::misc::network::CEntityFlags::Entity, HeaderResponse > m_sharedFileResponses
file responses of the shared files
void receivedSharedFileHeader(QNetworkReply *nwReplyPtr)
Received a reply of a header for a shared file.
swift::misc::network::CEntityFlags::Entity maskBySupportedEntities(swift::misc::network::CEntityFlags::Entity entities) const
Mask by supported entities.
swift::misc::network::CEntityFlags::Entity triggerLoadingDirectlyFromSharedFiles(swift::misc::network::CEntityFlags::Entity entities, bool checkCacheTsUpfront)
Start loading from shared files in own thread.
bool hasSharedFileHeaders(const swift::misc::network::CEntityFlags::Entity entities) const
Headers of shared file read (for single entity)?
bool hasReceivedFirstReply() const
Has received 1st reply?
bool overrideCacheFromFile(bool overrideNewerOnly, const QFileInfo &fileInfo, swift::misc::network::CEntityFlags::Entity entity, swift::misc::CStatusMessageList &msgs) const
Override cache from file.
virtual swift::misc::network::CUrl getDbServiceBaseUrl() const =0
Get the service URL, individual for each reader.
static const QString & parameterLatestTimestamp()
Name of latest timestamp.
static const swift::misc::network::CUrl & getDbUrl()
DB base URL.
swift::misc::db::CDbInfoList getDbInfoObjects() const
DB Info list (latest data timestamps from DB web service)
QString m_statusMessage
Returned status message from watchdog.
void startReadFromBackendInBackgroundThread(swift::misc::network::CEntityFlags::Entity entities, swift::misc::db::CDbFlags::DataRetrievalModeFlag mode, const QDateTime &newerThan=QDateTime())
Start reading in own thread (without config/caching)
virtual bool readFromJsonFilesInBackground(const QString &dir, swift::misc::network::CEntityFlags::Entity whatToRead, bool overrideNewer)=0
Data read from local data.
virtual swift::misc::network::CEntityFlags::Entity getSupportedEntities() const =0
Supported entities by this reader.
swift::misc::network::CUrl getBaseUrl(swift::misc::db::CDbFlags::DataRetrievalModeFlag mode) const
Base URL for mode (either a shared or DB URL)
void dataRead(swift::misc::network::CEntityFlags::Entity entities, swift::misc::network::CEntityFlags::ReadState state, int number, const QUrl &url)
Combined read signal.
virtual swift::misc::CStatusMessageList readFromJsonFiles(const QString &dir, swift::misc::network::CEntityFlags::Entity whatToRead, bool overrideNewer)=0
Data read from local data.
CDatabaseReaderConfigList m_config
DB reder configuration.
bool setHeaderInfoPart(HeaderResponse &headerResponse, QNetworkReply *nwReply) const
Set the header part.
static bool isChangedUrl(const swift::misc::network::CUrl &oldUrl, const swift::misc::network::CUrl &currentUrl)
Has URL been changed? Means we load from a different server.
static QString fileNameForMode(swift::misc::network::CEntityFlags::Entity entity, swift::misc::db::CDbFlags::DataRetrievalModeFlag mode)
File name for given mode, either php service or shared file name.
CDatabaseReader::JsonDatastoreResponse setStatusAndTransformReplyIntoDatastoreResponse(QNetworkReply *nwReply)
Check if terminated or error, otherwise split into array of objects.
void readInBackgroundThread(swift::misc::network::CEntityFlags::Entity entities, const QDateTime &newerThan)
Start reading in own thread.
bool m_1stReplyReceived
Successful connection? Does not mean data / authorizations are correct.
swift::misc::network::CEntityFlags::Entity triggerLoadingDirectlyFromDb(swift::misc::network::CEntityFlags::Entity entities, const QDateTime &newerThan)
Start loading from DB in own thread.
const QString & getStatusMessage() const
Status message (error message)
void logParseMessage(const QString &entity, int size, int msElapsed, const JsonDatastoreResponse &response) const
Parsing info message.
bool isSharedInfoObjectNewerThanCacheTimestamp(swift::misc::network::CEntityFlags::Entity entity) const
Is the shared info timestamp newer than cache timestamp?
bool hasCacheTimestampNewerThan(swift::misc::network::CEntityFlags::Entity entity, const QDateTime &threshold) const
Has entity a valid and newer timestamp.
void receivedSharedFileHeaderNonClosing(QNetworkReply *nwReplyPtr)
Received a reply of a header for a shared file.
static QJsonDocument databaseJsonToQJsonDocument(const QString &content)
Database JSON from content string, which can be compressed.
swift::misc::db::CDbInfoList getInfoObjects() const
Get info list (either shared or from DB)
void start(QThread::Priority priority=QThread::InheritPriority)
Starts a thread and moves the worker into it.
Definition: worker.cpp:166
static const QString & webservice()
Webservice.
static const QString & swiftDbWebservice()
Webservice with swift DB.
Class for emitting a log message.
Definition: logmessage.h:27
static void preformatted(const CStatusMessage &statusMessage)
Sends a verbatim, preformatted message to the log.
Derived & info(const char16_t(&format)[N])
Set the severity to info, providing a format string.
void push_back(const T &value)
Appends an element at the end of the sequence.
Definition: sequence.h:305
bool isEmpty() const
Synonym for empty.
Definition: sequence.h:285
Streamable status message, e.g.
constexpr static auto SeverityError
Status severities.
Status messages, e.g. from Core -> GUI.
Locations of important directories for swift files.
static const QString & staticDbFilesDirectory()
Where static DB files are located.
static QString currentThreadInfo()
Info about current thread, for debug messages.
Definition: threadutils.cpp:23
static bool isInThisThread(const QObject *toBeTested)
Is the current thread the object's thread?
Definition: threadutils.cpp:16
bool isAbandoned() const
For the task to check whether it can finish early.
Definition: worker.cpp:153
QDateTime getUtcTimestamp() const
Get timestamp.
DataRetrievalModeFlag
Which data to read, requires corresponding readers.
Definition: dbflags.h:25
Info about the latest models.
Definition: dbinfo.h:22
bool isValid() const
Valid?
Definition: dbinfo.cpp:23
Value object encapsulating a list of info objects.
Definition: dbinfolist.h:27
CDbInfo findFirstByEntityOrDefault(swift::misc::network::CEntityFlags::Entity entity) const
Find by entity.
Definition: dbinfolist.cpp:16
QString toQString(bool i18n=false) const
Cast as QString.
Definition: mixinstring.h:76
What and state of reading from web services.
Definition: entityflags.h:22
Value object encapsulating information of a location, kind of simplified CValueObject compliant versi...
Definition: url.h:27
bool isEmpty() const
Empty.
Definition: url.cpp:54
CUrl withAppendedPath(const QString &path) const
Append path.
Definition: url.cpp:115
const QString & getHost() const
Get host.
Definition: url.h:55
QString getFullUrl(bool withQuery=true) const
Qualified name.
Definition: url.cpp:84
Core data traits (aka cached values) and classes.
Classes interacting with the swift database (aka "datastore").
Backend services of the swift project, like dealing with the network or the simulators.
Definition: actionbind.cpp:7
Free functions in swift::misc.
auto singleShot(int msec, QObject *target, F &&task)
Starts a single-shot timer which will call a task in the thread of the given object when it times out...
Definition: threadutils.h:30
const swift::misc::network::CUrl & getUrl() const
URL loaded.
QString getUrlString() const
URL loaded as string.
void setValues(const QNetworkReply *nwReply)
Set reply values.
bool hasWarningOrAboveMessage() const
Warning or error message?
QString getLoadTimeString() const
Load time as string.
QString getLoadTimeStringWithStartedHint() const
Load time as string.
void setMessage(const swift::misc::CStatusMessage &lastErrorOrWarning)
Set the error/warning message.
Response from our database (depending on JSON DB backend generates)
bool isRestricted() const
Incremental data, restricted by query?
void setJsonArray(const QJsonArray &value)
Set the JSON array.
bool isLoadedFromDb() const
Is loaded from database.