/* Copyright (c) 2009 Volker Krause This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef AKONADI_ENTITYCACHE_P_H #define AKONADI_ENTITYCACHE_P_H #include #include #include #include #include #include #include #include #include #include #include "akonadiprivate_export.h" #include #include #include #include #include #include class Dummy; class KJob; typedef QList EntityIdList; Q_DECLARE_METATYPE(QList) namespace Akonadi { /** @internal QObject part of EntityCache. */ class AKONADI_TESTS_EXPORT EntityCacheBase : public QObject { Q_OBJECT public: explicit EntityCacheBase(Session *session, QObject *parent = 0); void setSession(Session *session); protected: Session *session; Q_SIGNALS: void dataAvailable(); private Q_SLOTS: virtual void processResult(KJob *job) = 0; }; template struct EntityCacheNode { EntityCacheNode() : pending(false) , invalid(false) { } EntityCacheNode(typename T::Id id) : entity(T(id)) , pending(true) , invalid(false) { } T entity; bool pending; bool invalid; }; /** * @internal * A in-memory FIFO cache for a small amount of Entity objects. */ template class EntityCache : public EntityCacheBase { public: typedef FetchScope_ FetchScope; explicit EntityCache(int maxCapacity, Session *session = 0, QObject *parent = 0) : EntityCacheBase(session, parent) , mCapacity(maxCapacity) { } ~EntityCache() { qDeleteAll(mCache); } /** Object is available in the cache and can be retrieved. */ bool isCached(typename T::Id id) const { EntityCacheNode *node = cacheNodeForId(id); return node && !node->pending; } /** Object has been requested but is not yet loaded into the cache or is already available. */ bool isRequested(typename T::Id id) const { return cacheNodeForId(id); } /** Returns the cached object if available, an empty instance otherwise. */ virtual T retrieve(typename T::Id id) const { EntityCacheNode *node = cacheNodeForId(id); if (node && !node->pending && !node->invalid) { return node->entity; } return T(); } /** Marks the cache entry as invalid, use in case the object has been deleted on the server. */ void invalidate(typename T::Id id) { EntityCacheNode *node = cacheNodeForId(id); if (node) { node->invalid = true; } } /** Triggers a re-fetching of a cache entry, use if it has changed on the server. */ void update(typename T::Id id, const FetchScope &scope) { EntityCacheNode *node = cacheNodeForId(id); if (node) { mCache.removeAll(node); if (node->pending) { request(id, scope); } delete node; } } /** Requests the object to be cached if it is not yet in the cache. @returns @c true if it was in the cache already. */ virtual bool ensureCached(typename T::Id id, const FetchScope &scope) { EntityCacheNode *node = cacheNodeForId(id); if (!node) { request(id, scope); return false; } return !node->pending; } /** Asks the cache to retrieve @p id. @p request is used as a token to indicate which request has been finished in the dataAvailable() signal. */ virtual void request(typename T::Id id, const FetchScope &scope) { Q_ASSERT(!isRequested(id)); shrinkCache(); EntityCacheNode *node = new EntityCacheNode(id); FetchJob *job = createFetchJob(id, scope); job->setProperty("EntityCacheNode", QVariant::fromValue(id)); connect(job, SIGNAL(result(KJob*)), SLOT(processResult(KJob*))); mCache.enqueue(node); } private: EntityCacheNode *cacheNodeForId(typename T::Id id) const { for (typename QQueue *>::const_iterator it = mCache.constBegin(), endIt = mCache.constEnd(); it != endIt; ++it) { if ((*it)->entity.id() == id) { return *it; } } return 0; } void processResult(KJob *job) { typename T::Id id = job->property("EntityCacheNode").template value(); // Error handling? if ( job->error() ) { kWarning() << job->errorString(); } EntityCacheNode *node = cacheNodeForId(id); if (!node) { return; // got replaced in the meantime } node->pending = false; extractResult(node, job); // make sure we find this node again if something went wrong here, // most likely the object got deleted from the server in the meantime if (node->entity.id() != id) { // TODO: Recursion guard? If this is called with non-existing ids, the if will never be true! node->entity.setId(id); node->invalid = true; } emit dataAvailable(); } void extractResult(EntityCacheNode *node, KJob *job) const; inline FetchJob *createFetchJob(typename T::Id id, const FetchScope &scope) { FetchJob *fetch = new FetchJob(T(id), session); fetch->setFetchScope(scope); return fetch; } /** Tries to reduce the cache size until at least one more object fits in. */ void shrinkCache() { while (mCache.size() >= mCapacity && !mCache.first()->pending) { delete mCache.dequeue(); } } private: QQueue *> mCache; int mCapacity; }; template<> inline void EntityCache::extractResult(EntityCacheNode *node, KJob *job) const { CollectionFetchJob *fetch = qobject_cast(job); Q_ASSERT(fetch); if (fetch->collections().isEmpty()) { node->entity = Collection(); } else { node->entity = fetch->collections().first(); } } template<> inline void EntityCache::extractResult(EntityCacheNode *node, KJob *job) const { ItemFetchJob *fetch = qobject_cast(job); Q_ASSERT(fetch); if (fetch->items().isEmpty()) { node->entity = Item(); } else { node->entity = fetch->items().first(); } } template<> inline void EntityCache::extractResult(EntityCacheNode *node, KJob *job) const { TagFetchJob *fetch = qobject_cast(job); Q_ASSERT(fetch); if (fetch->tags().isEmpty()) { node->entity = Tag(); } else { node->entity = fetch->tags().first(); } } template<> inline CollectionFetchJob *EntityCache::createFetchJob(Collection::Id id, const CollectionFetchScope &scope) { CollectionFetchJob *fetch = new CollectionFetchJob(Collection(id), CollectionFetchJob::Base, session); fetch->setFetchScope(scope); return fetch; } typedef EntityCache CollectionCache; typedef EntityCache ItemCache; typedef EntityCache TagCache; template struct EntityListCacheNode { EntityListCacheNode() : pending(false) , invalid(false) { } EntityListCacheNode(typename T::Id id) : entity(id) , pending(true) , invalid(false) { } T entity; bool pending; bool invalid; }; template class EntityListCache : public EntityCacheBase { public: typedef FetchScope_ FetchScope; explicit EntityListCache(int maxCapacity, Session *session = 0, QObject *parent = 0) : EntityCacheBase(session, parent) , mCapacity(maxCapacity) { } ~EntityListCache() { qDeleteAll(mCache); } /** Returns the cached object if available, an empty instance otherwise. */ typename T::List retrieve(const QList &ids) const { typename T::List list; foreach (Entity::Id id, ids) { EntityListCacheNode *node = mCache.value(id); if (!node || node->pending || node->invalid) { return typename T::List(); } list << node->entity; } return list; } /** Requests the object to be cached if it is not yet in the cache. @returns @c true if it was in the cache already. */ bool ensureCached(const QList &ids, const FetchScope &scope) { QList toRequest; bool result = true; foreach (Entity::Id id, ids) { EntityListCacheNode *node = mCache.value(id); if (!node) { toRequest << id; continue; } if (node->pending) { result = false; } } if (!toRequest.isEmpty()) { request(toRequest, scope, ids); return false; } return result; } /** Marks the cache entry as invalid, use in case the object has been deleted on the server. */ void invalidate(const QList &ids) { foreach (Entity::Id id, ids) { EntityListCacheNode *node = mCache.value(id); if (node) { node->invalid = true; } } } /** Triggers a re-fetching of a cache entry, use if it has changed on the server. */ void update(const QList &ids, const FetchScope &scope) { QList toRequest; foreach (Entity::Id id, ids) { EntityListCacheNode *node = mCache.value(id); if (node) { mCache.remove(id); if (node->pending) { toRequest << id; } delete node; } } if (!toRequest.isEmpty()) { request(toRequest, scope); } } /** Asks the cache to retrieve @p id. @p request is used as a token to indicate which request has been finished in the dataAvailable() signal. */ void request(const QList &ids, const FetchScope &scope, const QList &preserveIds = QList()) { Q_ASSERT(isNotRequested(ids)); shrinkCache(preserveIds); foreach (Entity::Id id, ids) { EntityListCacheNode *node = new EntityListCacheNode(id); mCache.insert(id, node); } FetchJob *job = createFetchJob(ids, scope); job->setProperty("EntityListCacheIds", QVariant::fromValue< QList >(ids)); connect(job, SIGNAL(result(KJob*)), SLOT(processResult(KJob*))); } bool isNotRequested(const QList &ids) const { foreach (Entity::Id id, ids) { if (mCache.contains(id)) { return false; } } return true; } /** Object is available in the cache and can be retrieved. */ bool isCached(const QList &ids) const { foreach (Entity::Id id, ids) { EntityListCacheNode *node = mCache.value(id); if (!node || node->pending) { return false; } } return true; } private: /** Tries to reduce the cache size until at least one more object fits in. */ void shrinkCache(const QList &preserveIds) { typename QHash< Entity::Id, EntityListCacheNode *>::Iterator iter = mCache.begin(); while (iter != mCache.end() && mCache.size() >= mCapacity) { if (iter.value()->pending || preserveIds.contains(iter.key())) { ++iter; continue; } delete iter.value(); iter = mCache.erase(iter); } } inline FetchJob *createFetchJob(const QList &ids, const FetchScope &scope) { FetchJob *job = new FetchJob(ids, session); job->setFetchScope(scope); return job; } void processResult(KJob *job) { if ( job->error() ) { kWarning() << job->errorString(); } const QList ids = job->property("EntityListCacheIds").value< QList >(); typename T::List entities; extractResults(job, entities); foreach (Entity::Id id, ids) { EntityListCacheNode *node = mCache.value(id); if (!node) { continue; // got replaced in the meantime } node->pending = false; T result; typename T::List::Iterator iter = entities.begin(); for (; iter != entities.end(); ++iter) { if ((*iter).id() == id) { result = *iter; entities.erase(iter); break; } } // make sure we find this node again if something went wrong here, // most likely the object got deleted from the server in the meantime if (!result.isValid()) { node->entity = T(id); node->invalid = true; } else { node->entity = result; } } emit dataAvailable(); } void extractResults(KJob *job, typename T::List &entities) const; private: QHash< Entity::Id, EntityListCacheNode *> mCache; int mCapacity; }; template<> inline void EntityListCache::extractResults(KJob *job, Collection::List &collections) const { CollectionFetchJob *fetch = qobject_cast(job); Q_ASSERT(fetch); collections = fetch->collections(); } template<> inline void EntityListCache::extractResults(KJob *job, Item::List &items) const { ItemFetchJob *fetch = qobject_cast(job); Q_ASSERT(fetch); items = fetch->items(); } template<> inline void EntityListCache::extractResults(KJob *job, Tag::List &tags) const { TagFetchJob *fetch = qobject_cast(job); Q_ASSERT(fetch); tags = fetch->tags(); } template<> inline CollectionFetchJob *EntityListCache::createFetchJob(const QList &ids, const CollectionFetchScope &scope) { CollectionFetchJob *fetch = new CollectionFetchJob(ids, CollectionFetchJob::Base, session); fetch->setFetchScope(scope); return fetch; } typedef EntityListCache CollectionListCache; typedef EntityListCache ItemListCache; typedef EntityListCache TagListCache; } #endif