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 | |
42 | class Dummy; |
43 | class KJob; |
44 | |
45 | typedef QList<Akonadi::Entity::Id> EntityIdList; |
46 | Q_DECLARE_METATYPE(QList<Akonadi::Entity::Id>) |
47 | |
48 | namespace Akonadi { |
49 | |
50 | /** |
51 | @internal |
52 | QObject part of EntityCache. |
53 | */ |
54 | class AKONADI_TESTS_EXPORT EntityCacheBase : public QObject |
55 | { |
56 | Q_OBJECT |
57 | public: |
58 | explicit EntityCacheBase(Session *session, QObject *parent = 0); |
59 | |
60 | void setSession(Session *session); |
61 | |
62 | protected: |
63 | Session *session; |
64 | |
65 | Q_SIGNALS: |
66 | void dataAvailable(); |
67 | |
68 | private Q_SLOTS: |
69 | virtual void processResult(KJob *job) = 0; |
70 | }; |
71 | |
72 | template <typename T> |
73 | struct 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 | */ |
95 | template<typename T, typename FetchJob, typename FetchScope_> |
96 | class EntityCache : public EntityCacheBase |
97 | { |
98 | public: |
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 | |
183 | private: |
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 (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 | |
233 | private: |
234 | QQueue<EntityCacheNode<T> *> mCache; |
235 | int mCapacity; |
236 | }; |
237 | |
238 | template<> inline void EntityCache<Collection, CollectionFetchJob, CollectionFetchScope>::(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 | |
249 | template<> inline void EntityCache<Item, ItemFetchJob, ItemFetchScope>::(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 | |
260 | template<> inline void EntityCache<Tag, TagFetchJob, TagFetchScope>::(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 | |
271 | template<> 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 | |
278 | typedef EntityCache<Collection, CollectionFetchJob, CollectionFetchScope> CollectionCache; |
279 | typedef EntityCache<Item, ItemFetchJob, ItemFetchScope> ItemCache; |
280 | typedef EntityCache<Tag, TagFetchJob, TagFetchScope> TagCache; |
281 | |
282 | template <typename T> |
283 | struct 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 | |
302 | template<typename T, typename FetchJob, typename FetchScope_> |
303 | class EntityListCache : public EntityCacheBase |
304 | { |
305 | public: |
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 | |
435 | private: |
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 (KJob *job, typename T::List &entities) const; |
498 | |
499 | private: |
500 | QHash< Entity::Id, EntityListCacheNode<T> *> mCache; |
501 | int mCapacity; |
502 | }; |
503 | |
504 | template<> inline void EntityListCache<Collection, CollectionFetchJob, CollectionFetchScope>::(KJob *job, Collection::List &collections) const |
505 | { |
506 | CollectionFetchJob *fetch = qobject_cast<CollectionFetchJob *>(job); |
507 | Q_ASSERT(fetch); |
508 | collections = fetch->collections(); |
509 | } |
510 | |
511 | template<> inline void EntityListCache<Item, ItemFetchJob, ItemFetchScope>::(KJob *job, Item::List &items) const |
512 | { |
513 | ItemFetchJob *fetch = qobject_cast<ItemFetchJob *>(job); |
514 | Q_ASSERT(fetch); |
515 | items = fetch->items(); |
516 | } |
517 | |
518 | template<> inline void EntityListCache<Tag, TagFetchJob, TagFetchScope>::(KJob *job, Tag::List &tags) const |
519 | { |
520 | TagFetchJob *fetch = qobject_cast<TagFetchJob *>(job); |
521 | Q_ASSERT(fetch); |
522 | tags = fetch->tags(); |
523 | } |
524 | |
525 | template<> |
526 | inline 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 | |
533 | typedef EntityListCache<Collection, CollectionFetchJob, CollectionFetchScope> CollectionListCache; |
534 | typedef EntityListCache<Item, ItemFetchJob, ItemFetchScope> ItemListCache; |
535 | typedef EntityListCache<Tag, TagFetchJob, TagFetchScope> TagListCache; |
536 | |
537 | } |
538 | |
539 | #endif |
540 | |