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 }