1/*
2 Copyright (c) 2009 Volker Krause <vkrause@kde.org>
3
4 This library is free software; you can redistribute it and/or modify it
5 under the terms of the GNU Library General Public License as published by
6 the Free Software Foundation; either version 2 of the License, or (at your
7 option) any later version.
8
9 This library is distributed in the hope that it will be useful, but WITHOUT
10 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11 FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public
12 License for more details.
13
14 You should have received a copy of the GNU Library General Public License
15 along with this library; see the file COPYING.LIB. If not, write to the
16 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
17 02110-1301, USA.
18*/
19
20#ifndef AKONADI_ENTITYCACHE_P_H
21#define AKONADI_ENTITYCACHE_P_H
22
23#include <akonadi/item.h>
24#include <akonadi/itemfetchjob.h>
25#include <akonadi/itemfetchscope.h>
26#include <akonadi/collection.h>
27#include <akonadi/collectionfetchjob.h>
28#include <akonadi/collectionfetchscope.h>
29#include <akonadi/tag.h>
30#include <akonadi/tagfetchjob.h>
31#include <akonadi/tagfetchscope.h>
32#include <akonadi/session.h>
33
34#include "akonadiprivate_export.h"
35
36#include <qobject.h>
37#include <QQueue>
38#include <QVariant>
39#include <QHash>
40#include <QtCore/QQueue>
41
42class Dummy;
43class KJob;
44
45typedef QList<Akonadi::Entity::Id> EntityIdList;
46Q_DECLARE_METATYPE(QList<Akonadi::Entity::Id>)
47
48namespace Akonadi {
49
50/**
51 @internal
52 QObject part of EntityCache.
53*/
54class AKONADI_TESTS_EXPORT EntityCacheBase : public QObject
55{
56 Q_OBJECT
57public:
58 explicit EntityCacheBase(Session *session, QObject *parent = 0);
59
60 void setSession(Session *session);
61
62protected:
63 Session *session;
64
65Q_SIGNALS:
66 void dataAvailable();
67
68private Q_SLOTS:
69 virtual void processResult(KJob *job) = 0;
70};
71
72template <typename T>
73struct EntityCacheNode
74{
75 EntityCacheNode()
76 : pending(false)
77 , invalid(false)
78 {
79 }
80 EntityCacheNode(typename T::Id id)
81 : entity(T(id))
82 , pending(true)
83 , invalid(false)
84 {
85 }
86 T entity;
87 bool pending;
88 bool invalid;
89};
90
91/**
92 * @internal
93 * A in-memory FIFO cache for a small amount of Entity objects.
94 */
95template<typename T, typename FetchJob, typename FetchScope_>
96class EntityCache : public EntityCacheBase
97{
98public:
99 typedef FetchScope_ FetchScope;
100 explicit EntityCache(int maxCapacity, Session *session = 0, QObject *parent = 0)
101 : EntityCacheBase(session, parent)
102 , mCapacity(maxCapacity)
103 {
104 }
105
106 ~EntityCache()
107 {
108 qDeleteAll(mCache);
109 }
110
111 /** Object is available in the cache and can be retrieved. */
112 bool isCached(typename T::Id id) const
113 {
114 EntityCacheNode<T> *node = cacheNodeForId(id);
115 return node && !node->pending;
116 }
117
118 /** Object has been requested but is not yet loaded into the cache or is already available. */
119 bool isRequested(typename T::Id id) const
120 {
121 return cacheNodeForId(id);
122 }
123
124 /** Returns the cached object if available, an empty instance otherwise. */
125 virtual T retrieve(typename T::Id id) const
126 {
127 EntityCacheNode<T> *node = cacheNodeForId(id);
128 if (node && !node->pending && !node->invalid) {
129 return node->entity;
130 }
131 return T();
132 }
133
134 /** Marks the cache entry as invalid, use in case the object has been deleted on the server. */
135 void invalidate(typename T::Id id)
136 {
137 EntityCacheNode<T> *node = cacheNodeForId(id);
138 if (node) {
139 node->invalid = true;
140 }
141 }
142
143 /** Triggers a re-fetching of a cache entry, use if it has changed on the server. */
144 void update(typename T::Id id, const FetchScope &scope)
145 {
146 EntityCacheNode<T> *node = cacheNodeForId(id);
147 if (node) {
148 mCache.removeAll(node);
149 if (node->pending) {
150 request(id, scope);
151 }
152 delete node;
153 }
154 }
155
156 /** Requests the object to be cached if it is not yet in the cache. @returns @c true if it was in the cache already. */
157 virtual bool ensureCached(typename T::Id id, const FetchScope &scope)
158 {
159 EntityCacheNode<T> *node = cacheNodeForId(id);
160 if (!node) {
161 request(id, scope);
162 return false;
163 }
164 return !node->pending;
165 }
166
167 /**
168 Asks the cache to retrieve @p id. @p request is used as
169 a token to indicate which request has been finished in the
170 dataAvailable() signal.
171 */
172 virtual void request(typename T::Id id, const FetchScope &scope)
173 {
174 Q_ASSERT(!isRequested(id));
175 shrinkCache();
176 EntityCacheNode<T> *node = new EntityCacheNode<T>(id);
177 FetchJob *job = createFetchJob(id, scope);
178 job->setProperty("EntityCacheNode", QVariant::fromValue<typename T::Id>(id));
179 connect(job, SIGNAL(result(KJob*)), SLOT(processResult(KJob*)));
180 mCache.enqueue(node);
181 }
182
183private:
184 EntityCacheNode<T> *cacheNodeForId(typename T::Id id) const
185 {
186 for (typename QQueue<EntityCacheNode<T> *>::const_iterator it = mCache.constBegin(), endIt = mCache.constEnd();
187 it != endIt; ++it) {
188 if ((*it)->entity.id() == id) {
189 return *it;
190 }
191 }
192 return 0;
193 }
194
195 void processResult(KJob *job)
196 {
197 // Error handling?
198 typename T::Id id = job->property("EntityCacheNode").template value<typename T::Id>();
199 EntityCacheNode<T> *node = cacheNodeForId(id);
200 if (!node) {
201 return; // got replaced in the meantime
202 }
203
204 node->pending = false;
205 extractResult(node, job);
206 // make sure we find this node again if something went wrong here,
207 // most likely the object got deleted from the server in the meantime
208 if (node->entity.id() != id) {
209 // TODO: Recursion guard? If this is called with non-existing ids, the if will never be true!
210 node->entity.setId(id);
211 node->invalid = true;
212 }
213 emit dataAvailable();
214 }
215
216 void extractResult(EntityCacheNode<T> *node, KJob *job) const;
217
218 inline FetchJob *createFetchJob(typename T::Id id, const FetchScope &scope)
219 {
220 FetchJob *fetch = new FetchJob(T(id), session);
221 fetch->setFetchScope(scope);
222 return fetch;
223 }
224
225 /** Tries to reduce the cache size until at least one more object fits in. */
226 void shrinkCache()
227 {
228 while (mCache.size() >= mCapacity && !mCache.first()->pending) {
229 delete mCache.dequeue();
230 }
231 }
232
233private:
234 QQueue<EntityCacheNode<T> *> mCache;
235 int mCapacity;
236};
237
238template<> inline void EntityCache<Collection, CollectionFetchJob, CollectionFetchScope>::extractResult(EntityCacheNode<Collection> *node, KJob *job) const
239{
240 CollectionFetchJob *fetch = qobject_cast<CollectionFetchJob *>(job);
241 Q_ASSERT(fetch);
242 if (fetch->collections().isEmpty()) {
243 node->entity = Collection();
244 } else {
245 node->entity = fetch->collections().first();
246 }
247}
248
249template<> inline void EntityCache<Item, ItemFetchJob, ItemFetchScope>::extractResult(EntityCacheNode<Item> *node, KJob *job) const
250{
251 ItemFetchJob *fetch = qobject_cast<ItemFetchJob *>(job);
252 Q_ASSERT(fetch);
253 if (fetch->items().isEmpty()) {
254 node->entity = Item();
255 } else {
256 node->entity = fetch->items().first();
257 }
258}
259
260template<> inline void EntityCache<Tag, TagFetchJob, TagFetchScope>::extractResult(EntityCacheNode<Tag> *node, KJob *job) const
261{
262 TagFetchJob *fetch = qobject_cast<TagFetchJob *>(job);
263 Q_ASSERT(fetch);
264 if (fetch->tags().isEmpty()) {
265 node->entity = Tag();
266 } else {
267 node->entity = fetch->tags().first();
268 }
269}
270
271template<> inline CollectionFetchJob *EntityCache<Collection, CollectionFetchJob, CollectionFetchScope>::createFetchJob(Collection::Id id, const CollectionFetchScope &scope)
272{
273 CollectionFetchJob *fetch = new CollectionFetchJob(Collection(id), CollectionFetchJob::Base, session);
274 fetch->setFetchScope(scope);
275 return fetch;
276}
277
278typedef EntityCache<Collection, CollectionFetchJob, CollectionFetchScope> CollectionCache;
279typedef EntityCache<Item, ItemFetchJob, ItemFetchScope> ItemCache;
280typedef EntityCache<Tag, TagFetchJob, TagFetchScope> TagCache;
281
282template <typename T>
283struct EntityListCacheNode
284{
285 EntityListCacheNode()
286 : pending(false)
287 , invalid(false)
288 {
289 }
290 EntityListCacheNode(typename T::Id id)
291 : entity(id)
292 , pending(true)
293 , invalid(false)
294 {
295 }
296
297 T entity;
298 bool pending;
299 bool invalid;
300};
301
302template<typename T, typename FetchJob, typename FetchScope_>
303class EntityListCache : public EntityCacheBase
304{
305public:
306 typedef FetchScope_ FetchScope;
307
308 explicit EntityListCache(int maxCapacity, Session *session = 0, QObject *parent = 0)
309 : EntityCacheBase(session, parent)
310 , mCapacity(maxCapacity)
311 {
312 }
313
314 ~EntityListCache()
315 {
316 qDeleteAll(mCache);
317 }
318
319 /** Returns the cached object if available, an empty instance otherwise. */
320 typename T::List retrieve(const QList<Entity::Id> &ids) const
321 {
322 typename T::List list;
323
324 foreach (Entity::Id id, ids) {
325 EntityListCacheNode<T> *node = mCache.value(id);
326 if (!node || node->pending || node->invalid) {
327 return typename T::List();
328 }
329
330 list << node->entity;
331 }
332
333 return list;
334 }
335
336 /** Requests the object to be cached if it is not yet in the cache. @returns @c true if it was in the cache already. */
337 bool ensureCached(const QList<Entity::Id> &ids, const FetchScope &scope)
338 {
339 QList<Entity::Id> toRequest;
340 bool result = true;
341
342 foreach (Entity::Id id, ids) {
343 EntityListCacheNode<T> *node = mCache.value(id);
344 if (!node) {
345 toRequest << id;
346 continue;
347 }
348
349 if (node->pending) {
350 result = false;
351 }
352 }
353
354 if (!toRequest.isEmpty()) {
355 request(toRequest, scope, ids);
356 return false;
357 }
358
359 return result;
360 }
361
362 /** Marks the cache entry as invalid, use in case the object has been deleted on the server. */
363 void invalidate(const QList<Entity::Id> &ids)
364 {
365 foreach (Entity::Id id, ids) {
366 EntityListCacheNode<T> *node = mCache.value(id);
367 if (node) {
368 node->invalid = true;
369 }
370 }
371 }
372
373 /** Triggers a re-fetching of a cache entry, use if it has changed on the server. */
374 void update(const QList<Entity::Id> &ids, const FetchScope &scope)
375 {
376 QList<Entity::Id> toRequest;
377
378 foreach (Entity::Id id, ids) {
379 EntityListCacheNode<T> *node = mCache.value(id);
380 if (node) {
381 mCache.remove(id);
382 if (node->pending) {
383 toRequest << id;
384 }
385 delete node;
386 }
387 }
388
389 if (!toRequest.isEmpty()) {
390 request(toRequest, scope);
391 }
392 }
393
394 /**
395 Asks the cache to retrieve @p id. @p request is used as
396 a token to indicate which request has been finished in the
397 dataAvailable() signal.
398 */
399 void request(const QList<Entity::Id> &ids, const FetchScope &scope, const QList<Entity::Id> &preserveIds = QList<Entity::Id>())
400 {
401 Q_ASSERT(isNotRequested(ids));
402 shrinkCache(preserveIds);
403 foreach (Entity::Id id, ids) {
404 EntityListCacheNode<T> *node = new EntityListCacheNode<T>(id);
405 mCache.insert(id, node);
406 }
407 FetchJob *job = createFetchJob(ids, scope);
408 job->setProperty("EntityListCacheIds", QVariant::fromValue< QList<Entity::Id> >(ids));
409 connect(job, SIGNAL(result(KJob*)), SLOT(processResult(KJob*)));
410 }
411
412 bool isNotRequested(const QList<Entity::Id> &ids) const
413 {
414 foreach (Entity::Id id, ids) {
415 if (mCache.contains(id)) {
416 return false;
417 }
418 }
419
420 return true;
421 }
422
423 /** Object is available in the cache and can be retrieved. */
424 bool isCached(const QList<Entity::Id> &ids) const
425 {
426 foreach (Entity::Id id, ids) {
427 EntityListCacheNode<T> *node = mCache.value(id);
428 if (!node || node->pending) {
429 return false;
430 }
431 }
432 return true;
433 }
434
435private:
436 /** Tries to reduce the cache size until at least one more object fits in. */
437 void shrinkCache(const QList<Entity::Id> &preserveIds)
438 {
439 typename
440 QHash< Entity::Id, EntityListCacheNode<T> *>::Iterator iter = mCache.begin();
441 while (iter != mCache.end() && mCache.size() >= mCapacity) {
442 if (iter.value()->pending || preserveIds.contains(iter.key())) {
443 ++iter;
444 continue;
445 }
446
447 delete iter.value();
448 iter = mCache.erase(iter);
449 }
450 }
451
452 inline FetchJob *createFetchJob(const QList<Entity::Id> &ids, const FetchScope &scope)
453 {
454 FetchJob *job = new FetchJob(ids, session);
455 job->setFetchScope(scope);
456 return job;
457 }
458
459 void processResult(KJob *job)
460 {
461 const QList<Entity::Id> ids = job->property("EntityListCacheIds").value< QList<Entity::Id> >();
462
463 typename T::List entities;
464 extractResults(job, entities);
465
466 foreach (Entity::Id id, ids) {
467 EntityListCacheNode<T> *node = mCache.value(id);
468 if (!node) {
469 continue; // got replaced in the meantime
470 }
471
472 node->pending = false;
473
474 T result;
475 typename T::List::Iterator iter = entities.begin();
476 for (; iter != entities.end(); ++iter) {
477 if ((*iter).id() == id) {
478 result = *iter;
479 entities.erase(iter);
480 break;
481 }
482 }
483
484 // make sure we find this node again if something went wrong here,
485 // most likely the object got deleted from the server in the meantime
486 if (!result.isValid()) {
487 node->entity = T(id);
488 node->invalid = true;
489 } else {
490 node->entity = result;
491 }
492 }
493
494 emit dataAvailable();
495 }
496
497 void extractResults(KJob *job, typename T::List &entities) const;
498
499private:
500 QHash< Entity::Id, EntityListCacheNode<T> *> mCache;
501 int mCapacity;
502};
503
504template<> inline void EntityListCache<Collection, CollectionFetchJob, CollectionFetchScope>::extractResults(KJob *job, Collection::List &collections) const
505{
506 CollectionFetchJob *fetch = qobject_cast<CollectionFetchJob *>(job);
507 Q_ASSERT(fetch);
508 collections = fetch->collections();
509}
510
511template<> inline void EntityListCache<Item, ItemFetchJob, ItemFetchScope>::extractResults(KJob *job, Item::List &items) const
512{
513 ItemFetchJob *fetch = qobject_cast<ItemFetchJob *>(job);
514 Q_ASSERT(fetch);
515 items = fetch->items();
516}
517
518template<> inline void EntityListCache<Tag, TagFetchJob, TagFetchScope>::extractResults(KJob *job, Tag::List &tags) const
519{
520 TagFetchJob *fetch = qobject_cast<TagFetchJob *>(job);
521 Q_ASSERT(fetch);
522 tags = fetch->tags();
523}
524
525template<>
526inline CollectionFetchJob *EntityListCache<Collection, CollectionFetchJob, CollectionFetchScope>::createFetchJob(const QList<Entity::Id> &ids, const CollectionFetchScope &scope)
527{
528 CollectionFetchJob *fetch = new CollectionFetchJob(ids, CollectionFetchJob::Base, session);
529 fetch->setFetchScope(scope);
530 return fetch;
531}
532
533typedef EntityListCache<Collection, CollectionFetchJob, CollectionFetchScope> CollectionListCache;
534typedef EntityListCache<Item, ItemFetchJob, ItemFetchScope> ItemListCache;
535typedef EntityListCache<Tag, TagFetchJob, TagFetchScope> TagListCache;
536
537}
538
539#endif
540