kde-playground/kdepimlibs/akonadi/entitycache_p.h
2015-04-14 21:49:29 +00:00

546 lines
16 KiB
C++

/*
Copyright (c) 2009 Volker Krause <vkrause@kde.org>
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 <akonadi/item.h>
#include <akonadi/itemfetchjob.h>
#include <akonadi/itemfetchscope.h>
#include <akonadi/collection.h>
#include <akonadi/collectionfetchjob.h>
#include <akonadi/collectionfetchscope.h>
#include <akonadi/tag.h>
#include <akonadi/tagfetchjob.h>
#include <akonadi/tagfetchscope.h>
#include <akonadi/session.h>
#include "akonadiprivate_export.h"
#include <kdebug.h>
#include <qobject.h>
#include <QQueue>
#include <QVariant>
#include <QHash>
#include <QtCore/QQueue>
class Dummy;
class KJob;
typedef QList<Akonadi::Entity::Id> EntityIdList;
Q_DECLARE_METATYPE(QList<Akonadi::Entity::Id>)
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 <typename T>
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<typename T, typename FetchJob, typename FetchScope_>
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<T> *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<T> *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<T> *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<T> *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<T> *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<T> *node = new EntityCacheNode<T>(id);
FetchJob *job = createFetchJob(id, scope);
job->setProperty("EntityCacheNode", QVariant::fromValue<typename T::Id>(id));
connect(job, SIGNAL(result(KJob*)), SLOT(processResult(KJob*)));
mCache.enqueue(node);
}
private:
EntityCacheNode<T> *cacheNodeForId(typename T::Id id) const
{
for (typename QQueue<EntityCacheNode<T> *>::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<typename T::Id>();
// Error handling?
if ( job->error() ) {
kWarning() << job->errorString();
}
EntityCacheNode<T> *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<T> *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<EntityCacheNode<T> *> mCache;
int mCapacity;
};
template<> inline void EntityCache<Collection, CollectionFetchJob, CollectionFetchScope>::extractResult(EntityCacheNode<Collection> *node, KJob *job) const
{
CollectionFetchJob *fetch = qobject_cast<CollectionFetchJob *>(job);
Q_ASSERT(fetch);
if (fetch->collections().isEmpty()) {
node->entity = Collection();
} else {
node->entity = fetch->collections().first();
}
}
template<> inline void EntityCache<Item, ItemFetchJob, ItemFetchScope>::extractResult(EntityCacheNode<Item> *node, KJob *job) const
{
ItemFetchJob *fetch = qobject_cast<ItemFetchJob *>(job);
Q_ASSERT(fetch);
if (fetch->items().isEmpty()) {
node->entity = Item();
} else {
node->entity = fetch->items().first();
}
}
template<> inline void EntityCache<Tag, TagFetchJob, TagFetchScope>::extractResult(EntityCacheNode<Tag> *node, KJob *job) const
{
TagFetchJob *fetch = qobject_cast<TagFetchJob *>(job);
Q_ASSERT(fetch);
if (fetch->tags().isEmpty()) {
node->entity = Tag();
} else {
node->entity = fetch->tags().first();
}
}
template<> inline CollectionFetchJob *EntityCache<Collection, CollectionFetchJob, CollectionFetchScope>::createFetchJob(Collection::Id id, const CollectionFetchScope &scope)
{
CollectionFetchJob *fetch = new CollectionFetchJob(Collection(id), CollectionFetchJob::Base, session);
fetch->setFetchScope(scope);
return fetch;
}
typedef EntityCache<Collection, CollectionFetchJob, CollectionFetchScope> CollectionCache;
typedef EntityCache<Item, ItemFetchJob, ItemFetchScope> ItemCache;
typedef EntityCache<Tag, TagFetchJob, TagFetchScope> TagCache;
template <typename T>
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<typename T, typename FetchJob, typename FetchScope_>
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<Entity::Id> &ids) const
{
typename T::List list;
foreach (Entity::Id id, ids) {
EntityListCacheNode<T> *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<Entity::Id> &ids, const FetchScope &scope)
{
QList<Entity::Id> toRequest;
bool result = true;
foreach (Entity::Id id, ids) {
EntityListCacheNode<T> *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<Entity::Id> &ids)
{
foreach (Entity::Id id, ids) {
EntityListCacheNode<T> *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<Entity::Id> &ids, const FetchScope &scope)
{
QList<Entity::Id> toRequest;
foreach (Entity::Id id, ids) {
EntityListCacheNode<T> *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<Entity::Id> &ids, const FetchScope &scope, const QList<Entity::Id> &preserveIds = QList<Entity::Id>())
{
Q_ASSERT(isNotRequested(ids));
shrinkCache(preserveIds);
foreach (Entity::Id id, ids) {
EntityListCacheNode<T> *node = new EntityListCacheNode<T>(id);
mCache.insert(id, node);
}
FetchJob *job = createFetchJob(ids, scope);
job->setProperty("EntityListCacheIds", QVariant::fromValue< QList<Entity::Id> >(ids));
connect(job, SIGNAL(result(KJob*)), SLOT(processResult(KJob*)));
}
bool isNotRequested(const QList<Entity::Id> &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<Entity::Id> &ids) const
{
foreach (Entity::Id id, ids) {
EntityListCacheNode<T> *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<Entity::Id> &preserveIds)
{
typename
QHash< Entity::Id, EntityListCacheNode<T> *>::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<Entity::Id> &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<Entity::Id> ids = job->property("EntityListCacheIds").value< QList<Entity::Id> >();
typename T::List entities;
extractResults(job, entities);
foreach (Entity::Id id, ids) {
EntityListCacheNode<T> *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<T> *> mCache;
int mCapacity;
};
template<> inline void EntityListCache<Collection, CollectionFetchJob, CollectionFetchScope>::extractResults(KJob *job, Collection::List &collections) const
{
CollectionFetchJob *fetch = qobject_cast<CollectionFetchJob *>(job);
Q_ASSERT(fetch);
collections = fetch->collections();
}
template<> inline void EntityListCache<Item, ItemFetchJob, ItemFetchScope>::extractResults(KJob *job, Item::List &items) const
{
ItemFetchJob *fetch = qobject_cast<ItemFetchJob *>(job);
Q_ASSERT(fetch);
items = fetch->items();
}
template<> inline void EntityListCache<Tag, TagFetchJob, TagFetchScope>::extractResults(KJob *job, Tag::List &tags) const
{
TagFetchJob *fetch = qobject_cast<TagFetchJob *>(job);
Q_ASSERT(fetch);
tags = fetch->tags();
}
template<>
inline CollectionFetchJob *EntityListCache<Collection, CollectionFetchJob, CollectionFetchScope>::createFetchJob(const QList<Entity::Id> &ids, const CollectionFetchScope &scope)
{
CollectionFetchJob *fetch = new CollectionFetchJob(ids, CollectionFetchJob::Base, session);
fetch->setFetchScope(scope);
return fetch;
}
typedef EntityListCache<Collection, CollectionFetchJob, CollectionFetchScope> CollectionListCache;
typedef EntityListCache<Item, ItemFetchJob, ItemFetchScope> ItemListCache;
typedef EntityListCache<Tag, TagFetchJob, TagFetchScope> TagListCache;
}
#endif