File indexing completed on 2024-06-16 04:50:13

0001 /*
0002     SPDX-FileCopyrightText: 2009 Volker Krause <vkrause@kde.org>
0003 
0004     SPDX-License-Identifier: LGPL-2.0-or-later
0005 */
0006 
0007 #pragma once
0008 
0009 #include "collection.h"
0010 #include "collectionfetchjob.h"
0011 #include "collectionfetchscope.h"
0012 #include "item.h"
0013 #include "itemfetchjob.h"
0014 #include "itemfetchscope.h"
0015 #include "session.h"
0016 #include "tag.h"
0017 #include "tagfetchjob.h"
0018 #include "tagfetchscope.h"
0019 
0020 #include "akonaditests_export.h"
0021 
0022 #include <QHash>
0023 #include <QObject>
0024 #include <QQueue>
0025 #include <QVariant>
0026 
0027 class KJob;
0028 
0029 Q_DECLARE_METATYPE(QList<qint64>)
0030 
0031 namespace Akonadi
0032 {
0033 /**
0034   @internal
0035   QObject part of EntityCache.
0036 */
0037 class AKONADI_TESTS_EXPORT EntityCacheBase : public QObject
0038 {
0039     Q_OBJECT
0040 public:
0041     explicit EntityCacheBase(Session *session, QObject *parent = nullptr);
0042 
0043     void setSession(Session *session);
0044 
0045 protected:
0046     Session *session = nullptr;
0047 
0048 Q_SIGNALS:
0049     void dataAvailable();
0050 
0051 private Q_SLOTS:
0052     virtual void processResult(KJob *job) = 0;
0053 };
0054 
0055 template<typename T>
0056 struct EntityCacheNode {
0057     EntityCacheNode()
0058         : pending(false)
0059         , invalid(false)
0060     {
0061     }
0062     EntityCacheNode(typename T::Id id)
0063         : entity(T(id))
0064         , pending(true)
0065         , invalid(false)
0066     {
0067     }
0068     T entity;
0069     bool pending;
0070     bool invalid;
0071 };
0072 
0073 /**
0074  * @internal
0075  * A in-memory FIFO cache for a small amount of Item or Collection objects.
0076  */
0077 template<typename T, typename FetchJob, typename FetchScope_>
0078 class EntityCache : public EntityCacheBase
0079 {
0080 public:
0081     using FetchScope = FetchScope_;
0082     explicit EntityCache(int maxCapacity, Session *session = nullptr, QObject *parent = nullptr)
0083         : EntityCacheBase(session, parent)
0084         , mCapacity(maxCapacity)
0085     {
0086     }
0087 
0088     ~EntityCache() override
0089     {
0090         qDeleteAll(mCache);
0091     }
0092 
0093     /** Object is available in the cache and can be retrieved. */
0094     bool isCached(typename T::Id id) const
0095     {
0096         EntityCacheNode<T> *node = cacheNodeForId(id);
0097         return node && !node->pending;
0098     }
0099 
0100     /** Object has been requested but is not yet loaded into the cache or is already available. */
0101     bool isRequested(typename T::Id id) const
0102     {
0103         return cacheNodeForId(id);
0104     }
0105 
0106     /** Returns the cached object if available, an empty instance otherwise. */
0107     virtual T retrieve(typename T::Id id) const
0108     {
0109         EntityCacheNode<T> *node = cacheNodeForId(id);
0110         if (node && !node->pending && !node->invalid) {
0111             return node->entity;
0112         }
0113         return T();
0114     }
0115 
0116     /** Marks the cache entry as invalid, use in case the object has been deleted on the server. */
0117     void invalidate(typename T::Id id)
0118     {
0119         EntityCacheNode<T> *node = cacheNodeForId(id);
0120         if (node) {
0121             node->invalid = true;
0122         }
0123     }
0124 
0125     /** Triggers a re-fetching of a cache entry, use if it has changed on the server. */
0126     void update(typename T::Id id, const FetchScope &scope)
0127     {
0128         EntityCacheNode<T> *node = cacheNodeForId(id);
0129         if (node) {
0130             mCache.removeAll(node);
0131             if (node->pending) {
0132                 request(id, scope);
0133             }
0134             delete node;
0135         }
0136     }
0137 
0138     /** Requests the object to be cached if it is not yet in the cache. @returns @c true if it was in the cache already. */
0139     virtual bool ensureCached(typename T::Id id, const FetchScope &scope)
0140     {
0141         EntityCacheNode<T> *node = cacheNodeForId(id);
0142         if (!node) {
0143             request(id, scope);
0144             return false;
0145         }
0146         return !node->pending;
0147     }
0148 
0149     /**
0150       Asks the cache to retrieve @p id. @p request is used as
0151       a token to indicate which request has been finished in the
0152       dataAvailable() signal.
0153     */
0154     virtual void request(typename T::Id id, const FetchScope &scope)
0155     {
0156         Q_ASSERT(!isRequested(id));
0157         shrinkCache();
0158         auto node = new EntityCacheNode<T>(id);
0159         FetchJob *job = createFetchJob(id, scope);
0160         job->setProperty("EntityCacheNode", QVariant::fromValue<typename T::Id>(id));
0161         connect(job, SIGNAL(result(KJob *)), SLOT(processResult(KJob *)));
0162         mCache.enqueue(node);
0163     }
0164 
0165 private:
0166     EntityCacheNode<T> *cacheNodeForId(typename T::Id id) const
0167     {
0168         for (typename QQueue<EntityCacheNode<T> *>::const_iterator it = mCache.constBegin(), endIt = mCache.constEnd(); it != endIt; ++it) {
0169             if ((*it)->entity.id() == id) {
0170                 return *it;
0171             }
0172         }
0173         return nullptr;
0174     }
0175 
0176     void processResult(KJob *job) override
0177     {
0178         if (job->error()) {
0179             // This can happen if we have stale notifications for items that have already been removed
0180         }
0181         auto id = job->property("EntityCacheNode").template value<typename T::Id>();
0182         EntityCacheNode<T> *node = cacheNodeForId(id);
0183         if (!node) {
0184             return; // got replaced in the meantime
0185         }
0186 
0187         node->pending = false;
0188         extractResult(node, job);
0189         // make sure we find this node again if something went wrong here,
0190         // most likely the object got deleted from the server in the meantime
0191         if (node->entity.id() != id) {
0192             // TODO: Recursion guard? If this is called with non-existing ids, the if will never be true!
0193             node->entity.setId(id);
0194             node->invalid = true;
0195         }
0196         Q_EMIT dataAvailable();
0197     }
0198 
0199     void extractResult(EntityCacheNode<T> *node, KJob *job) const;
0200 
0201     inline FetchJob *createFetchJob(typename T::Id id, const FetchScope &scope)
0202     {
0203         auto fetch = new FetchJob(T(id), session);
0204         fetch->setFetchScope(scope);
0205         return fetch;
0206     }
0207 
0208     /** Tries to reduce the cache size until at least one more object fits in. */
0209     void shrinkCache()
0210     {
0211         while (mCache.size() >= mCapacity && !mCache.first()->pending) {
0212             delete mCache.dequeue();
0213         }
0214     }
0215 
0216 private:
0217     QQueue<EntityCacheNode<T> *> mCache;
0218     int mCapacity;
0219 };
0220 
0221 template<>
0222 inline void EntityCache<Collection, CollectionFetchJob, CollectionFetchScope>::extractResult(EntityCacheNode<Collection> *node, KJob *job) const
0223 {
0224     auto fetch = qobject_cast<CollectionFetchJob *>(job);
0225     Q_ASSERT(fetch);
0226     if (fetch->collections().isEmpty()) {
0227         node->entity = Collection();
0228     } else {
0229         node->entity = fetch->collections().at(0);
0230     }
0231 }
0232 
0233 template<>
0234 inline void EntityCache<Item, ItemFetchJob, ItemFetchScope>::extractResult(EntityCacheNode<Item> *node, KJob *job) const
0235 {
0236     auto fetch = qobject_cast<ItemFetchJob *>(job);
0237     Q_ASSERT(fetch);
0238     if (fetch->items().isEmpty()) {
0239         node->entity = Item();
0240     } else {
0241         node->entity = fetch->items().at(0);
0242     }
0243 }
0244 
0245 template<>
0246 inline void EntityCache<Tag, TagFetchJob, TagFetchScope>::extractResult(EntityCacheNode<Tag> *node, KJob *job) const
0247 {
0248     auto fetch = qobject_cast<TagFetchJob *>(job);
0249     Q_ASSERT(fetch);
0250     if (fetch->tags().isEmpty()) {
0251         node->entity = Tag();
0252     } else {
0253         node->entity = fetch->tags().at(0);
0254     }
0255 }
0256 
0257 template<>
0258 inline CollectionFetchJob *EntityCache<Collection, CollectionFetchJob, CollectionFetchScope>::createFetchJob(Collection::Id id,
0259                                                                                                              const CollectionFetchScope &scope)
0260 {
0261     auto fetch = new CollectionFetchJob(Collection(id), CollectionFetchJob::Base, session);
0262     fetch->setFetchScope(scope);
0263     return fetch;
0264 }
0265 
0266 using CollectionCache = EntityCache<Collection, CollectionFetchJob, CollectionFetchScope>;
0267 using ItemCache = EntityCache<Item, ItemFetchJob, ItemFetchScope>;
0268 using TagCache = EntityCache<Tag, TagFetchJob, TagFetchScope>;
0269 
0270 template<typename T>
0271 struct EntityListCacheNode {
0272     EntityListCacheNode()
0273         : pending(false)
0274         , invalid(false)
0275     {
0276     }
0277     EntityListCacheNode(typename T::Id id)
0278         : entity(id)
0279         , pending(true)
0280         , invalid(false)
0281     {
0282     }
0283 
0284     T entity;
0285     bool pending;
0286     bool invalid;
0287 };
0288 
0289 template<typename T, typename FetchJob, typename FetchScope_>
0290 class EntityListCache : public EntityCacheBase
0291 {
0292 public:
0293     using FetchScope = FetchScope_;
0294 
0295     explicit EntityListCache(int maxCapacity, Session *session = nullptr, QObject *parent = nullptr)
0296         : EntityCacheBase(session, parent)
0297         , mCapacity(maxCapacity)
0298     {
0299     }
0300 
0301     ~EntityListCache() override
0302     {
0303         qDeleteAll(mCache);
0304     }
0305 
0306     /** Returns the cached object if available, an empty instance otherwise. */
0307     typename T::List retrieve(const QList<typename T::Id> &ids) const
0308     {
0309         typename T::List list;
0310 
0311         for (typename T::Id id : ids) {
0312             EntityListCacheNode<T> *node = mCache.value(id);
0313             if (!node || node->pending || node->invalid) {
0314                 return typename T::List();
0315             }
0316 
0317             list << node->entity;
0318         }
0319 
0320         return list;
0321     }
0322 
0323     /** Requests the object to be cached if it is not yet in the cache. @returns @c true if it was in the cache already. */
0324     bool ensureCached(const QList<typename T::Id> &ids, const FetchScope &scope)
0325     {
0326         QList<typename T::Id> toRequest;
0327         bool result = true;
0328 
0329         for (typename T::Id id : ids) {
0330             EntityListCacheNode<T> *node = mCache.value(id);
0331             if (!node) {
0332                 toRequest << id;
0333                 continue;
0334             }
0335 
0336             if (node->pending) {
0337                 result = false;
0338             }
0339         }
0340 
0341         if (!toRequest.isEmpty()) {
0342             request(toRequest, scope, ids);
0343             return false;
0344         }
0345 
0346         return result;
0347     }
0348 
0349     /** Marks the cache entry as invalid, use in case the object has been deleted on the server. */
0350     void invalidate(const QList<typename T::Id> &ids)
0351     {
0352         for (typename T::Id id : ids) {
0353             EntityListCacheNode<T> *node = mCache.value(id);
0354             if (node) {
0355                 node->invalid = true;
0356             }
0357         }
0358     }
0359 
0360     /** Triggers a re-fetching of a cache entry, use if it has changed on the server. */
0361     void update(const QList<typename T::Id> &ids, const FetchScope &scope)
0362     {
0363         QList<typename T::Id> toRequest;
0364 
0365         for (typename T::Id id : ids) {
0366             EntityListCacheNode<T> *node = mCache.value(id);
0367             if (node) {
0368                 mCache.remove(id);
0369                 if (node->pending) {
0370                     toRequest << id;
0371                 }
0372                 delete node;
0373             }
0374         }
0375 
0376         if (!toRequest.isEmpty()) {
0377             request(toRequest, scope);
0378         }
0379     }
0380 
0381     /**
0382       Asks the cache to retrieve @p id. @p request is used as
0383       a token to indicate which request has been finished in the
0384       dataAvailable() signal.
0385     */
0386     void request(const QList<typename T::Id> &ids, const FetchScope &scope, const QList<typename T::Id> &preserveIds = QList<typename T::Id>())
0387     {
0388         Q_ASSERT(isNotRequested(ids));
0389         shrinkCache(preserveIds);
0390         for (typename T::Id id : ids) {
0391             auto node = new EntityListCacheNode<T>(id);
0392             mCache.insert(id, node);
0393         }
0394         FetchJob *job = createFetchJob(ids, scope);
0395         job->setProperty("EntityListCacheIds", QVariant::fromValue<QList<typename T::Id>>(ids));
0396         connect(job, SIGNAL(result(KJob *)), SLOT(processResult(KJob *)));
0397     }
0398 
0399     bool isNotRequested(const QList<typename T::Id> &ids) const
0400     {
0401         for (typename T::Id id : ids) {
0402             if (mCache.contains(id)) {
0403                 return false;
0404             }
0405         }
0406 
0407         return true;
0408     }
0409 
0410     /** Object is available in the cache and can be retrieved. */
0411     bool isCached(const QList<typename T::Id> &ids) const
0412     {
0413         for (typename T::Id id : ids) {
0414             EntityListCacheNode<T> *node = mCache.value(id);
0415             if (!node || node->pending) {
0416                 return false;
0417             }
0418         }
0419         return true;
0420     }
0421 
0422 private:
0423     /** Tries to reduce the cache size until at least one more object fits in. */
0424     void shrinkCache(const QList<typename T::Id> &preserveIds)
0425     {
0426         typename QHash<typename T::Id, EntityListCacheNode<T> *>::Iterator iter = mCache.begin();
0427         while (iter != mCache.end() && mCache.size() >= mCapacity) {
0428             if (iter.value()->pending || preserveIds.contains(iter.key())) {
0429                 ++iter;
0430                 continue;
0431             }
0432 
0433             delete iter.value();
0434             iter = mCache.erase(iter);
0435         }
0436     }
0437 
0438     inline FetchJob *createFetchJob(const QList<typename T::Id> &ids, const FetchScope &scope)
0439     {
0440         auto job = new FetchJob(ids, session);
0441         job->setFetchScope(scope);
0442         return job;
0443     }
0444 
0445     void processResult(KJob *job) override
0446     {
0447         if (job->error()) {
0448             qWarning() << job->errorString();
0449         }
0450         const auto ids = job->property("EntityListCacheIds").value<QList<typename T::Id>>();
0451 
0452         typename T::List entities;
0453         extractResults(job, entities);
0454 
0455         for (typename T::Id id : ids) {
0456             EntityListCacheNode<T> *node = mCache.value(id);
0457             if (!node) {
0458                 continue; // got replaced in the meantime
0459             }
0460 
0461             node->pending = false;
0462 
0463             T result;
0464             typename T::List::Iterator iter = entities.begin();
0465             for (; iter != entities.end(); ++iter) {
0466                 if ((*iter).id() == id) {
0467                     result = *iter;
0468                     entities.erase(iter);
0469                     break;
0470                 }
0471             }
0472 
0473             // make sure we find this node again if something went wrong here,
0474             // most likely the object got deleted from the server in the meantime
0475             if (!result.isValid()) {
0476                 node->entity = T(id);
0477                 node->invalid = true;
0478             } else {
0479                 node->entity = result;
0480             }
0481         }
0482 
0483         Q_EMIT dataAvailable();
0484     }
0485 
0486     void extractResults(KJob *job, typename T::List &entities) const;
0487 
0488 private:
0489     QHash<typename T::Id, EntityListCacheNode<T> *> mCache;
0490     int mCapacity;
0491 };
0492 
0493 template<>
0494 inline void EntityListCache<Collection, CollectionFetchJob, CollectionFetchScope>::extractResults(KJob *job, Collection::List &collections) const
0495 {
0496     auto fetch = qobject_cast<CollectionFetchJob *>(job);
0497     Q_ASSERT(fetch);
0498     collections = fetch->collections();
0499 }
0500 
0501 template<>
0502 inline void EntityListCache<Item, ItemFetchJob, ItemFetchScope>::extractResults(KJob *job, Item::List &items) const
0503 {
0504     auto fetch = qobject_cast<ItemFetchJob *>(job);
0505     Q_ASSERT(fetch);
0506     items = fetch->items();
0507 }
0508 
0509 template<>
0510 inline void EntityListCache<Tag, TagFetchJob, TagFetchScope>::extractResults(KJob *job, Tag::List &tags) const
0511 {
0512     auto fetch = qobject_cast<TagFetchJob *>(job);
0513     Q_ASSERT(fetch);
0514     tags = fetch->tags();
0515 }
0516 
0517 template<>
0518 inline CollectionFetchJob *EntityListCache<Collection, CollectionFetchJob, CollectionFetchScope>::createFetchJob(const QList<Collection::Id> &ids,
0519                                                                                                                  const CollectionFetchScope &scope)
0520 {
0521     auto fetch = new CollectionFetchJob(ids, CollectionFetchJob::Base, session);
0522     fetch->setFetchScope(scope);
0523     return fetch;
0524 }
0525 
0526 using CollectionListCache = EntityListCache<Collection, CollectionFetchJob, CollectionFetchScope>;
0527 using ItemListCache = EntityListCache<Item, ItemFetchJob, ItemFetchScope>;
0528 using TagListCache = EntityListCache<Tag, TagFetchJob, TagFetchScope>;
0529 }