QGIS API Documentation  3.8.0-Zanzibar (11aff65)
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 
61  QgsAbstractContentCacheEntry &operator=( const QgsAbstractContentCacheEntry &rh ) = delete;
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  bool equal = other.path == path;
92  if ( equal && ( mFileModifiedCheckTimeout <= 0 || fileModifiedLastCheckTimer.hasExpired( mFileModifiedCheckTimeout ) ) )
93  equal = other.fileModified == fileModified;
94 
95  return equal;
96  }
97 
101  virtual int dataSize() const = 0;
102 
106  virtual void dump() const = 0;
107 
108  protected:
109 
115  virtual bool isEqual( const QgsAbstractContentCacheEntry *other ) const = 0;
116 
117  private:
118 #ifdef SIP_RUN
120 #endif
121 
122 };
123 
134 class CORE_EXPORT QgsAbstractContentCacheBase: public QObject
135 {
136  Q_OBJECT
137 
138  public:
139 
143  QgsAbstractContentCacheBase( QObject *parent );
144 
145  signals:
146 
150  void remoteContentFetched( const QString &url );
151 
152  protected:
153 
158  virtual bool checkReply( QNetworkReply *reply, const QString &path ) const
159  {
160  Q_UNUSED( reply )
161  Q_UNUSED( path )
162  return true;
163  }
164 
165  protected slots:
166 
173  virtual void onRemoteContentFetched( const QString &url, bool success );
174 
175 };
176 
177 #ifndef SIP_RUN
178 
192 template<class T>
194 {
195 
196  public:
197 
209  QgsAbstractContentCache( QObject *parent SIP_TRANSFERTHIS = nullptr,
210  const QString &typeString = QString(),
211  long maxCacheSize = 20000000,
212  int fileModifiedCheckTimeout = 30000 )
213  : QgsAbstractContentCacheBase( parent )
214  , mMutex( QMutex::Recursive )
215  , mMaxCacheSize( maxCacheSize )
216  , mFileModifiedCheckTimeout( fileModifiedCheckTimeout )
217  , mTypeString( typeString.isEmpty() ? QObject::tr( "Content" ) : typeString )
218  {
219  }
220 
222  {
223  qDeleteAll( mEntryLookup );
224  }
225 
226  protected:
227 
232  {
233  //only one entry in cache
234  if ( mLeastRecentEntry == mMostRecentEntry )
235  {
236  return;
237  }
238  T *entry = mLeastRecentEntry;
239  while ( entry && ( mTotalSize > mMaxCacheSize ) )
240  {
241  T *bkEntry = entry;
242  entry = static_cast< T * >( entry->nextEntry );
243 
244  takeEntryFromList( bkEntry );
245  mEntryLookup.remove( bkEntry->path, bkEntry );
246  mTotalSize -= bkEntry->dataSize();
247  delete bkEntry;
248  }
249  }
250 
260  QByteArray getContent( const QString &path, const QByteArray &missingContent, const QByteArray &fetchingContent ) 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  return fetchingContent;
316 
317  if ( mRemoteContentCache.contains( path ) )
318  {
319  // already fetched this content - phew. Just return what we already got.
320  return *mRemoteContentCache[ path ];
321  }
322 
323  mPendingRemoteUrls.insert( path );
324  //fire up task to fetch content in background
325  QNetworkRequest request( url );
326  QgsSetRequestInitiatorClass( request, QStringLiteral( "QgsAbstractContentCache<%1>" ).arg( mTypeString ) );
327  request.setAttribute( QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::PreferCache );
328  request.setAttribute( QNetworkRequest::CacheSaveControlAttribute, true );
329 
331  connect( task, &QgsNetworkContentFetcherTask::fetched, this, [this, task, path, missingContent]
332  {
333  QMutexLocker locker( &mMutex );
334 
335  QNetworkReply *reply = task->reply();
336  if ( !reply )
337  {
338  // canceled
339  QMetaObject::invokeMethod( const_cast< QgsAbstractContentCacheBase * >( qobject_cast< const QgsAbstractContentCacheBase * >( this ) ), "onRemoteContentFetched", Qt::QueuedConnection, Q_ARG( QString, path ), Q_ARG( bool, false ) );
340  return;
341  }
342 
343  if ( reply->error() != QNetworkReply::NoError )
344  {
345  QgsMessageLog::logMessage( tr( "%3 request failed [error: %1 - url: %2]" ).arg( reply->errorString(), path, mTypeString ), mTypeString );
346  return;
347  }
348 
349  bool ok = true;
350 
351  QVariant status = reply->attribute( QNetworkRequest::HttpStatusCodeAttribute );
352  if ( !status.isNull() && status.toInt() >= 400 )
353  {
354  QVariant phrase = reply->attribute( QNetworkRequest::HttpReasonPhraseAttribute );
355  QgsMessageLog::logMessage( tr( "%4 request error [status: %1 - reason phrase: %2] for %3" ).arg( status.toInt() ).arg( phrase.toString(), path, mTypeString ), mTypeString );
356  mRemoteContentCache.insert( path, new QByteArray( missingContent ) );
357  ok = false;
358  }
359 
360  if ( !checkReply( reply, path ) )
361  {
362  mRemoteContentCache.insert( path, new QByteArray( missingContent ) );
363  ok = false;
364  }
365 
366  if ( ok )
367  {
368  // read the content data
369  mRemoteContentCache.insert( path, new QByteArray( reply->readAll() ) );
370  }
371  QMetaObject::invokeMethod( const_cast< QgsAbstractContentCacheBase * >( qobject_cast< const QgsAbstractContentCacheBase * >( this ) ), "onRemoteContentFetched", Qt::QueuedConnection, Q_ARG( QString, path ), Q_ARG( bool, true ) );
372  } );
373 
375  return fetchingContent;
376  }
377 
378  void onRemoteContentFetched( const QString &url, bool success ) override
379  {
380  QMutexLocker locker( &mMutex );
381  mPendingRemoteUrls.remove( url );
382 
383  T *nextEntry = mLeastRecentEntry;
384  while ( T *entry = nextEntry )
385  {
386  nextEntry = static_cast< T * >( entry->nextEntry );
387  if ( entry->path == url )
388  {
389  takeEntryFromList( entry );
390  mEntryLookup.remove( entry->path, entry );
391  mTotalSize -= entry->dataSize();
392  delete entry;
393  }
394  }
395 
396  if ( success )
397  emit remoteContentFetched( url );
398  }
399 
409  T *findExistingEntry( T *entryTemplate )
410  {
411  //search entries in mEntryLookup
412  const QString path = entryTemplate->path;
413  T *currentEntry = nullptr;
414  const QList<T *> entries = mEntryLookup.values( path );
415  QDateTime modified;
416  for ( T *cacheEntry : entries )
417  {
418  if ( cacheEntry->isEqual( entryTemplate ) )
419  {
420  if ( mFileModifiedCheckTimeout <= 0 || cacheEntry->fileModifiedLastCheckTimer.hasExpired( mFileModifiedCheckTimeout ) )
421  {
422  if ( !modified.isValid() )
423  modified = QFileInfo( path ).lastModified();
424 
425  if ( cacheEntry->fileModified != modified )
426  continue;
427  }
428  currentEntry = cacheEntry;
429  break;
430  }
431  }
432 
433  //if not found: insert entryTemplate as a new entry
434  if ( !currentEntry )
435  {
436  currentEntry = insertCacheEntry( entryTemplate );
437  }
438  else
439  {
440  delete entryTemplate;
441  entryTemplate = nullptr;
442  takeEntryFromList( currentEntry );
443  if ( !mMostRecentEntry ) //list is empty
444  {
445  mMostRecentEntry = currentEntry;
446  mLeastRecentEntry = currentEntry;
447  }
448  else
449  {
450  mMostRecentEntry->nextEntry = currentEntry;
451  currentEntry->previousEntry = mMostRecentEntry;
452  currentEntry->nextEntry = nullptr;
453  mMostRecentEntry = currentEntry;
454  }
455  }
456 
457  //debugging
458  //printEntryList();
459 
460  return currentEntry;
461  }
462 
463  mutable QMutex mMutex;
465  long mTotalSize = 0;
466 
468  long mMaxCacheSize = 20000000;
469 
470  private:
471 
477  T *insertCacheEntry( T *entry )
478  {
479  entry->mFileModifiedCheckTimeout = mFileModifiedCheckTimeout;
480 
481  mEntryLookup.insert( entry->path, entry );
482 
483  //insert to most recent place in entry list
484  if ( !mMostRecentEntry ) //inserting first entry
485  {
486  mLeastRecentEntry = entry;
487  mMostRecentEntry = entry;
488  entry->previousEntry = nullptr;
489  entry->nextEntry = nullptr;
490  }
491  else
492  {
493  entry->previousEntry = mMostRecentEntry;
494  entry->nextEntry = nullptr;
495  mMostRecentEntry->nextEntry = entry;
496  mMostRecentEntry = entry;
497  }
498 
499  trimToMaximumSize();
500  return entry;
501  }
502 
503 
507  void takeEntryFromList( T *entry )
508  {
509  if ( !entry )
510  {
511  return;
512  }
513 
514  if ( entry->previousEntry )
515  {
516  entry->previousEntry->nextEntry = entry->nextEntry;
517  }
518  else
519  {
520  mLeastRecentEntry = static_cast< T * >( entry->nextEntry );
521  }
522  if ( entry->nextEntry )
523  {
524  entry->nextEntry->previousEntry = entry->previousEntry;
525  }
526  else
527  {
528  mMostRecentEntry = static_cast< T * >( entry->previousEntry );
529  }
530  }
531 
535  void printEntryList()
536  {
537  QgsDebugMsg( QStringLiteral( "****************cache entry list*************************" ) );
538  QgsDebugMsg( "Cache size: " + QString::number( mTotalSize ) );
539  T *entry = mLeastRecentEntry;
540  while ( entry )
541  {
542  QgsDebugMsg( QStringLiteral( "***Entry:" ) );
543  entry->dump();
544  entry = entry->nextEntry;
545  }
546  }
547 
549  QMultiHash< QString, T * > mEntryLookup;
550 
552  int mFileModifiedCheckTimeout = 30000;
553 
554  //The content cache keeps the entries on a double connected list, moving the current entry to the front.
555  //That way, removing entries for more space can start with the least used objects.
556  T *mLeastRecentEntry = nullptr;
557  T *mMostRecentEntry = nullptr;
558 
559  mutable QCache< QString, QByteArray > mRemoteContentCache;
560  mutable QSet< QString > mPendingRemoteUrls;
561 
562  QString mTypeString;
563 
564  friend class TestQgsSvgCache;
565  friend class TestQgsImageCache;
566 };
567 
568 #endif
569 
570 #endif // QGSABSTRACTCONTENTCACHE_H
#define QgsSetRequestInitiatorClass(request, _class)
Abstract base class for file content caches, such as SVG or raster image caches.
#define SIP_TRANSFERTHIS
Definition: qgis_sip.h:46
void fetched()
Emitted when the network content has been fetched, regardless of whether the fetch was successful or ...
#define QgsDebugMsg(str)
Definition: qgslogger.h:38
QgsAbstractContentCache(QObject *parent=nullptr, const QString &typeString=QString(), long maxCacheSize=20000000, int fileModifiedCheckTimeout=30000)
Constructor for QgsAbstractContentCache, with the specified parent object.
Base class for entries in a QgsAbstractContentCache.
void remoteContentFetched(const QString &url)
Emitted when the cache has finished retrieving content from a remote url.
QNetworkReply * reply()
Returns the network reply.
Handles HTTP network content fetching in a background task.
static QgsTaskManager * taskManager()
Returns the application&#39;s task manager, used for managing application wide background task handling...
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).
long addTask(QgsTask *task, int priority=0)
Adds a task to the manager.
void onRemoteContentFetched(const QString &url, bool success) override
Triggered after remote content (i.e.
T * findExistingEntry(T *entryTemplate)
Returns the existing entry from the cache which matches entryTemplate (deleting entryTemplate when do...
QDateTime fileModified
Timestamp when file was last modified.
QByteArray getContent(const QString &path, const QByteArray &missingContent, const QByteArray &fetchingContent) const
Gets the file content corresponding to the given path.
A QObject derived base class for QgsAbstractContentCache.
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...
QElapsedTimer fileModifiedLastCheckTimer
Time since last check of file modified date.
bool operator==(const QgsAbstractContentCacheEntry &other) const
void trimToMaximumSize()
Removes the least used cache entries until the maximum cache size is under the predefined size limit...
QString path
Represents the absolute path to a file, a remote URL, or a base64 encoded string. ...