From 10d19014fe2c9c02f2bc3e19732cbe340e316076 Mon Sep 17 00:00:00 2001 From: Christian Mollekopf Date: Tue, 10 Nov 2015 12:05:39 +0100 Subject: A result model The result model drives the data retrieval and provides the interace for consumers --- common/modelresult.h | 180 ++++++++++++++++++++++++++++++++++++++++++++++++ tests/CMakeLists.txt | 2 + tests/clientapitest.cpp | 117 ++++++++++++++++++++++++------- tests/querytest.cpp | 100 +++++++++++++++++++++++++++ 4 files changed, 376 insertions(+), 23 deletions(-) create mode 100644 common/modelresult.h create mode 100644 tests/querytest.cpp diff --git a/common/modelresult.h b/common/modelresult.h new file mode 100644 index 0000000..c23c41e --- /dev/null +++ b/common/modelresult.h @@ -0,0 +1,180 @@ +/* + * Copyright (C) 2014 Christian Mollekopf + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) version 3, or any + * later version accepted by the membership of KDE e.V. (or its + * successor approved by the membership of KDE e.V.), which shall + * act as a proxy defined in Section 6 of version 3 of the license. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + */ + +#pragma once + +#include +#include +#include +#include "query.h" +#include "clientapi.h" + +#include "resultprovider.h" + +template +class ModelResult : public QAbstractItemModel +{ +public: + + enum Roles { + DomainObjectRole = Qt::UserRole + 1 + }; + + ModelResult(const Akonadi2::Query &query, const QList &propertyColumns) + :QAbstractItemModel(), + mPropertyColumns(propertyColumns) + { + } + + static qint64 getIdentifier(const QModelIndex &idx) + { + if (!idx.isValid()) { + return 0; + } + return idx.internalId(); + } + + int rowCount(const QModelIndex &parent = QModelIndex()) const + { + return mTree[getIdentifier(parent)].size(); + } + + int columnCount(const QModelIndex &parent = QModelIndex()) const + { + return mPropertyColumns.size(); + } + + virtual QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const + { + if (role == DomainObjectRole) { + qWarning() << "trying to get entity " << index.internalId(); + Q_ASSERT(mEntities.contains(index.internalId())); + return QVariant::fromValue(mEntities.value(index.internalId())); + } + qDebug() << "Invalid role"; + return QVariant(); + } + + QModelIndex index(int row, int column, const QModelIndex & parent = QModelIndex()) const + { + auto id = getIdentifier(parent); + auto childId = mTree.value(id).at(row); + return createIndex(row, column, childId); + } + + QModelIndex parent(const QModelIndex &index) const + { + auto id = getIdentifier(index); + auto parentId = mParents.value(id); + auto grandParentId = mParents.value(parentId, 0); + auto row = mTree.value(grandParentId).indexOf(parentId); + return createIndex(row, 0, parentId); + } + + bool canFetchMore(const QModelIndex &parent) const + { + return mEntityChildrenFetched.value(parent.internalId()); + } + + void fetchMore(const QModelIndex &parent) + { + fetchEntities(parent); + } + + void fetchEntities(const QModelIndex &parent) + { + qDebug() << "Fetching entities"; + const auto id = getIdentifier(parent); + // beginResetModel(); + // mEntities.remove(id); + mEntityChildrenFetched[id] = true; + auto query = mQuery; + if (!parent.isValid()) { + qDebug() << "no parent"; + query.propertyFilter.insert("parent", QByteArray()); + } else { + qDebug() << "parent is valid"; + auto object = parent.data(DomainObjectRole).template value(); + Q_ASSERT(object); + query.propertyFilter.insert("parent", object->identifier()); + } + auto emitter = Akonadi2::Store::load(query); + emitter->onAdded([this, id, parent](const typename T::Ptr &value) { + auto childId = qHash(value->identifier()); + qDebug() << "Added entity " << childId; + const auto keys = mTree[id]; + int index = 0; + for (; index < keys.size(); index++) { + if (childId < keys.at(index)) { + break; + } + } + beginInsertRows(parent, index, index); + mEntities.insert(childId, value); + mTree[id].insert(index, childId); + mParents.insert(childId, id); + endInsertRows(); + }); + emitter->onModified([this, id, parent](const typename T::Ptr &value) { + auto childId = qHash(value->identifier()); + qDebug() << "Modified entity" << childId; + auto i = mTree[id].indexOf(childId); + mEntities.remove(childId); + mEntities.insert(childId, value); + //TODO check for change of parents + auto idx = index(i, 0, parent); + emit dataChanged(idx, idx); + }); + emitter->onRemoved([this, id, parent](const typename T::Ptr &value) { + auto childId = qHash(value->identifier()); + qDebug() << "Removed entity" << childId; + auto index = mTree[id].indexOf(qHash(value->identifier())); + beginRemoveRows(parent, index, index); + mEntities.remove(childId); + mTree[id].removeAll(childId); + mParents.remove(childId); + //TODO remove children + endRemoveRows(); + }); + emitter->onInitialResultSetComplete([this]() { + }); + emitter->onComplete([this, id]() { + mEmitter[id].clear(); + }); + emitter->onClear([this]() { + // beginResetModel(); + // mEntities.clear(); + // endResetModel(); + }); + mEmitter.insert(id, emitter); + // endResetModel(); + } + +private: + QMap >> mEmitter; + //TODO we should be able to directly use T as index, with an appropriate hash function, and thus have a QMap and QList + QMap mEntities; + QMap /* child entity id*/> mTree; + QMap mParents; + QMap mEntityChildrenFetched; + QList mPropertyColumns; + Akonadi2::Query mQuery; +}; + diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 5629cdb..9ed5a76 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -45,8 +45,10 @@ auto_tests ( genericfacadetest resourcecommunicationtest pipelinetest + querytest ) target_link_libraries(dummyresourcetest akonadi2_resource_dummy) target_link_libraries(dummyresourcebenchmark akonadi2_resource_dummy) +target_link_libraries(querytest akonadi2_resource_dummy) diff --git a/tests/clientapitest.cpp b/tests/clientapitest.cpp index 665d29b..5bfad4b 100644 --- a/tests/clientapitest.cpp +++ b/tests/clientapitest.cpp @@ -6,28 +6,45 @@ #include "facade.h" #include "synclistresult.h" #include "resourceconfig.h" +#include "modelresult.h" +#include "resultprovider.h" -class DummyResourceFacade : public Akonadi2::StoreFacade +template +class DummyResourceFacade : public Akonadi2::StoreFacade { public: + static std::shared_ptr > registerFacade() + { + auto facade = std::make_shared >(); + Akonadi2::FacadeFactory::instance().registerFacade >("dummyresource", + [facade](const QByteArray &instanceIdentifier) { + return facade; + } + ); + return facade; + } ~DummyResourceFacade(){}; - KAsync::Job create(const Akonadi2::ApplicationDomain::Event &domainObject) Q_DECL_OVERRIDE { return KAsync::null(); }; - KAsync::Job modify(const Akonadi2::ApplicationDomain::Event &domainObject) Q_DECL_OVERRIDE { return KAsync::null(); }; - KAsync::Job remove(const Akonadi2::ApplicationDomain::Event &domainObject) Q_DECL_OVERRIDE { return KAsync::null(); }; - KAsync::Job load(const Akonadi2::Query &query, const QSharedPointer > &resultProvider) Q_DECL_OVERRIDE + KAsync::Job create(const T &domainObject) Q_DECL_OVERRIDE { return KAsync::null(); }; + KAsync::Job modify(const T &domainObject) Q_DECL_OVERRIDE { return KAsync::null(); }; + KAsync::Job remove(const T &domainObject) Q_DECL_OVERRIDE { return KAsync::null(); }; + KAsync::Job load(const Akonadi2::Query &query, const QSharedPointer > &resultProvider) Q_DECL_OVERRIDE { capturedResultProvider = resultProvider; return KAsync::start([this, resultProvider, query]() { for (const auto &res : results) { - resultProvider->add(res); + qDebug() << "Parent filter " << query.propertyFilter.value("parent").toByteArray() << res->identifier(); + if (!query.propertyFilter.contains("parent") || query.propertyFilter.value("parent").toByteArray() == res->getProperty("parent").toByteArray()) { + resultProvider->add(res); + } } }); } - QList results; - QWeakPointer > capturedResultProvider; + QList results; + QWeakPointer > capturedResultProvider; }; + /** * Test of the client api implementation. * @@ -38,17 +55,6 @@ class ClientAPITest : public QObject Q_OBJECT private Q_SLOTS: - static std::shared_ptr registerDummyFacade() - { - auto facade = std::make_shared(); - Akonadi2::FacadeFactory::instance().registerFacade("dummyresource", - [facade](const QByteArray &instanceIdentifier) { - return facade; - } - ); - return facade; - } - void initTestCase() { Akonadi2::FacadeFactory::instance().resetFactory(); @@ -57,8 +63,8 @@ private Q_SLOTS: void testLoad() { - auto facade = registerDummyFacade(); - facade->results << QSharedPointer::create("resource", "id", 0, QSharedPointer()); + auto facade = DummyResourceFacade::registerFacade(); + facade->results << QSharedPointer::create("resource", "id", 0, QSharedPointer::create()); ResourceConfig::addResource("dummyresource.instance1", "dummyresource"); Akonadi2::Query query; @@ -73,8 +79,8 @@ private Q_SLOTS: //The query provider is supposed to delete itself void testQueryLifetime() { - auto facade = registerDummyFacade(); - facade->results << QSharedPointer::create("resource", "id", 0, QSharedPointer()); + auto facade = DummyResourceFacade::registerFacade(); + facade->results << QSharedPointer::create("resource", "id", 0, QSharedPointer::create()); ResourceConfig::addResource("dummyresource.instance1", "dummyresource"); Akonadi2::Query query; @@ -119,6 +125,71 @@ private Q_SLOTS: } } + void testModelSingle() + { + auto facade = DummyResourceFacade::registerFacade(); + facade->results << QSharedPointer::create("resource", "id", 0, QSharedPointer::create()); + ResourceConfig::addResource("dummyresource.instance1", "dummyresource"); + + Akonadi2::Query query; + query.resources << "dummyresource.instance1"; + query.liveQuery = false; + + auto model = new ModelResult(query, QList() << "summary" << "uid"); + model->fetchMore(QModelIndex()); + QTRY_COMPARE(model->rowCount(), 1); + } + + void testModelNested() + { + auto facade = DummyResourceFacade::registerFacade(); + auto folder = QSharedPointer::create("resource", "id", 0, QSharedPointer::create()); + auto subfolder = QSharedPointer::create("resource", "subId", 0, QSharedPointer::create()); + subfolder->setProperty("parent", "id"); + facade->results << folder << subfolder; + ResourceConfig::addResource("dummyresource.instance1", "dummyresource"); + + //Test + Akonadi2::Query query; + query.resources << "dummyresource.instance1"; + query.liveQuery = false; + + auto model = new ModelResult(query, QList() << "summary" << "uid"); + model->fetchMore(QModelIndex()); + QTRY_COMPARE(model->rowCount(), 1); + model->fetchMore(model->index(0, 0)); + QTRY_COMPARE(model->rowCount(model->index(0, 0)), 1); + } + + // void testModelNestedLive() + // { + // auto facade = DummyResourceFacade::registerFacade(); + // auto folder = QSharedPointer::create("resource", "id", 0, QSharedPointer::create()); + // auto subfolder = QSharedPointer::create("resource", "subId", 0, QSharedPointer::create()); + // subfolder->setProperty("parent", "id"); + // facade->results << folder << subfolder; + // ResourceConfig::addResource("dummyresource.instance1", "dummyresource"); + // + // //Test + // Akonadi2::Query query; + // query.resources << "dummyresource.instance1"; + // query.liveQuery = true + // + // auto model = new ModelResult(query, QList() << "summary" << "uid"); + // model->fetchMore(QModelIndex()); + // QTRY_COMPARE(model->rowCount(), 1); + // model->fetchMore(model->index(0, 0)); + // QTRY_COMPARE(model->rowCount(model->index(0, 0)), 1); + // + // auto resultProvider = facade->capturedResultProvider.toStrongRef(); + // + // //A modification can also be a move + // // resultProvider->modify(); + // + // // resultProvider->remove(); + // } + + }; QTEST_MAIN(ClientAPITest) diff --git a/tests/querytest.cpp b/tests/querytest.cpp new file mode 100644 index 0000000..9f4b3bb --- /dev/null +++ b/tests/querytest.cpp @@ -0,0 +1,100 @@ +#include + +#include + +#include "dummyresource/resourcefactory.h" +#include "clientapi.h" +#include "synclistresult.h" +#include "commands.h" +#include "resourceconfig.h" +#include "log.h" +#include "modelresult.h" + +/** + * Test of the query system using the dummy resource. + * + * This test requires the dummy resource installed. + */ +class QueryTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void initTestCase() + { + Akonadi2::Log::setDebugOutputLevel(Akonadi2::Log::Trace); + auto factory = Akonadi2::ResourceFactory::load("org.kde.dummy"); + QVERIFY(factory); + DummyResource::removeFromDisk("org.kde.dummy.instance1"); + ResourceConfig::addResource("org.kde.dummy.instance1", "org.kde.dummy"); + } + + void cleanup() + { + Akonadi2::Store::shutdown(QByteArray("org.kde.dummy.instance1")).exec().waitForFinished(); + DummyResource::removeFromDisk("org.kde.dummy.instance1"); + auto factory = Akonadi2::ResourceFactory::load("org.kde.dummy"); + QVERIFY(factory); + } + + void init() + { + qDebug(); + qDebug() << "-----------------------------------------"; + qDebug(); + } + + void testSingle() + { + //Setup + { + Akonadi2::ApplicationDomain::Mail mail("org.kde.dummy.instance1"); + Akonadi2::Store::create(mail).exec().waitForFinished(); + } + + //Test + Akonadi2::Query query; + query.resources << "org.kde.dummy.instance1"; + query.syncOnDemand = false; + query.processAll = true; + + auto model = new ModelResult(query, QList() << "summary" << "uid"); + model->fetchMore(QModelIndex()); + QTRY_COMPARE(model->rowCount(), 1); + } + + // void testTree() + // { + // //Setup + // { + // Akonadi2::ApplicationDomain::Folder folder("org.kde.dummy.instance1"); + // Akonadi2::Store::create(folder).exec().waitForFinished(); + // + // Akonadi2::Query query; + // query.resources << "org.kde.dummy.instance1"; + // query.syncOnDemand = false; + // query.processAll = true; + // + // auto model = new ModelResult(query, QList() << "summary" << "uid"); + // QTRY_COMPARE(model->rowCount(), 1); + // + // auto folderEntity = model->index(0, 0).data(ModelResult::DomainObjectRole).value(); + // + // Akonadi2::ApplicationDomain::Folder subfolder("org.kde.dummy.instance1"); + // subfolder.setProperty("parent", folderEntity.identifier()); + // Akonadi2::Store::create(subfolder).exec().waitForFinished(); + // } + // + // //Test + // Akonadi2::Query query; + // query.resources << "org.kde.dummy.instance1"; + // query.syncOnDemand = false; + // query.processAll = true; + // + // auto model = new ModelResult(query, QList() << "summary" << "uid"); + // QTRY_COMPARE(model->rowCount(), 1); + // QTRY_COMPARE(model->rowCount(model->index(0, 0)), 1); + // } +}; + +QTEST_MAIN(QueryTest) +#include "querytest.moc" -- cgit v1.2.3