QGIS API Documentation  3.16.0-Hannover (43b64b13f3)
qgsabstractcontentcache.h
Go to the documentation of this file.
1 /***************************************************************************
2  qgsabstractcontentcache.h
3  ---------------
4  begin : December 2018
5  copyright : (C) 2018 by Nyall Dawson
6  email : nyall dot dawson at gmail dot com
7  ***************************************************************************/
8 
9 /***************************************************************************
10  * *
11  * This program is free software; you can redistribute it and/or modify *
12  * it under the terms of the GNU General Public License as published by *
13  * the Free Software Foundation; either version 2 of the License, or *
14  * (at your option) any later version. *
15  * *
16  ***************************************************************************/
17 
18 #ifndef QGSABSTRACTCONTENTCACHE_H
19 #define QGSABSTRACTCONTENTCACHE_H
20 
21 #include "qgis_core.h"
22 #include "qgis_sip.h"
23 #include "qgslogger.h"
24 #include "qgsmessagelog.h"
25 #include "qgsapplication.h"
27 
28 #include <QObject>
29 #include <QMutex>
30 #include <QCache>
31 #include <QSet>
32 #include <QDateTime>
33 #include <QList>
35 #include <QNetworkReply>
36 
48 {
49  public:
50 
54  QgsAbstractContentCacheEntry( const QString &path ) ;
55 
56  virtual ~QgsAbstractContentCacheEntry() = default;
57 
62 
66  QString path;
67 
69  QDateTime fileModified;
70 
73 
75  int mFileModifiedCheckTimeout = 30000;
76 
81  QgsAbstractContentCacheEntry *nextEntry = nullptr;
82 
87  QgsAbstractContentCacheEntry *previousEntry = nullptr;
88 
89  bool operator==( const QgsAbstractContentCacheEntry &other ) const
90  {
91  return other.path == path;
92  }
93 
97  virtual int dataSize() const = 0;
98 
102  virtual void dump() const = 0;
103 
104  protected:
105 
111  virtual bool isEqual( const QgsAbstractContentCacheEntry *other ) const = 0;
112 
113  private:
114 #ifdef SIP_RUN
116 #endif
117 
118 };
119 
130 class CORE_EXPORT QgsAbstractContentCacheBase: public QObject
131 {
132  Q_OBJECT
133 
134  public:
135 
139  QgsAbstractContentCacheBase( QObject *parent );
140 
141  signals:
142 
146  void remoteContentFetched( const QString &url );
147 
148  protected:
149 
154  virtual bool checkReply( QNetworkReply *reply, const QString &path ) const
155  {
156  Q_UNUSED( reply )
157  Q_UNUSED( path )
158  return true;
159  }
160 
161  protected slots:
162 
169  virtual void onRemoteContentFetched( const QString &url, bool success );
170 
171 };
172 
173 #ifndef SIP_RUN
174 
188 template<class T>
190 {
191 
192  public:
193 
205  QgsAbstractContentCache( QObject *parent SIP_TRANSFERTHIS = nullptr,
206  const QString &typeString = QString(),
207  long maxCacheSize = 20000000,
208  int fileModifiedCheckTimeout = 30000 )
209  : QgsAbstractContentCacheBase( parent )
210  , mMutex( QMutex::Recursive )
211  , mMaxCacheSize( maxCacheSize )
212  , mFileModifiedCheckTimeout( fileModifiedCheckTimeout )
213  , mTypeString( typeString.isEmpty() ? QObject::tr( "Content" ) : typeString )
214  {
215  }
216 
218  {
219  qDeleteAll( mEntryLookup );
220  }
221 
222  protected:
223 
228  {
229  //only one entry in cache
230  if ( mLeastRecentEntry == mMostRecentEntry )
231  {
232  return;
233  }
234  T *entry = mLeastRecentEntry;
235  while ( entry && ( mTotalSize > mMaxCacheSize ) )
236  {
237  T *bkEntry = entry;
238  entry = static_cast< T * >( entry->nextEntry );
239 
240  takeEntryFromList( bkEntry );
241  mEntryLookup.remove( bkEntry->path, bkEntry );
242  mTotalSize -= bkEntry->dataSize();
243  delete bkEntry;
244  }
245  }
246 
260  QByteArray getContent( const QString &path, const QByteArray &missingContent, const QByteArray &fetchingContent, bool blocking = false ) const
261  {
262  // is it a path to local file?
263  QFile file( path );
264  if ( file.exists() )
265  {
266  if ( file.open( QIODevice::ReadOnly ) )
267  {
268  return file.readAll();
269  }
270  else
271  {
272  return missingContent;
273  }
274  }
275 
276  // maybe it's an embedded base64 string
277  if ( path.startsWith( QLatin1String( "base64:" ), Qt::CaseInsensitive ) )
278  {
279  QByteArray base64 = path.mid( 7 ).toLocal8Bit(); // strip 'base64:' prefix
280  return QByteArray::fromBase64( base64, QByteArray::OmitTrailingEquals );
281  }
282 
283  // maybe it's a url...
284  if ( !path.contains( QLatin1String( "://" ) ) ) // otherwise short, relative SVG paths might be considered URLs
285  {
286  return missingContent;
287  }
288 
289  QUrl url( path );
290  if ( !url.isValid() )
291  {
292  return missingContent;
293  }
294 
295  // check whether it's a url pointing to a local file
296  if ( url.scheme().compare( QLatin1String( "file" ), Qt::CaseInsensitive ) == 0 )
297  {
298  file.setFileName( url.toLocalFile() );
299  if ( file.exists() )
300  {
301  if ( file.open( QIODevice::ReadOnly ) )
302  {
303  return file.readAll();
304  }
305  }
306 
307  // not found...
308  return missingContent;
309  }
310 
311  QMutexLocker locker( &mMutex );
312 
313  // already a request in progress for this url
314  if ( mPendingRemoteUrls.contains( path ) )
315  {
316  // it's a non blocking request so return fetching content
317  if ( !blocking )
318  {
319  return fetchingContent;
320  }
321 
322  // it's a blocking request so try to find the task and wait for task finished
323  const auto constActiveTasks = QgsApplication::taskManager()->activeTasks();
324  for ( QgsTask *task : constActiveTasks )
325  {
326  // the network content fetcher task's description ends with the path
327  if ( !task->description().endsWith( path ) )
328  {
329  continue;
330  }
331 
332  // cast task to network content fetcher task
333  QgsNetworkContentFetcherTask *ncfTask = qobject_cast<QgsNetworkContentFetcherTask *>( task );
334  if ( ncfTask )
335  {
336  // wait for task finished
337  if ( waitForTaskFinished( ncfTask ) )
338  {
339  if ( mRemoteContentCache.contains( path ) )
340  {
341  // We got the file!
342  return *mRemoteContentCache[ path ];
343  }
344  }
345  }
346  // task found, no needs to continue
347  break;
348  }
349  // if no content returns the content is probably in remote content cache
350  // or a new task will be created
351  }
352 
353  if ( mRemoteContentCache.contains( path ) )
354  {
355  // already fetched this content - phew. Just return what we already got.
356  return *mRemoteContentCache[ path ];
357  }
358 
359  mPendingRemoteUrls.insert( path );
360  //fire up task to fetch content in background
361  QNetworkRequest request( url );
362  QgsSetRequestInitiatorClass( request, QStringLiteral( "QgsAbstractContentCache<%1>" ).arg( mTypeString ) );
363  request.setAttribute( QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::PreferCache );
364  request.setAttribute( QNetworkRequest::CacheSaveControlAttribute, true );
365 
367  connect( task, &QgsNetworkContentFetcherTask::fetched, this, [this, task, path, missingContent]
368  {
369  QMutexLocker locker( &mMutex );
370 
371  QNetworkReply *reply = task->reply();
372  if ( !reply )
373  {
374  // canceled
375  QMetaObject::invokeMethod( const_cast< QgsAbstractContentCacheBase * >( qobject_cast< const QgsAbstractContentCacheBase * >( this ) ), "onRemoteContentFetched", Qt::QueuedConnection, Q_ARG( QString, path ), Q_ARG( bool, false ) );
376  return;
377  }
378 
379  if ( reply->error() != QNetworkReply::NoError )
380  {
381  QgsMessageLog::logMessage( tr( "%3 request failed [error: %1 - url: %2]" ).arg( reply->errorString(), path, mTypeString ), mTypeString );
382  return;
383  }
384 
385  bool ok = true;
386 
387  QVariant status = reply->attribute( QNetworkRequest::HttpStatusCodeAttribute );
388  if ( !status.isNull() && status.toInt() >= 400 )
389  {
390  QVariant phrase = reply->attribute( QNetworkRequest::HttpReasonPhraseAttribute );
391  QgsMessageLog::logMessage( tr( "%4 request error [status: %1 - reason phrase: %2] for %3" ).arg( status.toInt() ).arg( phrase.toString(), path, mTypeString ), mTypeString );
392  mRemoteContentCache.insert( path, new QByteArray( missingContent ) );
393  ok = false;
394  }
395 
396  if ( !checkReply( reply, path ) )
397  {
398  mRemoteContentCache.insert( path, new QByteArray( missingContent ) );
399  ok = false;
400  }
401 
402  if ( ok )
403  {
404  // read the content data
405  const QByteArray ba = reply->readAll();
406 
407  // because of the fragility listed below in waitForTaskFinished, this slot may get called twice. In that case
408  // the second time will have an empty reply (we've already read it all...)
409  if ( !ba.isEmpty() )
410  mRemoteContentCache.insert( path, new QByteArray( ba ) );
411  }
412  QMetaObject::invokeMethod( const_cast< QgsAbstractContentCacheBase * >( qobject_cast< const QgsAbstractContentCacheBase * >( this ) ), "onRemoteContentFetched", Qt::QueuedConnection, Q_ARG( QString, path ), Q_ARG( bool, true ) );
413  } );
414 
416 
417  // if blocking, wait for finished
418  if ( blocking )
419  {
420  if ( waitForTaskFinished( task ) )
421  {
422  if ( mRemoteContentCache.contains( path ) )
423  {
424  // We got the file!
425  return *mRemoteContentCache[ path ];
426  }
427  }
428  }
429  return fetchingContent;
430  }
431 
432  void onRemoteContentFetched( const QString &url, bool success ) override
433  {
434  QMutexLocker locker( &mMutex );
435  mPendingRemoteUrls.remove( url );
436 
437  T *nextEntry = mLeastRecentEntry;
438  while ( T *entry = nextEntry )
439  {
440  nextEntry = static_cast< T * >( entry->nextEntry );
441  if ( entry->path == url )
442  {
443  takeEntryFromList( entry );
444  mEntryLookup.remove( entry->path, entry );
445  mTotalSize -= entry->dataSize();
446  delete entry;
447  }
448  }
449 
450  if ( success )
451  emit remoteContentFetched( url );
452  }
453 
465  {
466  // Wait up to timeout seconds for task finished
468  {
469  // The wait did not time out
470  // Third step, check status as complete
471  if ( task->status() == QgsTask::Complete )
472  {
473  // Fourth step, force the signal fetched to be sure reply has been checked
474 
475  // ARGH this is BAD BAD BAD. The connection will get called twice as a result!!!
476  task->fetched();
477  return true;
478  }
479  }
480  return false;
481  }
482 
492  T *findExistingEntry( T *entryTemplate )
493  {
494  //search entries in mEntryLookup
495  const QString path = entryTemplate->path;
496  T *currentEntry = nullptr;
497  const QList<T *> entries = mEntryLookup.values( path );
498  QDateTime modified;
499  for ( T *cacheEntry : entries )
500  {
501  if ( cacheEntry->isEqual( entryTemplate ) )
502  {
503  if ( mFileModifiedCheckTimeout <= 0 || cacheEntry->fileModifiedLastCheckTimer.hasExpired( mFileModifiedCheckTimeout ) )
504  {
505  if ( !modified.isValid() )
506  modified = QFileInfo( path ).lastModified();
507 
508  if ( cacheEntry->fileModified != modified )
509  continue;
510  else
511  cacheEntry->fileModifiedLastCheckTimer.restart();
512  }
513  currentEntry = cacheEntry;
514  break;
515  }
516  }
517 
518  //if not found: insert entryTemplate as a new entry
519  if ( !currentEntry )
520  {
521  currentEntry = insertCacheEntry( entryTemplate );
522  }
523  else
524  {
525  delete entryTemplate;
526  entryTemplate = nullptr;
527  ( void )entryTemplate;
528  takeEntryFromList( currentEntry );
529  if ( !mMostRecentEntry ) //list is empty
530  {
531  mMostRecentEntry = currentEntry;
532  mLeastRecentEntry = currentEntry;
533  }
534  else
535  {
536  mMostRecentEntry->nextEntry = currentEntry;
537  currentEntry->previousEntry = mMostRecentEntry;
538  currentEntry->nextEntry = nullptr;
539  mMostRecentEntry = currentEntry;
540  }
541  }
542 
543  //debugging
544  //printEntryList();
545 
546  return currentEntry;
547  }
548 
549  mutable QMutex mMutex;
551  long mTotalSize = 0;
552 
554  long mMaxCacheSize = 20000000;
555 
556  private:
557 
563  T *insertCacheEntry( T *entry )
564  {
565  entry->mFileModifiedCheckTimeout = mFileModifiedCheckTimeout;
566 
567  if ( !entry->path.startsWith( QLatin1String( "base64:" ) ) )
568  {
569  entry->fileModified = QFileInfo( entry->path ).lastModified();
570  entry->fileModifiedLastCheckTimer.start();
571  }
572 
573  mEntryLookup.insert( entry->path, entry );
574 
575  //insert to most recent place in entry list
576  if ( !mMostRecentEntry ) //inserting first entry
577  {
578  mLeastRecentEntry = entry;
579  mMostRecentEntry = entry;
580  entry->previousEntry = nullptr;
581  entry->nextEntry = nullptr;
582  }
583  else
584  {
585  entry->previousEntry = mMostRecentEntry;
586  entry->nextEntry = nullptr;
587  mMostRecentEntry->nextEntry = entry;
588  mMostRecentEntry = entry;
589  }
590 
591  trimToMaximumSize();
592  return entry;
593  }
594 
595 
599  void takeEntryFromList( T *entry )
600  {
601  if ( !entry )
602  {
603  return;
604  }
605 
606  if ( entry->previousEntry )
607  {
608  entry->previousEntry->nextEntry = entry->nextEntry;
609  }
610  else
611  {
612  mLeastRecentEntry = static_cast< T * >( entry->nextEntry );
613  }
614  if ( entry->nextEntry )
615  {
616  entry->nextEntry->previousEntry = entry->previousEntry;
617  }
618  else
619  {
620  mMostRecentEntry = static_cast< T * >( entry->previousEntry );
621  }
622  }
623 
627  void printEntryList()
628  {
629  QgsDebugMsg( QStringLiteral( "****************cache entry list*************************" ) );
630  QgsDebugMsg( "Cache size: " + QString::number( mTotalSize ) );
631  T *entry = mLeastRecentEntry;
632  while ( entry )
633  {
634  QgsDebugMsg( QStringLiteral( "***Entry:" ) );
635  entry->dump();
636  entry = entry->nextEntry;
637  }
638  }
639 
641  QMultiHash< QString, T * > mEntryLookup;
642 
644  int mFileModifiedCheckTimeout = 30000;
645 
646  //The content cache keeps the entries on a double connected list, moving the current entry to the front.
647  //That way, removing entries for more space can start with the least used objects.
648  T *mLeastRecentEntry = nullptr;
649  T *mMostRecentEntry = nullptr;
650 
651  mutable QCache< QString, QByteArray > mRemoteContentCache;
652  mutable QSet< QString > mPendingRemoteUrls;
653 
654  QString mTypeString;
655 
656  friend class TestQgsSvgCache;
657  friend class TestQgsImageCache;
658 };
659 
660 #endif
661 
662 #endif // QGSABSTRACTCONTENTCACHE_H
QgsTaskManager::activeTasks
QList< QgsTask * > activeTasks() const
Returns a list of the active (queued or running) tasks.
Definition: qgstaskmanager.cpp:652
QgsAbstractContentCacheEntry::fileModified
QDateTime fileModified
Timestamp when file was last modified.
Definition: qgsabstractcontentcache.h:69
QgsAbstractContentCache::QgsAbstractContentCache
QgsAbstractContentCache(QObject *parent=nullptr, const QString &typeString=QString(), long maxCacheSize=20000000, int fileModifiedCheckTimeout=30000)
Constructor for QgsAbstractContentCache, with the specified parent object.
Definition: qgsabstractcontentcache.h:205
QgsNetworkContentFetcherTask::fetched
void fetched()
Emitted when the network content has been fetched, regardless of whether the fetch was successful or ...
QgsNetworkContentFetcherTask::reply
QNetworkReply * reply()
Returns the network reply.
Definition: qgsnetworkcontentfetchertask.cpp:72
QgsNetworkContentFetcherTask
Handles HTTP network content fetching in a background task.
Definition: qgsnetworkcontentfetchertask.h:48
QgsAbstractContentCache::waitForTaskFinished
bool waitForTaskFinished(QgsNetworkContentFetcherTask *task) const
Blocks the current thread until the task finishes (or user's preset network timeout expires)
Definition: qgsabstractcontentcache.h:464
QgsTask::Complete
@ Complete
Task successfully completed.
Definition: qgstaskmanager.h:64
QgsAbstractContentCache::~QgsAbstractContentCache
~QgsAbstractContentCache() override
Definition: qgsabstractcontentcache.h:217
QgsAbstractContentCacheEntry::isEqual
virtual bool isEqual(const QgsAbstractContentCacheEntry *other) const =0
Tests whether this entry matches another entry.
QgsAbstractContentCacheEntry::operator==
bool operator==(const QgsAbstractContentCacheEntry &other) const
Definition: qgsabstractcontentcache.h:89
qgsnetworkcontentfetchertask.h
QgsAbstractContentCacheEntry::path
QString path
Represents the absolute path to a file, a remote URL, or a base64 encoded string.
Definition: qgsabstractcontentcache.h:66
QgsDebugMsg
#define QgsDebugMsg(str)
Definition: qgslogger.h:38
QgsSetRequestInitiatorClass
#define QgsSetRequestInitiatorClass(request, _class)
Definition: qgsnetworkaccessmanager.h:41
QgsAbstractContentCacheEntry::dump
virtual void dump() const =0
Dumps debugging strings containing the item's properties.
QgsTaskManager::addTask
long addTask(QgsTask *task, int priority=0)
Adds a task to the manager.
Definition: qgstaskmanager.cpp:416
qgsapplication.h
QgsAbstractContentCacheEntry::QgsAbstractContentCacheEntry
QgsAbstractContentCacheEntry(const QgsAbstractContentCacheEntry &rh)=delete
QgsAbstractContentCacheEntry cannot be copied.
QgsTask::status
TaskStatus status() const
Returns the current task status.
Definition: qgstaskmanager.h:113
qgsnetworkaccessmanager.h
QgsApplication::taskManager
static QgsTaskManager * taskManager()
Returns the application's task manager, used for managing application wide background task handling.
Definition: qgsapplication.cpp:2133
qgis_sip.h
QgsAbstractContentCache::onRemoteContentFetched
void onRemoteContentFetched(const QString &url, bool success) override
Triggered after remote content (i.e.
Definition: qgsabstractcontentcache.h:432
QgsAbstractContentCacheEntry::dataSize
virtual int dataSize() const =0
Returns the memory usage in bytes for the entry.
QgsAbstractContentCacheEntry::fileModifiedLastCheckTimer
QElapsedTimer fileModifiedLastCheckTimer
Time since last check of file modified date.
Definition: qgsabstractcontentcache.h:72
QgsAbstractContentCacheBase::checkReply
virtual bool checkReply(QNetworkReply *reply, const QString &path) const
Runs additional checks on a network reply to ensure that the reply content is consistent with that re...
Definition: qgsabstractcontentcache.h:154
QgsAbstractContentCacheEntry
Base class for entries in a QgsAbstractContentCache.
Definition: qgsabstractcontentcache.h:48
QgsAbstractContentCache::findExistingEntry
T * findExistingEntry(T *entryTemplate)
Returns the existing entry from the cache which matches entryTemplate (deleting entryTemplate when do...
Definition: qgsabstractcontentcache.h:492
QgsAbstractContentCacheEntry::~QgsAbstractContentCacheEntry
virtual ~QgsAbstractContentCacheEntry()=default
QgsMessageLog::logMessage
static void logMessage(const QString &message, const QString &tag=QString(), Qgis::MessageLevel level=Qgis::Warning, bool notifyUser=true)
Adds a message to the log instance (and creates it if necessary).
Definition: qgsmessagelog.cpp:27
QgsAbstractContentCacheBase::remoteContentFetched
void remoteContentFetched(const QString &url)
Emitted when the cache has finished retrieving content from a remote url.
QgsAbstractContentCacheEntry::operator=
QgsAbstractContentCacheEntry & operator=(const QgsAbstractContentCacheEntry &rh)=delete
QgsAbstractContentCacheEntry cannot be copied.
QgsNetworkAccessManager::timeout
static int timeout()
Returns the network timeout length, in milliseconds.
Definition: qgsnetworkaccessmanager.cpp:642
QgsAbstractContentCacheBase
A QObject derived base class for QgsAbstractContentCache.
Definition: qgsabstractcontentcache.h:131
qgslogger.h
QgsAbstractContentCache::trimToMaximumSize
void trimToMaximumSize()
Removes the least used cache entries until the maximum cache size is under the predefined size limit.
Definition: qgsabstractcontentcache.h:227
SIP_TRANSFERTHIS
#define SIP_TRANSFERTHIS
Definition: qgis_sip.h:53
QgsAbstractContentCache
Abstract base class for file content caches, such as SVG or raster image caches.
Definition: qgsabstractcontentcache.h:190
QgsTask::waitForFinished
bool waitForFinished(int timeout=30000)
Blocks the current thread until the task finishes or a maximum of timeout milliseconds.
Definition: qgstaskmanager.cpp:168
QgsAbstractContentCache::mMutex
QMutex mMutex
Definition: qgsabstractcontentcache.h:549
qgsmessagelog.h
QgsAbstractContentCache::getContent
QByteArray getContent(const QString &path, const QByteArray &missingContent, const QByteArray &fetchingContent, bool blocking=false) const
Gets the file content corresponding to the given path.
Definition: qgsabstractcontentcache.h:260
QgsTask
Abstract base class for long running background tasks.
Definition: qgstaskmanager.h:53