diff --git a/src/akonadi/akonadicachingstorage.cpp b/src/akonadi/akonadicachingstorage.cpp --- a/src/akonadi/akonadicachingstorage.cpp +++ b/src/akonadi/akonadicachingstorage.cpp @@ -26,8 +26,134 @@ #include "akonadicachingstorage.h" #include "akonadistorage.h" +#include "akonadicollectionfetchjobinterface.h" + +#include + using namespace Akonadi; +class CachingCollectionFetchJob : public KCompositeJob, public CollectionFetchJobInterface +{ + Q_OBJECT +public: + CachingCollectionFetchJob(const StorageInterface::Ptr &storage, + const Cache::Ptr &cache, + const Collection &collection, + StorageInterface::FetchDepth depth, + StorageInterface::FetchContentTypes types, + QObject *parent = nullptr) + : KCompositeJob(parent), + m_started(false), + m_storage(storage), + m_cache(cache), + m_collection(collection), + m_depth(depth), + m_types(types) + { + QTimer::singleShot(0, this, &CachingCollectionFetchJob::start); + } + + void start() override + { + if (m_started) + return; + + if (m_cache->isContentTypesPopulated(m_types)) { + QTimer::singleShot(0, this, &CachingCollectionFetchJob::retrieveFromCache); + } else { + auto job = m_storage->fetchCollections(Akonadi::Collection::root(), + Akonadi::StorageInterface::Recursive, + m_types); + job->setResource(m_resource); + addSubjob(job->kjob()); + } + + m_started = true; + } + + + Collection::List collections() const override + { + const auto isInputCollection = [this] (const Collection &collection) { + return collection.id() == m_collection.id() + || (!m_collection.remoteId().isEmpty() && collection.remoteId() == m_collection.remoteId()); + }; + + if (m_depth == StorageInterface::Base) { + auto it = std::find_if(m_collections.cbegin(), m_collections.cend(), isInputCollection); + if (it != m_collections.cend()) + return Collection::List() << *it; + else + return Collection::List(); + } + + auto collections = m_collections; + auto it = collections.begin(); + + if (m_depth == StorageInterface::FirstLevel) { + it = std::remove_if(collections.begin(), collections.end(), + [isInputCollection] (const Collection &collection) { + return !isInputCollection(collection.parentCollection()); + }); + } else { + it = std::remove_if(collections.begin(), collections.end(), + [isInputCollection] (const Collection &collection) { + auto parent = collection.parentCollection(); + while (parent.isValid() && !isInputCollection(parent)) + parent = parent.parentCollection(); + return !isInputCollection(parent); + }); + } + + collections.erase(it, collections.end()); + return collections; + } + + void setResource(const QString &resource) override + { + m_resource = resource; + } + +private: + void slotResult(KJob *kjob) override + { + if (kjob->error()) + return; + + auto job = dynamic_cast(kjob); + Q_ASSERT(job); + auto cachedCollections = job->collections(); + for (const auto &collection : job->collections()) { + auto parent = collection.parentCollection(); + while (parent.isValid() && parent != Akonadi::Collection::root()) { + if (!cachedCollections.contains(parent)) { + cachedCollections.append(parent); + } + parent = parent.parentCollection(); + } + } + m_cache->setCollections(m_types, cachedCollections); + m_collections = job->collections(); + emitResult(); + } + + void retrieveFromCache() + { + m_collections = m_cache->collections(m_types); + emitResult(); + } + + bool m_started; + StorageInterface::Ptr m_storage; + Cache::Ptr m_cache; + QString m_resource; + const Collection m_collection; + const StorageInterface::FetchDepth m_depth; + const StorageInterface::FetchContentTypes m_types; + Collection::List m_collections; +}; + + CachingStorage::CachingStorage(const Cache::Ptr &cache, const StorageInterface::Ptr &storage) : m_cache(cache), m_storage(storage) @@ -115,7 +241,7 @@ CollectionFetchJobInterface *CachingStorage::fetchCollections(Collection collection, StorageInterface::FetchDepth depth, FetchContentTypes types) { - return m_storage->fetchCollections(collection, depth, types); + return new CachingCollectionFetchJob(m_storage, m_cache, collection, depth, types); } ItemFetchJobInterface *CachingStorage::fetchItems(Collection collection) @@ -137,3 +263,5 @@ { return m_storage->fetchTags(); } + +#include "akonadicachingstorage.moc" diff --git a/tests/units/akonadi/akonadicachingstoragetest.cpp b/tests/units/akonadi/akonadicachingstoragetest.cpp --- a/tests/units/akonadi/akonadicachingstoragetest.cpp +++ b/tests/units/akonadi/akonadicachingstoragetest.cpp @@ -26,19 +26,223 @@ #include "akonadi/akonadicachingstorage.h" #include "akonadi/akonadiserializer.h" +#include "akonadi/akonadicollectionfetchjobinterface.h" +#include "akonadi/akonadiitemfetchjobinterface.h" +#include "akonadi/akonaditagfetchjobinterface.h" + #include "testlib/akonadifakedata.h" #include "testlib/gencollection.h" #include "testlib/gentodo.h" #include "testlib/gentag.h" #include "testlib/testhelpers.h" +Q_DECLARE_METATYPE(Akonadi::StorageInterface::FetchDepth) + using namespace Testlib; class AkonadiCachingStorageTest : public QObject { Q_OBJECT +public: + explicit AkonadiCachingStorageTest(QObject *parent = nullptr) + : QObject(parent) + { + qRegisterMetaType(); + qRegisterMetaType(); + qRegisterMetaType(); + } + + QStringList onlyWithSuffix(const QStringList &list, const QString &suffix) + { + return onlyWithSuffixes(list, {suffix}); + } + + QStringList onlyWithSuffixes(const QStringList &list, const QStringList &suffixes) + { + auto res = QStringList(); + std::copy_if(list.cbegin(), list.cend(), + std::back_inserter(res), + [suffixes](const QString &entry) { + for (const auto &suffix : suffixes) { + if (entry.endsWith(suffix)) + return true; + } + return false; + }); + return res; + } private slots: + void shouldCacheAllCollectionsPerFetchType_data() + { + QTest::addColumn("rootCollection"); + QTest::addColumn("fetchDepth"); + QTest::addColumn("contentTypesInt"); + QTest::addColumn("expectedFetchNames"); + QTest::addColumn("expectedCachedNames"); + + const auto allCollections = QStringList() << "42Task" << "43Task" << "44Note" << "45Stuff" + << "46Note" << "47Task" << "48Note" << "49Stuff" + << "50Stuff" << "51Task" << "52Note" << "53Stuff" + << "54Task" << "55Task" << "56Task" + << "57Note" << "58Note" << "59Note" + << "60Stuff" << "61Stuff" << "62Stuff"; + + const auto noteCollections = QStringList() << "42Task" << "44Note" + << "46Note" << "48Note" + << "50Stuff" << "52Note" + << "57Note" << "58Note" << "59Note"; + + const auto taskCollections = QStringList() << "42Task" << "43Task" + << "46Note" << "47Task" + << "50Stuff" << "51Task" + << "54Task" << "55Task" << "56Task"; + + const auto noteTaskCollections = QStringList() << "42Task" << "43Task" << "44Note" + << "46Note" << "47Task" << "48Note" + << "50Stuff" << "51Task" << "52Note" + << "54Task" << "55Task" << "56Task" + << "57Note" << "58Note" << "59Note"; + + QTest::newRow("rootRecursiveAll") << Akonadi::Collection::root() << Akonadi::StorageInterface::Recursive << int(Akonadi::StorageInterface::AllContent) + << allCollections << allCollections; + QTest::newRow("rootRecursiveTask") << Akonadi::Collection::root() << Akonadi::StorageInterface::Recursive << int(Akonadi::StorageInterface::Tasks) + << onlyWithSuffix(taskCollections, "Task") << taskCollections; + QTest::newRow("rootRecursiveNote") << Akonadi::Collection::root() << Akonadi::StorageInterface::Recursive << int(Akonadi::StorageInterface::Notes) + << onlyWithSuffix(noteCollections, "Note") << noteCollections; + QTest::newRow("rootRecursiveNoteTask") << Akonadi::Collection::root() << Akonadi::StorageInterface::Recursive + << int(Akonadi::StorageInterface::Notes|Akonadi::StorageInterface::Tasks) + << onlyWithSuffixes(noteTaskCollections, {"Task", "Note"}) << noteTaskCollections; + + QTest::newRow("60RecursiveAll") << Akonadi::Collection(60) << Akonadi::StorageInterface::Recursive << int(Akonadi::StorageInterface::AllContent) + << (QStringList() << "61Stuff" << "62Stuff") << allCollections; + QTest::newRow("54RecursiveTask") << Akonadi::Collection(54) << Akonadi::StorageInterface::Recursive << int(Akonadi::StorageInterface::Tasks) + << (QStringList() << "55Task" << "56Task") << taskCollections; + QTest::newRow("57RecursiveNote") << Akonadi::Collection(57) << Akonadi::StorageInterface::Recursive << int(Akonadi::StorageInterface::Notes) + << (QStringList() << "58Note" << "59Note") << noteCollections; + QTest::newRow("54RecursiveNoteTask") << Akonadi::Collection(54) << Akonadi::StorageInterface::Recursive + << int(Akonadi::StorageInterface::Notes|Akonadi::StorageInterface::Tasks) + << (QStringList() << "55Task" << "56Task") << noteTaskCollections; + QTest::newRow("57RecursiveNoteTask") << Akonadi::Collection(57) << Akonadi::StorageInterface::Recursive + << int(Akonadi::StorageInterface::Notes|Akonadi::StorageInterface::Tasks) + << (QStringList() << "58Note" << "59Note") << noteTaskCollections; + + QTest::newRow("60FirstLevelAll") << Akonadi::Collection(60) << Akonadi::StorageInterface::FirstLevel << int(Akonadi::StorageInterface::AllContent) + << (QStringList() << "61Stuff") << allCollections; + QTest::newRow("54FirstLevelTask") << Akonadi::Collection(54) << Akonadi::StorageInterface::FirstLevel << int(Akonadi::StorageInterface::Tasks) + << (QStringList() << "55Task") << taskCollections; + QTest::newRow("57FirstLevelNote") << Akonadi::Collection(57) << Akonadi::StorageInterface::FirstLevel << int(Akonadi::StorageInterface::Notes) + << (QStringList() << "58Note") << noteCollections; + QTest::newRow("54FirstLevelNoteTask") << Akonadi::Collection(54) << Akonadi::StorageInterface::FirstLevel + << int(Akonadi::StorageInterface::Notes|Akonadi::StorageInterface::Tasks) + << (QStringList() << "55Task") << noteTaskCollections; + QTest::newRow("57FirstLevelNoteTask") << Akonadi::Collection(57) << Akonadi::StorageInterface::FirstLevel + << int(Akonadi::StorageInterface::Notes|Akonadi::StorageInterface::Tasks) + << (QStringList() << "58Note") << noteTaskCollections; + + QTest::newRow("60BaseAll") << Akonadi::Collection(60) << Akonadi::StorageInterface::Base << int(Akonadi::StorageInterface::AllContent) + << (QStringList() << "60Stuff") << allCollections; + QTest::newRow("54BaseTask") << Akonadi::Collection(54) << Akonadi::StorageInterface::Base << int(Akonadi::StorageInterface::Tasks) + << (QStringList() << "54Task") << taskCollections; + QTest::newRow("57BaseNote") << Akonadi::Collection(57) << Akonadi::StorageInterface::Base << int(Akonadi::StorageInterface::Notes) + << (QStringList() << "57Note") << noteCollections; + QTest::newRow("54BaseNoteTask") << Akonadi::Collection(54) << Akonadi::StorageInterface::Base + << int(Akonadi::StorageInterface::Notes|Akonadi::StorageInterface::Tasks) + << (QStringList() << "54Task") << noteTaskCollections; + QTest::newRow("57BaseNoteTask") << Akonadi::Collection(57) << Akonadi::StorageInterface::Base + << int(Akonadi::StorageInterface::Notes|Akonadi::StorageInterface::Tasks) + << (QStringList() << "57Note") << noteTaskCollections; + } + + void shouldCacheAllCollectionsPerFetchType() + { + // GIVEN + AkonadiFakeData data; + + data.createCollection(GenCollection().withId(42).withName(QStringLiteral("42Task")).withRootAsParent().withTaskContent()); + data.createCollection(GenCollection().withId(43).withName(QStringLiteral("43Task")).withParent(42).withTaskContent()); + data.createCollection(GenCollection().withId(44).withName(QStringLiteral("44Note")).withParent(42).withNoteContent()); + data.createCollection(GenCollection().withId(45).withName(QStringLiteral("45Stuff")).withParent(42)); + + data.createCollection(GenCollection().withId(46).withName(QStringLiteral("46Note")).withRootAsParent().withNoteContent()); + data.createCollection(GenCollection().withId(47).withName(QStringLiteral("47Task")).withParent(46).withTaskContent()); + data.createCollection(GenCollection().withId(48).withName(QStringLiteral("48Note")).withParent(46).withNoteContent()); + data.createCollection(GenCollection().withId(49).withName(QStringLiteral("49Stuff")).withParent(46)); + + data.createCollection(GenCollection().withId(50).withName(QStringLiteral("50Stuff")).withRootAsParent()); + data.createCollection(GenCollection().withId(51).withName(QStringLiteral("51Task")).withParent(50).withTaskContent()); + data.createCollection(GenCollection().withId(52).withName(QStringLiteral("52Note")).withParent(50).withNoteContent()); + data.createCollection(GenCollection().withId(53).withName(QStringLiteral("53Stuff")).withParent(50)); + + data.createCollection(GenCollection().withId(54).withName(QStringLiteral("54Task")).withRootAsParent().withTaskContent()); + data.createCollection(GenCollection().withId(55).withName(QStringLiteral("55Task")).withParent(54).withTaskContent()); + data.createCollection(GenCollection().withId(56).withName(QStringLiteral("56Task")).withParent(55).withTaskContent()); + + data.createCollection(GenCollection().withId(57).withName(QStringLiteral("57Note")).withRootAsParent().withNoteContent()); + data.createCollection(GenCollection().withId(58).withName(QStringLiteral("58Note")).withParent(57).withNoteContent()); + data.createCollection(GenCollection().withId(59).withName(QStringLiteral("59Note")).withParent(58).withNoteContent()); + + data.createCollection(GenCollection().withId(60).withName(QStringLiteral("60Stuff")).withRootAsParent()); + data.createCollection(GenCollection().withId(61).withName(QStringLiteral("61Stuff")).withParent(60)); + data.createCollection(GenCollection().withId(62).withName(QStringLiteral("62Stuff")).withParent(61)); + + auto cache = Akonadi::Cache::Ptr::create(Akonadi::SerializerInterface::Ptr(new Akonadi::Serializer), + Akonadi::MonitorInterface::Ptr(data.createMonitor())); + Akonadi::CachingStorage storage(cache, Akonadi::StorageInterface::Ptr(data.createStorage())); + + // WHEN + QFETCH(Akonadi::Collection, rootCollection); + QFETCH(Akonadi::StorageInterface::FetchDepth, fetchDepth); + QFETCH(int, contentTypesInt); + const auto contentTypes = Akonadi::StorageInterface::FetchContentTypes(contentTypesInt); + auto job = storage.fetchCollections(rootCollection, fetchDepth, contentTypes); + QVERIFY2(job->kjob()->exec(), job->kjob()->errorString().toUtf8().constData()); + + // THEN + const auto toCollectionNames = [](const Akonadi::Collection::List &collections) { + auto res = QStringList(); + std::transform(collections.cbegin(), collections.cend(), + std::back_inserter(res), + std::mem_fn(&Akonadi::Collection::name)); + res.sort(); + return res; + }; + + QFETCH(QStringList, expectedFetchNames); + QFETCH(QStringList, expectedCachedNames); + + { + const auto collectionFetchNames = [job, toCollectionNames]{ + return toCollectionNames(job->collections()); + }(); + QCOMPARE(collectionFetchNames, expectedFetchNames); + + const auto collectionCachedNames = [cache, toCollectionNames]{ + const auto collections = cache->collections(Akonadi::StorageInterface::AllContent); + return toCollectionNames(collections); + }(); + QCOMPARE(collectionCachedNames, expectedCachedNames); + } + + // WHEN (second time shouldn't hit the original storage) + data.storageBehavior().setFetchCollectionsBehavior(rootCollection.id(), AkonadiFakeStorageBehavior::EmptyFetch); + data.storageBehavior().setFetchCollectionsErrorCode(rootCollection.id(), 128); + job = storage.fetchCollections(rootCollection, fetchDepth, contentTypes); + QVERIFY2(job->kjob()->exec(), job->kjob()->errorString().toUtf8().constData()); + + { + const auto collectionFetchNames = [job, toCollectionNames]{ + return toCollectionNames(job->collections()); + }(); + QCOMPARE(collectionFetchNames, expectedFetchNames); + + const auto collectionCachedNames = [cache, toCollectionNames]{ + const auto collections = cache->collections(Akonadi::StorageInterface::AllContent); + return toCollectionNames(collections); + }(); + QCOMPARE(collectionCachedNames, expectedCachedNames); + } + } }; ZANSHIN_TEST_MAIN(AkonadiCachingStorageTest)