From 9f6e0fbfd8cf23104eba5a78f89a69fab1a417f5 Mon Sep 17 00:00:00 2001 From: Christian Mollekopf Date: Mon, 9 Nov 2015 16:04:46 +0100 Subject: Added a folder type --- common/CMakeLists.txt | 3 ++ common/domain/applicationdomaintype.h | 2 + common/domain/dummy.fbs | 7 +++ common/domain/folder.cpp | 71 ++++++++++++++++++++++++++++++ common/domain/folder.fbs | 9 ++++ common/domain/folder.h | 56 +++++++++++++++++++++++ common/domainadaptor.h | 1 + examples/dummyresource/domainadaptor.cpp | 6 +++ examples/dummyresource/domainadaptor.h | 12 ++++- examples/dummyresource/facade.cpp | 22 +++------ examples/dummyresource/facade.h | 6 +-- examples/dummyresource/resourcefactory.cpp | 2 + 12 files changed, 173 insertions(+), 24 deletions(-) create mode 100644 common/domain/dummy.fbs create mode 100644 common/domain/folder.cpp create mode 100644 common/domain/folder.fbs create mode 100644 common/domain/folder.h diff --git a/common/CMakeLists.txt b/common/CMakeLists.txt index b4a4703..f24ec46 100644 --- a/common/CMakeLists.txt +++ b/common/CMakeLists.txt @@ -36,6 +36,7 @@ set(command_SRCS domain/applicationdomaintype.cpp domain/event.cpp domain/mail.cpp + domain/folder.cpp ${storage_SRCS}) add_library(${PROJECT_NAME} SHARED ${command_SRCS}) @@ -55,6 +56,8 @@ generate_flatbuffers( commands/revisionreplayed domain/event domain/mail + domain/folder + domain/dummy entity metadata queuedcommand diff --git a/common/domain/applicationdomaintype.h b/common/domain/applicationdomaintype.h index 5514d26..b4cf8c4 100644 --- a/common/domain/applicationdomaintype.h +++ b/common/domain/applicationdomaintype.h @@ -160,3 +160,5 @@ Q_DECLARE_METATYPE(Akonadi2::ApplicationDomain::Event) Q_DECLARE_METATYPE(Akonadi2::ApplicationDomain::Event::Ptr) Q_DECLARE_METATYPE(Akonadi2::ApplicationDomain::Mail) Q_DECLARE_METATYPE(Akonadi2::ApplicationDomain::Mail::Ptr) +Q_DECLARE_METATYPE(Akonadi2::ApplicationDomain::Folder) +Q_DECLARE_METATYPE(Akonadi2::ApplicationDomain::Folder::Ptr) diff --git a/common/domain/dummy.fbs b/common/domain/dummy.fbs new file mode 100644 index 0000000..8816b09 --- /dev/null +++ b/common/domain/dummy.fbs @@ -0,0 +1,7 @@ +namespace Akonadi2.ApplicationDomain.Buffer; + +table Dummy { +} + +root_type Dummy; +file_identifier "AKFB"; diff --git a/common/domain/folder.cpp b/common/domain/folder.cpp new file mode 100644 index 0000000..50f73c2 --- /dev/null +++ b/common/domain/folder.cpp @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2015 Christian Mollekopf + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the + * Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ +#include "folder.h" + +#include +#include +#include + +#include "../resultset.h" +#include "../index.h" +#include "../storage.h" +#include "../log.h" +#include "../propertymapper.h" +#include "../query.h" +#include "../definitions.h" + +#include "folder_generated.h" + +using namespace Akonadi2::ApplicationDomain; + +ResultSet TypeImplementation::queryIndexes(const Akonadi2::Query &query, const QByteArray &resourceInstanceIdentifier, QSet &appliedFilters, Akonadi2::Storage::Transaction &transaction) +{ + QVector keys; + return ResultSet(keys); +} + +void TypeImplementation::index(const QByteArray &identifier, const BufferAdaptor &bufferAdaptor, Akonadi2::Storage::Transaction &transaction) +{ +} + +void TypeImplementation::removeIndex(const QByteArray &identifier, const BufferAdaptor &bufferAdaptor, Akonadi2::Storage::Transaction &transaction) +{ +} + +QSharedPointer::Buffer> > TypeImplementation::initializeReadPropertyMapper() +{ + auto propertyMapper = QSharedPointer >::create(); + propertyMapper->addMapping("parent", &Buffer::parent); + propertyMapper->addMapping("name", &Buffer::name); + return propertyMapper; +} + +QSharedPointer::BufferBuilder> > TypeImplementation::initializeWritePropertyMapper() +{ + auto propertyMapper = QSharedPointer >::create(); + propertyMapper->addMapping("parent", [](const QVariant &value, flatbuffers::FlatBufferBuilder &fbb) -> std::function { + auto offset = variantToProperty(value, fbb); + return [offset](BufferBuilder &builder) { builder.add_parent(offset); }; + }); + propertyMapper->addMapping("name", [](const QVariant &value, flatbuffers::FlatBufferBuilder &fbb) -> std::function { + auto offset = variantToProperty(value, fbb); + return [offset](BufferBuilder &builder) { builder.add_name(offset); }; + }); + return propertyMapper; +} diff --git a/common/domain/folder.fbs b/common/domain/folder.fbs new file mode 100644 index 0000000..3476d58 --- /dev/null +++ b/common/domain/folder.fbs @@ -0,0 +1,9 @@ +namespace Akonadi2.ApplicationDomain.Buffer; + +table Folder { + name:string; + parent:string; +} + +root_type Folder; +file_identifier "AKFB"; diff --git a/common/domain/folder.h b/common/domain/folder.h new file mode 100644 index 0000000..545836f --- /dev/null +++ b/common/domain/folder.h @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2015 Christian Mollekopf + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the + * Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ +#pragma once + +#include "applicationdomaintype.h" + +#include "storage.h" + +class ResultSet; +class QByteArray; + +template +class ReadPropertyMapper; +template +class WritePropertyMapper; + +namespace Akonadi2 { + class Query; + +namespace ApplicationDomain { + namespace Buffer { + struct Folder; + struct FolderBuilder; + } + +template<> +class TypeImplementation { +public: + typedef Akonadi2::ApplicationDomain::Buffer::Folder Buffer; + typedef Akonadi2::ApplicationDomain::Buffer::FolderBuilder BufferBuilder; + static QSet indexedProperties(); + static ResultSet queryIndexes(const Akonadi2::Query &query, const QByteArray &resourceInstanceIdentifier, QSet &appliedFilters, Akonadi2::Storage::Transaction &transaction); + static void index(const QByteArray &identifier, const BufferAdaptor &bufferAdaptor, Akonadi2::Storage::Transaction &transaction); + static void removeIndex(const QByteArray &identifier, const BufferAdaptor &bufferAdaptor, Akonadi2::Storage::Transaction &transaction); + static QSharedPointer > initializeReadPropertyMapper(); + static QSharedPointer > initializeWritePropertyMapper(); +}; + +} +} diff --git a/common/domainadaptor.h b/common/domainadaptor.h index b14fbcd..620a658 100644 --- a/common/domainadaptor.h +++ b/common/domainadaptor.h @@ -26,6 +26,7 @@ #include "domain/applicationdomaintype.h" #include "domain/event.h" #include "domain/mail.h" +#include "domain/folder.h" #include "bufferadaptor.h" #include "entity_generated.h" #include "metadata_generated.h" diff --git a/examples/dummyresource/domainadaptor.cpp b/examples/dummyresource/domainadaptor.cpp index d08a783..74b170d 100644 --- a/examples/dummyresource/domainadaptor.cpp +++ b/examples/dummyresource/domainadaptor.cpp @@ -51,3 +51,9 @@ DummyMailAdaptorFactory::DummyMailAdaptorFactory() } +DummyFolderAdaptorFactory::DummyFolderAdaptorFactory() + : DomainTypeAdaptorFactory() +{ + +} + diff --git a/examples/dummyresource/domainadaptor.h b/examples/dummyresource/domainadaptor.h index add3e8e..e5856f8 100644 --- a/examples/dummyresource/domainadaptor.h +++ b/examples/dummyresource/domainadaptor.h @@ -21,6 +21,8 @@ #include "common/domainadaptor.h" #include "event_generated.h" #include "mail_generated.h" +#include "folder_generated.h" +#include "dummy_generated.h" #include "dummycalendar_generated.h" #include "entity_generated.h" @@ -31,10 +33,16 @@ public: virtual ~DummyEventAdaptorFactory() {}; }; -//TODO replace the resource specific event class by a mail class or a dummy class if no resource type is required. -class DummyMailAdaptorFactory : public DomainTypeAdaptorFactory +class DummyMailAdaptorFactory : public DomainTypeAdaptorFactory { public: DummyMailAdaptorFactory(); virtual ~DummyMailAdaptorFactory() {}; }; + +class DummyFolderAdaptorFactory : public DomainTypeAdaptorFactory +{ +public: + DummyFolderAdaptorFactory(); + virtual ~DummyFolderAdaptorFactory() {}; +}; diff --git a/examples/dummyresource/facade.cpp b/examples/dummyresource/facade.cpp index 5a9d722..f337bdc 100644 --- a/examples/dummyresource/facade.cpp +++ b/examples/dummyresource/facade.cpp @@ -30,6 +30,7 @@ DummyResourceFacade::~DummyResourceFacade() { } + DummyResourceMailFacade::DummyResourceMailFacade(const QByteArray &instanceIdentifier) : Akonadi2::GenericFacade(instanceIdentifier, QSharedPointer::create()) { @@ -39,25 +40,12 @@ DummyResourceMailFacade::~DummyResourceMailFacade() { } -static void addFolder(const QSharedPointer > &resultProvider, QByteArray uid, QString name, QString icon) + +DummyResourceFolderFacade::DummyResourceFolderFacade(const QByteArray &instanceIdentifier) + : Akonadi2::GenericFacade(instanceIdentifier, QSharedPointer::create()) { - auto folder = Akonadi2::ApplicationDomain::Folder::Ptr::create(); - folder->setProperty("name", name); - folder->setProperty("uid", uid); - resultProvider->add(folder); } -KAsync::Job load(const Akonadi2::Query &query, const QSharedPointer > &resultProvider) +DummyResourceFolderFacade::~DummyResourceFolderFacade() { - //Dummy implementation for a fixed set of folders - addFolder(resultProvider, "inbox", "INBOX", "mail-folder-inbox"); - addFolder(resultProvider, "sent", "Sent", "mail-folder-sent"); - addFolder(resultProvider, "trash", "Trash", "user-trash"); - addFolder(resultProvider, "drafts", "Drafts", "document-edit"); - addFolder(resultProvider, "1", "dragons", "folder"); - addFolder(resultProvider, "1", "super mega long tailed dragons", "folder"); - resultProvider->initialResultSetComplete(); - resultProvider->complete(); - return KAsync::null(); } - diff --git a/examples/dummyresource/facade.h b/examples/dummyresource/facade.h index 948116b..b00e1d7 100644 --- a/examples/dummyresource/facade.h +++ b/examples/dummyresource/facade.h @@ -36,13 +36,9 @@ public: virtual ~DummyResourceMailFacade(); }; -class DummyResourceFolderFacade : public Akonadi2::StoreFacade +class DummyResourceFolderFacade : public Akonadi2::GenericFacade { public: DummyResourceFolderFacade(const QByteArray &instanceIdentifier); virtual ~DummyResourceFolderFacade(); - virtual KAsync::Job create(const Akonadi2::ApplicationDomain::Folder &domainObject) { return KAsync::null(); }; - virtual KAsync::Job modify(const Akonadi2::ApplicationDomain::Folder &domainObject) { return KAsync::null(); }; - virtual KAsync::Job remove(const Akonadi2::ApplicationDomain::Folder &domainObject) { return KAsync::null(); }; - virtual KAsync::Job load(const Akonadi2::Query &query, const QSharedPointer > &resultProvider); }; diff --git a/examples/dummyresource/resourcefactory.cpp b/examples/dummyresource/resourcefactory.cpp index 397ca5f..edb3b42 100644 --- a/examples/dummyresource/resourcefactory.cpp +++ b/examples/dummyresource/resourcefactory.cpp @@ -40,6 +40,7 @@ //This is the resources entity type, and not the domain type #define ENTITY_TYPE_EVENT "event" #define ENTITY_TYPE_MAIL "mail" +#define ENTITY_TYPE_FOLDER "folder" /** * Index types: @@ -228,5 +229,6 @@ void DummyResourceFactory::registerFacades(Akonadi2::FacadeFactory &factory) { factory.registerFacade(PLUGIN_NAME); factory.registerFacade(PLUGIN_NAME); + factory.registerFacade(PLUGIN_NAME); } -- cgit v1.2.3 From fa1f58e8a83c6dc524ee0540f450065014e1a825 Mon Sep 17 00:00:00 2001 From: Christian Mollekopf Date: Tue, 10 Nov 2015 11:48:00 +0100 Subject: Cleanups --- common/domain/applicationdomaintype.cpp | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/common/domain/applicationdomaintype.cpp b/common/domain/applicationdomaintype.cpp index 1b5d870..c9a8bba 100644 --- a/common/domain/applicationdomaintype.cpp +++ b/common/domain/applicationdomaintype.cpp @@ -60,10 +60,13 @@ ApplicationDomainType& ApplicationDomainType::operator=(const ApplicationDomainT return *this; } -ApplicationDomainType::~ApplicationDomainType() {} +ApplicationDomainType::~ApplicationDomainType() +{ +} QVariant ApplicationDomainType::getProperty(const QByteArray &key) const { + Q_ASSERT(mAdaptor); if (!mAdaptor->availableProperties().contains(key)) { Warning() << "No such property available " << key; } @@ -72,7 +75,9 @@ QVariant ApplicationDomainType::getProperty(const QByteArray &key) const void ApplicationDomainType::setProperty(const QByteArray &key, const QVariant &value) { - mChangeSet.insert(key, value); mAdaptor->setProperty(key, value); + Q_ASSERT(mAdaptor); + mChangeSet.insert(key, value); + mAdaptor->setProperty(key, value); } QByteArrayList ApplicationDomainType::changedProperties() const -- cgit v1.2.3 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 From 09aafbd1373b5d1152ac7a453a140a7f76c2e90e Mon Sep 17 00:00:00 2001 From: Christian Mollekopf Date: Fri, 13 Nov 2015 19:34:47 +0100 Subject: It's starting to work --- common/clientapi.h | 32 +++- common/facade.h | 263 ++++++++++++++++++-------- common/facadeinterface.h | 4 +- common/modelresult.h | 139 +++++++------- common/resourcefacade.cpp | 2 +- common/resourcefacade.h | 2 +- common/resultprovider.h | 303 +++++++++++++++++++++++++++++- examples/dummyresource/resourcefacade.cpp | 2 +- examples/dummyresource/resourcefacade.h | 2 +- tests/clientapitest.cpp | 4 +- 10 files changed, 602 insertions(+), 151 deletions(-) diff --git a/common/clientapi.h b/common/clientapi.h index 9a32188..a424424 100644 --- a/common/clientapi.h +++ b/common/clientapi.h @@ -23,6 +23,7 @@ #include #include #include +#include #include #include @@ -101,11 +102,34 @@ public: /** * Asynchronusly load a dataset with tree structure information */ - // template - // static TreeSet loadTree(Query) - // { + template + static QSharedPointer loadModel(Query query) + { + auto model = QSharedPointer >::create(query, QList() << "summary" << "uid"); + auto resultProvider = QSharedPointer >::create(model); + + // Query all resources and aggregate results + KAsync::iterate(getResources(query.resources, ApplicationDomain::getTypeName())) + .template each([query, resultProvider](const QByteArray &resource, KAsync::Future &future) { + auto facade = FacadeFactory::instance().getFacade(resourceName(resource), resource); + if (facade) { + facade->load(query, resultProvider).template then([&future](){future.setFinished();}).exec(); + //Keep the facade alive for the lifetime of the resultSet. + resultProvider->setFacade(facade); + } else { + //Ignore the error and carry on + future.setFinished(); + } + }).template then([query, resultProvider]() { + resultProvider->initialResultSetComplete(); + if (!query.liveQuery) { + resultProvider->complete(); + } + }).exec(); + + return model; + } - // } template static std::shared_ptr > getFacade(const QByteArray &resourceInstanceIdentifier) { diff --git a/common/facade.h b/common/facade.h index 643ebec..eb55c98 100644 --- a/common/facade.h +++ b/common/facade.h @@ -135,68 +135,6 @@ public: if (!mResourceAccess) { mResourceAccess = QSharedPointer::create(resourceIdentifier); } - if (!mStorage) { - mStorage = QSharedPointer >::create(resourceIdentifier); - const auto bufferType = bufferTypeForDomainType(); - - mStorage->readEntity = [bufferType, this] (const Akonadi2::Storage::Transaction &transaction, const QByteArray &key, const std::function &resultCallback) - { - //This only works for a 1:1 mapping of resource to domain types. - //Not i.e. for tags that are stored as flags in each entity of an imap store. - //additional properties that don't have a 1:1 mapping (such as separately stored tags), - //could be added to the adaptor. - transaction.openDatabase(bufferType + ".main").findLatest(key, [=](const QByteArray &key, const QByteArray &value) -> bool { - Akonadi2::EntityBuffer buffer(value.data(), value.size()); - const Akonadi2::Entity &entity = buffer.entity(); - const auto metadataBuffer = Akonadi2::EntityBuffer::readBuffer(entity.metadata()); - Q_ASSERT(metadataBuffer); - const qint64 revision = metadataBuffer ? metadataBuffer->revision() : -1; - resultCallback(DomainType::Ptr::create(mResourceInstanceIdentifier, Akonadi2::Storage::uidFromKey(key), revision, mDomainTypeAdaptorFactory->createAdaptor(entity)), metadataBuffer->operation()); - return false; - }, - [](const Akonadi2::Storage::Error &error) { - qWarning() << "Error during query: " << error.message; - }); - }; - - mStorage->loadInitialResultSet = [bufferType, this] (const Akonadi2::Query &query, Akonadi2::Storage::Transaction &transaction, QSet &remainingFilters) -> ResultSet - { - QSet appliedFilters; - auto resultSet = Akonadi2::ApplicationDomain::TypeImplementation::queryIndexes(query, mResourceInstanceIdentifier, appliedFilters, transaction); - remainingFilters = query.propertyFilter.keys().toSet() - appliedFilters; - - //We do a full scan if there were no indexes available to create the initial set. - if (appliedFilters.isEmpty()) { - //TODO this should be replaced by an index lookup as well - return fullScan(transaction, bufferType); - } - return resultSet; - }; - - mStorage->loadIncrementalResultSet = [bufferType, this] (qint64 baseRevision, const Akonadi2::Query &query, Akonadi2::Storage::Transaction &transaction, QSet &remainingFilters) -> ResultSet - { - auto revisionCounter = QSharedPointer::create(baseRevision); - return ResultSet([bufferType, revisionCounter, &transaction, this]() -> QByteArray { - const qint64 topRevision = Akonadi2::Storage::maxRevision(transaction); - //Spit out the revision keys one by one. - while (*revisionCounter <= topRevision) { - const auto uid = Akonadi2::Storage::getUidFromRevision(transaction, *revisionCounter); - const auto type = Akonadi2::Storage::getTypeFromRevision(transaction, *revisionCounter); - Trace() << "Revision" << *revisionCounter << type << uid; - if (type != bufferType) { - //Skip revision - *revisionCounter += 1; - continue; - } - const auto key = Akonadi2::Storage::assembleKey(uid, *revisionCounter); - *revisionCounter += 1; - return key; - } - //We're done - return QByteArray(); - }); - }; - } } ~GenericFacade() @@ -237,13 +175,56 @@ public: } //TODO JOBAPI return job from sync continuation to execute it as subjob? - KAsync::Job load(const Akonadi2::Query &query, const QSharedPointer > &resultProvider) Q_DECL_OVERRIDE + KAsync::Job load(const Akonadi2::Query &query, const QSharedPointer > &resultProvider) Q_DECL_OVERRIDE { + { + QSet remainingFilters; + auto filter = [remainingFilters, query](const Akonadi2::ApplicationDomain::ApplicationDomainType::Ptr &domainObject) -> bool { + for (const auto &filterProperty : remainingFilters) { + //TODO implement other comparison operators than equality + if (domainObject->getProperty(filterProperty) != query.propertyFilter.value(filterProperty)) { + return false; + } + } + return true; + }; + + auto fetchEntities = [this, query, resultProvider, filter](const QByteArray &parent) { + Trace() << "Running fetchEntities" << parent; + Akonadi2::Storage storage(Akonadi2::storageLocation(), mResourceInstanceIdentifier); + storage.setDefaultErrorHandler([](const Akonadi2::Storage::Error &error) { + Warning() << "Error during query: " << error.store << error.message; + }); + + auto transaction = storage.createTransaction(Akonadi2::Storage::ReadOnly); + + auto modifiedQuery = query; + modifiedQuery.propertyFilter.insert("parent", parent); + //TODO + QSet appliedFilters; + auto resultSet = Akonadi2::ApplicationDomain::TypeImplementation::queryIndexes(modifiedQuery, mResourceInstanceIdentifier, appliedFilters, transaction); + QSet remainingFilters = query.propertyFilter.keys().toSet() - appliedFilters; + + //We do a full scan if there were no indexes available to create the initial set. + if (appliedFilters.isEmpty()) { + //TODO this should be replaced by an index lookup as well + resultSet = fullScan(transaction, bufferTypeForDomainType()); + } + auto filteredSet = filterSet(resultSet, filter, transaction, true); + replaySet(filteredSet, resultProvider); + resultProvider->setRevision(Akonadi2::Storage::maxRevision(transaction)); + qint64 newRevision = Akonadi2::Storage::maxRevision(transaction); + //TODO send newRevision to resource + // mResourceAccess->sendRevisionReplayedCommand(newRevision); + }; + resultProvider->setFetcher(fetchEntities); + } + auto runner = QSharedPointer::create(query); - QWeakPointer > weakResultProvider = resultProvider; - runner->setQuery([this, weakResultProvider, query] (qint64 oldRevision) -> KAsync::Job { - return KAsync::start([this, weakResultProvider, query, oldRevision](KAsync::Future &future) { - Trace() << "Executing query " << oldRevision; + QWeakPointer > weakResultProvider = resultProvider; + runner->setQuery([this, weakResultProvider, query] () -> KAsync::Job { + return KAsync::start([this, weakResultProvider, query](KAsync::Future &future) { + Trace() << "Executing query "; auto resultProvider = weakResultProvider.toStrongRef(); if (!resultProvider) { Warning() << "Tried executing query after result provider is already gone"; @@ -251,11 +232,10 @@ public: future.setFinished(); return; } - load(query, resultProvider, oldRevision).template then([&future, this](qint64 queriedRevision) { + executeQuery(query, resultProvider).template then([&future, this](qint64 queriedRevision) { //TODO set revision in result provider? //TODO update all existing results with new revision mResourceAccess->sendRevisionReplayedCommand(queriedRevision); - future.setValue(queriedRevision); future.setFinished(); }).exec(); }); @@ -272,9 +252,7 @@ public: //We have to capture the runner to keep it alive return synchronizeResource(query).template then([runner](KAsync::Future &future) { - runner->run().then([&future]() { - future.setFinished(); - }).exec(); + future.setFinished(); }, [](int error, const QString &errorString) { Warning() << "Error during sync " << error << errorString; @@ -293,17 +271,152 @@ private: return KAsync::null(); } - virtual KAsync::Job load(const Akonadi2::Query &query, const QSharedPointer > &resultProvider, qint64 oldRevision) + //TODO move into result provider? + void replaySet(ResultSet &resultSet, const QSharedPointer > &resultProvider) + { + while (resultSet.next([this, resultProvider](const Akonadi2::ApplicationDomain::ApplicationDomainType::Ptr &value, Akonadi2::Operation operation) -> bool { + switch (operation) { + case Akonadi2::Operation_Creation: + Trace() << "Got creation"; + //TODO Only copy in result provider + resultProvider->add(Akonadi2::ApplicationDomain::ApplicationDomainType::getInMemoryRepresentation(*value).template staticCast()); + // modelResult->add(); + break; + case Akonadi2::Operation_Modification: + Trace() << "Got modification"; + resultProvider->modify(Akonadi2::ApplicationDomain::ApplicationDomainType::getInMemoryRepresentation(*value).template staticCast()); + // modelResult->modify(); + break; + case Akonadi2::Operation_Removal: + Trace() << "Got removal"; + resultProvider->remove(Akonadi2::ApplicationDomain::ApplicationDomainType::getInMemoryRepresentation(*value).template staticCast()); + // modelResult->remove(); + break; + } + return true; + })){}; + } + + void readEntity(const Akonadi2::Storage::Transaction &transaction, const QByteArray &key, const std::function &resultCallback) + { + const auto bufferType = bufferTypeForDomainType(); + //This only works for a 1:1 mapping of resource to domain types. + //Not i.e. for tags that are stored as flags in each entity of an imap store. + //additional properties that don't have a 1:1 mapping (such as separately stored tags), + //could be added to the adaptor. + // + // Akonadi2::Storage::getLatest(transaction, bufferTye, key); + transaction.openDatabase(bufferType + ".main").findLatest(key, [=](const QByteArray &key, const QByteArray &value) -> bool { + Akonadi2::EntityBuffer buffer(value.data(), value.size()); + const Akonadi2::Entity &entity = buffer.entity(); + const auto metadataBuffer = Akonadi2::EntityBuffer::readBuffer(entity.metadata()); + Q_ASSERT(metadataBuffer); + const qint64 revision = metadataBuffer ? metadataBuffer->revision() : -1; + resultCallback(DomainType::Ptr::create(mResourceInstanceIdentifier, Akonadi2::Storage::uidFromKey(key), revision, mDomainTypeAdaptorFactory->createAdaptor(entity)), metadataBuffer->operation()); + return false; + }, + [](const Akonadi2::Storage::Error &error) { + qWarning() << "Error during query: " << error.message; + }); + } + + ResultSet loadIncrementalResultSet(qint64 baseRevision, const Akonadi2::Query &query, Akonadi2::Storage::Transaction &transaction, QSet &remainingFilters) + { + + const auto bufferType = bufferTypeForDomainType(); + auto revisionCounter = QSharedPointer::create(baseRevision); + //TODO apply filter from index + return ResultSet([bufferType, revisionCounter, &transaction, this]() -> QByteArray { + const qint64 topRevision = Akonadi2::Storage::maxRevision(transaction); + //Spit out the revision keys one by one. + while (*revisionCounter <= topRevision) { + const auto uid = Akonadi2::Storage::getUidFromRevision(transaction, *revisionCounter); + const auto type = Akonadi2::Storage::getTypeFromRevision(transaction, *revisionCounter); + Trace() << "Revision" << *revisionCounter << type << uid; + if (type != bufferType) { + //Skip revision + *revisionCounter += 1; + continue; + } + const auto key = Akonadi2::Storage::assembleKey(uid, *revisionCounter); + *revisionCounter += 1; + return key; + } + //We're done + return QByteArray(); + }); + } + + ResultSet filterSet(const ResultSet &resultSet, const std::function &filter, const Akonadi2::Storage::Transaction &transaction, bool initialQuery) { + auto resultSetPtr = QSharedPointer::create(resultSet); + + //Read through the source values and return whatever matches the filter + std::function)> generator = [this, resultSetPtr, &transaction, filter, initialQuery](std::function callback) -> bool { + while (resultSetPtr->next()) { + //TODO only necessary if we actually want to filter or neew the operation type (but not a big deal if we do it always I guess) + readEntity(transaction, resultSetPtr->id(), [this, filter, callback, initialQuery](const Akonadi2::ApplicationDomain::ApplicationDomainType::Ptr &domainObject, Akonadi2::Operation operation) { + //Always remove removals, they probably don't match due to non-available properties + if (filter(domainObject) || operation == Akonadi2::Operation_Removal) { + if (initialQuery) { + //We're not interested in removals during the initial query + if (operation != Akonadi2::Operation_Removal) { + callback(domainObject, Akonadi2::Operation_Creation); + } + } else { + callback(domainObject, operation); + } + } + }); + } + return false; + }; + return ResultSet(generator); + } + + virtual KAsync::Job executeQuery(const Akonadi2::Query &query, const QSharedPointer > &resultProvider) + { + /* + * This method gets called initially, and after every revision change. + * * We delegate loading of initial data to the result provider, os it can decide for itself what it needs to load. + * * Incremental updates are loaded directly, leaving it up to the model to discard the changes if they are not interesting + */ + const qint64 baseRevision = resultProvider->revision() + 1; + Trace() << "Running query " << baseRevision; + QSet remainingFilters; + auto filter = [remainingFilters, query, baseRevision](const Akonadi2::ApplicationDomain::ApplicationDomainType::Ptr &domainObject) -> bool { + for (const auto &filterProperty : remainingFilters) { + //TODO implement other comparison operators than equality + if (domainObject->getProperty(filterProperty) != query.propertyFilter.value(filterProperty)) { + return false; + } + } + return true; + }; + qint64 newRevision = 0; + + Trace() << "Fetching updates"; + Akonadi2::Storage storage(Akonadi2::storageLocation(), mResourceInstanceIdentifier); + storage.setDefaultErrorHandler([](const Akonadi2::Storage::Error &error) { + Warning() << "Error during query: " << error.store << error.message; + }); + + auto transaction = storage.createTransaction(Akonadi2::Storage::ReadOnly); + + auto resultSet = loadIncrementalResultSet(baseRevision, query, transaction, remainingFilters); + auto filteredSet = filterSet(resultSet, filter, transaction, false); + replaySet(filteredSet, resultProvider); + resultProvider->setRevision(Akonadi2::Storage::maxRevision(transaction)); + newRevision = Akonadi2::Storage::maxRevision(transaction); + return KAsync::start([=]() -> qint64 { - return mStorage->read(query, oldRevision, resultProvider); + return newRevision; }); } protected: //TODO use one resource access instance per application & per resource QSharedPointer mResourceAccess; - QSharedPointer > mStorage; DomainTypeAdaptorFactoryInterface::Ptr mDomainTypeAdaptorFactory; QByteArray mResourceInstanceIdentifier; }; diff --git a/common/facadeinterface.h b/common/facadeinterface.h index 3a38db8..571a1e8 100644 --- a/common/facadeinterface.h +++ b/common/facadeinterface.h @@ -45,7 +45,7 @@ public: virtual KAsync::Job create(const DomainType &domainObject) = 0; virtual KAsync::Job modify(const DomainType &domainObject) = 0; virtual KAsync::Job remove(const DomainType &domainObject) = 0; - virtual KAsync::Job load(const Query &query, const QSharedPointer > &resultProvider) = 0; + virtual KAsync::Job load(const Query &query, const QSharedPointer > &resultProvider) = 0; }; template @@ -67,7 +67,7 @@ public: return KAsync::error(-1, "Failed to create a facade"); } - KAsync::Job load(const Query &query, const QSharedPointer > &resultProvider) + KAsync::Job load(const Query &query, const QSharedPointer > &resultProvider) { return KAsync::error(-1, "Failed to create a facade"); } diff --git a/common/modelresult.h b/common/modelresult.h index c23c41e..756f4d6 100644 --- a/common/modelresult.h +++ b/common/modelresult.h @@ -24,11 +24,10 @@ #include #include #include "query.h" -#include "clientapi.h" #include "resultprovider.h" -template +template class ModelResult : public QAbstractItemModel { public: @@ -79,13 +78,18 @@ public: return createIndex(row, column, childId); } + QModelIndex createIndexFromId(const qint64 &id) const + { + auto grandParentId = mParents.value(id, 0); + auto row = mTree.value(grandParentId).indexOf(id); + return createIndex(row, 0, id); + } + 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); + return createIndexFromId(parentId); } bool canFetchMore(const QModelIndex &parent) const @@ -98,83 +102,92 @@ public: fetchEntities(parent); } + qint64 parentId(const Ptr &value) + { + return qHash(value->getProperty("parent").toByteArray()); + } + + void add(const Ptr &value) + { + auto childId = qHash(value->identifier()); + auto id = parentId(value); + auto parent = createIndexFromId(id); + 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(); + } + + void modify(const Ptr &value) + { + auto childId = qHash(value->identifier()); + auto id = parentId(value); + auto parent = createIndexFromId(id); + 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); + } + + void remove(const Ptr &value) + { + auto childId = qHash(value->identifier()); + auto id = parentId(value); + auto parent = createIndexFromId(id); + 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(); + } + void fetchEntities(const QModelIndex &parent) { qDebug() << "Fetching entities"; const auto id = getIdentifier(parent); - // beginResetModel(); - // mEntities.remove(id); mEntityChildrenFetched[id] = true; - auto query = mQuery; + QByteArray parentIdentifier; if (!parent.isValid()) { qDebug() << "no parent"; - query.propertyFilter.insert("parent", QByteArray()); } else { qDebug() << "parent is valid"; - auto object = parent.data(DomainObjectRole).template value(); + auto object = parent.data(DomainObjectRole).template value(); Q_ASSERT(object); - query.propertyFilter.insert("parent", object->identifier()); + parentIdentifier = 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(); + Trace() << "Loading entities"; + loadEntities(parentIdentifier); + } + + void setFetcher(const std::function &fetcher) + { + Trace() << "Setting fetcher"; + loadEntities = fetcher; } 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 mEntities; QMap /* child entity id*/> mTree; QMap mParents; QMap mEntityChildrenFetched; QList mPropertyColumns; Akonadi2::Query mQuery; + std::function loadEntities; }; diff --git a/common/resourcefacade.cpp b/common/resourcefacade.cpp index 54185f8..0b7c5a3 100644 --- a/common/resourcefacade.cpp +++ b/common/resourcefacade.cpp @@ -54,7 +54,7 @@ KAsync::Job ResourceFacade::remove(const Akonadi2::ApplicationDomain::Akon }); } -KAsync::Job ResourceFacade::load(const Akonadi2::Query &query, const QSharedPointer > &resultProvider) +KAsync::Job ResourceFacade::load(const Akonadi2::Query &query, const QSharedPointer > &resultProvider) { return KAsync::start([query, resultProvider]() { const auto configuredResources = ResourceConfig::getResources(); diff --git a/common/resourcefacade.h b/common/resourcefacade.h index 437ff75..850d380 100644 --- a/common/resourcefacade.h +++ b/common/resourcefacade.h @@ -37,5 +37,5 @@ public: KAsync::Job create(const Akonadi2::ApplicationDomain::AkonadiResource &resource) Q_DECL_OVERRIDE; KAsync::Job modify(const Akonadi2::ApplicationDomain::AkonadiResource &resource) Q_DECL_OVERRIDE; KAsync::Job remove(const Akonadi2::ApplicationDomain::AkonadiResource &resource) Q_DECL_OVERRIDE; - KAsync::Job load(const Akonadi2::Query &query, const QSharedPointer > &resultProvider) Q_DECL_OVERRIDE; + KAsync::Job load(const Akonadi2::Query &query, const QSharedPointer > &resultProvider) Q_DECL_OVERRIDE; }; diff --git a/common/resultprovider.h b/common/resultprovider.h index bc03152..43d21a4 100644 --- a/common/resultprovider.h +++ b/common/resultprovider.h @@ -34,11 +34,312 @@ namespace Akonadi2 { template class ResultEmitter; +template +class ResultProviderInterface +{ +public: + ResultProviderInterface() + : mRevision(0) + { + + } + + virtual void add(const T &value) = 0; + virtual void modify(const T &value) = 0; + virtual void remove(const T &value) = 0; + virtual void initialResultSetComplete() = 0; + virtual void complete() = 0; + virtual void clear() = 0; + virtual void setFetcher(const std::function &fetcher) + { + } + + virtual void setFacade(const std::shared_ptr &facade) = 0; + virtual void setQueryRunner(const QSharedPointer &runner) = 0; + + void setRevision(qint64 revision) + { + mRevision = revision; + } + + qint64 revision() const + { + return mRevision; + } + +private: + qint64 mRevision; +}; + +template +class ModelResultProvider : public ResultProviderInterface { +public: + ModelResultProvider(QWeakPointer > model) + : ResultProviderInterface(), + mModel(model) + { + + } + + void add(const Ptr &value) + { + if (auto model = mModel.toStrongRef()) { + model->add(value); + } + } + + void modify(const Ptr &value) + { + if (auto model = mModel.toStrongRef()) { + model->modify(value); + } + } + + void remove(const Ptr &value) + { + if (auto model = mModel.toStrongRef()) { + model->remove(value); + } + } + + void initialResultSetComplete() + { + // mResultEmitter->initialResultSetComplete(); + } + + void complete() + { + // mResultEmitter->complete(); + } + + void clear() + { + // mResultEmitter->clear(); + } + + // QSharedPointer > emitter() + // { + // if (!mResultEmitter) { + // //We have to go over a separate var and return that, otherwise we'd delete the emitter immediately again + // auto sharedPtr = QSharedPointer >(new ResultEmitter, [this](ResultEmitter *emitter){ done(); delete emitter; }); + // mResultEmitter = sharedPtr; + // return sharedPtr; + // } + // + // return mResultEmitter.toStrongRef(); + // } + + /** + * For lifetimemanagement only. + * We keep the runner alive as long as the result provider exists. + */ + void setFacade(const std::shared_ptr &facade) + { + mFacade = facade; + } + + void onDone(const std::function &callback) + { + mOnDoneCallback = callback; + } + + bool isDone() const + { + //The existance of the emitter currently defines wether we're done or not. + // return mResultEmitter.toStrongRef().isNull(); + return true; + } + + void setFetcher(const std::function &fetcher) + { + if (auto model = mModel.toStrongRef()) { + model->setFetcher(fetcher); + } + } + + void setQueryRunner(const QSharedPointer &runner) + { + mQueryRunner = runner; + } + + // qint64 fetch(const ResultSet &resultSet) + // { + // //Fetch a bunch + // // + // // Akonadi2::Storage storage(Akonadi2::storageLocation(), mResourceInstanceIdentifier); + // // storage.setDefaultErrorHandler([](const Akonadi2::Storage::Error &error) { + // // Warning() << "Error during query: " << error.store << error.message; + // // }); + // // + // // auto transaction = storage.createTransaction(Akonadi2::Storage::ReadOnly); + // + // // Log() << "Querying" << baseRevision << Akonadi2::Storage::maxRevision(transaction); + // // auto resultSet = getResultSet(query, transaction, baseRevision); + // while (resultSet.next([this](const Akonadi2::ApplicationDomain::ApplicationDomainType::Ptr &value, Akonadi2::Operation operation) -> bool { + // switch (operation) { + // case Akonadi2::Operation_Creation: + // Trace() << "Got creation"; + // //TODO Only copy in result provider + // add(Akonadi2::ApplicationDomain::ApplicationDomainType::getInMemoryRepresentation(*value).template staticCast()); + // break; + // case Akonadi2::Operation_Modification: + // Trace() << "Got modification"; + // modify(Akonadi2::ApplicationDomain::ApplicationDomainType::getInMemoryRepresentation(*value).template staticCast()); + // break; + // case Akonadi2::Operation_Removal: + // Trace() << "Got removal"; + // remove(Akonadi2::ApplicationDomain::ApplicationDomainType::getInMemoryRepresentation(*value).template staticCast()); + // break; + // } + // return true; + // })){}; + // // return Akonadi2::Storage::maxRevision(transaction); + // } + +private: + void done() + { + qWarning() << "done"; + if (mOnDoneCallback) { + mOnDoneCallback(); + mOnDoneCallback = std::function(); + } + } + + QWeakPointer > mModel; + QSharedPointer mQueryRunner; + std::shared_ptr mFacade; + std::function mOnDoneCallback; +}; + + + + + + +template +class SyncResultProvider : public ResultProviderInterface { +public: + void add(const T &value) + { + mResultEmitter->addHandler(value); + } + + void modify(const T &value) + { + mResultEmitter->modifyHandler(value); + } + + void remove(const T &value) + { + mResultEmitter->removeHandler(value); + } + + void initialResultSetComplete() + { + mResultEmitter->initialResultSetComplete(); + } + + void complete() + { + mResultEmitter->complete(); + } + + void clear() + { + mResultEmitter->clear(); + } + + QSharedPointer > emitter() + { + if (!mResultEmitter) { + //We have to go over a separate var and return that, otherwise we'd delete the emitter immediately again + auto sharedPtr = QSharedPointer >(new ResultEmitter, [this](ResultEmitter *emitter){ done(); delete emitter; }); + mResultEmitter = sharedPtr; + return sharedPtr; + } + + return mResultEmitter.toStrongRef(); + } + + /** + * For lifetimemanagement only. + * We keep the runner alive as long as the result provider exists. + */ + void setFacade(const std::shared_ptr &facade) + { + mFacade = facade; + } + + void onDone(const std::function &callback) + { + mOnDoneCallback = callback; + } + + bool isDone() const + { + //The existance of the emitter currently defines wether we're done or not. + return mResultEmitter.toStrongRef().isNull(); + } + + // qint64 fetch(const ResultSet &resultSet) + // { + // //Fetch a bunch + // // + // // Akonadi2::Storage storage(Akonadi2::storageLocation(), mResourceInstanceIdentifier); + // // storage.setDefaultErrorHandler([](const Akonadi2::Storage::Error &error) { + // // Warning() << "Error during query: " << error.store << error.message; + // // }); + // // + // // auto transaction = storage.createTransaction(Akonadi2::Storage::ReadOnly); + // + // // Log() << "Querying" << baseRevision << Akonadi2::Storage::maxRevision(transaction); + // // auto resultSet = getResultSet(query, transaction, baseRevision); + // while (resultSet.next([this](const Akonadi2::ApplicationDomain::ApplicationDomainType::Ptr &value, Akonadi2::Operation operation) -> bool { + // switch (operation) { + // case Akonadi2::Operation_Creation: + // Trace() << "Got creation"; + // //TODO Only copy in result provider + // add(Akonadi2::ApplicationDomain::ApplicationDomainType::getInMemoryRepresentation(*value).template staticCast()); + // break; + // case Akonadi2::Operation_Modification: + // Trace() << "Got modification"; + // modify(Akonadi2::ApplicationDomain::ApplicationDomainType::getInMemoryRepresentation(*value).template staticCast()); + // break; + // case Akonadi2::Operation_Removal: + // Trace() << "Got removal"; + // remove(Akonadi2::ApplicationDomain::ApplicationDomainType::getInMemoryRepresentation(*value).template staticCast()); + // break; + // } + // return true; + // })){}; + // // return Akonadi2::Storage::maxRevision(transaction); + // } + +private: + void done() + { + qWarning() << "done"; + if (mOnDoneCallback) { + mOnDoneCallback(); + mOnDoneCallback = std::function(); + } + } + + QWeakPointer > mResultEmitter; + std::shared_ptr mFacade; + std::function mOnDoneCallback; + QSharedPointer mThreadBoundary; +}; + + + + /* * The promise side for the result emitter */ template -class ResultProvider { +class ResultProvider : public ResultProviderInterface { private: void callInMainThreadOnEmitter(void (ResultEmitter::*f)()) { diff --git a/examples/dummyresource/resourcefacade.cpp b/examples/dummyresource/resourcefacade.cpp index df805e4..1090757 100644 --- a/examples/dummyresource/resourcefacade.cpp +++ b/examples/dummyresource/resourcefacade.cpp @@ -65,7 +65,7 @@ KAsync::Job DummyResourceConfigFacade::remove(const Akonadi2::ApplicationD return KAsync::null(); } -KAsync::Job DummyResourceConfigFacade::load(const Akonadi2::Query &query, const QSharedPointer > &resultProvider) +KAsync::Job DummyResourceConfigFacade::load(const Akonadi2::Query &query, const QSharedPointer > &resultProvider) { //Read configuration and list all available instances. //This includes runtime information about runing instances etc. diff --git a/examples/dummyresource/resourcefacade.h b/examples/dummyresource/resourcefacade.h index 5a5f46b..aa2ab05 100644 --- a/examples/dummyresource/resourcefacade.h +++ b/examples/dummyresource/resourcefacade.h @@ -42,7 +42,7 @@ public: //Remove instance KAsync::Job remove(const Akonadi2::ApplicationDomain::AkonadiResource &domainObject) Q_DECL_OVERRIDE; //Read configuration and available instances - KAsync::Job load(const Akonadi2::Query &query, const QSharedPointer > &resultProvider) Q_DECL_OVERRIDE; + KAsync::Job load(const Akonadi2::Query &query, const QSharedPointer > &resultProvider) Q_DECL_OVERRIDE; private: QSharedPointer getSettings(); diff --git a/tests/clientapitest.cpp b/tests/clientapitest.cpp index 5bfad4b..2b3cc46 100644 --- a/tests/clientapitest.cpp +++ b/tests/clientapitest.cpp @@ -27,7 +27,7 @@ public: 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 + KAsync::Job load(const Akonadi2::Query &query, const QSharedPointer > &resultProvider) Q_DECL_OVERRIDE { capturedResultProvider = resultProvider; return KAsync::start([this, resultProvider, query]() { @@ -41,7 +41,7 @@ public: } QList results; - QWeakPointer > capturedResultProvider; + QWeakPointer > capturedResultProvider; }; -- cgit v1.2.3 From 75c231f0758603120ec562af772b48b5f6ac0e24 Mon Sep 17 00:00:00 2001 From: Christian Mollekopf Date: Fri, 13 Nov 2015 23:31:41 +0100 Subject: DummyResourceTest and QueryTest are passing sync has been removed from the query code and is now a separate step --- common/clientapi.cpp | 2 +- common/facade.h | 147 ++++++++++++++-------------------- common/modelresult.h | 8 ++ common/resultprovider.h | 189 ++++---------------------------------------- tests/dummyresourcetest.cpp | 24 ++++++ tests/querytest.cpp | 24 +++++- tests/testimplementations.h | 4 +- 7 files changed, 132 insertions(+), 266 deletions(-) diff --git a/common/clientapi.cpp b/common/clientapi.cpp index f99ebb8..839e77b 100644 --- a/common/clientapi.cpp +++ b/common/clientapi.cpp @@ -95,7 +95,7 @@ KAsync::Job Store::synchronize(const Akonadi2::Query &query) .template each([query](const QByteArray &resource, KAsync::Future &future) { auto resourceAccess = QSharedPointer::create(resource); resourceAccess->open(); - resourceAccess->synchronizeResource(true, false).then([&future]() { + resourceAccess->synchronizeResource(query.syncOnDemand, query.processAll).then([&future, resourceAccess]() { future.setFinished(); }).exec(); }) diff --git a/common/facade.h b/common/facade.h index eb55c98..5be1c73 100644 --- a/common/facade.h +++ b/common/facade.h @@ -44,19 +44,15 @@ class QueryRunner : public QObject { Q_OBJECT public: - typedef std::function(qint64 oldRevision)> QueryFunction; + typedef std::function()> QueryFunction; - QueryRunner(const Akonadi2::Query &query) : mLatestRevision(0) {}; + QueryRunner(const Akonadi2::Query &query) {}; /** * Starts query */ KAsync::Job run(qint64 newRevision = 0) { - //TODO: JOBAPI: that last empty .then should not be necessary - //TODO: remove newRevision - return queryFunction(mLatestRevision + 1).then([this](qint64 revision) { - mLatestRevision = revision; - }).then([](){}); + return queryFunction(); } /** @@ -74,12 +70,11 @@ public slots: void revisionChanged(qint64 newRevision) { Trace() << "New revision: " << newRevision; - run(newRevision).exec(); + run().exec(); } private: QueryFunction queryFunction; - qint64 mLatestRevision; }; static inline ResultSet fullScan(const Akonadi2::Storage::Transaction &transaction, const QByteArray &bufferType) @@ -125,10 +120,9 @@ public: * @param resourceIdentifier is the identifier of the resource instance * @param adaptorFactory is the adaptor factory used to generate the mappings from domain to resource types and vice versa */ - GenericFacade(const QByteArray &resourceIdentifier, const DomainTypeAdaptorFactoryInterface::Ptr &adaptorFactory = DomainTypeAdaptorFactoryInterface::Ptr(), const QSharedPointer > storage = QSharedPointer >(), const QSharedPointer resourceAccess = QSharedPointer()) + GenericFacade(const QByteArray &resourceIdentifier, const DomainTypeAdaptorFactoryInterface::Ptr &adaptorFactory = DomainTypeAdaptorFactoryInterface::Ptr(), const QSharedPointer resourceAccess = QSharedPointer()) : Akonadi2::StoreFacade(), mResourceAccess(resourceAccess), - mStorage(storage), mDomainTypeAdaptorFactory(adaptorFactory), mResourceInstanceIdentifier(resourceIdentifier) { @@ -177,48 +171,28 @@ public: //TODO JOBAPI return job from sync continuation to execute it as subjob? KAsync::Job load(const Akonadi2::Query &query, const QSharedPointer > &resultProvider) Q_DECL_OVERRIDE { - { - QSet remainingFilters; - auto filter = [remainingFilters, query](const Akonadi2::ApplicationDomain::ApplicationDomainType::Ptr &domainObject) -> bool { - for (const auto &filterProperty : remainingFilters) { - //TODO implement other comparison operators than equality - if (domainObject->getProperty(filterProperty) != query.propertyFilter.value(filterProperty)) { - return false; - } - } - return true; - }; - - auto fetchEntities = [this, query, resultProvider, filter](const QByteArray &parent) { - Trace() << "Running fetchEntities" << parent; - Akonadi2::Storage storage(Akonadi2::storageLocation(), mResourceInstanceIdentifier); - storage.setDefaultErrorHandler([](const Akonadi2::Storage::Error &error) { - Warning() << "Error during query: " << error.store << error.message; - }); + auto fetchEntities = [this, query, resultProvider](const QByteArray &parent) { + Trace() << "Fetching initial set for parent:" << parent; - auto transaction = storage.createTransaction(Akonadi2::Storage::ReadOnly); + Akonadi2::Storage storage(Akonadi2::storageLocation(), mResourceInstanceIdentifier); + storage.setDefaultErrorHandler([](const Akonadi2::Storage::Error &error) { + Warning() << "Error during query: " << error.store << error.message; + }); - auto modifiedQuery = query; - modifiedQuery.propertyFilter.insert("parent", parent); - //TODO - QSet appliedFilters; - auto resultSet = Akonadi2::ApplicationDomain::TypeImplementation::queryIndexes(modifiedQuery, mResourceInstanceIdentifier, appliedFilters, transaction); - QSet remainingFilters = query.propertyFilter.keys().toSet() - appliedFilters; + auto transaction = storage.createTransaction(Akonadi2::Storage::ReadOnly); - //We do a full scan if there were no indexes available to create the initial set. - if (appliedFilters.isEmpty()) { - //TODO this should be replaced by an index lookup as well - resultSet = fullScan(transaction, bufferTypeForDomainType()); - } - auto filteredSet = filterSet(resultSet, filter, transaction, true); - replaySet(filteredSet, resultProvider); - resultProvider->setRevision(Akonadi2::Storage::maxRevision(transaction)); - qint64 newRevision = Akonadi2::Storage::maxRevision(transaction); - //TODO send newRevision to resource - // mResourceAccess->sendRevisionReplayedCommand(newRevision); - }; - resultProvider->setFetcher(fetchEntities); - } + auto modifiedQuery = query; + modifiedQuery.propertyFilter.insert("parent", parent); + + QSet remainingFilters; + auto resultSet = loadInitialResultSet(parent, modifiedQuery, transaction, remainingFilters); + auto filteredSet = filterSet(resultSet, getFilter(remainingFilters, query), transaction, true); + replaySet(filteredSet, resultProvider); + const qint64 newRevision = Akonadi2::Storage::maxRevision(transaction); + resultProvider->setRevision(newRevision); + mResourceAccess->sendRevisionReplayedCommand(newRevision); + }; + resultProvider->setFetcher(fetchEntities); auto runner = QSharedPointer::create(query); QWeakPointer > weakResultProvider = resultProvider; @@ -233,8 +207,6 @@ public: return; } executeQuery(query, resultProvider).template then([&future, this](qint64 queriedRevision) { - //TODO set revision in result provider? - //TODO update all existing results with new revision mResourceAccess->sendRevisionReplayedCommand(queriedRevision); future.setFinished(); }).exec(); @@ -249,27 +221,12 @@ public: mResourceAccess->open(); QObject::connect(mResourceAccess.data(), &Akonadi2::ResourceAccess::revisionChanged, runner.data(), &QueryRunner::revisionChanged); } + return KAsync::null(); //We have to capture the runner to keep it alive - return synchronizeResource(query).template then([runner](KAsync::Future &future) { - future.setFinished(); - }, - [](int error, const QString &errorString) { - Warning() << "Error during sync " << error << errorString; - }); } private: - KAsync::Job synchronizeResource(const Akonadi2::Query &query) - { - //TODO check if a sync is necessary - //TODO Only sync what was requested - //TODO timeout - if (query.syncOnDemand || query.processAll) { - return mResourceAccess->synchronizeResource(query.syncOnDemand, query.processAll); - } - return KAsync::null(); - } //TODO move into result provider? void replaySet(ResultSet &resultSet, const QSharedPointer > &resultProvider) @@ -280,17 +237,14 @@ private: Trace() << "Got creation"; //TODO Only copy in result provider resultProvider->add(Akonadi2::ApplicationDomain::ApplicationDomainType::getInMemoryRepresentation(*value).template staticCast()); - // modelResult->add(); break; case Akonadi2::Operation_Modification: Trace() << "Got modification"; resultProvider->modify(Akonadi2::ApplicationDomain::ApplicationDomainType::getInMemoryRepresentation(*value).template staticCast()); - // modelResult->modify(); break; case Akonadi2::Operation_Removal: Trace() << "Got removal"; resultProvider->remove(Akonadi2::ApplicationDomain::ApplicationDomainType::getInMemoryRepresentation(*value).template staticCast()); - // modelResult->remove(); break; } return true; @@ -320,13 +274,28 @@ private: }); } - ResultSet loadIncrementalResultSet(qint64 baseRevision, const Akonadi2::Query &query, Akonadi2::Storage::Transaction &transaction, QSet &remainingFilters) + ResultSet loadInitialResultSet(const QByteArray &parent, const Akonadi2::Query &query, Akonadi2::Storage::Transaction &transaction, QSet &remainingFilters) { + Trace() << "Fetching initial set for parent:" << parent; + //TODO + QSet appliedFilters; + auto resultSet = Akonadi2::ApplicationDomain::TypeImplementation::queryIndexes(query, mResourceInstanceIdentifier, appliedFilters, transaction); + remainingFilters = query.propertyFilter.keys().toSet() - appliedFilters; + + //We do a full scan if there were no indexes available to create the initial set. + if (appliedFilters.isEmpty()) { + //TODO this should be replaced by an index lookup as well + resultSet = fullScan(transaction, bufferTypeForDomainType()); + } + return resultSet; + } + ResultSet loadIncrementalResultSet(qint64 baseRevision, const Akonadi2::Query &query, Akonadi2::Storage::Transaction &transaction, QSet &remainingFilters) + { + Trace() << "Loading incremental result set starting from revision: " << baseRevision; const auto bufferType = bufferTypeForDomainType(); auto revisionCounter = QSharedPointer::create(baseRevision); - //TODO apply filter from index - return ResultSet([bufferType, revisionCounter, &transaction, this]() -> QByteArray { + return ResultSet([bufferType, revisionCounter, &transaction]() -> QByteArray { const qint64 topRevision = Akonadi2::Storage::maxRevision(transaction); //Spit out the revision keys one by one. while (*revisionCounter <= topRevision) { @@ -354,7 +323,7 @@ private: //Read through the source values and return whatever matches the filter std::function)> generator = [this, resultSetPtr, &transaction, filter, initialQuery](std::function callback) -> bool { while (resultSetPtr->next()) { - //TODO only necessary if we actually want to filter or neew the operation type (but not a big deal if we do it always I guess) + //readEntity is only necessary if we actually want to filter or know the operation type (but not a big deal if we do it always I guess) readEntity(transaction, resultSetPtr->id(), [this, filter, callback, initialQuery](const Akonadi2::ApplicationDomain::ApplicationDomainType::Ptr &domainObject, Akonadi2::Operation operation) { //Always remove removals, they probably don't match due to non-available properties if (filter(domainObject) || operation == Akonadi2::Operation_Removal) { @@ -374,6 +343,20 @@ private: return ResultSet(generator); } + + std::function getFilter(const QSet remainingFilters, const Akonadi2::Query &query) + { + return [remainingFilters, query](const Akonadi2::ApplicationDomain::ApplicationDomainType::Ptr &domainObject) -> bool { + for (const auto &filterProperty : remainingFilters) { + //TODO implement other comparison operators than equality + if (domainObject->getProperty(filterProperty) != query.propertyFilter.value(filterProperty)) { + return false; + } + } + return true; + }; + } + virtual KAsync::Job executeQuery(const Akonadi2::Query &query, const QSharedPointer > &resultProvider) { /* @@ -384,16 +367,6 @@ private: const qint64 baseRevision = resultProvider->revision() + 1; Trace() << "Running query " << baseRevision; QSet remainingFilters; - auto filter = [remainingFilters, query, baseRevision](const Akonadi2::ApplicationDomain::ApplicationDomainType::Ptr &domainObject) -> bool { - for (const auto &filterProperty : remainingFilters) { - //TODO implement other comparison operators than equality - if (domainObject->getProperty(filterProperty) != query.propertyFilter.value(filterProperty)) { - return false; - } - } - return true; - }; - qint64 newRevision = 0; Trace() << "Fetching updates"; Akonadi2::Storage storage(Akonadi2::storageLocation(), mResourceInstanceIdentifier); @@ -404,10 +377,10 @@ private: auto transaction = storage.createTransaction(Akonadi2::Storage::ReadOnly); auto resultSet = loadIncrementalResultSet(baseRevision, query, transaction, remainingFilters); - auto filteredSet = filterSet(resultSet, filter, transaction, false); + auto filteredSet = filterSet(resultSet, getFilter(remainingFilters, query), transaction, false); replaySet(filteredSet, resultProvider); resultProvider->setRevision(Akonadi2::Storage::maxRevision(transaction)); - newRevision = Akonadi2::Storage::maxRevision(transaction); + qint64 newRevision = Akonadi2::Storage::maxRevision(transaction); return KAsync::start([=]() -> qint64 { return newRevision; diff --git a/common/modelresult.h b/common/modelresult.h index 756f4d6..eabb868 100644 --- a/common/modelresult.h +++ b/common/modelresult.h @@ -111,6 +111,10 @@ public: { auto childId = qHash(value->identifier()); auto id = parentId(value); + //Ignore updates we get before the initial fetch is done + if (!mEntityChildrenFetched[id]) { + return; + } auto parent = createIndexFromId(id); qDebug() << "Added entity " << childId; const auto keys = mTree[id]; @@ -131,6 +135,10 @@ public: { auto childId = qHash(value->identifier()); auto id = parentId(value); + //Ignore updates we get before the initial fetch is done + if (!mEntityChildrenFetched[id]) { + return; + } auto parent = createIndexFromId(id); qDebug() << "Modified entity" << childId; auto i = mTree[id].indexOf(childId); diff --git a/common/resultprovider.h b/common/resultprovider.h index 43d21a4..0d23127 100644 --- a/common/resultprovider.h +++ b/common/resultprovider.h @@ -23,6 +23,9 @@ #include #include #include "threadboundary.h" +#include "resultset.h" +#include "log.h" +#include "modelresult.h" using namespace async; @@ -117,18 +120,6 @@ public: // mResultEmitter->clear(); } - // QSharedPointer > emitter() - // { - // if (!mResultEmitter) { - // //We have to go over a separate var and return that, otherwise we'd delete the emitter immediately again - // auto sharedPtr = QSharedPointer >(new ResultEmitter, [this](ResultEmitter *emitter){ done(); delete emitter; }); - // mResultEmitter = sharedPtr; - // return sharedPtr; - // } - // - // return mResultEmitter.toStrongRef(); - // } - /** * For lifetimemanagement only. * We keep the runner alive as long as the result provider exists. @@ -162,40 +153,6 @@ public: mQueryRunner = runner; } - // qint64 fetch(const ResultSet &resultSet) - // { - // //Fetch a bunch - // // - // // Akonadi2::Storage storage(Akonadi2::storageLocation(), mResourceInstanceIdentifier); - // // storage.setDefaultErrorHandler([](const Akonadi2::Storage::Error &error) { - // // Warning() << "Error during query: " << error.store << error.message; - // // }); - // // - // // auto transaction = storage.createTransaction(Akonadi2::Storage::ReadOnly); - // - // // Log() << "Querying" << baseRevision << Akonadi2::Storage::maxRevision(transaction); - // // auto resultSet = getResultSet(query, transaction, baseRevision); - // while (resultSet.next([this](const Akonadi2::ApplicationDomain::ApplicationDomainType::Ptr &value, Akonadi2::Operation operation) -> bool { - // switch (operation) { - // case Akonadi2::Operation_Creation: - // Trace() << "Got creation"; - // //TODO Only copy in result provider - // add(Akonadi2::ApplicationDomain::ApplicationDomainType::getInMemoryRepresentation(*value).template staticCast()); - // break; - // case Akonadi2::Operation_Modification: - // Trace() << "Got modification"; - // modify(Akonadi2::ApplicationDomain::ApplicationDomainType::getInMemoryRepresentation(*value).template staticCast()); - // break; - // case Akonadi2::Operation_Removal: - // Trace() << "Got removal"; - // remove(Akonadi2::ApplicationDomain::ApplicationDomainType::getInMemoryRepresentation(*value).template staticCast()); - // break; - // } - // return true; - // })){}; - // // return Akonadi2::Storage::maxRevision(transaction); - // } - private: void done() { @@ -212,129 +169,6 @@ private: std::function mOnDoneCallback; }; - - - - - -template -class SyncResultProvider : public ResultProviderInterface { -public: - void add(const T &value) - { - mResultEmitter->addHandler(value); - } - - void modify(const T &value) - { - mResultEmitter->modifyHandler(value); - } - - void remove(const T &value) - { - mResultEmitter->removeHandler(value); - } - - void initialResultSetComplete() - { - mResultEmitter->initialResultSetComplete(); - } - - void complete() - { - mResultEmitter->complete(); - } - - void clear() - { - mResultEmitter->clear(); - } - - QSharedPointer > emitter() - { - if (!mResultEmitter) { - //We have to go over a separate var and return that, otherwise we'd delete the emitter immediately again - auto sharedPtr = QSharedPointer >(new ResultEmitter, [this](ResultEmitter *emitter){ done(); delete emitter; }); - mResultEmitter = sharedPtr; - return sharedPtr; - } - - return mResultEmitter.toStrongRef(); - } - - /** - * For lifetimemanagement only. - * We keep the runner alive as long as the result provider exists. - */ - void setFacade(const std::shared_ptr &facade) - { - mFacade = facade; - } - - void onDone(const std::function &callback) - { - mOnDoneCallback = callback; - } - - bool isDone() const - { - //The existance of the emitter currently defines wether we're done or not. - return mResultEmitter.toStrongRef().isNull(); - } - - // qint64 fetch(const ResultSet &resultSet) - // { - // //Fetch a bunch - // // - // // Akonadi2::Storage storage(Akonadi2::storageLocation(), mResourceInstanceIdentifier); - // // storage.setDefaultErrorHandler([](const Akonadi2::Storage::Error &error) { - // // Warning() << "Error during query: " << error.store << error.message; - // // }); - // // - // // auto transaction = storage.createTransaction(Akonadi2::Storage::ReadOnly); - // - // // Log() << "Querying" << baseRevision << Akonadi2::Storage::maxRevision(transaction); - // // auto resultSet = getResultSet(query, transaction, baseRevision); - // while (resultSet.next([this](const Akonadi2::ApplicationDomain::ApplicationDomainType::Ptr &value, Akonadi2::Operation operation) -> bool { - // switch (operation) { - // case Akonadi2::Operation_Creation: - // Trace() << "Got creation"; - // //TODO Only copy in result provider - // add(Akonadi2::ApplicationDomain::ApplicationDomainType::getInMemoryRepresentation(*value).template staticCast()); - // break; - // case Akonadi2::Operation_Modification: - // Trace() << "Got modification"; - // modify(Akonadi2::ApplicationDomain::ApplicationDomainType::getInMemoryRepresentation(*value).template staticCast()); - // break; - // case Akonadi2::Operation_Removal: - // Trace() << "Got removal"; - // remove(Akonadi2::ApplicationDomain::ApplicationDomainType::getInMemoryRepresentation(*value).template staticCast()); - // break; - // } - // return true; - // })){}; - // // return Akonadi2::Storage::maxRevision(transaction); - // } - -private: - void done() - { - qWarning() << "done"; - if (mOnDoneCallback) { - mOnDoneCallback(); - mOnDoneCallback = std::function(); - } - } - - QWeakPointer > mResultEmitter; - std::shared_ptr mFacade; - std::function mOnDoneCallback; - QSharedPointer mThreadBoundary; -}; - - - - /* * The promise side for the result emitter */ @@ -434,18 +268,18 @@ public: } /** - * For lifetimemanagement only. - * We keep the runner alive as long as the result provider exists. - */ + * For lifetimemanagement only. + * We keep the runner alive as long as the result provider exists. + */ void setQueryRunner(const QSharedPointer &runner) { mQueryRunner = runner; } /** - * For lifetimemanagement only. - * We keep the runner alive as long as the result provider exists. - */ + * For lifetimemanagement only. + * We keep the runner alive as long as the result provider exists. + */ void setFacade(const std::shared_ptr &facade) { mFacade = facade; @@ -463,6 +297,11 @@ public: return mResultEmitter.toStrongRef().isNull(); } + void setFetcher(const std::function &fetcher) + { + fetcher(QByteArray()); + } + private: void done() { diff --git a/tests/dummyresourcetest.cpp b/tests/dummyresourcetest.cpp index d027266..3b90e6c 100644 --- a/tests/dummyresourcetest.cpp +++ b/tests/dummyresourcetest.cpp @@ -64,6 +64,9 @@ private Q_SLOTS: query.syncOnDemand = false; query.processAll = true; + //Ensure all local data is processed + Akonadi2::Store::synchronize(query).exec().waitForFinished(); + query.propertyFilter.insert("uid", "testuid"); async::SyncListResult result(Akonadi2::Store::load(query)); result.exec(); @@ -88,6 +91,9 @@ private Q_SLOTS: query.syncOnDemand = false; query.processAll = true; + //Ensure all local data is processed + Akonadi2::Store::synchronize(query).exec().waitForFinished(); + query.propertyFilter.insert("uid", "testuid"); async::SyncListResult result(Akonadi2::Store::load(query)); result.exec(); @@ -114,6 +120,9 @@ private Q_SLOTS: query.syncOnDemand = false; query.processAll = true; + //Ensure all local data is processed + Akonadi2::Store::synchronize(query).exec().waitForFinished(); + query.propertyFilter.insert("summary", "summaryValue2"); async::SyncListResult result(Akonadi2::Store::load(query)); result.exec(); @@ -145,6 +154,9 @@ private Q_SLOTS: query.syncOnDemand = true; query.processAll = true; + //Ensure all local data is processed + Akonadi2::Store::synchronize(query).exec().waitForFinished(); + async::SyncListResult result(Akonadi2::Store::load(query)); result.exec(); QVERIFY(!result.isEmpty()); @@ -160,6 +172,9 @@ private Q_SLOTS: query.syncOnDemand = true; query.processAll = true; + //Ensure all local data is processed + Akonadi2::Store::synchronize(query).exec().waitForFinished(); + async::SyncListResult result(Akonadi2::Store::load(query)); result.exec(); QVERIFY(!result.isEmpty()); @@ -182,6 +197,9 @@ private Q_SLOTS: query.processAll = true; query.propertyFilter.insert("uid", "testuid"); + //Ensure all local data is processed + Akonadi2::Store::synchronize(query).exec().waitForFinished(); + //Test create Akonadi2::ApplicationDomain::Event event2; { @@ -198,6 +216,9 @@ private Q_SLOTS: event2.setProperty("summary", "summaryValue2"); Akonadi2::Store::modify(event2).exec().waitForFinished(); + //Ensure all local data is processed + Akonadi2::Store::synchronize(query).exec().waitForFinished(); + //Test modify { async::SyncListResult result(Akonadi2::Store::load(query)); @@ -210,6 +231,9 @@ private Q_SLOTS: Akonadi2::Store::remove(event2).exec().waitForFinished(); + //Ensure all local data is processed + Akonadi2::Store::synchronize(query).exec().waitForFinished(); + //Test remove { async::SyncListResult result(Akonadi2::Store::load(query)); diff --git a/tests/querytest.cpp b/tests/querytest.cpp index 9f4b3bb..47d977b 100644 --- a/tests/querytest.cpp +++ b/tests/querytest.cpp @@ -57,11 +57,33 @@ private Q_SLOTS: query.syncOnDemand = false; query.processAll = true; - auto model = new ModelResult(query, QList() << "summary" << "uid"); + auto model = Akonadi2::Store::loadModel(query); model->fetchMore(QModelIndex()); QTRY_COMPARE(model->rowCount(), 1); } + void testSingleWithDelay() + { + //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; + query.liveQuery = true; + + auto model = Akonadi2::Store::loadModel(query); + QTest::qWait(200); + model->fetchMore(QModelIndex()); + QVERIFY(model->rowCount() < 2); + QTRY_COMPARE(model->rowCount(), 1); + } + // void testTree() // { // //Setup diff --git a/tests/testimplementations.h b/tests/testimplementations.h index eee78b0..1436c68 100644 --- a/tests/testimplementations.h +++ b/tests/testimplementations.h @@ -85,8 +85,8 @@ public Q_SLOTS: class TestResourceFacade : public Akonadi2::GenericFacade { public: - TestResourceFacade(const QByteArray &instanceIdentifier, const QSharedPointer > storage, const QSharedPointer resourceAccess) - : Akonadi2::GenericFacade(instanceIdentifier, QSharedPointer::create(), storage, resourceAccess) + TestResourceFacade(const QByteArray &instanceIdentifier, const QSharedPointer resourceAccess) + : Akonadi2::GenericFacade(instanceIdentifier, QSharedPointer::create(), resourceAccess) { } -- cgit v1.2.3 From 25b08ded08e7b581215a4e89f43c4a1509c13f4a Mon Sep 17 00:00:00 2001 From: Christian Mollekopf Date: Sat, 14 Nov 2015 00:06:40 +0100 Subject: Query test adjustments --- tests/querytest.cpp | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/tests/querytest.cpp b/tests/querytest.cpp index 47d977b..e4f1d0d 100644 --- a/tests/querytest.cpp +++ b/tests/querytest.cpp @@ -55,8 +55,10 @@ private Q_SLOTS: Akonadi2::Query query; query.resources << "org.kde.dummy.instance1"; query.syncOnDemand = false; - query.processAll = true; + query.processAll = false; + query.liveQuery = true; + //We fetch before the data is available and rely on the live query mechanism to deliver the actual data auto model = Akonadi2::Store::loadModel(query); model->fetchMore(QModelIndex()); QTRY_COMPARE(model->rowCount(), 1); @@ -75,10 +77,14 @@ private Q_SLOTS: query.resources << "org.kde.dummy.instance1"; query.syncOnDemand = false; query.processAll = true; - query.liveQuery = true; + query.liveQuery = false; + //We fetch after the data is available and don't rely on the live query mechanism to deliver the actual data auto model = Akonadi2::Store::loadModel(query); - QTest::qWait(200); + + //Ensure all local data is processed + Akonadi2::Store::synchronize(query).exec().waitForFinished(); + model->fetchMore(QModelIndex()); QVERIFY(model->rowCount() < 2); QTRY_COMPARE(model->rowCount(), 1); -- cgit v1.2.3 From ec902175d53d371b03d8e754d917e196cd15aafa Mon Sep 17 00:00:00 2001 From: Christian Mollekopf Date: Sun, 15 Nov 2015 09:46:27 +0100 Subject: Fixed clientapitest --- tests/clientapitest.cpp | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/tests/clientapitest.cpp b/tests/clientapitest.cpp index 2b3cc46..2ce64d3 100644 --- a/tests/clientapitest.cpp +++ b/tests/clientapitest.cpp @@ -30,14 +30,15 @@ public: 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->setFetcher([query, resultProvider, this](const QByteArray &) { + for (const auto &res : results) { 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); } } }); + return KAsync::null(); } QList results; @@ -135,7 +136,7 @@ private Q_SLOTS: query.resources << "dummyresource.instance1"; query.liveQuery = false; - auto model = new ModelResult(query, QList() << "summary" << "uid"); + auto model = Akonadi2::Store::loadModel(query); model->fetchMore(QModelIndex()); QTRY_COMPARE(model->rowCount(), 1); } @@ -154,7 +155,7 @@ private Q_SLOTS: query.resources << "dummyresource.instance1"; query.liveQuery = false; - auto model = new ModelResult(query, QList() << "summary" << "uid"); + auto model = Akonadi2::Store::loadModel(query); model->fetchMore(QModelIndex()); QTRY_COMPARE(model->rowCount(), 1); model->fetchMore(model->index(0, 0)); -- cgit v1.2.3 From d4b10a3de396eebc6c815093e9e1725ece270e9e Mon Sep 17 00:00:00 2001 From: Christian Mollekopf Date: Sun, 15 Nov 2015 11:09:31 +0100 Subject: Working folder tree query --- common/domain/folder.cpp | 28 +++++++++ common/modelresult.h | 5 +- examples/dummyresource/resourcefactory.cpp | 6 ++ tests/querytest.cpp | 98 ++++++++++++++++++++---------- 4 files changed, 102 insertions(+), 35 deletions(-) diff --git a/common/domain/folder.cpp b/common/domain/folder.cpp index 50f73c2..82f6c1f 100644 --- a/common/domain/folder.cpp +++ b/common/domain/folder.cpp @@ -37,15 +37,43 @@ using namespace Akonadi2::ApplicationDomain; ResultSet TypeImplementation::queryIndexes(const Akonadi2::Query &query, const QByteArray &resourceInstanceIdentifier, QSet &appliedFilters, Akonadi2::Storage::Transaction &transaction) { QVector keys; + if (query.propertyFilter.contains("parent")) { + Index index("folder.index.parent", transaction); + auto lookupKey = query.propertyFilter.value("parent").toByteArray(); + if (lookupKey.isEmpty()) { + lookupKey = "toplevel"; + } + index.lookup(lookupKey, [&](const QByteArray &value) { + keys << value; + }, + [](const Index::Error &error) { + Warning() << "Error in uid index: " << error.message; + }); + appliedFilters << "parent"; + } + Trace() << "Index lookup found " << keys.size() << " keys."; return ResultSet(keys); } void TypeImplementation::index(const QByteArray &identifier, const BufferAdaptor &bufferAdaptor, Akonadi2::Storage::Transaction &transaction) { + const auto parent = bufferAdaptor.getProperty("parent"); + Trace() << "indexing " << identifier << " with parent " << parent.toByteArray(); + if (parent.isValid()) { + Index("folder.index.parent", transaction).add(parent.toByteArray(), identifier); + } else { + Index("folder.index.parent", transaction).add("toplevel", identifier); + } } void TypeImplementation::removeIndex(const QByteArray &identifier, const BufferAdaptor &bufferAdaptor, Akonadi2::Storage::Transaction &transaction) { + const auto parent = bufferAdaptor.getProperty("parent"); + if (parent.isValid()) { + Index("folder.index.parent", transaction).remove(parent.toByteArray(), identifier); + } else { + Index("folder.index.parent", transaction).remove("toplevel", identifier); + } } QSharedPointer::Buffer> > TypeImplementation::initializeReadPropertyMapper() diff --git a/common/modelresult.h b/common/modelresult.h index eabb868..8ca6daa 100644 --- a/common/modelresult.h +++ b/common/modelresult.h @@ -116,7 +116,7 @@ public: return; } auto parent = createIndexFromId(id); - qDebug() << "Added entity " << childId; + qDebug() << "Added entity " << childId << value->identifier(); const auto keys = mTree[id]; int index = 0; for (; index < keys.size(); index++) { @@ -166,7 +166,6 @@ public: void fetchEntities(const QModelIndex &parent) { - qDebug() << "Fetching entities"; const auto id = getIdentifier(parent); mEntityChildrenFetched[id] = true; QByteArray parentIdentifier; @@ -178,7 +177,7 @@ public: Q_ASSERT(object); parentIdentifier = object->identifier(); } - Trace() << "Loading entities"; + Trace() << "Loading child entities of: " << parentIdentifier; loadEntities(parentIdentifier); } diff --git a/examples/dummyresource/resourcefactory.cpp b/examples/dummyresource/resourcefactory.cpp index edb3b42..bed1d71 100644 --- a/examples/dummyresource/resourcefactory.cpp +++ b/examples/dummyresource/resourcefactory.cpp @@ -114,6 +114,12 @@ DummyResource::DummyResource(const QByteArray &instanceIdentifier, const QShared mPipeline->setPreprocessors(ENTITY_TYPE_MAIL, QVector() << mailIndexer); mPipeline->setAdaptorFactory(ENTITY_TYPE_MAIL, mailFactory); } + { + auto folderFactory = QSharedPointer::create(); + auto folderIndexer = new IndexUpdater("folder.index.rid", ENTITY_TYPE_FOLDER); + mPipeline->setPreprocessors(ENTITY_TYPE_FOLDER, QVector() << folderIndexer); + mPipeline->setAdaptorFactory(ENTITY_TYPE_FOLDER, folderFactory); + } } void DummyResource::createEvent(const QByteArray &ridBuffer, const QMap &data, flatbuffers::FlatBufferBuilder &entityFbb) diff --git a/tests/querytest.cpp b/tests/querytest.cpp index e4f1d0d..677dbac 100644 --- a/tests/querytest.cpp +++ b/tests/querytest.cpp @@ -90,38 +90,72 @@ private Q_SLOTS: 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); - // } + void testFolder() + { + //Setup + { + Akonadi2::ApplicationDomain::Folder folder("org.kde.dummy.instance1"); + Akonadi2::Store::create(folder).exec().waitForFinished(); + } + + //Test + Akonadi2::Query query; + query.resources << "org.kde.dummy.instance1"; + query.syncOnDemand = false; + query.processAll = false; + query.liveQuery = true; + + //We fetch before the data is available and rely on the live query mechanism to deliver the actual data + auto model = Akonadi2::Store::loadModel(query); + model->fetchMore(QModelIndex()); + QTRY_COMPARE(model->rowCount(), 1); + auto folderEntity = model->index(0, 0).data(ModelResult::DomainObjectRole).value(); + QVERIFY(!folderEntity->identifier().isEmpty()); + } + + void testFolderTree() + { + //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; + + //Ensure all local data is processed + Akonadi2::Store::synchronize(query).exec().waitForFinished(); + + auto model = Akonadi2::Store::loadModel(query); + model->fetchMore(QModelIndex()); + QTRY_COMPARE(model->rowCount(), 1); + + auto folderEntity = model->index(0, 0).data(ModelResult::DomainObjectRole).value(); + QVERIFY(!folderEntity->identifier().isEmpty()); + + 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; + + //Ensure all local data is processed + Akonadi2::Store::synchronize(query).exec().waitForFinished(); + + //We fetch after the data is available and don't rely on the live query mechanism to deliver the actual data + auto model = Akonadi2::Store::loadModel(query); + model->fetchMore(QModelIndex()); + QTRY_COMPARE(model->rowCount(), 1); + model->fetchMore(model->index(0, 0)); + QTRY_COMPARE(model->rowCount(model->index(0, 0)), 1); + } }; QTEST_MAIN(QueryTest) -- cgit v1.2.3 From 972f3a4e96876e4c36162a11062e40863d88a2a1 Mon Sep 17 00:00:00 2001 From: Christian Mollekopf Date: Sun, 15 Nov 2015 12:46:26 +0100 Subject: Cleanup --- common/CMakeLists.txt | 1 - common/entitystorage.cpp | 74 ----------------------- common/entitystorage.h | 126 --------------------------------------- common/facade.h | 3 +- common/resourceaccess.cpp | 2 +- tests/genericfacadebenchmark.cpp | 3 +- tests/genericfacadetest.cpp | 39 ++++++------ tests/testimplementations.h | 26 -------- 8 files changed, 22 insertions(+), 252 deletions(-) delete mode 100644 common/entitystorage.cpp delete mode 100644 common/entitystorage.h diff --git a/common/CMakeLists.txt b/common/CMakeLists.txt index f24ec46..bdb9eac 100644 --- a/common/CMakeLists.txt +++ b/common/CMakeLists.txt @@ -15,7 +15,6 @@ set(command_SRCS definitions.cpp log.cpp entitybuffer.cpp - entitystorage.cpp clientapi.cpp facadefactory.cpp commands.cpp diff --git a/common/entitystorage.cpp b/common/entitystorage.cpp deleted file mode 100644 index 5d4df9f..0000000 --- a/common/entitystorage.cpp +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Copyright (C) 2014 Christian Mollekopf - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * This program 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 General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program; if not, write to the - * Free Software Foundation, Inc., - * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - */ - -#include "entitystorage.h" - -ResultSet EntityStorageBase::filteredSet(const ResultSet &resultSet, const std::function &filter, const Akonadi2::Storage::Transaction &transaction, bool initialQuery) -{ - auto resultSetPtr = QSharedPointer::create(resultSet); - - //Read through the source values and return whatever matches the filter - std::function)> generator = [this, resultSetPtr, &transaction, filter, initialQuery](std::function callback) -> bool { - while (resultSetPtr->next()) { - readEntity(transaction, resultSetPtr->id(), [this, filter, callback, initialQuery](const Akonadi2::ApplicationDomain::ApplicationDomainType::Ptr &domainObject, Akonadi2::Operation operation) { - //Always remove removals, they probably don't match due to non-available properties - if (filter(domainObject) || operation == Akonadi2::Operation_Removal) { - if (initialQuery) { - //We're not interested in removals during the initial query - if (operation != Akonadi2::Operation_Removal) { - callback(domainObject, Akonadi2::Operation_Creation); - } - } else { - callback(domainObject, operation); - } - } - }); - } - return false; - }; - return ResultSet(generator); -} - - -ResultSet EntityStorageBase::getResultSet(const Akonadi2::Query &query, Akonadi2::Storage::Transaction &transaction, qint64 baseRevision) -{ - QSet remainingFilters = query.propertyFilter.keys().toSet(); - ResultSet resultSet; - const bool initialQuery = (baseRevision == 1); - if (initialQuery) { - Trace() << "Initial result set update"; - resultSet = loadInitialResultSet(query, transaction, remainingFilters); - } else { - //TODO fallback in case the old revision is no longer available to clear + redo complete initial scan - Trace() << "Incremental result set update" << baseRevision; - resultSet = loadIncrementalResultSet(baseRevision, query, transaction, remainingFilters); - } - - auto filter = [remainingFilters, query, baseRevision](const Akonadi2::ApplicationDomain::ApplicationDomainType::Ptr &domainObject) -> bool { - for (const auto &filterProperty : remainingFilters) { - //TODO implement other comparison operators than equality - if (domainObject->getProperty(filterProperty) != query.propertyFilter.value(filterProperty)) { - return false; - } - } - return true; - }; - - return filteredSet(resultSet, filter, transaction, initialQuery); -} diff --git a/common/entitystorage.h b/common/entitystorage.h deleted file mode 100644 index 8e73083..0000000 --- a/common/entitystorage.h +++ /dev/null @@ -1,126 +0,0 @@ -/* - * Copyright (C) 2014 Christian Mollekopf - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * This program 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 General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program; if not, write to the - * Free Software Foundation, Inc., - * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - */ -#pragma once - -#include - -#include "query.h" -#include "domainadaptor.h" -#include "entitybuffer.h" -#include "log.h" -#include "storage.h" -#include "resultset.h" -#include "resultprovider.h" -#include "definitions.h" - -/** - * Wraps storage, entity adaptor factory and indexes into one. - * - */ -class EntityStorageBase -{ -public: - typedef std::function &remainingFilters)> InitialResultLoader; - typedef std::function &remainingFilters)> IncrementalResultLoader; - typedef std::function &resultCallback)> EntityReader; - - /** - * Returns the initial result set that still needs to be filtered. - * - * To make this efficient indexes should be chosen that are as selective as possible. - */ - InitialResultLoader loadInitialResultSet; - /** - * Returns the incremental result set that still needs to be filtered. - */ - IncrementalResultLoader loadIncrementalResultSet; - - /** - * Loads a single entity by uid from storage. - */ - EntityReader readEntity; - -protected: - EntityStorageBase(const QByteArray &instanceIdentifier) - : mResourceInstanceIdentifier(instanceIdentifier) - { - - } - - virtual Akonadi2::ApplicationDomain::ApplicationDomainType::Ptr copy(const Akonadi2::ApplicationDomain::ApplicationDomainType &) = 0; - - ResultSet getResultSet(const Akonadi2::Query &query, Akonadi2::Storage::Transaction &transaction, qint64 baseRevision); - - QByteArray mResourceInstanceIdentifier; - -private: - ResultSet filteredSet(const ResultSet &resultSet, const std::function &filter, const Akonadi2::Storage::Transaction &transaction, bool isInitialQuery); -}; - -template -class EntityStorage : public EntityStorageBase -{ - -public: - - EntityStorage(const QByteArray &instanceIdentifier) - : EntityStorageBase(instanceIdentifier) - { - } - -protected: - Akonadi2::ApplicationDomain::ApplicationDomainType::Ptr copy(const Akonadi2::ApplicationDomain::ApplicationDomainType &object) Q_DECL_OVERRIDE - { - return Akonadi2::ApplicationDomain::ApplicationDomainType::getInMemoryRepresentation(object); - } - -public: - - virtual qint64 read(const Akonadi2::Query &query, qint64 baseRevision, const QSharedPointer > &resultProvider) - { - Akonadi2::Storage storage(Akonadi2::storageLocation(), mResourceInstanceIdentifier); - storage.setDefaultErrorHandler([](const Akonadi2::Storage::Error &error) { - Warning() << "Error during query: " << error.store << error.message; - }); - - auto transaction = storage.createTransaction(Akonadi2::Storage::ReadOnly); - - Log() << "Querying" << baseRevision << Akonadi2::Storage::maxRevision(transaction); - auto resultSet = getResultSet(query, transaction, baseRevision); - while(resultSet.next([this, resultProvider](const Akonadi2::ApplicationDomain::ApplicationDomainType::Ptr &value, Akonadi2::Operation operation) -> bool { - switch (operation) { - case Akonadi2::Operation_Creation: - Trace() << "Got creation"; - resultProvider->add(copy(*value).template staticCast()); - break; - case Akonadi2::Operation_Modification: - Trace() << "Got modification"; - resultProvider->modify(copy(*value).template staticCast()); - break; - case Akonadi2::Operation_Removal: - Trace() << "Got removal"; - resultProvider->remove(copy(*value).template staticCast()); - break; - } - return true; - })){}; - return Akonadi2::Storage::maxRevision(transaction); - } - -}; diff --git a/common/facade.h b/common/facade.h index 5be1c73..6e45e08 100644 --- a/common/facade.h +++ b/common/facade.h @@ -29,7 +29,8 @@ #include "domainadaptor.h" #include "log.h" #include "resultset.h" -#include "entitystorage.h" +#include "storage.h" +#include "definitions.h" /** * A QueryRunner runs a query and updates the corresponding result set. diff --git a/common/resourceaccess.cpp b/common/resourceaccess.cpp index bd9e2c9..88f785f 100644 --- a/common/resourceaccess.cpp +++ b/common/resourceaccess.cpp @@ -340,7 +340,7 @@ KAsync::Job ResourceAccess::sendRevisionReplayedCommand(qint64 revision) void ResourceAccess::open() { if (d->socket && d->socket->isValid()) { - log("Socket valid, so not opening again"); + Trace() << "Socket valid, so not opening again"; return; } if (d->openingSocket) { diff --git a/tests/genericfacadebenchmark.cpp b/tests/genericfacadebenchmark.cpp index 29c91d7..94d6f41 100644 --- a/tests/genericfacadebenchmark.cpp +++ b/tests/genericfacadebenchmark.cpp @@ -56,8 +56,7 @@ private Q_SLOTS: QBENCHMARK { auto resultSet = QSharedPointer >::create(); auto resourceAccess = QSharedPointer::create(); - auto storage = QSharedPointer >::create("identifier"); - TestResourceFacade facade(identifier, storage, resourceAccess); + TestResourceFacade facade(identifier, resourceAccess); async::SyncListResult result(resultSet->emitter()); diff --git a/tests/genericfacadetest.cpp b/tests/genericfacadetest.cpp index 67320c3..9e7500f 100644 --- a/tests/genericfacadetest.cpp +++ b/tests/genericfacadetest.cpp @@ -17,6 +17,7 @@ * Test for the generic facade implementation. * * This test doesn't use the actual storage and thus only tests the update logic of the facade. + * //FIXME this now uses the actual storage */ class GenericFacadeTest : public QObject { @@ -34,10 +35,9 @@ private Q_SLOTS: query.liveQuery = false; auto resultSet = QSharedPointer >::create(); - auto storage = QSharedPointer::create("identifier"); auto resourceAccess = QSharedPointer::create(); - storage->mResults << Akonadi2::ApplicationDomain::Event::Ptr::create(); - TestResourceFacade facade("identifier", storage, resourceAccess); + // storage->mResults << Akonadi2::ApplicationDomain::Event::Ptr::create(); + TestResourceFacade facade("identifier", resourceAccess); async::SyncListResult result(resultSet->emitter()); @@ -56,10 +56,9 @@ private Q_SLOTS: query.liveQuery = true; auto resultSet = QSharedPointer >::create(); - auto storage = QSharedPointer::create("identifier"); auto resourceAccess = QSharedPointer::create(); - storage->mResults << Akonadi2::ApplicationDomain::Event::Ptr::create(); - TestResourceFacade facade("identifier", storage, resourceAccess); + // storage->mResults << Akonadi2::ApplicationDomain::Event::Ptr::create(); + TestResourceFacade facade("identifier", resourceAccess); async::SyncListResult result(resultSet->emitter()); @@ -70,9 +69,9 @@ private Q_SLOTS: QCOMPARE(result.size(), 1); //Enter a second result - storage->mResults.clear(); - storage->mResults << QSharedPointer::create("resource", "id2", 0, QSharedPointer()); - storage->mLatestRevision = 2; + // storage->mResults.clear(); + // storage->mResults << QSharedPointer::create("resource", "id2", 0, QSharedPointer()); + // storage->mLatestRevision = 2; resourceAccess->emit revisionChanged(2); //Hack to get event loop in synclistresult to abort again @@ -88,12 +87,11 @@ private Q_SLOTS: query.liveQuery = true; auto resultSet = QSharedPointer >::create(); - auto storage = QSharedPointer::create("identifier"); auto resourceAccess = QSharedPointer::create(); auto entity = QSharedPointer::create("resource", "id2", 0, QSharedPointer::create()); entity->setProperty("test", "test1"); - storage->mResults << entity; - TestResourceFacade facade("identifier", storage, resourceAccess); + // storage->mResults << entity; + TestResourceFacade facade("identifier", resourceAccess); async::SyncListResult result(resultSet->emitter()); @@ -104,11 +102,11 @@ private Q_SLOTS: QCOMPARE(result.size(), 1); //Modify the entity again - storage->mResults.clear(); + // storage->mResults.clear(); entity = QSharedPointer::create("resource", "id2", 0, QSharedPointer::create()); entity->setProperty("test", "test2"); - storage->mModifications << entity; - storage->mLatestRevision = 2; + // storage->mModifications << entity; + // storage->mLatestRevision = 2; resourceAccess->emit revisionChanged(2); //Hack to get event loop in synclistresult to abort again @@ -125,11 +123,10 @@ private Q_SLOTS: query.liveQuery = true; auto resultSet = QSharedPointer >::create(); - auto storage = QSharedPointer::create("identifier"); auto resourceAccess = QSharedPointer::create(); auto entity = QSharedPointer::create("resource", "id2", 0, QSharedPointer()); - storage->mResults << entity; - TestResourceFacade facade("identifier", storage, resourceAccess); + // storage->mResults << entity; + TestResourceFacade facade("identifier", resourceAccess); async::SyncListResult result(resultSet->emitter()); @@ -140,9 +137,9 @@ private Q_SLOTS: QCOMPARE(result.size(), 1); //Remove the entity again - storage->mResults.clear(); - storage->mRemovals << entity; - storage->mLatestRevision = 2; + // storage->mResults.clear(); + // storage->mRemovals << entity; + // storage->mLatestRevision = 2; resourceAccess->emit revisionChanged(2); //Hack to get event loop in synclistresult to abort again diff --git a/tests/testimplementations.h b/tests/testimplementations.h index 1436c68..c371456 100644 --- a/tests/testimplementations.h +++ b/tests/testimplementations.h @@ -21,13 +21,11 @@ #include -#include #include #include #include #include #include -#include #include //Replace with something different @@ -44,30 +42,6 @@ public: virtual ~TestEventAdaptorFactory() {}; }; -class TestEntityStorage : public EntityStorage -{ -public: - using EntityStorage::EntityStorage; - virtual qint64 read(const Akonadi2::Query &query, qint64 oldRevision, const QSharedPointer > &resultProvider) Q_DECL_OVERRIDE - { - for (const auto &res : mResults) { - resultProvider->add(res); - } - for (const auto &res : mModifications) { - resultProvider->modify(res); - } - for (const auto &res : mRemovals) { - resultProvider->remove(res); - } - return mLatestRevision; - } - - QList mResults; - QList mModifications; - QList mRemovals; - qint64 mLatestRevision; -}; - class TestResourceAccess : public Akonadi2::ResourceAccessInterface { Q_OBJECT -- cgit v1.2.3 From b68a67fdbe0eb73aaef648ceb686824c7fbc1552 Mon Sep 17 00:00:00 2001 From: Christian Mollekopf Date: Tue, 17 Nov 2015 09:43:36 +0100 Subject: Facade cleanup --- common/facade.h | 91 ++++++++++++++++++++++++++------------------------------- 1 file changed, 41 insertions(+), 50 deletions(-) diff --git a/common/facade.h b/common/facade.h index 6e45e08..dcbe589 100644 --- a/common/facade.h +++ b/common/facade.h @@ -172,45 +172,31 @@ public: //TODO JOBAPI return job from sync continuation to execute it as subjob? KAsync::Job load(const Akonadi2::Query &query, const QSharedPointer > &resultProvider) Q_DECL_OVERRIDE { - auto fetchEntities = [this, query, resultProvider](const QByteArray &parent) { - Trace() << "Fetching initial set for parent:" << parent; - - Akonadi2::Storage storage(Akonadi2::storageLocation(), mResourceInstanceIdentifier); - storage.setDefaultErrorHandler([](const Akonadi2::Storage::Error &error) { - Warning() << "Error during query: " << error.store << error.message; - }); - - auto transaction = storage.createTransaction(Akonadi2::Storage::ReadOnly); - - auto modifiedQuery = query; - modifiedQuery.propertyFilter.insert("parent", parent); + QWeakPointer > weakResultProvider = resultProvider; - QSet remainingFilters; - auto resultSet = loadInitialResultSet(parent, modifiedQuery, transaction, remainingFilters); - auto filteredSet = filterSet(resultSet, getFilter(remainingFilters, query), transaction, true); - replaySet(filteredSet, resultProvider); - const qint64 newRevision = Akonadi2::Storage::maxRevision(transaction); - resultProvider->setRevision(newRevision); - mResourceAccess->sendRevisionReplayedCommand(newRevision); - }; - resultProvider->setFetcher(fetchEntities); + //We delegate loading of initial data to the result provider, os it can decide for itself what it needs to load. + resultProvider->setFetcher([this, query, weakResultProvider](const QByteArray &parent) { + if (auto resultProvider = weakResultProvider.toStrongRef()) { + const qint64 newRevision = executeInitialQuery(query, parent, resultProvider); + mResourceAccess->sendRevisionReplayedCommand(newRevision); + } else { + Warning() << "Tried executing query after result provider is already gone"; + } + }); auto runner = QSharedPointer::create(query); - QWeakPointer > weakResultProvider = resultProvider; + //Incremental updates are always loaded directly, leaving it up to the result to discard the changes if they are not interesting runner->setQuery([this, weakResultProvider, query] () -> KAsync::Job { return KAsync::start([this, weakResultProvider, query](KAsync::Future &future) { Trace() << "Executing query "; - auto resultProvider = weakResultProvider.toStrongRef(); - if (!resultProvider) { + if (auto resultProvider = weakResultProvider.toStrongRef()) { + const qint64 newRevision = executeIncrementalQuery(query, resultProvider); + mResourceAccess->sendRevisionReplayedCommand(newRevision); + } else { Warning() << "Tried executing query after result provider is already gone"; future.setError(0, QString()); - future.setFinished(); - return; } - executeQuery(query, resultProvider).template then([&future, this](qint64 queriedRevision) { - mResourceAccess->sendRevisionReplayedCommand(queriedRevision); - future.setFinished(); - }).exec(); + future.setFinished(); }); }); @@ -230,13 +216,12 @@ public: private: //TODO move into result provider? - void replaySet(ResultSet &resultSet, const QSharedPointer > &resultProvider) + static void replaySet(ResultSet &resultSet, const QSharedPointer > &resultProvider) { - while (resultSet.next([this, resultProvider](const Akonadi2::ApplicationDomain::ApplicationDomainType::Ptr &value, Akonadi2::Operation operation) -> bool { + while (resultSet.next([resultProvider](const Akonadi2::ApplicationDomain::ApplicationDomainType::Ptr &value, Akonadi2::Operation operation) -> bool { switch (operation) { case Akonadi2::Operation_Creation: Trace() << "Got creation"; - //TODO Only copy in result provider resultProvider->add(Akonadi2::ApplicationDomain::ApplicationDomainType::getInMemoryRepresentation(*value).template staticCast()); break; case Akonadi2::Operation_Modification: @@ -358,34 +343,40 @@ private: }; } - virtual KAsync::Job executeQuery(const Akonadi2::Query &query, const QSharedPointer > &resultProvider) + qint64 load(const Akonadi2::Query &query, const std::function &)> &baseSetRetriever, const QSharedPointer > &resultProvider) { - /* - * This method gets called initially, and after every revision change. - * * We delegate loading of initial data to the result provider, os it can decide for itself what it needs to load. - * * Incremental updates are loaded directly, leaving it up to the model to discard the changes if they are not interesting - */ - const qint64 baseRevision = resultProvider->revision() + 1; - Trace() << "Running query " << baseRevision; - QSet remainingFilters; - - Trace() << "Fetching updates"; Akonadi2::Storage storage(Akonadi2::storageLocation(), mResourceInstanceIdentifier); storage.setDefaultErrorHandler([](const Akonadi2::Storage::Error &error) { Warning() << "Error during query: " << error.store << error.message; }); - auto transaction = storage.createTransaction(Akonadi2::Storage::ReadOnly); - auto resultSet = loadIncrementalResultSet(baseRevision, query, transaction, remainingFilters); + QSet remainingFilters; + auto resultSet = baseSetRetriever(transaction, remainingFilters); auto filteredSet = filterSet(resultSet, getFilter(remainingFilters, query), transaction, false); replaySet(filteredSet, resultProvider); resultProvider->setRevision(Akonadi2::Storage::maxRevision(transaction)); - qint64 newRevision = Akonadi2::Storage::maxRevision(transaction); + return Akonadi2::Storage::maxRevision(transaction); + } - return KAsync::start([=]() -> qint64 { - return newRevision; - }); + + qint64 executeIncrementalQuery(const Akonadi2::Query &query, const QSharedPointer > &resultProvider) + { + const qint64 baseRevision = resultProvider->revision() + 1; + Trace() << "Running incremental query " << baseRevision; + return load(query, [&](Akonadi2::Storage::Transaction &transaction, QSet &remainingFilters) -> ResultSet { + return loadIncrementalResultSet(baseRevision, query, transaction, remainingFilters); + }, resultProvider); + } + + qint64 executeInitialQuery(const Akonadi2::Query &query, const QByteArray &parent, const QSharedPointer > &resultProvider) + { + Trace() << "Running initial query for parent:" << parent; + auto modifiedQuery = query; + modifiedQuery.propertyFilter.insert("parent", parent); + return load(modifiedQuery, [&](Akonadi2::Storage::Transaction &transaction, QSet &remainingFilters) -> ResultSet { + return loadInitialResultSet(parent, modifiedQuery, transaction, remainingFilters); + }, resultProvider); } protected: -- cgit v1.2.3 From 0f24357d01bd8a278f03793db863d3f71ac37ef2 Mon Sep 17 00:00:00 2001 From: Christian Mollekopf Date: Wed, 18 Nov 2015 00:51:55 +0100 Subject: Don't use a smart pointer for the result provider We're not doing any lifetime management anyways. --- common/clientapi.h | 14 ++++-- common/facade.h | 77 ++++++++++++++----------------- common/facadeinterface.h | 4 +- common/resourcefacade.cpp | 10 ++-- common/resourcefacade.h | 2 +- examples/dummyresource/resourcefacade.cpp | 6 +-- tests/clientapitest.cpp | 12 ++--- tests/genericfacadebenchmark.cpp | 2 +- tests/genericfacadetest.cpp | 8 ++-- 9 files changed, 66 insertions(+), 69 deletions(-) diff --git a/common/clientapi.h b/common/clientapi.h index a424424..707e81d 100644 --- a/common/clientapi.h +++ b/common/clientapi.h @@ -35,6 +35,8 @@ #include "facadefactory.h" #include "log.h" +Q_DECLARE_METATYPE(std::shared_ptr); + namespace async { //This should abstract if we execute from eventloop or in thread. //It supposed to allow the caller to finish the current method before executing the runner. @@ -75,9 +77,8 @@ public: // Query all resources and aggregate results KAsync::iterate(getResources(query.resources, ApplicationDomain::getTypeName())) .template each([query, resultSet](const QByteArray &resource, KAsync::Future &future) { - auto facade = FacadeFactory::instance().getFacade(resourceName(resource), resource); - if (facade) { - facade->load(query, resultSet).template then([&future](){future.setFinished();}).exec(); + if (auto facade = FacadeFactory::instance().getFacade(resourceName(resource), resource)) { + facade->load(query, *resultSet).template then([&future](){future.setFinished();}).exec(); //Keep the facade alive for the lifetime of the resultSet. resultSet->setFacade(facade); } else { @@ -106,15 +107,18 @@ public: static QSharedPointer loadModel(Query query) { auto model = QSharedPointer >::create(query, QList() << "summary" << "uid"); - auto resultProvider = QSharedPointer >::create(model); + auto resultProvider = std::make_shared >(model); + //Keep the resultprovider alive for as long as the model lives + model->setProperty("resultProvider", QVariant::fromValue(std::shared_ptr(resultProvider))); // Query all resources and aggregate results KAsync::iterate(getResources(query.resources, ApplicationDomain::getTypeName())) .template each([query, resultProvider](const QByteArray &resource, KAsync::Future &future) { auto facade = FacadeFactory::instance().getFacade(resourceName(resource), resource); if (facade) { - facade->load(query, resultProvider).template then([&future](){future.setFinished();}).exec(); + facade->load(query, *resultProvider).template then([&future](){future.setFinished();}).exec(); //Keep the facade alive for the lifetime of the resultSet. + //FIXME this would have to become a list resultProvider->setFacade(facade); } else { //Ignore the error and carry on diff --git a/common/facade.h b/common/facade.h index dcbe589..82fd5ff 100644 --- a/common/facade.h +++ b/common/facade.h @@ -169,68 +169,54 @@ public: return mResourceAccess->sendDeleteCommand(domainObject.identifier(), domainObject.revision(), bufferTypeForDomainType()); } - //TODO JOBAPI return job from sync continuation to execute it as subjob? - KAsync::Job load(const Akonadi2::Query &query, const QSharedPointer > &resultProvider) Q_DECL_OVERRIDE + KAsync::Job load(const Akonadi2::Query &query, Akonadi2::ResultProviderInterface &resultProvider) Q_DECL_OVERRIDE { - QWeakPointer > weakResultProvider = resultProvider; - //We delegate loading of initial data to the result provider, os it can decide for itself what it needs to load. - resultProvider->setFetcher([this, query, weakResultProvider](const QByteArray &parent) { - if (auto resultProvider = weakResultProvider.toStrongRef()) { - const qint64 newRevision = executeInitialQuery(query, parent, resultProvider); - mResourceAccess->sendRevisionReplayedCommand(newRevision); - } else { - Warning() << "Tried executing query after result provider is already gone"; - } + resultProvider.setFetcher([this, query, &resultProvider](const QByteArray &parent) { + const qint64 newRevision = executeInitialQuery(query, parent, resultProvider); + mResourceAccess->sendRevisionReplayedCommand(newRevision); }); - auto runner = QSharedPointer::create(query); - //Incremental updates are always loaded directly, leaving it up to the result to discard the changes if they are not interesting - runner->setQuery([this, weakResultProvider, query] () -> KAsync::Job { - return KAsync::start([this, weakResultProvider, query](KAsync::Future &future) { - Trace() << "Executing query "; - if (auto resultProvider = weakResultProvider.toStrongRef()) { - const qint64 newRevision = executeIncrementalQuery(query, resultProvider); - mResourceAccess->sendRevisionReplayedCommand(newRevision); - } else { - Warning() << "Tried executing query after result provider is already gone"; - future.setError(0, QString()); - } - future.setFinished(); - }); - }); //In case of a live query we keep the runner for as long alive as the result provider exists if (query.liveQuery) { - resultProvider->setQueryRunner(runner); + auto runner = QSharedPointer::create(query); + //Incremental updates are always loaded directly, leaving it up to the result to discard the changes if they are not interesting + runner->setQuery([this, query, &resultProvider] () -> KAsync::Job { + return KAsync::start([this, query, &resultProvider](KAsync::Future &future) { + Trace() << "Executing query "; + const qint64 newRevision = executeIncrementalQuery(query, resultProvider); + mResourceAccess->sendRevisionReplayedCommand(newRevision); + future.setFinished(); + }); + }); + resultProvider.setQueryRunner(runner); //Ensure the connection is open, if it wasn't already opened //TODO If we are not connected already, we have to check for the latest revision once connected, otherwise we could miss some updates mResourceAccess->open(); QObject::connect(mResourceAccess.data(), &Akonadi2::ResourceAccess::revisionChanged, runner.data(), &QueryRunner::revisionChanged); } return KAsync::null(); - - //We have to capture the runner to keep it alive } private: //TODO move into result provider? - static void replaySet(ResultSet &resultSet, const QSharedPointer > &resultProvider) + static void replaySet(ResultSet &resultSet, Akonadi2::ResultProviderInterface &resultProvider) { - while (resultSet.next([resultProvider](const Akonadi2::ApplicationDomain::ApplicationDomainType::Ptr &value, Akonadi2::Operation operation) -> bool { + while (resultSet.next([&resultProvider](const Akonadi2::ApplicationDomain::ApplicationDomainType::Ptr &value, Akonadi2::Operation operation) -> bool { switch (operation) { case Akonadi2::Operation_Creation: Trace() << "Got creation"; - resultProvider->add(Akonadi2::ApplicationDomain::ApplicationDomainType::getInMemoryRepresentation(*value).template staticCast()); + resultProvider.add(Akonadi2::ApplicationDomain::ApplicationDomainType::getInMemoryRepresentation(*value).template staticCast()); break; case Akonadi2::Operation_Modification: Trace() << "Got modification"; - resultProvider->modify(Akonadi2::ApplicationDomain::ApplicationDomainType::getInMemoryRepresentation(*value).template staticCast()); + resultProvider.modify(Akonadi2::ApplicationDomain::ApplicationDomainType::getInMemoryRepresentation(*value).template staticCast()); break; case Akonadi2::Operation_Removal: Trace() << "Got removal"; - resultProvider->remove(Akonadi2::ApplicationDomain::ApplicationDomainType::getInMemoryRepresentation(*value).template staticCast()); + resultProvider.remove(Akonadi2::ApplicationDomain::ApplicationDomainType::getInMemoryRepresentation(*value).template staticCast()); break; } return true; @@ -281,6 +267,7 @@ private: Trace() << "Loading incremental result set starting from revision: " << baseRevision; const auto bufferType = bufferTypeForDomainType(); auto revisionCounter = QSharedPointer::create(baseRevision); + remainingFilters = query.propertyFilter.keys().toSet(); return ResultSet([bufferType, revisionCounter, &transaction]() -> QByteArray { const qint64 topRevision = Akonadi2::Storage::maxRevision(transaction); //Spit out the revision keys one by one. @@ -334,16 +321,22 @@ private: { return [remainingFilters, query](const Akonadi2::ApplicationDomain::ApplicationDomainType::Ptr &domainObject) -> bool { for (const auto &filterProperty : remainingFilters) { - //TODO implement other comparison operators than equality - if (domainObject->getProperty(filterProperty) != query.propertyFilter.value(filterProperty)) { - return false; + const auto property = domainObject->getProperty(filterProperty); + if (property.isValid()) { + //TODO implement other comparison operators than equality + if (property != query.propertyFilter.value(filterProperty)) { + Trace() << "Filtering entity due to property mismatch: " << domainObject->getProperty(filterProperty); + return false; + } + } else { + Warning() << "Ignored property filter because value is invalid: " << filterProperty; } } return true; }; } - qint64 load(const Akonadi2::Query &query, const std::function &)> &baseSetRetriever, const QSharedPointer > &resultProvider) + qint64 load(const Akonadi2::Query &query, const std::function &)> &baseSetRetriever, Akonadi2::ResultProviderInterface &resultProvider) { Akonadi2::Storage storage(Akonadi2::storageLocation(), mResourceInstanceIdentifier); storage.setDefaultErrorHandler([](const Akonadi2::Storage::Error &error) { @@ -355,21 +348,21 @@ private: auto resultSet = baseSetRetriever(transaction, remainingFilters); auto filteredSet = filterSet(resultSet, getFilter(remainingFilters, query), transaction, false); replaySet(filteredSet, resultProvider); - resultProvider->setRevision(Akonadi2::Storage::maxRevision(transaction)); + resultProvider.setRevision(Akonadi2::Storage::maxRevision(transaction)); return Akonadi2::Storage::maxRevision(transaction); } - qint64 executeIncrementalQuery(const Akonadi2::Query &query, const QSharedPointer > &resultProvider) + qint64 executeIncrementalQuery(const Akonadi2::Query &query, Akonadi2::ResultProviderInterface &resultProvider) { - const qint64 baseRevision = resultProvider->revision() + 1; + const qint64 baseRevision = resultProvider.revision() + 1; Trace() << "Running incremental query " << baseRevision; return load(query, [&](Akonadi2::Storage::Transaction &transaction, QSet &remainingFilters) -> ResultSet { return loadIncrementalResultSet(baseRevision, query, transaction, remainingFilters); }, resultProvider); } - qint64 executeInitialQuery(const Akonadi2::Query &query, const QByteArray &parent, const QSharedPointer > &resultProvider) + qint64 executeInitialQuery(const Akonadi2::Query &query, const QByteArray &parent, Akonadi2::ResultProviderInterface &resultProvider) { Trace() << "Running initial query for parent:" << parent; auto modifiedQuery = query; diff --git a/common/facadeinterface.h b/common/facadeinterface.h index 571a1e8..7ec21bc 100644 --- a/common/facadeinterface.h +++ b/common/facadeinterface.h @@ -45,7 +45,7 @@ public: virtual KAsync::Job create(const DomainType &domainObject) = 0; virtual KAsync::Job modify(const DomainType &domainObject) = 0; virtual KAsync::Job remove(const DomainType &domainObject) = 0; - virtual KAsync::Job load(const Query &query, const QSharedPointer > &resultProvider) = 0; + virtual KAsync::Job load(const Query &query, Akonadi2::ResultProviderInterface &resultProvider) = 0; }; template @@ -67,7 +67,7 @@ public: return KAsync::error(-1, "Failed to create a facade"); } - KAsync::Job load(const Query &query, const QSharedPointer > &resultProvider) + KAsync::Job load(const Query &query, Akonadi2::ResultProviderInterface &resultProvider) { return KAsync::error(-1, "Failed to create a facade"); } diff --git a/common/resourcefacade.cpp b/common/resourcefacade.cpp index 0b7c5a3..1796271 100644 --- a/common/resourcefacade.cpp +++ b/common/resourcefacade.cpp @@ -54,9 +54,9 @@ KAsync::Job ResourceFacade::remove(const Akonadi2::ApplicationDomain::Akon }); } -KAsync::Job ResourceFacade::load(const Akonadi2::Query &query, const QSharedPointer > &resultProvider) +KAsync::Job ResourceFacade::load(const Akonadi2::Query &query, Akonadi2::ResultProviderInterface &resultProvider) { - return KAsync::start([query, resultProvider]() { + return KAsync::start([query, &resultProvider]() { const auto configuredResources = ResourceConfig::getResources(); for (const auto &res : configuredResources.keys()) { const auto type = configuredResources.value(res); @@ -64,12 +64,12 @@ KAsync::Job ResourceFacade::load(const Akonadi2::Query &query, const QShar auto resource = Akonadi2::ApplicationDomain::AkonadiResource::Ptr::create(); resource->setProperty("identifier", res); resource->setProperty("type", type); - resultProvider->add(resource); + resultProvider.add(resource); } } //TODO initialResultSetComplete should be implicit - resultProvider->initialResultSetComplete(); - resultProvider->complete(); + resultProvider.initialResultSetComplete(); + resultProvider.complete(); }); } diff --git a/common/resourcefacade.h b/common/resourcefacade.h index 850d380..123b481 100644 --- a/common/resourcefacade.h +++ b/common/resourcefacade.h @@ -37,5 +37,5 @@ public: KAsync::Job create(const Akonadi2::ApplicationDomain::AkonadiResource &resource) Q_DECL_OVERRIDE; KAsync::Job modify(const Akonadi2::ApplicationDomain::AkonadiResource &resource) Q_DECL_OVERRIDE; KAsync::Job remove(const Akonadi2::ApplicationDomain::AkonadiResource &resource) Q_DECL_OVERRIDE; - KAsync::Job load(const Akonadi2::Query &query, const QSharedPointer > &resultProvider) Q_DECL_OVERRIDE; + KAsync::Job load(const Akonadi2::Query &query, Akonadi2::ResultProviderInterface &resultProvider) Q_DECL_OVERRIDE; }; diff --git a/examples/dummyresource/resourcefacade.cpp b/examples/dummyresource/resourcefacade.cpp index 1090757..af0ebe6 100644 --- a/examples/dummyresource/resourcefacade.cpp +++ b/examples/dummyresource/resourcefacade.cpp @@ -65,20 +65,20 @@ KAsync::Job DummyResourceConfigFacade::remove(const Akonadi2::ApplicationD return KAsync::null(); } -KAsync::Job DummyResourceConfigFacade::load(const Akonadi2::Query &query, const QSharedPointer > &resultProvider) +KAsync::Job DummyResourceConfigFacade::load(const Akonadi2::Query &query, Akonadi2::ResultProviderInterface &resultProvider) { //Read configuration and list all available instances. //This includes runtime information about runing instances etc. //Part of this is generic, and part is accessing the resource specific configuration. //FIXME this currently does not support live queries (because we're not inheriting from GenericFacade) //FIXME only read what was requested in the query? - return KAsync::start([resultProvider, this]() { + return KAsync::start([&resultProvider, this]() { auto settings = getSettings(); auto memoryAdaptor = QSharedPointer::create(); //TODO copy settings to adaptor // //TODO use correct instance identifier //TODO key == instance identifier ? - resultProvider->add(QSharedPointer::create("org.kde.dummy.instance1", "org.kde.dummy.config", 0, memoryAdaptor)); + resultProvider.add(QSharedPointer::create("org.kde.dummy.instance1", "org.kde.dummy.config", 0, memoryAdaptor)); }); } diff --git a/tests/clientapitest.cpp b/tests/clientapitest.cpp index 2ce64d3..9202e29 100644 --- a/tests/clientapitest.cpp +++ b/tests/clientapitest.cpp @@ -27,14 +27,14 @@ public: 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 + KAsync::Job load(const Akonadi2::Query &query, Akonadi2::ResultProviderInterface &resultProvider) Q_DECL_OVERRIDE { - capturedResultProvider = resultProvider; - resultProvider->setFetcher([query, resultProvider, this](const QByteArray &) { + capturedResultProvider = &resultProvider; + resultProvider.setFetcher([query, &resultProvider, this](const QByteArray &) { for (const auto &res : results) { 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); + resultProvider.add(res); } } }); @@ -42,7 +42,7 @@ public: } QList results; - QWeakPointer > capturedResultProvider; + Akonadi2::ResultProviderInterface *capturedResultProvider; }; @@ -94,7 +94,7 @@ private Q_SLOTS: QCOMPARE(result.size(), 1); } //It's running in a separate thread, so we have to wait for a moment until the query provider deletes itself. - QTRY_VERIFY(!facade->capturedResultProvider); + // QTRY_VERIFY(!facade->capturedResultProvider); } //TODO: This test doesn't belong to this testsuite diff --git a/tests/genericfacadebenchmark.cpp b/tests/genericfacadebenchmark.cpp index 94d6f41..8b00666 100644 --- a/tests/genericfacadebenchmark.cpp +++ b/tests/genericfacadebenchmark.cpp @@ -60,7 +60,7 @@ private Q_SLOTS: async::SyncListResult result(resultSet->emitter()); - facade.load(query, resultSet).exec().waitForFinished(); + facade.load(query, *resultSet).exec().waitForFinished(); resultSet->initialResultSetComplete(); //We have to wait for the events that deliver the results to be processed by the eventloop diff --git a/tests/genericfacadetest.cpp b/tests/genericfacadetest.cpp index 9e7500f..bb95f4e 100644 --- a/tests/genericfacadetest.cpp +++ b/tests/genericfacadetest.cpp @@ -41,7 +41,7 @@ private Q_SLOTS: async::SyncListResult result(resultSet->emitter()); - facade.load(query, resultSet).exec().waitForFinished(); + facade.load(query, *resultSet).exec().waitForFinished(); resultSet->initialResultSetComplete(); //We have to wait for the events that deliver the results to be processed by the eventloop @@ -62,7 +62,7 @@ private Q_SLOTS: async::SyncListResult result(resultSet->emitter()); - facade.load(query, resultSet).exec().waitForFinished(); + facade.load(query, *resultSet).exec().waitForFinished(); resultSet->initialResultSetComplete(); result.exec(); @@ -95,7 +95,7 @@ private Q_SLOTS: async::SyncListResult result(resultSet->emitter()); - facade.load(query, resultSet).exec().waitForFinished(); + facade.load(query, *resultSet).exec().waitForFinished(); resultSet->initialResultSetComplete(); result.exec(); @@ -130,7 +130,7 @@ private Q_SLOTS: async::SyncListResult result(resultSet->emitter()); - facade.load(query, resultSet).exec().waitForFinished(); + facade.load(query, *resultSet).exec().waitForFinished(); resultSet->initialResultSetComplete(); result.exec(); -- cgit v1.2.3 From b42047ad90470ecab329375fdacff03564c80074 Mon Sep 17 00:00:00 2001 From: Christian Mollekopf Date: Wed, 18 Nov 2015 23:15:25 +0100 Subject: fixup --- common/facade.h | 7 ++----- examples/dummyresource/resourcefacade.h | 2 +- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/common/facade.h b/common/facade.h index 82fd5ff..f5c05f9 100644 --- a/common/facade.h +++ b/common/facade.h @@ -246,10 +246,8 @@ private: }); } - ResultSet loadInitialResultSet(const QByteArray &parent, const Akonadi2::Query &query, Akonadi2::Storage::Transaction &transaction, QSet &remainingFilters) + ResultSet loadInitialResultSet(const Akonadi2::Query &query, Akonadi2::Storage::Transaction &transaction, QSet &remainingFilters) { - Trace() << "Fetching initial set for parent:" << parent; - //TODO QSet appliedFilters; auto resultSet = Akonadi2::ApplicationDomain::TypeImplementation::queryIndexes(query, mResourceInstanceIdentifier, appliedFilters, transaction); remainingFilters = query.propertyFilter.keys().toSet() - appliedFilters; @@ -264,7 +262,6 @@ private: ResultSet loadIncrementalResultSet(qint64 baseRevision, const Akonadi2::Query &query, Akonadi2::Storage::Transaction &transaction, QSet &remainingFilters) { - Trace() << "Loading incremental result set starting from revision: " << baseRevision; const auto bufferType = bufferTypeForDomainType(); auto revisionCounter = QSharedPointer::create(baseRevision); remainingFilters = query.propertyFilter.keys().toSet(); @@ -368,7 +365,7 @@ private: auto modifiedQuery = query; modifiedQuery.propertyFilter.insert("parent", parent); return load(modifiedQuery, [&](Akonadi2::Storage::Transaction &transaction, QSet &remainingFilters) -> ResultSet { - return loadInitialResultSet(parent, modifiedQuery, transaction, remainingFilters); + return loadInitialResultSet(modifiedQuery, transaction, remainingFilters); }, resultProvider); } diff --git a/examples/dummyresource/resourcefacade.h b/examples/dummyresource/resourcefacade.h index aa2ab05..82e54fd 100644 --- a/examples/dummyresource/resourcefacade.h +++ b/examples/dummyresource/resourcefacade.h @@ -42,7 +42,7 @@ public: //Remove instance KAsync::Job remove(const Akonadi2::ApplicationDomain::AkonadiResource &domainObject) Q_DECL_OVERRIDE; //Read configuration and available instances - KAsync::Job load(const Akonadi2::Query &query, const QSharedPointer > &resultProvider) Q_DECL_OVERRIDE; + KAsync::Job load(const Akonadi2::Query &query, Akonadi2::ResultProviderInterface &resultProvider) Q_DECL_OVERRIDE; private: QSharedPointer getSettings(); -- cgit v1.2.3 From ef205affdb73bfdbef5830996e6336e583660fbc Mon Sep 17 00:00:00 2001 From: Christian Mollekopf Date: Thu, 19 Nov 2015 09:37:42 +0100 Subject: Use the new modelresult in the dummyclient --- common/clientapi.h | 4 ++++ common/modelresult.h | 11 +++++++++-- examples/client/main.cpp | 22 ++++++++++++---------- 3 files changed, 25 insertions(+), 12 deletions(-) diff --git a/common/clientapi.h b/common/clientapi.h index 707e81d..179bb5c 100644 --- a/common/clientapi.h +++ b/common/clientapi.h @@ -100,6 +100,10 @@ public: return resultSet->emitter(); } + enum Roles { + DomainObjectRole = Qt::UserRole + 1 //Must be the same as in ModelResult + }; + /** * Asynchronusly load a dataset with tree structure information */ diff --git a/common/modelresult.h b/common/modelresult.h index 8ca6daa..3b45955 100644 --- a/common/modelresult.h +++ b/common/modelresult.h @@ -63,11 +63,18 @@ public: 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"; + if (role == Qt::DisplayRole) { + if (index.column() < mPropertyColumns.size()) { + Q_ASSERT(mEntities.contains(index.internalId())); + auto entity = mEntities.value(index.internalId()); + return entity->getProperty(mPropertyColumns.at(index.column())).toString(); + } else { + return "No data available"; + } + } return QVariant(); } diff --git a/examples/client/main.cpp b/examples/client/main.cpp index 0a1a725..75fcf18 100644 --- a/examples/client/main.cpp +++ b/examples/client/main.cpp @@ -23,14 +23,13 @@ #include "common/clientapi.h" #include "common/resource.h" -#include "common/listmodelresult.h" #include "common/storage.h" #include "common/domain/event.h" #include "common/resourceconfig.h" #include "console.h" #include -#include +#include #include #include #include @@ -43,8 +42,8 @@ public: View(QAbstractItemModel *model) : QWidget() { - auto listView = new QListView(this); - listView->setModel(model); + auto modelView = new QTreeView(this); + modelView->setModel(model); resize(1000, 1500); auto topLayout = new QVBoxLayout(this); @@ -61,21 +60,23 @@ public: QObject::connect(syncButton, &QPushButton::pressed, []() { Akonadi2::Query query; query.resources << "org.kde.dummy.instance1"; - Akonadi2::Store::synchronize(query); + query.syncOnDemand = true; + Akonadi2::Store::synchronize(query).exec(); }); auto removeButton = new QPushButton(this); removeButton->setText("Remove"); - QObject::connect(removeButton, &QPushButton::pressed, [listView]() { - for (auto index :listView->selectionModel()->selectedIndexes()) { - auto object = index.data(DomainObjectRole).value(); + QObject::connect(removeButton, &QPushButton::pressed, [modelView]() { + for (auto index :modelView->selectionModel()->selectedIndexes()) { + auto object = index.data(Akonadi2::Store::DomainObjectRole).value(); Akonadi2::Store::remove(*object).exec(); } }); topLayout->addWidget(titleLabel); topLayout->addWidget(syncButton); - topLayout->addWidget(listView, 10); + topLayout->addWidget(modelView, 10); + model->fetchMore(QModelIndex()); show(); } @@ -123,8 +124,9 @@ int main(int argc, char *argv[]) query.syncOnDemand = false; query.processAll = false; query.liveQuery = true; + query.requestedProperties << "summary" << "uid"; - auto model = QSharedPointer >::create(Akonadi2::Store::load(query), QList() << "summary" << "uid"); + auto model = Akonadi2::Store::loadModel(query); auto view = QSharedPointer >::create(model.data()); return app.exec(); -- cgit v1.2.3 From 8d5684292ef92f32487ba32df716a00c4a0841b5 Mon Sep 17 00:00:00 2001 From: Christian Mollekopf Date: Thu, 19 Nov 2015 17:37:39 +0100 Subject: Loading data with the new model for the test client --- common/clientapi.h | 2 +- common/domain/folder.cpp | 1 + common/facade.h | 1 - common/modelresult.h | 6 +++++- examples/client/main.cpp | 7 ++++--- examples/dummyresource/dummystore.h | 2 +- examples/dummyresource/resourcefactory.cpp | 24 ++++++++++++++++++++++++ examples/dummyresource/resourcefactory.h | 1 + 8 files changed, 37 insertions(+), 7 deletions(-) diff --git a/common/clientapi.h b/common/clientapi.h index 179bb5c..6b11ad5 100644 --- a/common/clientapi.h +++ b/common/clientapi.h @@ -110,7 +110,7 @@ public: template static QSharedPointer loadModel(Query query) { - auto model = QSharedPointer >::create(query, QList() << "summary" << "uid"); + auto model = QSharedPointer >::create(query, query.requestedProperties.toList()); auto resultProvider = std::make_shared >(model); //Keep the resultprovider alive for as long as the model lives model->setProperty("resultProvider", QVariant::fromValue(std::shared_ptr(resultProvider))); diff --git a/common/domain/folder.cpp b/common/domain/folder.cpp index 82f6c1f..989d2c4 100644 --- a/common/domain/folder.cpp +++ b/common/domain/folder.cpp @@ -60,6 +60,7 @@ void TypeImplementation::index(const QByteArray &identifier, const Buffe const auto parent = bufferAdaptor.getProperty("parent"); Trace() << "indexing " << identifier << " with parent " << parent.toByteArray(); if (parent.isValid()) { + Q_ASSERT(!parent.toByteArray().isEmpty()); Index("folder.index.parent", transaction).add(parent.toByteArray(), identifier); } else { Index("folder.index.parent", transaction).add("toplevel", identifier); diff --git a/common/facade.h b/common/facade.h index f5c05f9..d150d60 100644 --- a/common/facade.h +++ b/common/facade.h @@ -184,7 +184,6 @@ public: //Incremental updates are always loaded directly, leaving it up to the result to discard the changes if they are not interesting runner->setQuery([this, query, &resultProvider] () -> KAsync::Job { return KAsync::start([this, query, &resultProvider](KAsync::Future &future) { - Trace() << "Executing query "; const qint64 newRevision = executeIncrementalQuery(query, resultProvider); mResourceAccess->sendRevisionReplayedCommand(newRevision); future.setFinished(); diff --git a/common/modelresult.h b/common/modelresult.h index 3b45955..1675e60 100644 --- a/common/modelresult.h +++ b/common/modelresult.h @@ -131,7 +131,11 @@ public: break; } } - beginInsertRows(parent, index, index); + if (mEntities.contains(childId)) { + qWarning() << "Entity already in model " << value->identifier(); + return; + } + beginInsertRows(QModelIndex(), index, index); mEntities.insert(childId, value); mTree[id].insert(index, childId); mParents.insert(childId, id); diff --git a/examples/client/main.cpp b/examples/client/main.cpp index 75fcf18..794fc58 100644 --- a/examples/client/main.cpp +++ b/examples/client/main.cpp @@ -25,6 +25,7 @@ #include "common/resource.h" #include "common/storage.h" #include "common/domain/event.h" +#include "common/domain/folder.h" #include "common/resourceconfig.h" #include "console.h" @@ -124,10 +125,10 @@ int main(int argc, char *argv[]) query.syncOnDemand = false; query.processAll = false; query.liveQuery = true; - query.requestedProperties << "summary" << "uid"; + query.requestedProperties << "name"; - auto model = Akonadi2::Store::loadModel(query); - auto view = QSharedPointer >::create(model.data()); + auto model = Akonadi2::Store::loadModel(query); + auto view = QSharedPointer >::create(model.data()); return app.exec(); } diff --git a/examples/dummyresource/dummystore.h b/examples/dummyresource/dummystore.h index 4ecd5ea..c730118 100644 --- a/examples/dummyresource/dummystore.h +++ b/examples/dummyresource/dummystore.h @@ -23,7 +23,6 @@ class DummyStore { public: - //TODO proper singleton static DummyStore &instance() { static DummyStore instance; @@ -32,4 +31,5 @@ public: QMap > events() const; QMap > mails() const; + QMap > folders() const; }; diff --git a/examples/dummyresource/resourcefactory.cpp b/examples/dummyresource/resourcefactory.cpp index bed1d71..2a5a3e8 100644 --- a/examples/dummyresource/resourcefactory.cpp +++ b/examples/dummyresource/resourcefactory.cpp @@ -163,6 +163,27 @@ void DummyResource::createMail(const QByteArray &ridBuffer, const QMap &data, flatbuffers::FlatBufferBuilder &entityFbb) +{ + //Map the source format to the buffer format (which happens to be an exact copy here) + auto name = m_fbb.CreateString(data.value("name").toString().toStdString()); + flatbuffers::Offset parent; + bool hasParent = false; + if (!data.value("parent").toString().isEmpty()) { + hasParent = true; + parent = m_fbb.CreateString(data.value("parent").toString().toStdString()); + } + + auto builder = Akonadi2::ApplicationDomain::Buffer::FolderBuilder(m_fbb); + builder.add_name(name); + if (hasParent) { + builder.add_parent(parent); + } + auto buffer = builder.Finish(); + Akonadi2::ApplicationDomain::Buffer::FinishFolderBuffer(m_fbb, buffer); + Akonadi2::EntityBuffer::assembleEntityBuffer(entityFbb, 0, 0, 0, 0, m_fbb.GetBufferPointer(), m_fbb.GetSize()); +} + void DummyResource::synchronize(const QString &bufferType, const QMap > &data, Akonadi2::Storage::Transaction &transaction, std::function &data, flatbuffers::FlatBufferBuilder &entityFbb)> createEntity) { Index uidIndex("index.uid", transaction); @@ -209,6 +230,9 @@ KAsync::Job DummyResource::synchronizeWithSource() synchronize(ENTITY_TYPE_MAIL, DummyStore::instance().mails(), transaction, [this](const QByteArray &ridBuffer, const QMap &data, flatbuffers::FlatBufferBuilder &entityFbb) { createMail(ridBuffer, data, entityFbb); }); + synchronize(ENTITY_TYPE_FOLDER, DummyStore::instance().folders(), transaction, [this](const QByteArray &ridBuffer, const QMap &data, flatbuffers::FlatBufferBuilder &entityFbb) { + createFolder(ridBuffer, data, entityFbb); + }); f.setFinished(); }); diff --git a/examples/dummyresource/resourcefactory.h b/examples/dummyresource/resourcefactory.h index 196d29a..67681ae 100644 --- a/examples/dummyresource/resourcefactory.h +++ b/examples/dummyresource/resourcefactory.h @@ -38,6 +38,7 @@ public: private: void createEvent(const QByteArray &rid, const QMap &data, flatbuffers::FlatBufferBuilder &entityFbb); void createMail(const QByteArray &rid, const QMap &data, flatbuffers::FlatBufferBuilder &entityFbb); + void createFolder(const QByteArray &rid, const QMap &data, flatbuffers::FlatBufferBuilder &entityFbb); void synchronize(const QString &bufferType, const QMap > &data, Akonadi2::Storage::Transaction &transaction, std::function &data, flatbuffers::FlatBufferBuilder &entityFbb)> createEntity); }; -- cgit v1.2.3 From c4a6746e4420b580fe35cc89783de4dbc3205ac6 Mon Sep 17 00:00:00 2001 From: Christian Mollekopf Date: Thu, 19 Nov 2015 18:14:09 +0100 Subject: The parent is always an object, so we might as well make that explicit --- common/facade.h | 13 +++++++++---- common/modelresult.h | 17 ++++------------- common/resultprovider.h | 8 ++++---- tests/clientapitest.cpp | 2 +- 4 files changed, 18 insertions(+), 22 deletions(-) diff --git a/common/facade.h b/common/facade.h index d150d60..8b8a2a8 100644 --- a/common/facade.h +++ b/common/facade.h @@ -172,7 +172,7 @@ public: KAsync::Job load(const Akonadi2::Query &query, Akonadi2::ResultProviderInterface &resultProvider) Q_DECL_OVERRIDE { //We delegate loading of initial data to the result provider, os it can decide for itself what it needs to load. - resultProvider.setFetcher([this, query, &resultProvider](const QByteArray &parent) { + resultProvider.setFetcher([this, query, &resultProvider](const typename DomainType::Ptr &parent) { const qint64 newRevision = executeInitialQuery(query, parent, resultProvider); mResourceAccess->sendRevisionReplayedCommand(newRevision); }); @@ -358,11 +358,16 @@ private: }, resultProvider); } - qint64 executeInitialQuery(const Akonadi2::Query &query, const QByteArray &parent, Akonadi2::ResultProviderInterface &resultProvider) + qint64 executeInitialQuery(const Akonadi2::Query &query, const typename DomainType::Ptr &parent, Akonadi2::ResultProviderInterface &resultProvider) { - Trace() << "Running initial query for parent:" << parent; auto modifiedQuery = query; - modifiedQuery.propertyFilter.insert("parent", parent); + if (parent) { + Trace() << "Running initial query for parent:" << parent->identifier(); + modifiedQuery.propertyFilter.insert("parent", parent->identifier()); + } else { + Trace() << "Running initial query for toplevel"; + modifiedQuery.propertyFilter.insert("parent", QVariant()); + } return load(modifiedQuery, [&](Akonadi2::Storage::Transaction &transaction, QSet &remainingFilters) -> ResultSet { return loadInitialResultSet(modifiedQuery, transaction, remainingFilters); }, resultProvider); diff --git a/common/modelresult.h b/common/modelresult.h index 1675e60..26f96d8 100644 --- a/common/modelresult.h +++ b/common/modelresult.h @@ -179,20 +179,11 @@ public: { const auto id = getIdentifier(parent); mEntityChildrenFetched[id] = true; - QByteArray parentIdentifier; - if (!parent.isValid()) { - qDebug() << "no parent"; - } else { - qDebug() << "parent is valid"; - auto object = parent.data(DomainObjectRole).template value(); - Q_ASSERT(object); - parentIdentifier = object->identifier(); - } - Trace() << "Loading child entities of: " << parentIdentifier; - loadEntities(parentIdentifier); + Trace() << "Loading child entities"; + loadEntities(parent.data(DomainObjectRole).template value()); } - void setFetcher(const std::function &fetcher) + void setFetcher(const std::function &fetcher) { Trace() << "Setting fetcher"; loadEntities = fetcher; @@ -206,6 +197,6 @@ private: QMap mEntityChildrenFetched; QList mPropertyColumns; Akonadi2::Query mQuery; - std::function loadEntities; + std::function loadEntities; }; diff --git a/common/resultprovider.h b/common/resultprovider.h index 0d23127..921cd6b 100644 --- a/common/resultprovider.h +++ b/common/resultprovider.h @@ -53,7 +53,7 @@ public: virtual void initialResultSetComplete() = 0; virtual void complete() = 0; virtual void clear() = 0; - virtual void setFetcher(const std::function &fetcher) + virtual void setFetcher(const std::function &fetcher) { } @@ -141,7 +141,7 @@ public: return true; } - void setFetcher(const std::function &fetcher) + void setFetcher(const std::function &fetcher) { if (auto model = mModel.toStrongRef()) { model->setFetcher(fetcher); @@ -297,9 +297,9 @@ public: return mResultEmitter.toStrongRef().isNull(); } - void setFetcher(const std::function &fetcher) + void setFetcher(const std::function &fetcher) { - fetcher(QByteArray()); + fetcher(T()); } private: diff --git a/tests/clientapitest.cpp b/tests/clientapitest.cpp index 9202e29..ce11221 100644 --- a/tests/clientapitest.cpp +++ b/tests/clientapitest.cpp @@ -30,7 +30,7 @@ public: KAsync::Job load(const Akonadi2::Query &query, Akonadi2::ResultProviderInterface &resultProvider) Q_DECL_OVERRIDE { capturedResultProvider = &resultProvider; - resultProvider.setFetcher([query, &resultProvider, this](const QByteArray &) { + resultProvider.setFetcher([query, &resultProvider, this](const typename T::Ptr &) { for (const auto &res : results) { qDebug() << "Parent filter " << query.propertyFilter.value("parent").toByteArray() << res->identifier(); if (!query.propertyFilter.contains("parent") || query.propertyFilter.value("parent").toByteArray() == res->getProperty("parent").toByteArray()) { -- cgit v1.2.3 From 94a2cd6ec21bf0466a9a50d6e4a0a956ed47bc82 Mon Sep 17 00:00:00 2001 From: Christian Mollekopf Date: Thu, 19 Nov 2015 23:23:56 +0100 Subject: Move implementations to the cpp file. I finally figured out how to do that with cpp files. It requires instantiating the code with all expected classes, but that's not a big problem since we know all types. This will hopefully greatly reduce the compiletimes... --- common/clientapi.cpp | 151 ++++++++++++++++++++++++- common/clientapi.h | 109 ++---------------- common/domain/applicationdomaintype.h | 2 + common/modelresult.cpp | 203 ++++++++++++++++++++++++++++++++++ common/modelresult.h | 167 +++------------------------- examples/dummyresource/dummystore.cpp | 22 ++++ 6 files changed, 400 insertions(+), 254 deletions(-) create mode 100644 common/modelresult.cpp diff --git a/common/clientapi.cpp b/common/clientapi.cpp index 839e77b..02f8ce6 100644 --- a/common/clientapi.cpp +++ b/common/clientapi.cpp @@ -1,13 +1,40 @@ +/* + * 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 . + */ #include "clientapi.h" + +#include +#include +#include +#include +#include +#include + #include "resourceaccess.h" #include "commands.h" #include "resourcefacade.h" #include "log.h" #include "definitions.h" #include "resourceconfig.h" -#include -#include +#include "facadefactory.h" +#include "log.h" #define ASYNCINTHREAD @@ -69,6 +96,114 @@ QList Store::getResources(const QList &resourceFilter, c return resources; } +template +QSharedPointer > Store::load(Query query) +{ + auto resultSet = QSharedPointer >::create(); + + //Execute the search in a thread. + //We must guarantee that the emitter is returned before the first result is emitted. + //The result provider must be threadsafe. + async::run([query, resultSet](){ + QEventLoop eventLoop; + resultSet->onDone([&eventLoop](){ + eventLoop.quit(); + }); + // Query all resources and aggregate results + KAsync::iterate(getResources(query.resources, ApplicationDomain::getTypeName())) + .template each([query, resultSet](const QByteArray &resource, KAsync::Future &future) { + if (auto facade = FacadeFactory::instance().getFacade(resourceName(resource), resource)) { + facade->load(query, *resultSet).template then([&future](){future.setFinished();}).exec(); + //Keep the facade alive for the lifetime of the resultSet. + resultSet->setFacade(facade); + } else { + //Ignore the error and carry on + future.setFinished(); + } + }).template then([query, resultSet]() { + resultSet->initialResultSetComplete(); + if (!query.liveQuery) { + resultSet->complete(); + } + }).exec(); + + //Keep the thread alive until the result is ready + if (!resultSet->isDone()) { + eventLoop.exec(); + } + }); + return resultSet->emitter(); +} + +template +QSharedPointer Store::loadModel(Query query) +{ + auto model = QSharedPointer >::create(query, query.requestedProperties.toList()); + auto resultProvider = std::make_shared >(model); + //Keep the resultprovider alive for as long as the model lives + model->setProperty("resultProvider", QVariant::fromValue(std::shared_ptr(resultProvider))); + + // Query all resources and aggregate results + KAsync::iterate(getResources(query.resources, ApplicationDomain::getTypeName())) + .template each([query, resultProvider](const QByteArray &resource, KAsync::Future &future) { + auto facade = FacadeFactory::instance().getFacade(resourceName(resource), resource); + if (facade) { + facade->load(query, *resultProvider).template then([&future](){future.setFinished();}).exec(); + //Keep the facade alive for the lifetime of the resultSet. + //FIXME this would have to become a list + resultProvider->setFacade(facade); + } else { + //Ignore the error and carry on + future.setFinished(); + } + }).template then([query, resultProvider]() { + resultProvider->initialResultSetComplete(); + if (!query.liveQuery) { + resultProvider->complete(); + } + }).exec(); + + return model; +} + +template +static std::shared_ptr > getFacade(const QByteArray &resourceInstanceIdentifier) +{ + if (auto facade = FacadeFactory::instance().getFacade(resourceName(resourceInstanceIdentifier), resourceInstanceIdentifier)) { + return facade; + } + return std::make_shared >(); +} + +template +KAsync::Job Store::create(const DomainType &domainObject) { + //Potentially move to separate thread as well + auto facade = getFacade(domainObject.resourceInstanceIdentifier()); + return facade->create(domainObject).template then([facade](){}, [](int errorCode, const QString &error) { + Warning() << "Failed to create"; + }); +} + +template +KAsync::Job Store::modify(const DomainType &domainObject) +{ + //Potentially move to separate thread as well + auto facade = getFacade(domainObject.resourceInstanceIdentifier()); + return facade->modify(domainObject).template then([facade](){}, [](int errorCode, const QString &error) { + Warning() << "Failed to modify"; + }); +} + +template +KAsync::Job Store::remove(const DomainType &domainObject) +{ + //Potentially move to separate thread as well + auto facade = getFacade(domainObject.resourceInstanceIdentifier()); + return facade->remove(domainObject).template then([facade](){}, [](int errorCode, const QString &error) { + Warning() << "Failed to remove"; + }); +} + KAsync::Job Store::shutdown(const QByteArray &identifier) { Trace() << "shutdown"; @@ -103,4 +238,16 @@ KAsync::Job Store::synchronize(const Akonadi2::Query &query) .template then([](){}); } +#define REGISTER_TYPE(T) template KAsync::Job Store::remove(const T &domainObject); \ + template KAsync::Job Store::create(const T &domainObject); \ + template KAsync::Job Store::modify(const T &domainObject); \ + template QSharedPointer > Store::load(Query query); \ + template QSharedPointer Store::loadModel(Query query); \ + +REGISTER_TYPE(ApplicationDomain::Event); +REGISTER_TYPE(ApplicationDomain::Mail); +REGISTER_TYPE(ApplicationDomain::Folder); +REGISTER_TYPE(ApplicationDomain::AkonadiResource); + } // namespace Akonadi2 + diff --git a/common/clientapi.h b/common/clientapi.h index 6b11ad5..c48c6e9 100644 --- a/common/clientapi.h +++ b/common/clientapi.h @@ -22,21 +22,17 @@ #include #include -#include -#include -#include -#include #include #include "query.h" #include "resultprovider.h" #include "applicationdomaintype.h" -#include "facadefactory.h" -#include "log.h" Q_DECLARE_METATYPE(std::shared_ptr); +class QAbstractItemModel; + namespace async { //This should abstract if we execute from eventloop or in thread. //It supposed to allow the caller to finish the current method before executing the runner. @@ -62,43 +58,7 @@ public: * Asynchronusly load a dataset */ template - static QSharedPointer > load(Query query) - { - auto resultSet = QSharedPointer >::create(); - - //Execute the search in a thread. - //We must guarantee that the emitter is returned before the first result is emitted. - //The result provider must be threadsafe. - async::run([query, resultSet](){ - QEventLoop eventLoop; - resultSet->onDone([&eventLoop](){ - eventLoop.quit(); - }); - // Query all resources and aggregate results - KAsync::iterate(getResources(query.resources, ApplicationDomain::getTypeName())) - .template each([query, resultSet](const QByteArray &resource, KAsync::Future &future) { - if (auto facade = FacadeFactory::instance().getFacade(resourceName(resource), resource)) { - facade->load(query, *resultSet).template then([&future](){future.setFinished();}).exec(); - //Keep the facade alive for the lifetime of the resultSet. - resultSet->setFacade(facade); - } else { - //Ignore the error and carry on - future.setFinished(); - } - }).template then([query, resultSet]() { - resultSet->initialResultSetComplete(); - if (!query.liveQuery) { - resultSet->complete(); - } - }).exec(); - - //Keep the thread alive until the result is ready - if (!resultSet->isDone()) { - eventLoop.exec(); - } - }); - return resultSet->emitter(); - } + static QSharedPointer > load(Query query); enum Roles { DomainObjectRole = Qt::UserRole + 1 //Must be the same as in ModelResult @@ -108,56 +68,13 @@ public: * Asynchronusly load a dataset with tree structure information */ template - static QSharedPointer loadModel(Query query) - { - auto model = QSharedPointer >::create(query, query.requestedProperties.toList()); - auto resultProvider = std::make_shared >(model); - //Keep the resultprovider alive for as long as the model lives - model->setProperty("resultProvider", QVariant::fromValue(std::shared_ptr(resultProvider))); - - // Query all resources and aggregate results - KAsync::iterate(getResources(query.resources, ApplicationDomain::getTypeName())) - .template each([query, resultProvider](const QByteArray &resource, KAsync::Future &future) { - auto facade = FacadeFactory::instance().getFacade(resourceName(resource), resource); - if (facade) { - facade->load(query, *resultProvider).template then([&future](){future.setFinished();}).exec(); - //Keep the facade alive for the lifetime of the resultSet. - //FIXME this would have to become a list - resultProvider->setFacade(facade); - } else { - //Ignore the error and carry on - future.setFinished(); - } - }).template then([query, resultProvider]() { - resultProvider->initialResultSetComplete(); - if (!query.liveQuery) { - resultProvider->complete(); - } - }).exec(); - - return model; - } - - template - static std::shared_ptr > getFacade(const QByteArray &resourceInstanceIdentifier) - { - if (auto facade = FacadeFactory::instance().getFacade(resourceName(resourceInstanceIdentifier), resourceInstanceIdentifier)) { - return facade; - } - return std::make_shared >(); - } + static QSharedPointer loadModel(Query query); /** * Create a new entity. */ template - static KAsync::Job create(const DomainType &domainObject) { - //Potentially move to separate thread as well - auto facade = getFacade(domainObject.resourceInstanceIdentifier()); - return facade->create(domainObject).template then([facade](){}, [](int errorCode, const QString &error) { - Warning() << "Failed to create"; - }); - } + static KAsync::Job create(const DomainType &domainObject); /** * Modify an entity. @@ -165,25 +82,13 @@ public: * This includes moving etc. since these are also simple settings on a property. */ template - static KAsync::Job modify(const DomainType &domainObject) { - //Potentially move to separate thread as well - auto facade = getFacade(domainObject.resourceInstanceIdentifier()); - return facade->modify(domainObject).template then([facade](){}, [](int errorCode, const QString &error) { - Warning() << "Failed to modify"; - }); - } + static KAsync::Job modify(const DomainType &domainObject); /** * Remove an entity. */ template - static KAsync::Job remove(const DomainType &domainObject) { - //Potentially move to separate thread as well - auto facade = getFacade(domainObject.resourceInstanceIdentifier()); - return facade->remove(domainObject).template then([facade](){}, [](int errorCode, const QString &error) { - Warning() << "Failed to remove"; - }); - } + static KAsync::Job remove(const DomainType &domainObject); /** * Shutdown resource. diff --git a/common/domain/applicationdomaintype.h b/common/domain/applicationdomaintype.h index b4cf8c4..227ab4d 100644 --- a/common/domain/applicationdomaintype.h +++ b/common/domain/applicationdomaintype.h @@ -162,3 +162,5 @@ Q_DECLARE_METATYPE(Akonadi2::ApplicationDomain::Mail) Q_DECLARE_METATYPE(Akonadi2::ApplicationDomain::Mail::Ptr) Q_DECLARE_METATYPE(Akonadi2::ApplicationDomain::Folder) Q_DECLARE_METATYPE(Akonadi2::ApplicationDomain::Folder::Ptr) +Q_DECLARE_METATYPE(Akonadi2::ApplicationDomain::AkonadiResource) +Q_DECLARE_METATYPE(Akonadi2::ApplicationDomain::AkonadiResource::Ptr) diff --git a/common/modelresult.cpp b/common/modelresult.cpp new file mode 100644 index 0000000..1abcc62 --- /dev/null +++ b/common/modelresult.cpp @@ -0,0 +1,203 @@ +/* + * 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 . + */ +#include "modelresult.h" + +#include + +#include "domain/folder.h" +#include "log.h" + +template +ModelResult::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(); +} + +template +qint64 ModelResult::parentId(const Ptr &value) +{ + return qHash(value->getProperty("parent").toByteArray()); +} + +template +int ModelResult::rowCount(const QModelIndex &parent) const +{ + return mTree[getIdentifier(parent)].size(); +} + +template +int ModelResult::columnCount(const QModelIndex &parent) const +{ + return mPropertyColumns.size(); +} + +template +QVariant ModelResult::data(const QModelIndex &index, int role) const +{ + if (role == DomainObjectRole) { + Q_ASSERT(mEntities.contains(index.internalId())); + return QVariant::fromValue(mEntities.value(index.internalId())); + } + if (role == Qt::DisplayRole) { + if (index.column() < mPropertyColumns.size()) { + Q_ASSERT(mEntities.contains(index.internalId())); + auto entity = mEntities.value(index.internalId()); + return entity->getProperty(mPropertyColumns.at(index.column())).toString(); + } else { + return "No data available"; + } + } + return QVariant(); +} + +template +QModelIndex ModelResult::index(int row, int column, const QModelIndex &parent) const +{ + auto id = getIdentifier(parent); + auto childId = mTree.value(id).at(row); + return createIndex(row, column, childId); +} + +template +QModelIndex ModelResult::createIndexFromId(const qint64 &id) const +{ + auto grandParentId = mParents.value(id, 0); + auto row = mTree.value(grandParentId).indexOf(id); + return createIndex(row, 0, id); +} + +template +QModelIndex ModelResult::parent(const QModelIndex &index) const +{ + auto id = getIdentifier(index); + auto parentId = mParents.value(id); + return createIndexFromId(parentId); +} + +template +bool ModelResult::canFetchMore(const QModelIndex &parent) const +{ + qDebug() << "Can fetch more: " << parent << mEntityChildrenFetched.value(parent.internalId()); + return mEntityChildrenFetched.value(parent.internalId()); +} + +template +void ModelResult::fetchMore(const QModelIndex &parent) +{ + qDebug() << "Fetch more: " << parent; + fetchEntities(parent); +} + +template +void ModelResult::add(const Ptr &value) +{ + auto childId = qHash(value->identifier()); + auto id = parentId(value); + //Ignore updates we get before the initial fetch is done + if (!mEntityChildrenFetched[id]) { + return; + } + auto parent = createIndexFromId(id); + qDebug() << "Added entity " << childId << value->identifier() << id; + const auto keys = mTree[id]; + int index = 0; + for (; index < keys.size(); index++) { + if (childId < keys.at(index)) { + break; + } + } + if (mEntities.contains(childId)) { + qWarning() << "Entity already in model " << value->identifier(); + return; + } + qDebug() << "Inserting rows " << index << parent; + beginInsertRows(QModelIndex(), index, index); + mEntities.insert(childId, value); + mTree[id].insert(index, childId); + mParents.insert(childId, id); + endInsertRows(); + qDebug() << "Inserted rows " << mTree[id].size(); +} + + +template +void ModelResult::remove(const Ptr &value) +{ + auto childId = qHash(value->identifier()); + auto id = parentId(value); + auto parent = createIndexFromId(id); + 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(); +} + +template +void ModelResult::fetchEntities(const QModelIndex &parent) +{ + const auto id = getIdentifier(parent); + mEntityChildrenFetched[id] = true; + Trace() << "Loading child entities"; + loadEntities(parent.data(DomainObjectRole).template value()); +} + +template +void ModelResult::setFetcher(const std::function &fetcher) +{ + Trace() << "Setting fetcher"; + loadEntities = fetcher; +} + +template +void ModelResult::modify(const Ptr &value) +{ + auto childId = qHash(value->identifier()); + auto id = parentId(value); + //Ignore updates we get before the initial fetch is done + if (!mEntityChildrenFetched[id]) { + return; + } + auto parent = createIndexFromId(id); + 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); +} + +template class ModelResult; +template class ModelResult; +template class ModelResult; +template class ModelResult; diff --git a/common/modelresult.h b/common/modelresult.h index 26f96d8..40a9d9d 100644 --- a/common/modelresult.h +++ b/common/modelresult.h @@ -23,10 +23,9 @@ #include #include #include +#include #include "query.h" -#include "resultprovider.h" - template class ModelResult : public QAbstractItemModel { @@ -36,160 +35,28 @@ public: 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) { - Q_ASSERT(mEntities.contains(index.internalId())); - return QVariant::fromValue(mEntities.value(index.internalId())); - } - if (role == Qt::DisplayRole) { - if (index.column() < mPropertyColumns.size()) { - Q_ASSERT(mEntities.contains(index.internalId())); - auto entity = mEntities.value(index.internalId()); - return entity->getProperty(mPropertyColumns.at(index.column())).toString(); - } else { - return "No data available"; - } - } - 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 createIndexFromId(const qint64 &id) const - { - auto grandParentId = mParents.value(id, 0); - auto row = mTree.value(grandParentId).indexOf(id); - return createIndex(row, 0, id); - } + ModelResult(const Akonadi2::Query &query, const QList &propertyColumns); - QModelIndex parent(const QModelIndex &index) const - { - auto id = getIdentifier(index); - auto parentId = mParents.value(id); - return createIndexFromId(parentId); - } + int rowCount(const QModelIndex &parent = QModelIndex()) const; + int columnCount(const QModelIndex &parent = QModelIndex()) const; + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const; + QModelIndex index(int row, int column, const QModelIndex & parent = QModelIndex()) const; + QModelIndex parent(const QModelIndex &index) const; - bool canFetchMore(const QModelIndex &parent) const - { - return mEntityChildrenFetched.value(parent.internalId()); - } + bool canFetchMore(const QModelIndex &parent) const; + void fetchMore(const QModelIndex &parent); - void fetchMore(const QModelIndex &parent) - { - fetchEntities(parent); - } + void add(const Ptr &value); + void modify(const Ptr &value); + void remove(const Ptr &value); - qint64 parentId(const Ptr &value) - { - return qHash(value->getProperty("parent").toByteArray()); - } - - void add(const Ptr &value) - { - auto childId = qHash(value->identifier()); - auto id = parentId(value); - //Ignore updates we get before the initial fetch is done - if (!mEntityChildrenFetched[id]) { - return; - } - auto parent = createIndexFromId(id); - qDebug() << "Added entity " << childId << value->identifier(); - const auto keys = mTree[id]; - int index = 0; - for (; index < keys.size(); index++) { - if (childId < keys.at(index)) { - break; - } - } - if (mEntities.contains(childId)) { - qWarning() << "Entity already in model " << value->identifier(); - return; - } - beginInsertRows(QModelIndex(), index, index); - mEntities.insert(childId, value); - mTree[id].insert(index, childId); - mParents.insert(childId, id); - endInsertRows(); - } - - void modify(const Ptr &value) - { - auto childId = qHash(value->identifier()); - auto id = parentId(value); - //Ignore updates we get before the initial fetch is done - if (!mEntityChildrenFetched[id]) { - return; - } - auto parent = createIndexFromId(id); - 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); - } - - void remove(const Ptr &value) - { - auto childId = qHash(value->identifier()); - auto id = parentId(value); - auto parent = createIndexFromId(id); - 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(); - } - - void fetchEntities(const QModelIndex &parent) - { - const auto id = getIdentifier(parent); - mEntityChildrenFetched[id] = true; - Trace() << "Loading child entities"; - loadEntities(parent.data(DomainObjectRole).template value()); - } - - void setFetcher(const std::function &fetcher) - { - Trace() << "Setting fetcher"; - loadEntities = fetcher; - } + void setFetcher(const std::function &fetcher); private: + static qint64 parentId(const Ptr &value); + QModelIndex createIndexFromId(const qint64 &id) const; + void fetchEntities(const QModelIndex &parent); + //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; diff --git a/examples/dummyresource/dummystore.cpp b/examples/dummyresource/dummystore.cpp index 0356ec0..39ecfe4 100644 --- a/examples/dummyresource/dummystore.cpp +++ b/examples/dummyresource/dummystore.cpp @@ -36,6 +36,13 @@ static QMap createMail(int i) return mail; } +static QMap createFolder(int i) +{ + QMap folder; + folder.insert("name", QString("folder%1").arg(i)); + return folder; +} + QMap > populateEvents() { QMap> content; @@ -54,8 +61,18 @@ QMap > populateMails() return content; } +QMap > populateFolders() +{ + QMap> content; + for (int i = 0; i < 5000; i++) { + content.insert(QString("key%1").arg(i), createFolder(i)); + } + return content; +} + static QMap > s_eventSource = populateEvents(); static QMap > s_mailSource = populateMails(); +static QMap > s_folderSource = populateFolders(); QMap > DummyStore::events() const { @@ -66,3 +83,8 @@ QMap > DummyStore::mails() const { return s_mailSource; } + +QMap > DummyStore::folders() const +{ + return s_folderSource; +} -- cgit v1.2.3 From ddb28417ccbcd22e771b7610c1727eac63471609 Mon Sep 17 00:00:00 2001 From: Christian Mollekopf Date: Thu, 19 Nov 2015 23:47:34 +0100 Subject: Moved facade implementation to cpp file --- common/CMakeLists.txt | 1 + common/facade.cpp | 352 +++++++++++++++++++++++++++++++++++++++++++++++++- common/facade.h | 335 +++-------------------------------------------- 3 files changed, 370 insertions(+), 318 deletions(-) diff --git a/common/CMakeLists.txt b/common/CMakeLists.txt index bdb9eac..01056d0 100644 --- a/common/CMakeLists.txt +++ b/common/CMakeLists.txt @@ -12,6 +12,7 @@ else (STORAGE_unqlite) endif (STORAGE_unqlite) set(command_SRCS + modelresult.cpp definitions.cpp log.cpp entitybuffer.cpp diff --git a/common/facade.cpp b/common/facade.cpp index e51b32a..b4931cf 100644 --- a/common/facade.cpp +++ b/common/facade.cpp @@ -1,5 +1,5 @@ /* - * Copyright (C) 2015 Christian Mollekopf + * Copyright (C) 2015 Christian Mollekopf * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -18,3 +18,353 @@ */ #include "facade.h" + +#include "commands.h" +#include "domainadaptor.h" +#include "log.h" +#include "storage.h" +#include "definitions.h" + +using namespace Akonadi2; + +/** + * A QueryRunner runs a query and updates the corresponding result set. + * + * The lifetime of the QueryRunner is defined by the resut set (otherwise it's doing useless work), + * and by how long a result set must be updated. If the query is one off the runner dies after the execution, + * otherwise it lives on the react to changes and updates the corresponding result set. + * + * QueryRunner has to keep ResourceAccess alive in order to keep getting updates. + */ +class QueryRunner : public QObject +{ + Q_OBJECT +public: + typedef std::function()> QueryFunction; + + QueryRunner(const Akonadi2::Query &query) {}; + /** + * Starts query + */ + KAsync::Job run(qint64 newRevision = 0) + { + return queryFunction(); + } + + /** + * Set the query to run + */ + void setQuery(const QueryFunction &query) + { + queryFunction = query; + } + +public slots: + /** + * Rerun query with new revision + */ + void revisionChanged(qint64 newRevision) + { + Trace() << "New revision: " << newRevision; + run().exec(); + } + +private: + QueryFunction queryFunction; +}; + +static inline ResultSet fullScan(const Akonadi2::Storage::Transaction &transaction, const QByteArray &bufferType) +{ + //TODO use a result set with an iterator, to read values on demand + QVector keys; + transaction.openDatabase(bufferType + ".main").scan(QByteArray(), [&](const QByteArray &key, const QByteArray &value) -> bool { + //Skip internals + if (Akonadi2::Storage::isInternalKey(key)) { + return true; + } + keys << Akonadi2::Storage::uidFromKey(key); + return true; + }, + [](const Akonadi2::Storage::Error &error) { + qWarning() << "Error during query: " << error.message; + }); + + Trace() << "Full scan found " << keys.size() << " results"; + return ResultSet(keys); +} + + + +template +GenericFacade::GenericFacade(const QByteArray &resourceIdentifier, const DomainTypeAdaptorFactoryInterface::Ptr &adaptorFactory , const QSharedPointer resourceAccess) + : Akonadi2::StoreFacade(), + mResourceAccess(resourceAccess), + mDomainTypeAdaptorFactory(adaptorFactory), + mResourceInstanceIdentifier(resourceIdentifier) +{ + if (!mResourceAccess) { + mResourceAccess = QSharedPointer::create(resourceIdentifier); + } +} + +template +GenericFacade::~GenericFacade() +{ +} + +template +QByteArray GenericFacade::bufferTypeForDomainType() +{ + //We happen to have a one to one mapping + return Akonadi2::ApplicationDomain::getTypeName(); +} + +template +KAsync::Job GenericFacade::create(const DomainType &domainObject) +{ + if (!mDomainTypeAdaptorFactory) { + Warning() << "No domain type adaptor factory available"; + return KAsync::error(); + } + flatbuffers::FlatBufferBuilder entityFbb; + mDomainTypeAdaptorFactory->createBuffer(domainObject, entityFbb); + return mResourceAccess->sendCreateCommand(bufferTypeForDomainType(), QByteArray::fromRawData(reinterpret_cast(entityFbb.GetBufferPointer()), entityFbb.GetSize())); +} + +template +KAsync::Job GenericFacade::modify(const DomainType &domainObject) +{ + if (!mDomainTypeAdaptorFactory) { + Warning() << "No domain type adaptor factory available"; + return KAsync::error(); + } + flatbuffers::FlatBufferBuilder entityFbb; + mDomainTypeAdaptorFactory->createBuffer(domainObject, entityFbb); + return mResourceAccess->sendModifyCommand(domainObject.identifier(), domainObject.revision(), bufferTypeForDomainType(), QByteArrayList(), QByteArray::fromRawData(reinterpret_cast(entityFbb.GetBufferPointer()), entityFbb.GetSize())); +} + +template +KAsync::Job GenericFacade::remove(const DomainType &domainObject) +{ + return mResourceAccess->sendDeleteCommand(domainObject.identifier(), domainObject.revision(), bufferTypeForDomainType()); +} + +template +KAsync::Job GenericFacade::load(const Akonadi2::Query &query, Akonadi2::ResultProviderInterface &resultProvider) +{ + //We delegate loading of initial data to the result provider, os it can decide for itself what it needs to load. + resultProvider.setFetcher([this, query, &resultProvider](const typename DomainType::Ptr &parent) { + const qint64 newRevision = executeInitialQuery(query, parent, resultProvider); + mResourceAccess->sendRevisionReplayedCommand(newRevision); + }); + + + //In case of a live query we keep the runner for as long alive as the result provider exists + if (query.liveQuery) { + auto runner = QSharedPointer::create(query); + //Incremental updates are always loaded directly, leaving it up to the result to discard the changes if they are not interesting + runner->setQuery([this, query, &resultProvider] () -> KAsync::Job { + return KAsync::start([this, query, &resultProvider](KAsync::Future &future) { + const qint64 newRevision = executeIncrementalQuery(query, resultProvider); + mResourceAccess->sendRevisionReplayedCommand(newRevision); + future.setFinished(); + }); + }); + resultProvider.setQueryRunner(runner); + //Ensure the connection is open, if it wasn't already opened + //TODO If we are not connected already, we have to check for the latest revision once connected, otherwise we could miss some updates + mResourceAccess->open(); + QObject::connect(mResourceAccess.data(), &Akonadi2::ResourceAccess::revisionChanged, runner.data(), &QueryRunner::revisionChanged); + } + return KAsync::null(); +} + + //TODO move into result provider? +template +void GenericFacade::replaySet(ResultSet &resultSet, Akonadi2::ResultProviderInterface &resultProvider) +{ + while (resultSet.next([&resultProvider](const Akonadi2::ApplicationDomain::ApplicationDomainType::Ptr &value, Akonadi2::Operation operation) -> bool { + switch (operation) { + case Akonadi2::Operation_Creation: + Trace() << "Got creation"; + resultProvider.add(Akonadi2::ApplicationDomain::ApplicationDomainType::getInMemoryRepresentation(*value).template staticCast()); + break; + case Akonadi2::Operation_Modification: + Trace() << "Got modification"; + resultProvider.modify(Akonadi2::ApplicationDomain::ApplicationDomainType::getInMemoryRepresentation(*value).template staticCast()); + break; + case Akonadi2::Operation_Removal: + Trace() << "Got removal"; + resultProvider.remove(Akonadi2::ApplicationDomain::ApplicationDomainType::getInMemoryRepresentation(*value).template staticCast()); + break; + } + return true; + })){}; +} + +template +void GenericFacade::readEntity(const Akonadi2::Storage::Transaction &transaction, const QByteArray &key, const std::function &resultCallback) +{ + const auto bufferType = bufferTypeForDomainType(); + //This only works for a 1:1 mapping of resource to domain types. + //Not i.e. for tags that are stored as flags in each entity of an imap store. + //additional properties that don't have a 1:1 mapping (such as separately stored tags), + //could be added to the adaptor. + // + // Akonadi2::Storage::getLatest(transaction, bufferTye, key); + transaction.openDatabase(bufferType + ".main").findLatest(key, [=](const QByteArray &key, const QByteArray &value) -> bool { + Akonadi2::EntityBuffer buffer(value.data(), value.size()); + const Akonadi2::Entity &entity = buffer.entity(); + const auto metadataBuffer = Akonadi2::EntityBuffer::readBuffer(entity.metadata()); + Q_ASSERT(metadataBuffer); + const qint64 revision = metadataBuffer ? metadataBuffer->revision() : -1; + resultCallback(DomainType::Ptr::create(mResourceInstanceIdentifier, Akonadi2::Storage::uidFromKey(key), revision, mDomainTypeAdaptorFactory->createAdaptor(entity)), metadataBuffer->operation()); + return false; + }, + [](const Akonadi2::Storage::Error &error) { + qWarning() << "Error during query: " << error.message; + }); +} + +template +ResultSet GenericFacade::loadInitialResultSet(const Akonadi2::Query &query, Akonadi2::Storage::Transaction &transaction, QSet &remainingFilters) +{ + QSet appliedFilters; + auto resultSet = Akonadi2::ApplicationDomain::TypeImplementation::queryIndexes(query, mResourceInstanceIdentifier, appliedFilters, transaction); + remainingFilters = query.propertyFilter.keys().toSet() - appliedFilters; + + //We do a full scan if there were no indexes available to create the initial set. + if (appliedFilters.isEmpty()) { + //TODO this should be replaced by an index lookup as well + resultSet = fullScan(transaction, bufferTypeForDomainType()); + } + return resultSet; +} + +template +ResultSet GenericFacade::loadIncrementalResultSet(qint64 baseRevision, const Akonadi2::Query &query, Akonadi2::Storage::Transaction &transaction, QSet &remainingFilters) +{ + const auto bufferType = bufferTypeForDomainType(); + auto revisionCounter = QSharedPointer::create(baseRevision); + remainingFilters = query.propertyFilter.keys().toSet(); + return ResultSet([bufferType, revisionCounter, &transaction]() -> QByteArray { + const qint64 topRevision = Akonadi2::Storage::maxRevision(transaction); + //Spit out the revision keys one by one. + while (*revisionCounter <= topRevision) { + const auto uid = Akonadi2::Storage::getUidFromRevision(transaction, *revisionCounter); + const auto type = Akonadi2::Storage::getTypeFromRevision(transaction, *revisionCounter); + Trace() << "Revision" << *revisionCounter << type << uid; + if (type != bufferType) { + //Skip revision + *revisionCounter += 1; + continue; + } + const auto key = Akonadi2::Storage::assembleKey(uid, *revisionCounter); + *revisionCounter += 1; + return key; + } + Trace() << "Finished reading incremental result set:" << *revisionCounter; + //We're done + return QByteArray(); + }); +} + +template +ResultSet GenericFacade::filterSet(const ResultSet &resultSet, const std::function &filter, const Akonadi2::Storage::Transaction &transaction, bool initialQuery) +{ + auto resultSetPtr = QSharedPointer::create(resultSet); + + //Read through the source values and return whatever matches the filter + std::function)> generator = [this, resultSetPtr, &transaction, filter, initialQuery](std::function callback) -> bool { + while (resultSetPtr->next()) { + //readEntity is only necessary if we actually want to filter or know the operation type (but not a big deal if we do it always I guess) + readEntity(transaction, resultSetPtr->id(), [this, filter, callback, initialQuery](const Akonadi2::ApplicationDomain::ApplicationDomainType::Ptr &domainObject, Akonadi2::Operation operation) { + //Always remove removals, they probably don't match due to non-available properties + if (filter(domainObject) || operation == Akonadi2::Operation_Removal) { + if (initialQuery) { + //We're not interested in removals during the initial query + if (operation != Akonadi2::Operation_Removal) { + callback(domainObject, Akonadi2::Operation_Creation); + } + } else { + callback(domainObject, operation); + } + } + }); + } + return false; + }; + return ResultSet(generator); +} + + +template +std::function GenericFacade::getFilter(const QSet remainingFilters, const Akonadi2::Query &query) +{ + return [remainingFilters, query](const Akonadi2::ApplicationDomain::ApplicationDomainType::Ptr &domainObject) -> bool { + for (const auto &filterProperty : remainingFilters) { + const auto property = domainObject->getProperty(filterProperty); + if (property.isValid()) { + //TODO implement other comparison operators than equality + if (property != query.propertyFilter.value(filterProperty)) { + Trace() << "Filtering entity due to property mismatch: " << domainObject->getProperty(filterProperty); + return false; + } + } else { + Warning() << "Ignored property filter because value is invalid: " << filterProperty; + } + } + return true; + }; +} + +template +qint64 GenericFacade::load(const Akonadi2::Query &query, const std::function &)> &baseSetRetriever, Akonadi2::ResultProviderInterface &resultProvider) +{ + Akonadi2::Storage storage(Akonadi2::storageLocation(), mResourceInstanceIdentifier); + storage.setDefaultErrorHandler([](const Akonadi2::Storage::Error &error) { + Warning() << "Error during query: " << error.store << error.message; + }); + auto transaction = storage.createTransaction(Akonadi2::Storage::ReadOnly); + + QSet remainingFilters; + auto resultSet = baseSetRetriever(transaction, remainingFilters); + auto filteredSet = filterSet(resultSet, getFilter(remainingFilters, query), transaction, false); + replaySet(filteredSet, resultProvider); + resultProvider.setRevision(Akonadi2::Storage::maxRevision(transaction)); + return Akonadi2::Storage::maxRevision(transaction); +} + + +template +qint64 GenericFacade::executeIncrementalQuery(const Akonadi2::Query &query, Akonadi2::ResultProviderInterface &resultProvider) +{ + const qint64 baseRevision = resultProvider.revision() + 1; + Trace() << "Running incremental query " << baseRevision; + return load(query, [&](Akonadi2::Storage::Transaction &transaction, QSet &remainingFilters) -> ResultSet { + return loadIncrementalResultSet(baseRevision, query, transaction, remainingFilters); + }, resultProvider); +} + +template +qint64 GenericFacade::executeInitialQuery(const Akonadi2::Query &query, const typename DomainType::Ptr &parent, Akonadi2::ResultProviderInterface &resultProvider) +{ + auto modifiedQuery = query; + if (parent) { + Trace() << "Running initial query for parent:" << parent->identifier(); + modifiedQuery.propertyFilter.insert("parent", parent->identifier()); + } else { + Trace() << "Running initial query for toplevel"; + modifiedQuery.propertyFilter.insert("parent", QVariant()); + } + return load(modifiedQuery, [&](Akonadi2::Storage::Transaction &transaction, QSet &remainingFilters) -> ResultSet { + return loadInitialResultSet(modifiedQuery, transaction, remainingFilters); + }, resultProvider); +} + +template class Akonadi2::GenericFacade; +template class Akonadi2::GenericFacade; +template class Akonadi2::GenericFacade; +// template class Akonadi2::GenericFacade; + +#include "facade.moc" diff --git a/common/facade.h b/common/facade.h index 8b8a2a8..aa50941 100644 --- a/common/facade.h +++ b/common/facade.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2014 Christian Mollekopf + * Copyright (C) 2014 Christian Mollekopf * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -25,79 +25,8 @@ #include #include "resourceaccess.h" -#include "commands.h" -#include "domainadaptor.h" -#include "log.h" #include "resultset.h" -#include "storage.h" -#include "definitions.h" - -/** - * A QueryRunner runs a query and updates the corresponding result set. - * - * The lifetime of the QueryRunner is defined by the resut set (otherwise it's doing useless work), - * and by how long a result set must be updated. If the query is one off the runner dies after the execution, - * otherwise it lives on the react to changes and updates the corresponding result set. - * - * QueryRunner has to keep ResourceAccess alive in order to keep getting updates. - */ -class QueryRunner : public QObject -{ - Q_OBJECT -public: - typedef std::function()> QueryFunction; - - QueryRunner(const Akonadi2::Query &query) {}; - /** - * Starts query - */ - KAsync::Job run(qint64 newRevision = 0) - { - return queryFunction(); - } - - /** - * Set the query to run - */ - void setQuery(const QueryFunction &query) - { - queryFunction = query; - } - -public slots: - /** - * Rerun query with new revision - */ - void revisionChanged(qint64 newRevision) - { - Trace() << "New revision: " << newRevision; - run().exec(); - } - -private: - QueryFunction queryFunction; -}; - -static inline ResultSet fullScan(const Akonadi2::Storage::Transaction &transaction, const QByteArray &bufferType) -{ - //TODO use a result set with an iterator, to read values on demand - QVector keys; - transaction.openDatabase(bufferType + ".main").scan(QByteArray(), [&](const QByteArray &key, const QByteArray &value) -> bool { - //Skip internals - if (Akonadi2::Storage::isInternalKey(key)) { - return true; - } - keys << Akonadi2::Storage::uidFromKey(key); - return true; - }, - [](const Akonadi2::Storage::Error &error) { - qWarning() << "Error during query: " << error.message; - }); - - Trace() << "Full scan found " << keys.size() << " results"; - return ResultSet(keys); -} - +#include "domainadaptor.h" namespace Akonadi2 { /** @@ -121,257 +50,29 @@ public: * @param resourceIdentifier is the identifier of the resource instance * @param adaptorFactory is the adaptor factory used to generate the mappings from domain to resource types and vice versa */ - GenericFacade(const QByteArray &resourceIdentifier, const DomainTypeAdaptorFactoryInterface::Ptr &adaptorFactory = DomainTypeAdaptorFactoryInterface::Ptr(), const QSharedPointer resourceAccess = QSharedPointer()) - : Akonadi2::StoreFacade(), - mResourceAccess(resourceAccess), - mDomainTypeAdaptorFactory(adaptorFactory), - mResourceInstanceIdentifier(resourceIdentifier) - { - if (!mResourceAccess) { - mResourceAccess = QSharedPointer::create(resourceIdentifier); - } - } - - ~GenericFacade() - { - } - - static QByteArray bufferTypeForDomainType() - { - //We happen to have a one to one mapping - return Akonadi2::ApplicationDomain::getTypeName(); - } - - KAsync::Job create(const DomainType &domainObject) Q_DECL_OVERRIDE - { - if (!mDomainTypeAdaptorFactory) { - Warning() << "No domain type adaptor factory available"; - return KAsync::error(); - } - flatbuffers::FlatBufferBuilder entityFbb; - mDomainTypeAdaptorFactory->createBuffer(domainObject, entityFbb); - return mResourceAccess->sendCreateCommand(bufferTypeForDomainType(), QByteArray::fromRawData(reinterpret_cast(entityFbb.GetBufferPointer()), entityFbb.GetSize())); - } - - KAsync::Job modify(const DomainType &domainObject) Q_DECL_OVERRIDE - { - if (!mDomainTypeAdaptorFactory) { - Warning() << "No domain type adaptor factory available"; - return KAsync::error(); - } - flatbuffers::FlatBufferBuilder entityFbb; - mDomainTypeAdaptorFactory->createBuffer(domainObject, entityFbb); - return mResourceAccess->sendModifyCommand(domainObject.identifier(), domainObject.revision(), bufferTypeForDomainType(), QByteArrayList(), QByteArray::fromRawData(reinterpret_cast(entityFbb.GetBufferPointer()), entityFbb.GetSize())); - } - - KAsync::Job remove(const DomainType &domainObject) Q_DECL_OVERRIDE - { - return mResourceAccess->sendDeleteCommand(domainObject.identifier(), domainObject.revision(), bufferTypeForDomainType()); - } - - KAsync::Job load(const Akonadi2::Query &query, Akonadi2::ResultProviderInterface &resultProvider) Q_DECL_OVERRIDE - { - //We delegate loading of initial data to the result provider, os it can decide for itself what it needs to load. - resultProvider.setFetcher([this, query, &resultProvider](const typename DomainType::Ptr &parent) { - const qint64 newRevision = executeInitialQuery(query, parent, resultProvider); - mResourceAccess->sendRevisionReplayedCommand(newRevision); - }); - + GenericFacade(const QByteArray &resourceIdentifier, const DomainTypeAdaptorFactoryInterface::Ptr &adaptorFactory = DomainTypeAdaptorFactoryInterface::Ptr(), const QSharedPointer resourceAccess = QSharedPointer()); + ~GenericFacade(); - //In case of a live query we keep the runner for as long alive as the result provider exists - if (query.liveQuery) { - auto runner = QSharedPointer::create(query); - //Incremental updates are always loaded directly, leaving it up to the result to discard the changes if they are not interesting - runner->setQuery([this, query, &resultProvider] () -> KAsync::Job { - return KAsync::start([this, query, &resultProvider](KAsync::Future &future) { - const qint64 newRevision = executeIncrementalQuery(query, resultProvider); - mResourceAccess->sendRevisionReplayedCommand(newRevision); - future.setFinished(); - }); - }); - resultProvider.setQueryRunner(runner); - //Ensure the connection is open, if it wasn't already opened - //TODO If we are not connected already, we have to check for the latest revision once connected, otherwise we could miss some updates - mResourceAccess->open(); - QObject::connect(mResourceAccess.data(), &Akonadi2::ResourceAccess::revisionChanged, runner.data(), &QueryRunner::revisionChanged); - } - return KAsync::null(); - } + static QByteArray bufferTypeForDomainType(); + KAsync::Job create(const DomainType &domainObject) Q_DECL_OVERRIDE; + KAsync::Job modify(const DomainType &domainObject) Q_DECL_OVERRIDE; + KAsync::Job remove(const DomainType &domainObject) Q_DECL_OVERRIDE; + KAsync::Job load(const Akonadi2::Query &query, Akonadi2::ResultProviderInterface &resultProvider) Q_DECL_OVERRIDE; private: - //TODO move into result provider? - static void replaySet(ResultSet &resultSet, Akonadi2::ResultProviderInterface &resultProvider) - { - while (resultSet.next([&resultProvider](const Akonadi2::ApplicationDomain::ApplicationDomainType::Ptr &value, Akonadi2::Operation operation) -> bool { - switch (operation) { - case Akonadi2::Operation_Creation: - Trace() << "Got creation"; - resultProvider.add(Akonadi2::ApplicationDomain::ApplicationDomainType::getInMemoryRepresentation(*value).template staticCast()); - break; - case Akonadi2::Operation_Modification: - Trace() << "Got modification"; - resultProvider.modify(Akonadi2::ApplicationDomain::ApplicationDomainType::getInMemoryRepresentation(*value).template staticCast()); - break; - case Akonadi2::Operation_Removal: - Trace() << "Got removal"; - resultProvider.remove(Akonadi2::ApplicationDomain::ApplicationDomainType::getInMemoryRepresentation(*value).template staticCast()); - break; - } - return true; - })){}; - } - - void readEntity(const Akonadi2::Storage::Transaction &transaction, const QByteArray &key, const std::function &resultCallback) - { - const auto bufferType = bufferTypeForDomainType(); - //This only works for a 1:1 mapping of resource to domain types. - //Not i.e. for tags that are stored as flags in each entity of an imap store. - //additional properties that don't have a 1:1 mapping (such as separately stored tags), - //could be added to the adaptor. - // - // Akonadi2::Storage::getLatest(transaction, bufferTye, key); - transaction.openDatabase(bufferType + ".main").findLatest(key, [=](const QByteArray &key, const QByteArray &value) -> bool { - Akonadi2::EntityBuffer buffer(value.data(), value.size()); - const Akonadi2::Entity &entity = buffer.entity(); - const auto metadataBuffer = Akonadi2::EntityBuffer::readBuffer(entity.metadata()); - Q_ASSERT(metadataBuffer); - const qint64 revision = metadataBuffer ? metadataBuffer->revision() : -1; - resultCallback(DomainType::Ptr::create(mResourceInstanceIdentifier, Akonadi2::Storage::uidFromKey(key), revision, mDomainTypeAdaptorFactory->createAdaptor(entity)), metadataBuffer->operation()); - return false; - }, - [](const Akonadi2::Storage::Error &error) { - qWarning() << "Error during query: " << error.message; - }); - } - - ResultSet loadInitialResultSet(const Akonadi2::Query &query, Akonadi2::Storage::Transaction &transaction, QSet &remainingFilters) - { - QSet appliedFilters; - auto resultSet = Akonadi2::ApplicationDomain::TypeImplementation::queryIndexes(query, mResourceInstanceIdentifier, appliedFilters, transaction); - remainingFilters = query.propertyFilter.keys().toSet() - appliedFilters; - - //We do a full scan if there were no indexes available to create the initial set. - if (appliedFilters.isEmpty()) { - //TODO this should be replaced by an index lookup as well - resultSet = fullScan(transaction, bufferTypeForDomainType()); - } - return resultSet; - } - - ResultSet loadIncrementalResultSet(qint64 baseRevision, const Akonadi2::Query &query, Akonadi2::Storage::Transaction &transaction, QSet &remainingFilters) - { - const auto bufferType = bufferTypeForDomainType(); - auto revisionCounter = QSharedPointer::create(baseRevision); - remainingFilters = query.propertyFilter.keys().toSet(); - return ResultSet([bufferType, revisionCounter, &transaction]() -> QByteArray { - const qint64 topRevision = Akonadi2::Storage::maxRevision(transaction); - //Spit out the revision keys one by one. - while (*revisionCounter <= topRevision) { - const auto uid = Akonadi2::Storage::getUidFromRevision(transaction, *revisionCounter); - const auto type = Akonadi2::Storage::getTypeFromRevision(transaction, *revisionCounter); - Trace() << "Revision" << *revisionCounter << type << uid; - if (type != bufferType) { - //Skip revision - *revisionCounter += 1; - continue; - } - const auto key = Akonadi2::Storage::assembleKey(uid, *revisionCounter); - *revisionCounter += 1; - return key; - } - //We're done - return QByteArray(); - }); - } - - ResultSet filterSet(const ResultSet &resultSet, const std::function &filter, const Akonadi2::Storage::Transaction &transaction, bool initialQuery) - { - auto resultSetPtr = QSharedPointer::create(resultSet); - - //Read through the source values and return whatever matches the filter - std::function)> generator = [this, resultSetPtr, &transaction, filter, initialQuery](std::function callback) -> bool { - while (resultSetPtr->next()) { - //readEntity is only necessary if we actually want to filter or know the operation type (but not a big deal if we do it always I guess) - readEntity(transaction, resultSetPtr->id(), [this, filter, callback, initialQuery](const Akonadi2::ApplicationDomain::ApplicationDomainType::Ptr &domainObject, Akonadi2::Operation operation) { - //Always remove removals, they probably don't match due to non-available properties - if (filter(domainObject) || operation == Akonadi2::Operation_Removal) { - if (initialQuery) { - //We're not interested in removals during the initial query - if (operation != Akonadi2::Operation_Removal) { - callback(domainObject, Akonadi2::Operation_Creation); - } - } else { - callback(domainObject, operation); - } - } - }); - } - return false; - }; - return ResultSet(generator); - } - - - std::function getFilter(const QSet remainingFilters, const Akonadi2::Query &query) - { - return [remainingFilters, query](const Akonadi2::ApplicationDomain::ApplicationDomainType::Ptr &domainObject) -> bool { - for (const auto &filterProperty : remainingFilters) { - const auto property = domainObject->getProperty(filterProperty); - if (property.isValid()) { - //TODO implement other comparison operators than equality - if (property != query.propertyFilter.value(filterProperty)) { - Trace() << "Filtering entity due to property mismatch: " << domainObject->getProperty(filterProperty); - return false; - } - } else { - Warning() << "Ignored property filter because value is invalid: " << filterProperty; - } - } - return true; - }; - } - - qint64 load(const Akonadi2::Query &query, const std::function &)> &baseSetRetriever, Akonadi2::ResultProviderInterface &resultProvider) - { - Akonadi2::Storage storage(Akonadi2::storageLocation(), mResourceInstanceIdentifier); - storage.setDefaultErrorHandler([](const Akonadi2::Storage::Error &error) { - Warning() << "Error during query: " << error.store << error.message; - }); - auto transaction = storage.createTransaction(Akonadi2::Storage::ReadOnly); - - QSet remainingFilters; - auto resultSet = baseSetRetriever(transaction, remainingFilters); - auto filteredSet = filterSet(resultSet, getFilter(remainingFilters, query), transaction, false); - replaySet(filteredSet, resultProvider); - resultProvider.setRevision(Akonadi2::Storage::maxRevision(transaction)); - return Akonadi2::Storage::maxRevision(transaction); - } + static void replaySet(ResultSet &resultSet, Akonadi2::ResultProviderInterface &resultProvider); + void readEntity(const Akonadi2::Storage::Transaction &transaction, const QByteArray &key, const std::function &resultCallback); - qint64 executeIncrementalQuery(const Akonadi2::Query &query, Akonadi2::ResultProviderInterface &resultProvider) - { - const qint64 baseRevision = resultProvider.revision() + 1; - Trace() << "Running incremental query " << baseRevision; - return load(query, [&](Akonadi2::Storage::Transaction &transaction, QSet &remainingFilters) -> ResultSet { - return loadIncrementalResultSet(baseRevision, query, transaction, remainingFilters); - }, resultProvider); - } + ResultSet loadInitialResultSet(const Akonadi2::Query &query, Akonadi2::Storage::Transaction &transaction, QSet &remainingFilters); + ResultSet loadIncrementalResultSet(qint64 baseRevision, const Akonadi2::Query &query, Akonadi2::Storage::Transaction &transaction, QSet &remainingFilters); - qint64 executeInitialQuery(const Akonadi2::Query &query, const typename DomainType::Ptr &parent, Akonadi2::ResultProviderInterface &resultProvider) - { - auto modifiedQuery = query; - if (parent) { - Trace() << "Running initial query for parent:" << parent->identifier(); - modifiedQuery.propertyFilter.insert("parent", parent->identifier()); - } else { - Trace() << "Running initial query for toplevel"; - modifiedQuery.propertyFilter.insert("parent", QVariant()); - } - return load(modifiedQuery, [&](Akonadi2::Storage::Transaction &transaction, QSet &remainingFilters) -> ResultSet { - return loadInitialResultSet(modifiedQuery, transaction, remainingFilters); - }, resultProvider); - } + ResultSet filterSet(const ResultSet &resultSet, const std::function &filter, const Akonadi2::Storage::Transaction &transaction, bool initialQuery); + std::function getFilter(const QSet remainingFilters, const Akonadi2::Query &query); + qint64 load(const Akonadi2::Query &query, const std::function &)> &baseSetRetriever, Akonadi2::ResultProviderInterface &resultProvider); + qint64 executeIncrementalQuery(const Akonadi2::Query &query, Akonadi2::ResultProviderInterface &resultProvider); + qint64 executeInitialQuery(const Akonadi2::Query &query, const typename DomainType::Ptr &parent, Akonadi2::ResultProviderInterface &resultProvider); protected: //TODO use one resource access instance per application & per resource -- cgit v1.2.3 From 0b967e06a1a50c1f540b941d381680cdf3ac4706 Mon Sep 17 00:00:00 2001 From: Christian Mollekopf Date: Sat, 21 Nov 2015 02:29:35 +0100 Subject: Fixed build --- common/facade.cpp | 1 - tests/clientapitest.cpp | 1 + tests/genericfacadebenchmark.cpp | 1 + tests/pipelinetest.cpp | 1 + 4 files changed, 3 insertions(+), 1 deletion(-) diff --git a/common/facade.cpp b/common/facade.cpp index b4931cf..f534319 100644 --- a/common/facade.cpp +++ b/common/facade.cpp @@ -20,7 +20,6 @@ #include "facade.h" #include "commands.h" -#include "domainadaptor.h" #include "log.h" #include "storage.h" #include "definitions.h" diff --git a/tests/clientapitest.cpp b/tests/clientapitest.cpp index ce11221..4883b5e 100644 --- a/tests/clientapitest.cpp +++ b/tests/clientapitest.cpp @@ -8,6 +8,7 @@ #include "resourceconfig.h" #include "modelresult.h" #include "resultprovider.h" +#include "facadefactory.h" template class DummyResourceFacade : public Akonadi2::StoreFacade diff --git a/tests/genericfacadebenchmark.cpp b/tests/genericfacadebenchmark.cpp index 8b00666..703acd1 100644 --- a/tests/genericfacadebenchmark.cpp +++ b/tests/genericfacadebenchmark.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include "event_generated.h" diff --git a/tests/pipelinetest.cpp b/tests/pipelinetest.cpp index 47090a8..2ede69d 100644 --- a/tests/pipelinetest.cpp +++ b/tests/pipelinetest.cpp @@ -19,6 +19,7 @@ #include "pipeline.h" #include "log.h" #include "domainadaptor.h" +#include "definitions.h" static void removeFromDisk(const QString &name) { -- cgit v1.2.3 From ec92f856854a35bd888b883802a1ef618cc9f69c Mon Sep 17 00:00:00 2001 From: Christian Mollekopf Date: Sat, 21 Nov 2015 02:29:57 +0100 Subject: Don't try to fetch more once the parent is fetched. We're not doing partial fetches yet --- common/modelresult.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/common/modelresult.cpp b/common/modelresult.cpp index 1abcc62..5b9e24f 100644 --- a/common/modelresult.cpp +++ b/common/modelresult.cpp @@ -104,7 +104,7 @@ template bool ModelResult::canFetchMore(const QModelIndex &parent) const { qDebug() << "Can fetch more: " << parent << mEntityChildrenFetched.value(parent.internalId()); - return mEntityChildrenFetched.value(parent.internalId()); + return !mEntityChildrenFetched.value(parent.internalId(), false); } template @@ -133,7 +133,7 @@ void ModelResult::add(const Ptr &value) } } if (mEntities.contains(childId)) { - qWarning() << "Entity already in model " << value->identifier(); + Warning() << "Entity already in model " << value->identifier(); return; } qDebug() << "Inserting rows " << index << parent; -- cgit v1.2.3 From 110817a23463c71eacbc986af3ae509462758a3c Mon Sep 17 00:00:00 2001 From: Christian Mollekopf Date: Sat, 21 Nov 2015 11:07:47 +0100 Subject: Separated DomainTypeAdaptorFactoryInterface --- common/domainadaptor.h | 12 ++------- common/domaintypeadaptorfactoryinterface.h | 42 ++++++++++++++++++++++++++++++ common/facade.cpp | 1 + common/facade.h | 4 ++- 4 files changed, 48 insertions(+), 11 deletions(-) create mode 100644 common/domaintypeadaptorfactoryinterface.h diff --git a/common/domainadaptor.h b/common/domainadaptor.h index 620a658..b541e23 100644 --- a/common/domainadaptor.h +++ b/common/domainadaptor.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2014 Christian Mollekopf + * Copyright (C) 2014 Christian Mollekopf * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -23,6 +23,7 @@ #include #include +#include "domaintypeadaptorfactoryinterface.h" #include "domain/applicationdomaintype.h" #include "domain/event.h" #include "domain/mail.h" @@ -124,15 +125,6 @@ public: QSharedPointer > mResourceMapper; }; -class DomainTypeAdaptorFactoryInterface -{ -public: - typedef QSharedPointer Ptr; - virtual ~DomainTypeAdaptorFactoryInterface() {}; - virtual QSharedPointer createAdaptor(const Akonadi2::Entity &entity) = 0; - virtual void createBuffer(const Akonadi2::ApplicationDomain::ApplicationDomainType &domainType, flatbuffers::FlatBufferBuilder &fbb, void const *metadataData = 0, size_t metadataSize = 0) = 0; -}; - /** * The factory should define how to go from an entitybuffer (local + resource buffer), to a domain type adapter. * It defines how values are split accross local and resource buffer. diff --git a/common/domaintypeadaptorfactoryinterface.h b/common/domaintypeadaptorfactoryinterface.h new file mode 100644 index 0000000..8c99aa1 --- /dev/null +++ b/common/domaintypeadaptorfactoryinterface.h @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2014 Christian Mollekopf + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the + * Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ +#pragma once + +#include + +namespace Akonadi2 { + namespace ApplicationDomain { + class BufferAdaptor; + class ApplicationDomainType; + } + struct Entity; +} + +namespace flatbuffers { + class FlatBufferBuilder; +} + +class DomainTypeAdaptorFactoryInterface +{ +public: + typedef QSharedPointer Ptr; + virtual ~DomainTypeAdaptorFactoryInterface() {}; + virtual QSharedPointer createAdaptor(const Akonadi2::Entity &entity) = 0; + virtual void createBuffer(const Akonadi2::ApplicationDomain::ApplicationDomainType &domainType, flatbuffers::FlatBufferBuilder &fbb, void const *metadataData = 0, size_t metadataSize = 0) = 0; +}; diff --git a/common/facade.cpp b/common/facade.cpp index f534319..08f7500 100644 --- a/common/facade.cpp +++ b/common/facade.cpp @@ -23,6 +23,7 @@ #include "log.h" #include "storage.h" #include "definitions.h" +#include "domainadaptor.h" using namespace Akonadi2; diff --git a/common/facade.h b/common/facade.h index aa50941..794e35e 100644 --- a/common/facade.h +++ b/common/facade.h @@ -26,9 +26,11 @@ #include "resourceaccess.h" #include "resultset.h" -#include "domainadaptor.h" +#include "domaintypeadaptorfactoryinterface.h" +#include "storage.h" namespace Akonadi2 { + /** * Default facade implementation for resources that are implemented in a separate process using the ResourceAccess class. * -- cgit v1.2.3 From 9ad96df6cd1526de32bff2b4f98491dd8318f760 Mon Sep 17 00:00:00 2001 From: Christian Mollekopf Date: Tue, 24 Nov 2015 23:00:45 +0100 Subject: Use Query::parentProperty to express tree queries That way we don't have to hardcode the parent property, and we can use the property to express non-tree queries as well. --- common/facade.cpp | 7 +++---- common/query.h | 1 + tests/querytest.cpp | 1 + 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/common/facade.cpp b/common/facade.cpp index 08f7500..59972bf 100644 --- a/common/facade.cpp +++ b/common/facade.cpp @@ -350,12 +350,12 @@ template qint64 GenericFacade::executeInitialQuery(const Akonadi2::Query &query, const typename DomainType::Ptr &parent, Akonadi2::ResultProviderInterface &resultProvider) { auto modifiedQuery = query; - if (parent) { + if (parent && !query.parentProperty.isEmpty()) { Trace() << "Running initial query for parent:" << parent->identifier(); - modifiedQuery.propertyFilter.insert("parent", parent->identifier()); + modifiedQuery.propertyFilter.insert(query.parentProperty, parent->identifier()); } else { Trace() << "Running initial query for toplevel"; - modifiedQuery.propertyFilter.insert("parent", QVariant()); + modifiedQuery.propertyFilter.insert(query.parentProperty, QVariant()); } return load(modifiedQuery, [&](Akonadi2::Storage::Transaction &transaction, QSet &remainingFilters) -> ResultSet { return loadInitialResultSet(modifiedQuery, transaction, remainingFilters); @@ -365,6 +365,5 @@ qint64 GenericFacade::executeInitialQuery(const Akonadi2::Query &que template class Akonadi2::GenericFacade; template class Akonadi2::GenericFacade; template class Akonadi2::GenericFacade; -// template class Akonadi2::GenericFacade; #include "facade.moc" diff --git a/common/query.h b/common/query.h index 0cad9fb..5313fa9 100644 --- a/common/query.h +++ b/common/query.h @@ -53,6 +53,7 @@ public: QHash propertyFilter; //Properties to retrieve QSet requestedProperties; + QByteArray parentProperty; bool syncOnDemand; bool processAll; //If live query is false, this query will not continuously be updated diff --git a/tests/querytest.cpp b/tests/querytest.cpp index 677dbac..fdfb609 100644 --- a/tests/querytest.cpp +++ b/tests/querytest.cpp @@ -145,6 +145,7 @@ private Q_SLOTS: query.resources << "org.kde.dummy.instance1"; query.syncOnDemand = false; query.processAll = true; + query.parentProperty = "parent"; //Ensure all local data is processed Akonadi2::Store::synchronize(query).exec().waitForFinished(); -- cgit v1.2.3 From e4a4d72fd206fc2d5c1095b39b2839e53cd114bb Mon Sep 17 00:00:00 2001 From: Christian Mollekopf Date: Wed, 25 Nov 2015 00:37:42 +0100 Subject: Optimize findLast This just gave a 700% boost to query performance from ~2k to 14k reads per second... --- common/storage_lmdb.cpp | 71 ++++++++++++++++++++++++++++++++++++++++--------- tests/storagetest.cpp | 30 +++++++++++++++++++++ 2 files changed, 89 insertions(+), 12 deletions(-) diff --git a/common/storage_lmdb.cpp b/common/storage_lmdb.cpp index 1516e69..a247e38 100644 --- a/common/storage_lmdb.cpp +++ b/common/storage_lmdb.cpp @@ -253,22 +253,69 @@ int Storage::NamedDatabase::scan(const QByteArray &k, return numberOfRetrievedValues; } -void Storage::NamedDatabase::findLatest(const QByteArray &uid, + +void Storage::NamedDatabase::findLatest(const QByteArray &k, const std::function &resultHandler, const std::function &errorHandler) const { - QByteArray latestKey; - scan(uid, [&](const QByteArray &key, const QByteArray &value) -> bool { - latestKey = key; - return true; - }, - errorHandler, true); + if (!d || !d->transaction) { + //Not an error. We rely on this to read nothing from non-existing databases. + return; + } - scan(latestKey, [=](const QByteArray &key, const QByteArray &value) -> bool { - resultHandler(key, value); - return false; - }, - errorHandler); + int rc; + MDB_val key; + MDB_val data; + MDB_cursor *cursor; + + key.mv_data = (void*)k.constData(); + key.mv_size = k.size(); + + rc = mdb_cursor_open(d->transaction, d->dbi, &cursor); + if (rc) { + Error error(d->name.toLatin1(), getErrorCode(rc), QByteArray("Error during mdb_cursor open: ") + QByteArray(mdb_strerror(rc))); + errorHandler ? errorHandler(error) : d->defaultErrorHandler(error); + return; + } + + MDB_cursor_op op = MDB_SET_RANGE; + if ((rc = mdb_cursor_get(cursor, &key, &data, op)) == 0) { + //The first lookup will find a key that is equal or greather than our key + if (QByteArray::fromRawData((char*)key.mv_data, key.mv_size).startsWith(k)) { + bool advanced = false; + while (QByteArray::fromRawData((char*)key.mv_data, key.mv_size).startsWith(k)) { + advanced = true; + MDB_cursor_op nextOp = MDB_NEXT; + rc = mdb_cursor_get(cursor, &key, &data, nextOp); + if (rc) { + break; + } + } + if (advanced) { + MDB_cursor_op prefOp = MDB_PREV; + //We read past the end above, just take the last value + if (rc == MDB_NOTFOUND) { + prefOp = MDB_LAST; + } + rc = mdb_cursor_get(cursor, &key, &data, prefOp); + resultHandler(QByteArray::fromRawData((char*)key.mv_data, key.mv_size), QByteArray::fromRawData((char*)data.mv_data, data.mv_size)); + } + } + } + + //We never find the last value + if (rc == MDB_NOTFOUND) { + rc = 0; + } + + mdb_cursor_close(cursor); + + if (rc) { + Error error(d->name.toLatin1(), getErrorCode(rc), QByteArray("Key: ") + k + " : " + QByteArray(mdb_strerror(rc))); + errorHandler ? errorHandler(error) : d->defaultErrorHandler(error); + } + + return; } diff --git a/tests/storagetest.cpp b/tests/storagetest.cpp index bef8755..d950961 100644 --- a/tests/storagetest.cpp +++ b/tests/storagetest.cpp @@ -376,6 +376,7 @@ private Q_SLOTS: db.write("sub1","value1"); db.write("sub2","value2"); db.write("wub3","value3"); + db.write("wub4","value4"); QByteArray result; db.findLatest("sub", [&](const QByteArray &key, const QByteArray &value) { result = value; @@ -384,6 +385,35 @@ private Q_SLOTS: QCOMPARE(result, QByteArray("value2")); } + void testFindLatestInSingle() + { + Akonadi2::Storage store(testDataPath, dbName, Akonadi2::Storage::ReadWrite); + auto transaction = store.createTransaction(Akonadi2::Storage::ReadWrite); + auto db = transaction.openDatabase("test", nullptr, false); + db.write("sub2","value2"); + QByteArray result; + db.findLatest("sub", [&](const QByteArray &key, const QByteArray &value) { + result = value; + }); + + QCOMPARE(result, QByteArray("value2")); + } + + void testFindLast() + { + Akonadi2::Storage store(testDataPath, dbName, Akonadi2::Storage::ReadWrite); + auto transaction = store.createTransaction(Akonadi2::Storage::ReadWrite); + auto db = transaction.openDatabase("test", nullptr, false); + db.write("sub2","value2"); + db.write("wub3","value3"); + QByteArray result; + db.findLatest("wub", [&](const QByteArray &key, const QByteArray &value) { + result = value; + }); + + QCOMPARE(result, QByteArray("value3")); + } + void testRecordRevision() { Akonadi2::Storage store(testDataPath, dbName, Akonadi2::Storage::ReadWrite); -- cgit v1.2.3 From 00e6b843e9f2881faccb312594a0e91c42df0096 Mon Sep 17 00:00:00 2001 From: Christian Mollekopf Date: Wed, 25 Nov 2015 09:22:03 +0100 Subject: Less noise --- common/facade.cpp | 6 +++--- common/modelresult.cpp | 6 +++--- common/resourceaccess.cpp | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/common/facade.cpp b/common/facade.cpp index 59972bf..850d28b 100644 --- a/common/facade.cpp +++ b/common/facade.cpp @@ -186,15 +186,15 @@ void GenericFacade::replaySet(ResultSet &resultSet, Akonadi2::Result while (resultSet.next([&resultProvider](const Akonadi2::ApplicationDomain::ApplicationDomainType::Ptr &value, Akonadi2::Operation operation) -> bool { switch (operation) { case Akonadi2::Operation_Creation: - Trace() << "Got creation"; + // Trace() << "Got creation"; resultProvider.add(Akonadi2::ApplicationDomain::ApplicationDomainType::getInMemoryRepresentation(*value).template staticCast()); break; case Akonadi2::Operation_Modification: - Trace() << "Got modification"; + // Trace() << "Got modification"; resultProvider.modify(Akonadi2::ApplicationDomain::ApplicationDomainType::getInMemoryRepresentation(*value).template staticCast()); break; case Akonadi2::Operation_Removal: - Trace() << "Got removal"; + // Trace() << "Got removal"; resultProvider.remove(Akonadi2::ApplicationDomain::ApplicationDomainType::getInMemoryRepresentation(*value).template staticCast()); break; } diff --git a/common/modelresult.cpp b/common/modelresult.cpp index 5b9e24f..4102cda 100644 --- a/common/modelresult.cpp +++ b/common/modelresult.cpp @@ -124,7 +124,7 @@ void ModelResult::add(const Ptr &value) return; } auto parent = createIndexFromId(id); - qDebug() << "Added entity " << childId << value->identifier() << id; + // qDebug() << "Added entity " << childId << value->identifier() << id; const auto keys = mTree[id]; int index = 0; for (; index < keys.size(); index++) { @@ -136,13 +136,13 @@ void ModelResult::add(const Ptr &value) Warning() << "Entity already in model " << value->identifier(); return; } - qDebug() << "Inserting rows " << index << parent; + // qDebug() << "Inserting rows " << index << parent; beginInsertRows(QModelIndex(), index, index); mEntities.insert(childId, value); mTree[id].insert(index, childId); mParents.insert(childId, id); endInsertRows(); - qDebug() << "Inserted rows " << mTree[id].size(); + // qDebug() << "Inserted rows " << mTree[id].size(); } diff --git a/common/resourceaccess.cpp b/common/resourceaccess.cpp index 88f785f..1b46b82 100644 --- a/common/resourceaccess.cpp +++ b/common/resourceaccess.cpp @@ -340,7 +340,7 @@ KAsync::Job ResourceAccess::sendRevisionReplayedCommand(qint64 revision) void ResourceAccess::open() { if (d->socket && d->socket->isValid()) { - Trace() << "Socket valid, so not opening again"; + // Trace() << "Socket valid, so not opening again"; return; } if (d->openingSocket) { -- cgit v1.2.3 From a4acb7e251cba5ba6d66bf6235736202255c4eac Mon Sep 17 00:00:00 2001 From: Christian Mollekopf Date: Wed, 25 Nov 2015 09:37:59 +0100 Subject: Only use the parent index when it's available --- common/facade.cpp | 14 ++++++++------ common/modelresult.cpp | 8 ++++++-- common/modelresult.h | 2 +- 3 files changed, 15 insertions(+), 9 deletions(-) diff --git a/common/facade.cpp b/common/facade.cpp index 850d28b..68770b5 100644 --- a/common/facade.cpp +++ b/common/facade.cpp @@ -350,12 +350,14 @@ template qint64 GenericFacade::executeInitialQuery(const Akonadi2::Query &query, const typename DomainType::Ptr &parent, Akonadi2::ResultProviderInterface &resultProvider) { auto modifiedQuery = query; - if (parent && !query.parentProperty.isEmpty()) { - Trace() << "Running initial query for parent:" << parent->identifier(); - modifiedQuery.propertyFilter.insert(query.parentProperty, parent->identifier()); - } else { - Trace() << "Running initial query for toplevel"; - modifiedQuery.propertyFilter.insert(query.parentProperty, QVariant()); + if (!query.parentProperty.isEmpty()) { + if (parent) { + Trace() << "Running initial query for parent:" << parent->identifier(); + modifiedQuery.propertyFilter.insert(query.parentProperty, parent->identifier()); + } else { + Trace() << "Running initial query for toplevel"; + modifiedQuery.propertyFilter.insert(query.parentProperty, QVariant()); + } } return load(modifiedQuery, [&](Akonadi2::Storage::Transaction &transaction, QSet &remainingFilters) -> ResultSet { return loadInitialResultSet(modifiedQuery, transaction, remainingFilters); diff --git a/common/modelresult.cpp b/common/modelresult.cpp index 4102cda..935e2e8 100644 --- a/common/modelresult.cpp +++ b/common/modelresult.cpp @@ -27,7 +27,8 @@ template ModelResult::ModelResult(const Akonadi2::Query &query, const QList &propertyColumns) :QAbstractItemModel(), - mPropertyColumns(propertyColumns) + mPropertyColumns(propertyColumns), + mQuery(query) { } @@ -42,7 +43,10 @@ static qint64 getIdentifier(const QModelIndex &idx) template qint64 ModelResult::parentId(const Ptr &value) { - return qHash(value->getProperty("parent").toByteArray()); + if (!mQuery.parentProperty.isEmpty()) { + return qHash(value->getProperty(mQuery.parentProperty).toByteArray()); + } + return qHash(QByteArray()); } template diff --git a/common/modelresult.h b/common/modelresult.h index 40a9d9d..66dfce5 100644 --- a/common/modelresult.h +++ b/common/modelresult.h @@ -53,7 +53,7 @@ public: void setFetcher(const std::function &fetcher); private: - static qint64 parentId(const Ptr &value); + qint64 parentId(const Ptr &value); QModelIndex createIndexFromId(const qint64 &id) const; void fetchEntities(const QModelIndex &parent); -- cgit v1.2.3 From 89aa339dd91765d67b4606938e60358f41d33884 Mon Sep 17 00:00:00 2001 From: Christian Mollekopf Date: Wed, 25 Nov 2015 15:40:41 +0100 Subject: Fixed modifications. Without this modifications are ignored also in incremental queries. --- common/facade.cpp | 6 +++--- common/facade.h | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/common/facade.cpp b/common/facade.cpp index 68770b5..2806f4d 100644 --- a/common/facade.cpp +++ b/common/facade.cpp @@ -329,7 +329,7 @@ qint64 GenericFacade::load(const Akonadi2::Query &query, const std:: QSet remainingFilters; auto resultSet = baseSetRetriever(transaction, remainingFilters); - auto filteredSet = filterSet(resultSet, getFilter(remainingFilters, query), transaction, false); + auto filteredSet = filterSet(resultSet, getFilter(remainingFilters, query), transaction, initialQuery); replaySet(filteredSet, resultProvider); resultProvider.setRevision(Akonadi2::Storage::maxRevision(transaction)); return Akonadi2::Storage::maxRevision(transaction); @@ -343,7 +343,7 @@ qint64 GenericFacade::executeIncrementalQuery(const Akonadi2::Query Trace() << "Running incremental query " << baseRevision; return load(query, [&](Akonadi2::Storage::Transaction &transaction, QSet &remainingFilters) -> ResultSet { return loadIncrementalResultSet(baseRevision, query, transaction, remainingFilters); - }, resultProvider); + }, resultProvider, false); } template @@ -361,7 +361,7 @@ qint64 GenericFacade::executeInitialQuery(const Akonadi2::Query &que } return load(modifiedQuery, [&](Akonadi2::Storage::Transaction &transaction, QSet &remainingFilters) -> ResultSet { return loadInitialResultSet(modifiedQuery, transaction, remainingFilters); - }, resultProvider); + }, resultProvider, true); } template class Akonadi2::GenericFacade; diff --git a/common/facade.h b/common/facade.h index 794e35e..df09d73 100644 --- a/common/facade.h +++ b/common/facade.h @@ -72,7 +72,7 @@ private: ResultSet filterSet(const ResultSet &resultSet, const std::function &filter, const Akonadi2::Storage::Transaction &transaction, bool initialQuery); std::function getFilter(const QSet remainingFilters, const Akonadi2::Query &query); - qint64 load(const Akonadi2::Query &query, const std::function &)> &baseSetRetriever, Akonadi2::ResultProviderInterface &resultProvider); + qint64 load(const Akonadi2::Query &query, const std::function &)> &baseSetRetriever, Akonadi2::ResultProviderInterface &resultProvider, bool initialQuery); qint64 executeIncrementalQuery(const Akonadi2::Query &query, Akonadi2::ResultProviderInterface &resultProvider); qint64 executeInitialQuery(const Akonadi2::Query &query, const typename DomainType::Ptr &parent, Akonadi2::ResultProviderInterface &resultProvider); -- cgit v1.2.3 From 60b4be31c40e7c4681f5ce462af84dd92872ec5f Mon Sep 17 00:00:00 2001 From: Christian Mollekopf Date: Wed, 25 Nov 2015 15:42:21 +0100 Subject: Separate the default index updater from other generic indexers --- examples/dummyresource/resourcefactory.cpp | 53 ++++++++++++++++++++---------- 1 file changed, 36 insertions(+), 17 deletions(-) diff --git a/examples/dummyresource/resourcefactory.cpp b/examples/dummyresource/resourcefactory.cpp index 2a5a3e8..e524c3f 100644 --- a/examples/dummyresource/resourcefactory.cpp +++ b/examples/dummyresource/resourcefactory.cpp @@ -52,7 +52,6 @@ * * range indexes like what date range an event affects. * * group indexes like tree hierarchies as nested sets */ -template class IndexUpdater : public Akonadi2::Preprocessor { public: IndexUpdater(const QByteArray &index, const QByteArray &type) @@ -64,21 +63,17 @@ public: void newEntity(const QByteArray &uid, qint64 revision, const Akonadi2::ApplicationDomain::BufferAdaptor &newEntity, Akonadi2::Storage::Transaction &transaction) Q_DECL_OVERRIDE { - Akonadi2::ApplicationDomain::TypeImplementation::index(uid, newEntity, transaction); add(newEntity.getProperty("remoteId"), uid, transaction); } void modifiedEntity(const QByteArray &uid, qint64 revision, const Akonadi2::ApplicationDomain::BufferAdaptor &oldEntity, const Akonadi2::ApplicationDomain::BufferAdaptor &newEntity, Akonadi2::Storage::Transaction &transaction) Q_DECL_OVERRIDE { - Akonadi2::ApplicationDomain::TypeImplementation::removeIndex(uid, oldEntity, transaction); - Akonadi2::ApplicationDomain::TypeImplementation::index(uid, newEntity, transaction); remove(oldEntity.getProperty("remoteId"), uid, transaction); add(newEntity.getProperty("remoteId"), uid, transaction); } void deletedEntity(const QByteArray &uid, qint64 revision, const Akonadi2::ApplicationDomain::BufferAdaptor &oldEntity, Akonadi2::Storage::Transaction &transaction) Q_DECL_OVERRIDE { - Akonadi2::ApplicationDomain::TypeImplementation::removeIndex(uid, oldEntity, transaction); remove(oldEntity.getProperty("remoteId"), uid, transaction); } @@ -92,6 +87,7 @@ private: void remove(const QVariant &value, const QByteArray &uid, Akonadi2::Storage::Transaction &transaction) { + //TODO hide notfound error Index(mIndexIdentifier, transaction).remove(value.toByteArray(), uid); } @@ -99,26 +95,49 @@ private: QByteArray mBufferType; }; +template +class DefaultIndexUpdater : public Akonadi2::Preprocessor { +public: + void newEntity(const QByteArray &uid, qint64 revision, const Akonadi2::ApplicationDomain::BufferAdaptor &newEntity, Akonadi2::Storage::Transaction &transaction) Q_DECL_OVERRIDE + { + Akonadi2::ApplicationDomain::TypeImplementation::index(uid, newEntity, transaction); + } + + void modifiedEntity(const QByteArray &uid, qint64 revision, const Akonadi2::ApplicationDomain::BufferAdaptor &oldEntity, const Akonadi2::ApplicationDomain::BufferAdaptor &newEntity, Akonadi2::Storage::Transaction &transaction) Q_DECL_OVERRIDE + { + Akonadi2::ApplicationDomain::TypeImplementation::removeIndex(uid, oldEntity, transaction); + Akonadi2::ApplicationDomain::TypeImplementation::index(uid, newEntity, transaction); + } + + void deletedEntity(const QByteArray &uid, qint64 revision, const Akonadi2::ApplicationDomain::BufferAdaptor &oldEntity, Akonadi2::Storage::Transaction &transaction) Q_DECL_OVERRIDE + { + Akonadi2::ApplicationDomain::TypeImplementation::removeIndex(uid, oldEntity, transaction); + } +}; + DummyResource::DummyResource(const QByteArray &instanceIdentifier, const QSharedPointer &pipeline) : Akonadi2::GenericResource(instanceIdentifier, pipeline) { { - auto eventFactory = QSharedPointer::create(); - auto eventIndexer = new IndexUpdater("event.index.rid", ENTITY_TYPE_EVENT); - mPipeline->setPreprocessors(ENTITY_TYPE_EVENT, QVector() << eventIndexer); - mPipeline->setAdaptorFactory(ENTITY_TYPE_EVENT, eventFactory); + QVector eventPreprocessors; + eventPreprocessors << new DefaultIndexUpdater; + eventPreprocessors << new IndexUpdater("event.index.rid", ENTITY_TYPE_EVENT); + mPipeline->setPreprocessors(ENTITY_TYPE_EVENT, eventPreprocessors); + mPipeline->setAdaptorFactory(ENTITY_TYPE_EVENT, QSharedPointer::create()); } { - auto mailFactory = QSharedPointer::create(); - auto mailIndexer = new IndexUpdater("mail.index.rid", ENTITY_TYPE_MAIL); - mPipeline->setPreprocessors(ENTITY_TYPE_MAIL, QVector() << mailIndexer); - mPipeline->setAdaptorFactory(ENTITY_TYPE_MAIL, mailFactory); + QVector mailPreprocessors; + mailPreprocessors << new DefaultIndexUpdater; + mailPreprocessors << new IndexUpdater("mail.index.rid", ENTITY_TYPE_MAIL); + mPipeline->setPreprocessors(ENTITY_TYPE_MAIL, mailPreprocessors); + mPipeline->setAdaptorFactory(ENTITY_TYPE_MAIL, QSharedPointer::create()); } { - auto folderFactory = QSharedPointer::create(); - auto folderIndexer = new IndexUpdater("folder.index.rid", ENTITY_TYPE_FOLDER); - mPipeline->setPreprocessors(ENTITY_TYPE_FOLDER, QVector() << folderIndexer); - mPipeline->setAdaptorFactory(ENTITY_TYPE_FOLDER, folderFactory); + QVector folderPreprocessors; + folderPreprocessors << new DefaultIndexUpdater; + folderPreprocessors << new IndexUpdater("folder.index.rid", ENTITY_TYPE_FOLDER); + mPipeline->setPreprocessors(ENTITY_TYPE_FOLDER, folderPreprocessors); + mPipeline->setAdaptorFactory(ENTITY_TYPE_FOLDER, QSharedPointer::create()); } } -- cgit v1.2.3 From 27164870a7a664daaca4ab6d3e3893a91d4eab5a Mon Sep 17 00:00:00 2001 From: Christian Mollekopf Date: Thu, 26 Nov 2015 14:28:34 +0100 Subject: Avoid repeatedly opening the name db. Although, the benchmarks say it doesn't really have an impact on performance. --- common/facade.cpp | 16 ++++++++-------- common/facade.h | 4 ++-- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/common/facade.cpp b/common/facade.cpp index 2806f4d..92124fc 100644 --- a/common/facade.cpp +++ b/common/facade.cpp @@ -203,16 +203,15 @@ void GenericFacade::replaySet(ResultSet &resultSet, Akonadi2::Result } template -void GenericFacade::readEntity(const Akonadi2::Storage::Transaction &transaction, const QByteArray &key, const std::function &resultCallback) +void GenericFacade::readEntity(const Akonadi2::Storage::NamedDatabase &db, const QByteArray &key, const std::function &resultCallback) { - const auto bufferType = bufferTypeForDomainType(); //This only works for a 1:1 mapping of resource to domain types. //Not i.e. for tags that are stored as flags in each entity of an imap store. //additional properties that don't have a 1:1 mapping (such as separately stored tags), //could be added to the adaptor. // // Akonadi2::Storage::getLatest(transaction, bufferTye, key); - transaction.openDatabase(bufferType + ".main").findLatest(key, [=](const QByteArray &key, const QByteArray &value) -> bool { + db.findLatest(key, [=](const QByteArray &key, const QByteArray &value) -> bool { Akonadi2::EntityBuffer buffer(value.data(), value.size()); const Akonadi2::Entity &entity = buffer.entity(); const auto metadataBuffer = Akonadi2::EntityBuffer::readBuffer(entity.metadata()); @@ -270,15 +269,15 @@ ResultSet GenericFacade::loadIncrementalResultSet(qint64 baseRevisio } template -ResultSet GenericFacade::filterSet(const ResultSet &resultSet, const std::function &filter, const Akonadi2::Storage::Transaction &transaction, bool initialQuery) +ResultSet GenericFacade::filterSet(const ResultSet &resultSet, const std::function &filter, const Akonadi2::Storage::NamedDatabase &db, bool initialQuery) { auto resultSetPtr = QSharedPointer::create(resultSet); //Read through the source values and return whatever matches the filter - std::function)> generator = [this, resultSetPtr, &transaction, filter, initialQuery](std::function callback) -> bool { + std::function)> generator = [this, resultSetPtr, &db, filter, initialQuery](std::function callback) -> bool { while (resultSetPtr->next()) { //readEntity is only necessary if we actually want to filter or know the operation type (but not a big deal if we do it always I guess) - readEntity(transaction, resultSetPtr->id(), [this, filter, callback, initialQuery](const Akonadi2::ApplicationDomain::ApplicationDomainType::Ptr &domainObject, Akonadi2::Operation operation) { + readEntity(db, resultSetPtr->id(), [this, filter, callback, initialQuery](const Akonadi2::ApplicationDomain::ApplicationDomainType::Ptr &domainObject, Akonadi2::Operation operation) { //Always remove removals, they probably don't match due to non-available properties if (filter(domainObject) || operation == Akonadi2::Operation_Removal) { if (initialQuery) { @@ -319,17 +318,18 @@ std::function -qint64 GenericFacade::load(const Akonadi2::Query &query, const std::function &)> &baseSetRetriever, Akonadi2::ResultProviderInterface &resultProvider) +qint64 GenericFacade::load(const Akonadi2::Query &query, const std::function &)> &baseSetRetriever, Akonadi2::ResultProviderInterface &resultProvider, bool initialQuery) { Akonadi2::Storage storage(Akonadi2::storageLocation(), mResourceInstanceIdentifier); storage.setDefaultErrorHandler([](const Akonadi2::Storage::Error &error) { Warning() << "Error during query: " << error.store << error.message; }); auto transaction = storage.createTransaction(Akonadi2::Storage::ReadOnly); + auto db = transaction.openDatabase(bufferTypeForDomainType() + ".main"); QSet remainingFilters; auto resultSet = baseSetRetriever(transaction, remainingFilters); - auto filteredSet = filterSet(resultSet, getFilter(remainingFilters, query), transaction, initialQuery); + auto filteredSet = filterSet(resultSet, getFilter(remainingFilters, query), db, initialQuery); replaySet(filteredSet, resultProvider); resultProvider.setRevision(Akonadi2::Storage::maxRevision(transaction)); return Akonadi2::Storage::maxRevision(transaction); diff --git a/common/facade.h b/common/facade.h index df09d73..d8b878b 100644 --- a/common/facade.h +++ b/common/facade.h @@ -65,12 +65,12 @@ private: //TODO move into result provider? static void replaySet(ResultSet &resultSet, Akonadi2::ResultProviderInterface &resultProvider); - void readEntity(const Akonadi2::Storage::Transaction &transaction, const QByteArray &key, const std::function &resultCallback); + void readEntity(const Akonadi2::Storage::NamedDatabase &db, const QByteArray &key, const std::function &resultCallback); ResultSet loadInitialResultSet(const Akonadi2::Query &query, Akonadi2::Storage::Transaction &transaction, QSet &remainingFilters); ResultSet loadIncrementalResultSet(qint64 baseRevision, const Akonadi2::Query &query, Akonadi2::Storage::Transaction &transaction, QSet &remainingFilters); - ResultSet filterSet(const ResultSet &resultSet, const std::function &filter, const Akonadi2::Storage::Transaction &transaction, bool initialQuery); + ResultSet filterSet(const ResultSet &resultSet, const std::function &filter, const Akonadi2::Storage::NamedDatabase &db, bool initialQuery); std::function getFilter(const QSet remainingFilters, const Akonadi2::Query &query); qint64 load(const Akonadi2::Query &query, const std::function &)> &baseSetRetriever, Akonadi2::ResultProviderInterface &resultProvider, bool initialQuery); qint64 executeIncrementalQuery(const Akonadi2::Query &query, Akonadi2::ResultProviderInterface &resultProvider); -- cgit v1.2.3 From 13af56e436f49df32d3b2f6f223cf1dec2eabaac Mon Sep 17 00:00:00 2001 From: Christian Mollekopf Date: Thu, 26 Nov 2015 14:29:46 +0100 Subject: Use the new model api in the benchmark and split tests up. This way it's possible to i.e. repeatedly only run the reading part. --- tests/dummyresourcebenchmark.cpp | 40 ++++++++++++++++++++++++++-------------- 1 file changed, 26 insertions(+), 14 deletions(-) diff --git a/tests/dummyresourcebenchmark.cpp b/tests/dummyresourcebenchmark.cpp index 242ac76..609b8dc 100644 --- a/tests/dummyresourcebenchmark.cpp +++ b/tests/dummyresourcebenchmark.cpp @@ -10,6 +10,7 @@ #include "synclistresult.h" #include "pipeline.h" #include "log.h" +#include "resourceconfig.h" #include "event_generated.h" #include "entity_generated.h" @@ -24,18 +25,20 @@ class DummyResourceBenchmark : public QObject { Q_OBJECT +private: + int num; private Q_SLOTS: void initTestCase() { Akonadi2::Log::setDebugOutputLevel(Akonadi2::Log::Warning); 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"); + num = 5000; } void cleanup() { - DummyResource::removeFromDisk("org.kde.dummy.instance1"); } static KAsync::Job waitForCompletion(QList > &futures) @@ -68,11 +71,12 @@ private Q_SLOTS: }); } - void testWriteToFacadeAndQueryByUid() + void testWriteToFacade() { + DummyResource::removeFromDisk("org.kde.dummy.instance1"); + QTime time; time.start(); - int num = 100; QList > waitCondition; for (int i = 0; i < num; i++) { Akonadi2::ApplicationDomain::Event event("org.kde.dummy.instance1"); @@ -90,13 +94,17 @@ private Q_SLOTS: query.resources << "org.kde.dummy.instance1"; query.syncOnDemand = false; query.processAll = true; - - query.propertyFilter.insert("uid", "nonexistantuid"); - async::SyncListResult result(Akonadi2::Store::load(query)); - result.exec(); + Akonadi2::Store::synchronize(query).exec().waitForFinished(); } auto allProcessedTime = time.elapsed(); + qDebug() << "Append to messagequeue " << appendTime; + qDebug() << "All processed: " << allProcessedTime << "/sec " << num*1000/allProcessedTime; + } + void testQueryByUid() + { + QTime time; + time.start(); //Measure query { time.start(); @@ -106,20 +114,18 @@ private Q_SLOTS: query.processAll = false; query.propertyFilter.insert("uid", "testuid"); - async::SyncListResult result(Akonadi2::Store::load(query)); - result.exec(); - QCOMPARE(result.size(), num); + auto model = Akonadi2::Store::loadModel(query); + model->fetchMore(QModelIndex()); + QTRY_COMPARE(model->rowCount(QModelIndex()), num); } - qDebug() << "Append to messagequeue " << appendTime; - qDebug() << "All processed: " << allProcessedTime << "/sec " << num*1000/allProcessedTime; qDebug() << "Query Time: " << time.elapsed() << "/sec " << num*1000/time.elapsed(); } void testWriteInProcess() { + DummyResource::removeFromDisk("org.kde.dummy.instance1"); QTime time; time.start(); - int num = 100; auto pipeline = QSharedPointer::create("org.kde.dummy.instance1"); DummyResource resource("org.kde.dummy.instance1", pipeline); @@ -191,6 +197,12 @@ private Q_SLOTS: Akonadi2::Commands::FinishCreateEntityBuffer(fbb, location); } } + + //This allows to run individual parts without doing a cleanup, but still cleaning up normally + void testCleanupForCompleteTest() + { + DummyResource::removeFromDisk("org.kde.dummy.instance1"); + } }; QTEST_MAIN(DummyResourceBenchmark) -- cgit v1.2.3 From 5b41b26a349967acf2197f9f9228526193fd826e Mon Sep 17 00:00:00 2001 From: Christian Mollekopf Date: Fri, 27 Nov 2015 17:30:04 +0100 Subject: Introduced a QueryRunner object The QueryRunner object lives for the duration of the query (so just for the initial query for non-live queries, and for the lifetime of the result model for live queries). It's supposed to handle all the threading internally and decouple the lifetime of the facade. --- common/CMakeLists.txt | 1 + common/clientapi.cpp | 62 ++----- common/facade.cpp | 282 +---------------------------- common/facade.h | 17 +- common/facadeinterface.h | 29 ++- common/modelresult.cpp | 22 +++ common/modelresult.h | 6 +- common/queryrunner.cpp | 292 ++++++++++++++++++++++++++++++ common/queryrunner.h | 107 +++++++++++ common/resourceaccess.h | 2 + common/resourcefacade.cpp | 17 +- common/resourcefacade.h | 3 +- common/resultprovider.h | 150 +++------------ common/threadboundary.cpp | 5 +- examples/dummyresource/CMakeLists.txt | 2 +- examples/dummyresource/resourcefacade.cpp | 84 --------- examples/dummyresource/resourcefacade.h | 49 ----- tests/CMakeLists.txt | 12 +- tests/clientapitest.cpp | 86 +++++---- tests/dummyresourcebenchmark.cpp | 2 - 20 files changed, 580 insertions(+), 650 deletions(-) create mode 100644 common/queryrunner.cpp create mode 100644 common/queryrunner.h delete mode 100644 examples/dummyresource/resourcefacade.cpp delete mode 100644 examples/dummyresource/resourcefacade.h diff --git a/common/CMakeLists.txt b/common/CMakeLists.txt index 01056d0..be312b9 100644 --- a/common/CMakeLists.txt +++ b/common/CMakeLists.txt @@ -26,6 +26,7 @@ set(command_SRCS resource.cpp genericresource.cpp resourceaccess.cpp + queryrunner.cpp listener.cpp storage_common.cpp threadboundary.cpp diff --git a/common/clientapi.cpp b/common/clientapi.cpp index 02f8ce6..b24dfa8 100644 --- a/common/clientapi.cpp +++ b/common/clientapi.cpp @@ -34,6 +34,7 @@ #include "definitions.h" #include "resourceconfig.h" #include "facadefactory.h" +#include "modelresult.h" #include "log.h" #define ASYNCINTHREAD @@ -100,38 +101,8 @@ template QSharedPointer > Store::load(Query query) { auto resultSet = QSharedPointer >::create(); - - //Execute the search in a thread. - //We must guarantee that the emitter is returned before the first result is emitted. - //The result provider must be threadsafe. - async::run([query, resultSet](){ - QEventLoop eventLoop; - resultSet->onDone([&eventLoop](){ - eventLoop.quit(); - }); - // Query all resources and aggregate results - KAsync::iterate(getResources(query.resources, ApplicationDomain::getTypeName())) - .template each([query, resultSet](const QByteArray &resource, KAsync::Future &future) { - if (auto facade = FacadeFactory::instance().getFacade(resourceName(resource), resource)) { - facade->load(query, *resultSet).template then([&future](){future.setFinished();}).exec(); - //Keep the facade alive for the lifetime of the resultSet. - resultSet->setFacade(facade); - } else { - //Ignore the error and carry on - future.setFinished(); - } - }).template then([query, resultSet]() { - resultSet->initialResultSetComplete(); - if (!query.liveQuery) { - resultSet->complete(); - } - }).exec(); - - //Keep the thread alive until the result is ready - if (!resultSet->isDone()) { - eventLoop.exec(); - } - }); + qWarning() << "Main thread " << QThread::currentThreadId(); + //FIXME remove return resultSet->emitter(); } @@ -139,28 +110,29 @@ template QSharedPointer Store::loadModel(Query query) { auto model = QSharedPointer >::create(query, query.requestedProperties.toList()); - auto resultProvider = std::make_shared >(model); - //Keep the resultprovider alive for as long as the model lives - model->setProperty("resultProvider", QVariant::fromValue(std::shared_ptr(resultProvider))); + + //* Client defines lifetime of model + //* The model lifetime defines the duration of live-queries + //* The facade needs to life for the duration of any calls being made (assuming we get rid of any internal callbacks + //* The emitter needs to live or the duration of query (respectively, the model) + //* The result provider needs to live for as long as results are provided (until the last thread exits). // Query all resources and aggregate results KAsync::iterate(getResources(query.resources, ApplicationDomain::getTypeName())) - .template each([query, resultProvider](const QByteArray &resource, KAsync::Future &future) { + .template each([query, model](const QByteArray &resource, KAsync::Future &future) { auto facade = FacadeFactory::instance().getFacade(resourceName(resource), resource); if (facade) { - facade->load(query, *resultProvider).template then([&future](){future.setFinished();}).exec(); - //Keep the facade alive for the lifetime of the resultSet. - //FIXME this would have to become a list - resultProvider->setFacade(facade); + Trace() << "Trying to fetch from resource"; + auto result = facade->load(query); + auto emitter = result.second; + //TODO use aggregating emitter instead + model->setEmitter(emitter); + model->fetchMore(QModelIndex()); + result.first.template then([&future](){future.setFinished();}).exec(); } else { //Ignore the error and carry on future.setFinished(); } - }).template then([query, resultProvider]() { - resultProvider->initialResultSetComplete(); - if (!query.liveQuery) { - resultProvider->complete(); - } }).exec(); return model; diff --git a/common/facade.cpp b/common/facade.cpp index 92124fc..1d6b9a7 100644 --- a/common/facade.cpp +++ b/common/facade.cpp @@ -24,76 +24,10 @@ #include "storage.h" #include "definitions.h" #include "domainadaptor.h" +#include "queryrunner.h" using namespace Akonadi2; -/** - * A QueryRunner runs a query and updates the corresponding result set. - * - * The lifetime of the QueryRunner is defined by the resut set (otherwise it's doing useless work), - * and by how long a result set must be updated. If the query is one off the runner dies after the execution, - * otherwise it lives on the react to changes and updates the corresponding result set. - * - * QueryRunner has to keep ResourceAccess alive in order to keep getting updates. - */ -class QueryRunner : public QObject -{ - Q_OBJECT -public: - typedef std::function()> QueryFunction; - - QueryRunner(const Akonadi2::Query &query) {}; - /** - * Starts query - */ - KAsync::Job run(qint64 newRevision = 0) - { - return queryFunction(); - } - - /** - * Set the query to run - */ - void setQuery(const QueryFunction &query) - { - queryFunction = query; - } - -public slots: - /** - * Rerun query with new revision - */ - void revisionChanged(qint64 newRevision) - { - Trace() << "New revision: " << newRevision; - run().exec(); - } - -private: - QueryFunction queryFunction; -}; - -static inline ResultSet fullScan(const Akonadi2::Storage::Transaction &transaction, const QByteArray &bufferType) -{ - //TODO use a result set with an iterator, to read values on demand - QVector keys; - transaction.openDatabase(bufferType + ".main").scan(QByteArray(), [&](const QByteArray &key, const QByteArray &value) -> bool { - //Skip internals - if (Akonadi2::Storage::isInternalKey(key)) { - return true; - } - keys << Akonadi2::Storage::uidFromKey(key); - return true; - }, - [](const Akonadi2::Storage::Error &error) { - qWarning() << "Error during query: " << error.message; - }); - - Trace() << "Full scan found " << keys.size() << " results"; - return ResultSet(keys); -} - - template GenericFacade::GenericFacade(const QByteArray &resourceIdentifier, const DomainTypeAdaptorFactoryInterface::Ptr &adaptorFactory , const QSharedPointer resourceAccess) @@ -150,220 +84,14 @@ KAsync::Job GenericFacade::remove(const DomainType &domainObje } template -KAsync::Job GenericFacade::load(const Akonadi2::Query &query, Akonadi2::ResultProviderInterface &resultProvider) -{ - //We delegate loading of initial data to the result provider, os it can decide for itself what it needs to load. - resultProvider.setFetcher([this, query, &resultProvider](const typename DomainType::Ptr &parent) { - const qint64 newRevision = executeInitialQuery(query, parent, resultProvider); - mResourceAccess->sendRevisionReplayedCommand(newRevision); - }); - - - //In case of a live query we keep the runner for as long alive as the result provider exists - if (query.liveQuery) { - auto runner = QSharedPointer::create(query); - //Incremental updates are always loaded directly, leaving it up to the result to discard the changes if they are not interesting - runner->setQuery([this, query, &resultProvider] () -> KAsync::Job { - return KAsync::start([this, query, &resultProvider](KAsync::Future &future) { - const qint64 newRevision = executeIncrementalQuery(query, resultProvider); - mResourceAccess->sendRevisionReplayedCommand(newRevision); - future.setFinished(); - }); - }); - resultProvider.setQueryRunner(runner); - //Ensure the connection is open, if it wasn't already opened - //TODO If we are not connected already, we have to check for the latest revision once connected, otherwise we could miss some updates - mResourceAccess->open(); - QObject::connect(mResourceAccess.data(), &Akonadi2::ResourceAccess::revisionChanged, runner.data(), &QueryRunner::revisionChanged); - } - return KAsync::null(); -} - - //TODO move into result provider? -template -void GenericFacade::replaySet(ResultSet &resultSet, Akonadi2::ResultProviderInterface &resultProvider) -{ - while (resultSet.next([&resultProvider](const Akonadi2::ApplicationDomain::ApplicationDomainType::Ptr &value, Akonadi2::Operation operation) -> bool { - switch (operation) { - case Akonadi2::Operation_Creation: - // Trace() << "Got creation"; - resultProvider.add(Akonadi2::ApplicationDomain::ApplicationDomainType::getInMemoryRepresentation(*value).template staticCast()); - break; - case Akonadi2::Operation_Modification: - // Trace() << "Got modification"; - resultProvider.modify(Akonadi2::ApplicationDomain::ApplicationDomainType::getInMemoryRepresentation(*value).template staticCast()); - break; - case Akonadi2::Operation_Removal: - // Trace() << "Got removal"; - resultProvider.remove(Akonadi2::ApplicationDomain::ApplicationDomainType::getInMemoryRepresentation(*value).template staticCast()); - break; - } - return true; - })){}; -} - -template -void GenericFacade::readEntity(const Akonadi2::Storage::NamedDatabase &db, const QByteArray &key, const std::function &resultCallback) -{ - //This only works for a 1:1 mapping of resource to domain types. - //Not i.e. for tags that are stored as flags in each entity of an imap store. - //additional properties that don't have a 1:1 mapping (such as separately stored tags), - //could be added to the adaptor. - // - // Akonadi2::Storage::getLatest(transaction, bufferTye, key); - db.findLatest(key, [=](const QByteArray &key, const QByteArray &value) -> bool { - Akonadi2::EntityBuffer buffer(value.data(), value.size()); - const Akonadi2::Entity &entity = buffer.entity(); - const auto metadataBuffer = Akonadi2::EntityBuffer::readBuffer(entity.metadata()); - Q_ASSERT(metadataBuffer); - const qint64 revision = metadataBuffer ? metadataBuffer->revision() : -1; - resultCallback(DomainType::Ptr::create(mResourceInstanceIdentifier, Akonadi2::Storage::uidFromKey(key), revision, mDomainTypeAdaptorFactory->createAdaptor(entity)), metadataBuffer->operation()); - return false; - }, - [](const Akonadi2::Storage::Error &error) { - qWarning() << "Error during query: " << error.message; - }); -} - -template -ResultSet GenericFacade::loadInitialResultSet(const Akonadi2::Query &query, Akonadi2::Storage::Transaction &transaction, QSet &remainingFilters) +QPair, typename ResultEmitter::Ptr> GenericFacade::load(const Akonadi2::Query &query) { - QSet appliedFilters; - auto resultSet = Akonadi2::ApplicationDomain::TypeImplementation::queryIndexes(query, mResourceInstanceIdentifier, appliedFilters, transaction); - remainingFilters = query.propertyFilter.keys().toSet() - appliedFilters; - - //We do a full scan if there were no indexes available to create the initial set. - if (appliedFilters.isEmpty()) { - //TODO this should be replaced by an index lookup as well - resultSet = fullScan(transaction, bufferTypeForDomainType()); - } - return resultSet; -} - -template -ResultSet GenericFacade::loadIncrementalResultSet(qint64 baseRevision, const Akonadi2::Query &query, Akonadi2::Storage::Transaction &transaction, QSet &remainingFilters) -{ - const auto bufferType = bufferTypeForDomainType(); - auto revisionCounter = QSharedPointer::create(baseRevision); - remainingFilters = query.propertyFilter.keys().toSet(); - return ResultSet([bufferType, revisionCounter, &transaction]() -> QByteArray { - const qint64 topRevision = Akonadi2::Storage::maxRevision(transaction); - //Spit out the revision keys one by one. - while (*revisionCounter <= topRevision) { - const auto uid = Akonadi2::Storage::getUidFromRevision(transaction, *revisionCounter); - const auto type = Akonadi2::Storage::getTypeFromRevision(transaction, *revisionCounter); - Trace() << "Revision" << *revisionCounter << type << uid; - if (type != bufferType) { - //Skip revision - *revisionCounter += 1; - continue; - } - const auto key = Akonadi2::Storage::assembleKey(uid, *revisionCounter); - *revisionCounter += 1; - return key; - } - Trace() << "Finished reading incremental result set:" << *revisionCounter; - //We're done - return QByteArray(); - }); -} - -template -ResultSet GenericFacade::filterSet(const ResultSet &resultSet, const std::function &filter, const Akonadi2::Storage::NamedDatabase &db, bool initialQuery) -{ - auto resultSetPtr = QSharedPointer::create(resultSet); - - //Read through the source values and return whatever matches the filter - std::function)> generator = [this, resultSetPtr, &db, filter, initialQuery](std::function callback) -> bool { - while (resultSetPtr->next()) { - //readEntity is only necessary if we actually want to filter or know the operation type (but not a big deal if we do it always I guess) - readEntity(db, resultSetPtr->id(), [this, filter, callback, initialQuery](const Akonadi2::ApplicationDomain::ApplicationDomainType::Ptr &domainObject, Akonadi2::Operation operation) { - //Always remove removals, they probably don't match due to non-available properties - if (filter(domainObject) || operation == Akonadi2::Operation_Removal) { - if (initialQuery) { - //We're not interested in removals during the initial query - if (operation != Akonadi2::Operation_Removal) { - callback(domainObject, Akonadi2::Operation_Creation); - } - } else { - callback(domainObject, operation); - } - } - }); - } - return false; - }; - return ResultSet(generator); -} - - -template -std::function GenericFacade::getFilter(const QSet remainingFilters, const Akonadi2::Query &query) -{ - return [remainingFilters, query](const Akonadi2::ApplicationDomain::ApplicationDomainType::Ptr &domainObject) -> bool { - for (const auto &filterProperty : remainingFilters) { - const auto property = domainObject->getProperty(filterProperty); - if (property.isValid()) { - //TODO implement other comparison operators than equality - if (property != query.propertyFilter.value(filterProperty)) { - Trace() << "Filtering entity due to property mismatch: " << domainObject->getProperty(filterProperty); - return false; - } - } else { - Warning() << "Ignored property filter because value is invalid: " << filterProperty; - } - } - return true; - }; -} - -template -qint64 GenericFacade::load(const Akonadi2::Query &query, const std::function &)> &baseSetRetriever, Akonadi2::ResultProviderInterface &resultProvider, bool initialQuery) -{ - Akonadi2::Storage storage(Akonadi2::storageLocation(), mResourceInstanceIdentifier); - storage.setDefaultErrorHandler([](const Akonadi2::Storage::Error &error) { - Warning() << "Error during query: " << error.store << error.message; - }); - auto transaction = storage.createTransaction(Akonadi2::Storage::ReadOnly); - auto db = transaction.openDatabase(bufferTypeForDomainType() + ".main"); - - QSet remainingFilters; - auto resultSet = baseSetRetriever(transaction, remainingFilters); - auto filteredSet = filterSet(resultSet, getFilter(remainingFilters, query), db, initialQuery); - replaySet(filteredSet, resultProvider); - resultProvider.setRevision(Akonadi2::Storage::maxRevision(transaction)); - return Akonadi2::Storage::maxRevision(transaction); + //The runner lives for the lifetime of the query + auto runner = new QueryRunner(query, mResourceAccess, mResourceInstanceIdentifier, mDomainTypeAdaptorFactory, bufferTypeForDomainType()); + return qMakePair(KAsync::null(), runner->emitter()); } -template -qint64 GenericFacade::executeIncrementalQuery(const Akonadi2::Query &query, Akonadi2::ResultProviderInterface &resultProvider) -{ - const qint64 baseRevision = resultProvider.revision() + 1; - Trace() << "Running incremental query " << baseRevision; - return load(query, [&](Akonadi2::Storage::Transaction &transaction, QSet &remainingFilters) -> ResultSet { - return loadIncrementalResultSet(baseRevision, query, transaction, remainingFilters); - }, resultProvider, false); -} - -template -qint64 GenericFacade::executeInitialQuery(const Akonadi2::Query &query, const typename DomainType::Ptr &parent, Akonadi2::ResultProviderInterface &resultProvider) -{ - auto modifiedQuery = query; - if (!query.parentProperty.isEmpty()) { - if (parent) { - Trace() << "Running initial query for parent:" << parent->identifier(); - modifiedQuery.propertyFilter.insert(query.parentProperty, parent->identifier()); - } else { - Trace() << "Running initial query for toplevel"; - modifiedQuery.propertyFilter.insert(query.parentProperty, QVariant()); - } - } - return load(modifiedQuery, [&](Akonadi2::Storage::Transaction &transaction, QSet &remainingFilters) -> ResultSet { - return loadInitialResultSet(modifiedQuery, transaction, remainingFilters); - }, resultProvider, true); -} - template class Akonadi2::GenericFacade; template class Akonadi2::GenericFacade; template class Akonadi2::GenericFacade; diff --git a/common/facade.h b/common/facade.h index d8b878b..de67e05 100644 --- a/common/facade.h +++ b/common/facade.h @@ -59,22 +59,7 @@ public: KAsync::Job create(const DomainType &domainObject) Q_DECL_OVERRIDE; KAsync::Job modify(const DomainType &domainObject) Q_DECL_OVERRIDE; KAsync::Job remove(const DomainType &domainObject) Q_DECL_OVERRIDE; - KAsync::Job load(const Akonadi2::Query &query, Akonadi2::ResultProviderInterface &resultProvider) Q_DECL_OVERRIDE; - -private: - //TODO move into result provider? - static void replaySet(ResultSet &resultSet, Akonadi2::ResultProviderInterface &resultProvider); - - void readEntity(const Akonadi2::Storage::NamedDatabase &db, const QByteArray &key, const std::function &resultCallback); - - ResultSet loadInitialResultSet(const Akonadi2::Query &query, Akonadi2::Storage::Transaction &transaction, QSet &remainingFilters); - ResultSet loadIncrementalResultSet(qint64 baseRevision, const Akonadi2::Query &query, Akonadi2::Storage::Transaction &transaction, QSet &remainingFilters); - - ResultSet filterSet(const ResultSet &resultSet, const std::function &filter, const Akonadi2::Storage::NamedDatabase &db, bool initialQuery); - std::function getFilter(const QSet remainingFilters, const Akonadi2::Query &query); - qint64 load(const Akonadi2::Query &query, const std::function &)> &baseSetRetriever, Akonadi2::ResultProviderInterface &resultProvider, bool initialQuery); - qint64 executeIncrementalQuery(const Akonadi2::Query &query, Akonadi2::ResultProviderInterface &resultProvider); - qint64 executeInitialQuery(const Akonadi2::Query &query, const typename DomainType::Ptr &parent, Akonadi2::ResultProviderInterface &resultProvider); + QPair, typename ResultEmitter::Ptr> load(const Akonadi2::Query &query) Q_DECL_OVERRIDE; protected: //TODO use one resource access instance per application & per resource diff --git a/common/facadeinterface.h b/common/facadeinterface.h index 7ec21bc..318abf3 100644 --- a/common/facadeinterface.h +++ b/common/facadeinterface.h @@ -23,6 +23,7 @@ #include #include #include +#include #include "applicationdomaintype.h" #include "resultprovider.h" @@ -42,10 +43,32 @@ class StoreFacade { public: virtual ~StoreFacade(){}; QByteArray type() const { return ApplicationDomain::getTypeName(); } + + /** + * Create an entity in the store. + * + * The job returns succefully once the task has been successfully placed in the queue + */ virtual KAsync::Job create(const DomainType &domainObject) = 0; + + /** + * Modify an entity in the store. + * + * The job returns succefully once the task has been successfully placed in the queue + */ virtual KAsync::Job modify(const DomainType &domainObject) = 0; + + /** + * Remove an entity from the store. + * + * The job returns succefully once the task has been successfully placed in the queue + */ virtual KAsync::Job remove(const DomainType &domainObject) = 0; - virtual KAsync::Job load(const Query &query, Akonadi2::ResultProviderInterface &resultProvider) = 0; + + /** + * Load entities from the store. + */ + virtual QPair, typename Akonadi2::ResultEmitter::Ptr > load(const Query &query) = 0; }; template @@ -67,9 +90,9 @@ public: return KAsync::error(-1, "Failed to create a facade"); } - KAsync::Job load(const Query &query, Akonadi2::ResultProviderInterface &resultProvider) + QPair, typename Akonadi2::ResultEmitter::Ptr > load(const Query &query) { - return KAsync::error(-1, "Failed to create a facade"); + return qMakePair(KAsync::null(), typename Akonadi2::ResultEmitter::Ptr()); } }; diff --git a/common/modelresult.cpp b/common/modelresult.cpp index 935e2e8..65eaba9 100644 --- a/common/modelresult.cpp +++ b/common/modelresult.cpp @@ -182,6 +182,28 @@ void ModelResult::setFetcher(const std::function +void ModelResult::setEmitter(const typename Akonadi2::ResultEmitter::Ptr &emitter) +{ + setFetcher(emitter->mFetcher); + emitter->onAdded([this](const Ptr &value) { + this->add(value); + }); + emitter->onModified([this](const Ptr &value) { + this->modify(value); + }); + emitter->onRemoved([this](const Ptr &value) { + this->remove(value); + }); + emitter->onInitialResultSetComplete([this]() { + }); + emitter->onComplete([this]() { + }); + emitter->onClear([this]() { + }); + mEmitter = emitter; +} + template void ModelResult::modify(const Ptr &value) { diff --git a/common/modelresult.h b/common/modelresult.h index 66dfce5..eb6c86b 100644 --- a/common/modelresult.h +++ b/common/modelresult.h @@ -23,20 +23,23 @@ #include #include #include +#include #include #include "query.h" +#include "resultprovider.h" template class ModelResult : public QAbstractItemModel { public: - enum Roles { DomainObjectRole = Qt::UserRole + 1 }; ModelResult(const Akonadi2::Query &query, const QList &propertyColumns); + void setEmitter(const typename Akonadi2::ResultEmitter::Ptr &); + int rowCount(const QModelIndex &parent = QModelIndex()) const; int columnCount(const QModelIndex &parent = QModelIndex()) const; QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const; @@ -65,5 +68,6 @@ private: QList mPropertyColumns; Akonadi2::Query mQuery; std::function loadEntities; + typename Akonadi2::ResultEmitter::Ptr mEmitter; }; diff --git a/common/queryrunner.cpp b/common/queryrunner.cpp new file mode 100644 index 0000000..4159112 --- /dev/null +++ b/common/queryrunner.cpp @@ -0,0 +1,292 @@ +/* + Copyright (c) 2015 Christian Mollekopf + + 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. +*/ +#include "queryrunner.h" + +#include +#include +#include +#include "commands.h" +#include "log.h" +#include "storage.h" +#include "definitions.h" +#include "domainadaptor.h" + +using namespace Akonadi2; + +static inline ResultSet fullScan(const Akonadi2::Storage::Transaction &transaction, const QByteArray &bufferType) +{ + //TODO use a result set with an iterator, to read values on demand + QVector keys; + transaction.openDatabase(bufferType + ".main").scan(QByteArray(), [&](const QByteArray &key, const QByteArray &value) -> bool { + //Skip internals + if (Akonadi2::Storage::isInternalKey(key)) { + return true; + } + keys << Akonadi2::Storage::uidFromKey(key); + return true; + }, + [](const Akonadi2::Storage::Error &error) { + qWarning() << "Error during query: " << error.message; + }); + + Trace() << "Full scan found " << keys.size() << " results"; + return ResultSet(keys); +} + +template +QueryRunner::QueryRunner(const Akonadi2::Query &query, const Akonadi2::ResourceAccessInterface::Ptr &resourceAccess, const QByteArray &instanceIdentifier, const DomainTypeAdaptorFactoryInterface::Ptr &factory, const QByteArray &bufferType) + : QueryRunnerBase(), + mResourceAccess(resourceAccess), + mResultProvider(new ResultProvider), + mDomainTypeAdaptorFactory(factory), + mQuery(query), + mResourceInstanceIdentifier(instanceIdentifier), + mBufferType(bufferType) +{ + Trace() << "Starting query"; + //We delegate loading of initial data to the result provider, os it can decide for itself what it needs to load. + mResultProvider->setFetcher([this, query](const typename DomainType::Ptr &parent) { + Trace() << "Running fetcher"; + + // auto watcher = new QFutureWatcher; + // QObject::connect(watcher, &QFutureWatcher::finished, [](qint64 newRevision) { + // mResourceAccess->sendRevisionReplayedCommand(newRevision); + // }); + // auto future = QtConcurrent::run([&resultProvider]() -> qint64 { + // const qint64 newRevision = executeInitialQuery(query, parent, resultProvider); + // return newRevision; + // }); + // watcher->setFuture(future); + const qint64 newRevision = executeInitialQuery(query, parent, *mResultProvider); + mResourceAccess->sendRevisionReplayedCommand(newRevision); + }); + + + //In case of a live query we keep the runner for as long alive as the result provider exists + if (query.liveQuery) { + //Incremental updates are always loaded directly, leaving it up to the result to discard the changes if they are not interesting + setQuery([this, query] () -> KAsync::Job { + return KAsync::start([this, query](KAsync::Future &future) { + //TODO execute in thread + const qint64 newRevision = executeIncrementalQuery(query, *mResultProvider); + mResourceAccess->sendRevisionReplayedCommand(newRevision); + future.setFinished(); + }); + }); + //Ensure the connection is open, if it wasn't already opened + //TODO If we are not connected already, we have to check for the latest revision once connected, otherwise we could miss some updates + mResourceAccess->open(); + QObject::connect(mResourceAccess.data(), &Akonadi2::ResourceAccess::revisionChanged, this, &QueryRunner::revisionChanged); + } +} + +template +typename Akonadi2::ResultEmitter::Ptr QueryRunner::emitter() +{ + return mResultProvider->emitter(); +} + +//TODO move into result provider? +template +void QueryRunner::replaySet(ResultSet &resultSet, Akonadi2::ResultProviderInterface &resultProvider) +{ + // Trace() << "Replay set"; + while (resultSet.next([&resultProvider](const Akonadi2::ApplicationDomain::ApplicationDomainType::Ptr &value, Akonadi2::Operation operation) -> bool { + switch (operation) { + case Akonadi2::Operation_Creation: + // Trace() << "Got creation"; + resultProvider.add(Akonadi2::ApplicationDomain::ApplicationDomainType::getInMemoryRepresentation(*value).template staticCast()); + break; + case Akonadi2::Operation_Modification: + // Trace() << "Got modification"; + resultProvider.modify(Akonadi2::ApplicationDomain::ApplicationDomainType::getInMemoryRepresentation(*value).template staticCast()); + break; + case Akonadi2::Operation_Removal: + // Trace() << "Got removal"; + resultProvider.remove(Akonadi2::ApplicationDomain::ApplicationDomainType::getInMemoryRepresentation(*value).template staticCast()); + break; + } + return true; + })){}; +} + +template +void QueryRunner::readEntity(const Akonadi2::Storage::NamedDatabase &db, const QByteArray &key, const std::function &resultCallback) +{ + //This only works for a 1:1 mapping of resource to domain types. + //Not i.e. for tags that are stored as flags in each entity of an imap store. + //additional properties that don't have a 1:1 mapping (such as separately stored tags), + //could be added to the adaptor. + db.findLatest(key, [=](const QByteArray &key, const QByteArray &value) -> bool { + Akonadi2::EntityBuffer buffer(value.data(), value.size()); + const Akonadi2::Entity &entity = buffer.entity(); + const auto metadataBuffer = Akonadi2::EntityBuffer::readBuffer(entity.metadata()); + Q_ASSERT(metadataBuffer); + const qint64 revision = metadataBuffer ? metadataBuffer->revision() : -1; + resultCallback(DomainType::Ptr::create(mResourceInstanceIdentifier, Akonadi2::Storage::uidFromKey(key), revision, mDomainTypeAdaptorFactory->createAdaptor(entity)), metadataBuffer->operation()); + return false; + }, + [](const Akonadi2::Storage::Error &error) { + qWarning() << "Error during query: " << error.message; + }); +} + +template +ResultSet QueryRunner::loadInitialResultSet(const Akonadi2::Query &query, Akonadi2::Storage::Transaction &transaction, QSet &remainingFilters) +{ + QSet appliedFilters; + auto resultSet = Akonadi2::ApplicationDomain::TypeImplementation::queryIndexes(query, mResourceInstanceIdentifier, appliedFilters, transaction); + remainingFilters = query.propertyFilter.keys().toSet() - appliedFilters; + + //We do a full scan if there were no indexes available to create the initial set. + if (appliedFilters.isEmpty()) { + //TODO this should be replaced by an index lookup as well + resultSet = fullScan(transaction, mBufferType); + } + return resultSet; +} + +template +ResultSet QueryRunner::loadIncrementalResultSet(qint64 baseRevision, const Akonadi2::Query &query, Akonadi2::Storage::Transaction &transaction, QSet &remainingFilters) +{ + const auto bufferType = mBufferType; + auto revisionCounter = QSharedPointer::create(baseRevision); + remainingFilters = query.propertyFilter.keys().toSet(); + return ResultSet([bufferType, revisionCounter, &transaction]() -> QByteArray { + const qint64 topRevision = Akonadi2::Storage::maxRevision(transaction); + //Spit out the revision keys one by one. + while (*revisionCounter <= topRevision) { + const auto uid = Akonadi2::Storage::getUidFromRevision(transaction, *revisionCounter); + const auto type = Akonadi2::Storage::getTypeFromRevision(transaction, *revisionCounter); + // Trace() << "Revision" << *revisionCounter << type << uid; + if (type != bufferType) { + //Skip revision + *revisionCounter += 1; + continue; + } + const auto key = Akonadi2::Storage::assembleKey(uid, *revisionCounter); + *revisionCounter += 1; + return key; + } + Trace() << "Finished reading incremental result set:" << *revisionCounter; + //We're done + return QByteArray(); + }); +} + +template +ResultSet QueryRunner::filterSet(const ResultSet &resultSet, const std::function &filter, const Akonadi2::Storage::NamedDatabase &db, bool initialQuery) +{ + auto resultSetPtr = QSharedPointer::create(resultSet); + + //Read through the source values and return whatever matches the filter + std::function)> generator = [this, resultSetPtr, &db, filter, initialQuery](std::function callback) -> bool { + while (resultSetPtr->next()) { + //readEntity is only necessary if we actually want to filter or know the operation type (but not a big deal if we do it always I guess) + readEntity(db, resultSetPtr->id(), [this, filter, callback, initialQuery](const Akonadi2::ApplicationDomain::ApplicationDomainType::Ptr &domainObject, Akonadi2::Operation operation) { + //Always remove removals, they probably don't match due to non-available properties + if (filter(domainObject) || operation == Akonadi2::Operation_Removal) { + Trace() << "entity is not filtered" << initialQuery; + if (initialQuery) { + //We're not interested in removals during the initial query + if (operation != Akonadi2::Operation_Removal) { + callback(domainObject, Akonadi2::Operation_Creation); + } + } else { + callback(domainObject, operation); + } + } + }); + } + return false; + }; + return ResultSet(generator); +} + + +template +std::function QueryRunner::getFilter(const QSet remainingFilters, const Akonadi2::Query &query) +{ + return [remainingFilters, query](const Akonadi2::ApplicationDomain::ApplicationDomainType::Ptr &domainObject) -> bool { + for (const auto &filterProperty : remainingFilters) { + const auto property = domainObject->getProperty(filterProperty); + if (property.isValid()) { + //TODO implement other comparison operators than equality + if (property != query.propertyFilter.value(filterProperty)) { + Trace() << "Filtering entity due to property mismatch: " << domainObject->getProperty(filterProperty); + return false; + } + } else { + Warning() << "Ignored property filter because value is invalid: " << filterProperty; + } + } + return true; + }; +} + +template +qint64 QueryRunner::load(const Akonadi2::Query &query, const std::function &)> &baseSetRetriever, Akonadi2::ResultProviderInterface &resultProvider, bool initialQuery) +{ + Akonadi2::Storage storage(Akonadi2::storageLocation(), mResourceInstanceIdentifier); + storage.setDefaultErrorHandler([](const Akonadi2::Storage::Error &error) { + Warning() << "Error during query: " << error.store << error.message; + }); + auto transaction = storage.createTransaction(Akonadi2::Storage::ReadOnly); + auto db = transaction.openDatabase(mBufferType + ".main"); + + QSet remainingFilters; + auto resultSet = baseSetRetriever(transaction, remainingFilters); + auto filteredSet = filterSet(resultSet, getFilter(remainingFilters, query), db, initialQuery); + replaySet(filteredSet, resultProvider); + resultProvider.setRevision(Akonadi2::Storage::maxRevision(transaction)); + return Akonadi2::Storage::maxRevision(transaction); +} + + +template +qint64 QueryRunner::executeIncrementalQuery(const Akonadi2::Query &query, Akonadi2::ResultProviderInterface &resultProvider) +{ + const qint64 baseRevision = resultProvider.revision() + 1; + Trace() << "Running incremental query " << baseRevision; + return load(query, [&](Akonadi2::Storage::Transaction &transaction, QSet &remainingFilters) -> ResultSet { + return loadIncrementalResultSet(baseRevision, query, transaction, remainingFilters); + }, resultProvider, false); +} + +template +qint64 QueryRunner::executeInitialQuery(const Akonadi2::Query &query, const typename DomainType::Ptr &parent, Akonadi2::ResultProviderInterface &resultProvider) +{ + auto modifiedQuery = query; + if (!query.parentProperty.isEmpty()) { + if (parent) { + Trace() << "Running initial query for parent:" << parent->identifier(); + modifiedQuery.propertyFilter.insert(query.parentProperty, parent->identifier()); + } else { + Trace() << "Running initial query for toplevel"; + modifiedQuery.propertyFilter.insert(query.parentProperty, QVariant()); + } + } + return load(modifiedQuery, [&](Akonadi2::Storage::Transaction &transaction, QSet &remainingFilters) -> ResultSet { + return loadInitialResultSet(modifiedQuery, transaction, remainingFilters); + }, resultProvider, true); +} + +template class QueryRunner; +template class QueryRunner; +template class QueryRunner; diff --git a/common/queryrunner.h b/common/queryrunner.h new file mode 100644 index 0000000..e2af9de --- /dev/null +++ b/common/queryrunner.h @@ -0,0 +1,107 @@ +/* + Copyright (c) 2015 Christian Mollekopf + + 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. +*/ + +#pragma once + +#include +#include "facadeinterface.h" +#include "resourceaccess.h" +#include "resultprovider.h" +#include "domaintypeadaptorfactoryinterface.h" +#include "storage.h" +#include "query.h" + +/** + * A QueryRunner runs a query and updates the corresponding result set. + * + * The lifetime of the QueryRunner is defined by the resut set (otherwise it's doing useless work), + * and by how long a result set must be updated. If the query is one off the runner dies after the execution, + * otherwise it lives on the react to changes and updates the corresponding result set. + * + * QueryRunner has to keep ResourceAccess alive in order to keep getting updates. + */ + +class QueryRunnerBase : public QObject +{ + Q_OBJECT +protected: + typedef std::function()> QueryFunction; + + /** + * Set the query to run + */ + void setQuery(const QueryFunction &query) + { + queryFunction = query; + } + + +protected slots: + /** + * Rerun query with new revision + */ + void revisionChanged(qint64 newRevision) + { + Trace() << "New revision: " << newRevision; + run().exec(); + } + +private: + /** + * Starts query + */ + KAsync::Job run(qint64 newRevision = 0) + { + return queryFunction(); + } + + QueryFunction queryFunction; +}; + +template +class QueryRunner : public QueryRunnerBase +{ +public: + QueryRunner(const Akonadi2::Query &query, const Akonadi2::ResourceAccessInterface::Ptr &, const QByteArray &instanceIdentifier, const DomainTypeAdaptorFactoryInterface::Ptr &, const QByteArray &bufferType); + + typename Akonadi2::ResultEmitter::Ptr emitter(); + +private: + static void replaySet(ResultSet &resultSet, Akonadi2::ResultProviderInterface &resultProvider); + + void readEntity(const Akonadi2::Storage::NamedDatabase &db, const QByteArray &key, const std::function &resultCallback); + + ResultSet loadInitialResultSet(const Akonadi2::Query &query, Akonadi2::Storage::Transaction &transaction, QSet &remainingFilters); + ResultSet loadIncrementalResultSet(qint64 baseRevision, const Akonadi2::Query &query, Akonadi2::Storage::Transaction &transaction, QSet &remainingFilters); + + ResultSet filterSet(const ResultSet &resultSet, const std::function &filter, const Akonadi2::Storage::NamedDatabase &db, bool initialQuery); + std::function getFilter(const QSet remainingFilters, const Akonadi2::Query &query); + qint64 load(const Akonadi2::Query &query, const std::function &)> &baseSetRetriever, Akonadi2::ResultProviderInterface &resultProvider, bool initialQuery); + qint64 executeIncrementalQuery(const Akonadi2::Query &query, Akonadi2::ResultProviderInterface &resultProvider); + qint64 executeInitialQuery(const Akonadi2::Query &query, const typename DomainType::Ptr &parent, Akonadi2::ResultProviderInterface &resultProvider); + +private: + QSharedPointer > mResultProvider; + QSharedPointer mResourceAccess; + DomainTypeAdaptorFactoryInterface::Ptr mDomainTypeAdaptorFactory; + QByteArray mResourceInstanceIdentifier; + QByteArray mBufferType; + Akonadi2::Query mQuery; +}; + diff --git a/common/resourceaccess.h b/common/resourceaccess.h index 8e27054..e87a1f7 100644 --- a/common/resourceaccess.h +++ b/common/resourceaccess.h @@ -37,6 +37,8 @@ class ResourceAccessInterface : public QObject { Q_OBJECT public: + typedef QSharedPointer Ptr; + ResourceAccessInterface() {} virtual ~ResourceAccessInterface() {} virtual KAsync::Job sendCommand(int commandId) = 0; diff --git a/common/resourcefacade.cpp b/common/resourcefacade.cpp index 1796271..3d207e4 100644 --- a/common/resourcefacade.cpp +++ b/common/resourcefacade.cpp @@ -54,9 +54,15 @@ KAsync::Job ResourceFacade::remove(const Akonadi2::ApplicationDomain::Akon }); } -KAsync::Job ResourceFacade::load(const Akonadi2::Query &query, Akonadi2::ResultProviderInterface &resultProvider) +QPair, typename Akonadi2::ResultEmitter::Ptr > ResourceFacade::load(const Akonadi2::Query &query) { - return KAsync::start([query, &resultProvider]() { + auto resultProvider = new Akonadi2::ResultProvider(); + auto emitter = resultProvider->emitter(); + resultProvider->setFetcher([](const QSharedPointer &) {}); + resultProvider->onDone([resultProvider]() { + delete resultProvider; + }); + auto job = KAsync::start([query, resultProvider]() { const auto configuredResources = ResourceConfig::getResources(); for (const auto &res : configuredResources.keys()) { const auto type = configuredResources.value(res); @@ -64,12 +70,13 @@ KAsync::Job ResourceFacade::load(const Akonadi2::Query &query, Akonadi2::R auto resource = Akonadi2::ApplicationDomain::AkonadiResource::Ptr::create(); resource->setProperty("identifier", res); resource->setProperty("type", type); - resultProvider.add(resource); + resultProvider->add(resource); } } //TODO initialResultSetComplete should be implicit - resultProvider.initialResultSetComplete(); - resultProvider.complete(); + resultProvider->initialResultSetComplete(); + resultProvider->complete(); }); + return qMakePair(job, emitter); } diff --git a/common/resourcefacade.h b/common/resourcefacade.h index 123b481..38e0c0e 100644 --- a/common/resourcefacade.h +++ b/common/resourcefacade.h @@ -37,5 +37,6 @@ public: KAsync::Job create(const Akonadi2::ApplicationDomain::AkonadiResource &resource) Q_DECL_OVERRIDE; KAsync::Job modify(const Akonadi2::ApplicationDomain::AkonadiResource &resource) Q_DECL_OVERRIDE; KAsync::Job remove(const Akonadi2::ApplicationDomain::AkonadiResource &resource) Q_DECL_OVERRIDE; - KAsync::Job load(const Akonadi2::Query &query, Akonadi2::ResultProviderInterface &resultProvider) Q_DECL_OVERRIDE; + QPair, typename Akonadi2::ResultEmitter::Ptr > load(const Akonadi2::Query &query) Q_DECL_OVERRIDE; }; + diff --git a/common/resultprovider.h b/common/resultprovider.h index 921cd6b..86382ef 100644 --- a/common/resultprovider.h +++ b/common/resultprovider.h @@ -20,12 +20,12 @@ #pragma once +#include #include #include #include "threadboundary.h" #include "resultset.h" #include "log.h" -#include "modelresult.h" using namespace async; @@ -53,12 +53,7 @@ public: virtual void initialResultSetComplete() = 0; virtual void complete() = 0; virtual void clear() = 0; - virtual void setFetcher(const std::function &fetcher) - { - } - - virtual void setFacade(const std::shared_ptr &facade) = 0; - virtual void setQueryRunner(const QSharedPointer &runner) = 0; + virtual void setFetcher(const std::function &fetcher) = 0; void setRevision(qint64 revision) { @@ -74,101 +69,6 @@ private: qint64 mRevision; }; -template -class ModelResultProvider : public ResultProviderInterface { -public: - ModelResultProvider(QWeakPointer > model) - : ResultProviderInterface(), - mModel(model) - { - - } - - void add(const Ptr &value) - { - if (auto model = mModel.toStrongRef()) { - model->add(value); - } - } - - void modify(const Ptr &value) - { - if (auto model = mModel.toStrongRef()) { - model->modify(value); - } - } - - void remove(const Ptr &value) - { - if (auto model = mModel.toStrongRef()) { - model->remove(value); - } - } - - void initialResultSetComplete() - { - // mResultEmitter->initialResultSetComplete(); - } - - void complete() - { - // mResultEmitter->complete(); - } - - void clear() - { - // mResultEmitter->clear(); - } - - /** - * For lifetimemanagement only. - * We keep the runner alive as long as the result provider exists. - */ - void setFacade(const std::shared_ptr &facade) - { - mFacade = facade; - } - - void onDone(const std::function &callback) - { - mOnDoneCallback = callback; - } - - bool isDone() const - { - //The existance of the emitter currently defines wether we're done or not. - // return mResultEmitter.toStrongRef().isNull(); - return true; - } - - void setFetcher(const std::function &fetcher) - { - if (auto model = mModel.toStrongRef()) { - model->setFetcher(fetcher); - } - } - - void setQueryRunner(const QSharedPointer &runner) - { - mQueryRunner = runner; - } - -private: - void done() - { - qWarning() << "done"; - if (mOnDoneCallback) { - mOnDoneCallback(); - mOnDoneCallback = std::function(); - } - } - - QWeakPointer > mModel; - QSharedPointer mQueryRunner; - std::shared_ptr mFacade; - std::function mOnDoneCallback; -}; - /* * The promise side for the result emitter */ @@ -204,6 +104,12 @@ private: } public: + typedef QSharedPointer > Ptr; + + ~ResultProvider() + { + } + //Called from worker thread void add(const T &value) { @@ -261,30 +167,16 @@ public: //We have to go over a separate var and return that, otherwise we'd delete the emitter immediately again auto sharedPtr = QSharedPointer >(new ResultEmitter, [this](ResultEmitter *emitter){ mThreadBoundary->callInMainThread([this]() {done();}); delete emitter; }); mResultEmitter = sharedPtr; + sharedPtr->setFetcher([this](const T &parent) { + Q_ASSERT(mFetcher); + mFetcher(parent); + }); return sharedPtr; } return mResultEmitter.toStrongRef(); } - /** - * For lifetimemanagement only. - * We keep the runner alive as long as the result provider exists. - */ - void setQueryRunner(const QSharedPointer &runner) - { - mQueryRunner = runner; - } - - /** - * For lifetimemanagement only. - * We keep the runner alive as long as the result provider exists. - */ - void setFacade(const std::shared_ptr &facade) - { - mFacade = facade; - } - void onDone(const std::function &callback) { mThreadBoundary = QSharedPointer::create(); @@ -299,7 +191,7 @@ public: void setFetcher(const std::function &fetcher) { - fetcher(T()); + mFetcher = fetcher; } private: @@ -307,16 +199,17 @@ private: { qWarning() << "done"; if (mOnDoneCallback) { - mOnDoneCallback(); + auto callback = mOnDoneCallback; mOnDoneCallback = std::function(); + //This may delete this object + callback(); } } QWeakPointer > mResultEmitter; - QSharedPointer mQueryRunner; - std::shared_ptr mFacade; std::function mOnDoneCallback; QSharedPointer mThreadBoundary; + std::function mFetcher; }; /* @@ -334,6 +227,8 @@ private: template class ResultEmitter { public: + typedef QSharedPointer > Ptr; + void onAdded(const std::function &handler) { addHandler = handler; @@ -394,6 +289,13 @@ public: clearHandler(); } + void setFetcher(const std::function &fetcher) + { + mFetcher = fetcher; + } + + std::function mFetcher; + private: friend class ResultProvider; diff --git a/common/threadboundary.cpp b/common/threadboundary.cpp index 47ec508..48fd11a 100644 --- a/common/threadboundary.cpp +++ b/common/threadboundary.cpp @@ -24,6 +24,9 @@ Q_DECLARE_METATYPE(std::function); namespace async { ThreadBoundary::ThreadBoundary(): QObject() { qRegisterMetaType >("std::function"); } -ThreadBoundary:: ~ThreadBoundary() {} +ThreadBoundary:: ~ThreadBoundary() +{ +} + } diff --git a/examples/dummyresource/CMakeLists.txt b/examples/dummyresource/CMakeLists.txt index e4b51dd..1e80f81 100644 --- a/examples/dummyresource/CMakeLists.txt +++ b/examples/dummyresource/CMakeLists.txt @@ -4,7 +4,7 @@ add_definitions(-DQT_PLUGIN) include_directories(${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_BINARY_DIR}) -add_library(${PROJECT_NAME} SHARED facade.cpp resourcefactory.cpp domainadaptor.cpp resourcefacade.cpp dummystore.cpp) +add_library(${PROJECT_NAME} SHARED facade.cpp resourcefactory.cpp domainadaptor.cpp dummystore.cpp) generate_flatbuffers(${PROJECT_NAME} dummycalendar) qt5_use_modules(${PROJECT_NAME} Core Network) target_link_libraries(${PROJECT_NAME} akonadi2common) diff --git a/examples/dummyresource/resourcefacade.cpp b/examples/dummyresource/resourcefacade.cpp deleted file mode 100644 index af0ebe6..0000000 --- a/examples/dummyresource/resourcefacade.cpp +++ /dev/null @@ -1,84 +0,0 @@ -/* - * Copyright (C) 2014 Christian Mollekopf - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * This program 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 General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program; if not, write to the - * Free Software Foundation, Inc., - * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - */ - -#include "resourcefacade.h" - -#include -#include - -DummyResourceConfigFacade::DummyResourceConfigFacade() - : Akonadi2::StoreFacade() -{ - -} - -DummyResourceConfigFacade::~DummyResourceConfigFacade() -{ - -} - -QSharedPointer DummyResourceConfigFacade::getSettings() -{ - //FIXME deal with resource instances - const QString instanceIdentifier = "dummyresource.instance1"; - //FIXME Use config location - return QSharedPointer::create(QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + "/akonadi2/" + "org.kde." + instanceIdentifier + "/settings.ini", QSettings::IniFormat); -} - -KAsync::Job DummyResourceConfigFacade::create(const Akonadi2::ApplicationDomain::AkonadiResource &domainObject) -{ - //TODO create resource instance - //This can be generalized in a base implementation - return KAsync::null(); -} - -KAsync::Job DummyResourceConfigFacade::modify(const Akonadi2::ApplicationDomain::AkonadiResource &domainObject) -{ - //modify configuration - //This part is likely resource specific, but could be partially generalized - return KAsync::start([domainObject, this]() { - auto settings = getSettings(); - //TODO Write properties to file - }); -} - -KAsync::Job DummyResourceConfigFacade::remove(const Akonadi2::ApplicationDomain::AkonadiResource &domainObject) -{ - //TODO remove resource instance - //This can be generalized in a base implementation - return KAsync::null(); -} - -KAsync::Job DummyResourceConfigFacade::load(const Akonadi2::Query &query, Akonadi2::ResultProviderInterface &resultProvider) -{ - //Read configuration and list all available instances. - //This includes runtime information about runing instances etc. - //Part of this is generic, and part is accessing the resource specific configuration. - //FIXME this currently does not support live queries (because we're not inheriting from GenericFacade) - //FIXME only read what was requested in the query? - return KAsync::start([&resultProvider, this]() { - auto settings = getSettings(); - auto memoryAdaptor = QSharedPointer::create(); - //TODO copy settings to adaptor - // - //TODO use correct instance identifier - //TODO key == instance identifier ? - resultProvider.add(QSharedPointer::create("org.kde.dummy.instance1", "org.kde.dummy.config", 0, memoryAdaptor)); - }); -} diff --git a/examples/dummyresource/resourcefacade.h b/examples/dummyresource/resourcefacade.h deleted file mode 100644 index 82e54fd..0000000 --- a/examples/dummyresource/resourcefacade.h +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright (C) 2014 Christian Mollekopf - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * This program 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 General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program; if not, write to the - * Free Software Foundation, Inc., - * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - */ - -#pragma once - -#include -#include -#include -#include - -namespace Akonadi2 { - class Query; -} - -class QSettings; - -class DummyResourceConfigFacade : public Akonadi2::StoreFacade -{ -public: - DummyResourceConfigFacade(); - ~DummyResourceConfigFacade(); - //Create an instance - KAsync::Job create(const Akonadi2::ApplicationDomain::AkonadiResource &domainObject) Q_DECL_OVERRIDE; - //Modify configuration - KAsync::Job modify(const Akonadi2::ApplicationDomain::AkonadiResource &domainObject) Q_DECL_OVERRIDE; - //Remove instance - KAsync::Job remove(const Akonadi2::ApplicationDomain::AkonadiResource &domainObject) Q_DECL_OVERRIDE; - //Read configuration and available instances - KAsync::Job load(const Akonadi2::Query &query, Akonadi2::ResultProviderInterface &resultProvider) Q_DECL_OVERRIDE; - -private: - QSharedPointer getSettings(); -}; diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 9ed5a76..b26797c 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -30,25 +30,25 @@ endmacro(auto_tests) manual_tests ( storagebenchmark dummyresourcebenchmark - genericresourcebenchmark - genericfacadebenchmark +# genericresourcebenchmark +# genericfacadebenchmark ) auto_tests ( clientapitest storagetest - dummyresourcetest + # dummyresourcetest domainadaptortest messagequeuetest indextest - genericresourcetest - genericfacadetest + # genericresourcetest + # genericfacadetest resourcecommunicationtest pipelinetest querytest ) -target_link_libraries(dummyresourcetest akonadi2_resource_dummy) +# 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 4883b5e..d76fac8 100644 --- a/tests/clientapitest.cpp +++ b/tests/clientapitest.cpp @@ -4,7 +4,6 @@ #include "clientapi.h" #include "facade.h" -#include "synclistresult.h" #include "resourceconfig.h" #include "modelresult.h" #include "resultprovider.h" @@ -28,22 +27,35 @@ public: 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, Akonadi2::ResultProviderInterface &resultProvider) Q_DECL_OVERRIDE + QPair, typename Akonadi2::ResultEmitter::Ptr > load(const Akonadi2::Query &query) Q_DECL_OVERRIDE { - capturedResultProvider = &resultProvider; - resultProvider.setFetcher([query, &resultProvider, this](const typename T::Ptr &) { - for (const auto &res : results) { + // capturedResultProvider = &resultProvider; + Trace() << "lkjsdflkjsdfljsdfljsdlfj"; + + auto resultProvider = new Akonadi2::ResultProvider(); + resultProvider->onDone([resultProvider]() { + Trace() << "Result provider is done"; + delete resultProvider; + }); + //We have to do it this way, otherwise we're not setting the fetcher right + auto emitter = resultProvider->emitter(); + + resultProvider->setFetcher([query, resultProvider, this](const typename T::Ptr &) { + Trace() << "Running the fetcher"; + for (const auto &res : results) { 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); + resultProvider->add(res); } } }); - return KAsync::null(); + auto job = KAsync::start([query, resultProvider]() { + }); + return qMakePair(job, emitter); } QList results; - Akonadi2::ResultProviderInterface *capturedResultProvider; + // Akonadi2::ResultProviderInterface *capturedResultProvider; }; @@ -61,24 +73,25 @@ private Q_SLOTS: { Akonadi2::FacadeFactory::instance().resetFactory(); ResourceConfig::clear(); + Akonadi2::Log::setDebugOutputLevel(Akonadi2::Log::Trace); } - void testLoad() - { - 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; - - async::SyncListResult result(Akonadi2::Store::load(query)); - result.exec(); - QCOMPARE(result.size(), 1); - } - - //The query provider is supposed to delete itself + // void testLoad() + // { + // 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; + // + // async::SyncListResult result(Akonadi2::Store::load(query)); + // result.exec(); + // QCOMPARE(result.size(), 1); + // } + // + // //The query provider is supposed to delete itself void testQueryLifetime() { auto facade = DummyResourceFacade::registerFacade(); @@ -90,12 +103,12 @@ private Q_SLOTS: query.liveQuery = true; { - async::SyncListResult result(Akonadi2::Store::load(query)); - result.exec(); - QCOMPARE(result.size(), 1); + auto model = Akonadi2::Store::loadModel(query); + QTRY_COMPARE(model->rowCount(QModelIndex()), 1); } //It's running in a separate thread, so we have to wait for a moment until the query provider deletes itself. // QTRY_VERIFY(!facade->capturedResultProvider); + QTest::qWait(300); } //TODO: This test doesn't belong to this testsuite @@ -112,18 +125,22 @@ private Q_SLOTS: { Akonadi2::Query query; query.propertyFilter.insert("type", "dummyresource"); - async::SyncListResult result(Akonadi2::Store::load(query)); - result.exec(); - QCOMPARE(result.size(), 1); + // async::SyncListResult result(Akonadi2::Store::load(query)); + auto model = Akonadi2::Store::loadModel(query); + // result.exec(); + QTRY_COMPARE(model->rowCount(QModelIndex()), 1); } Akonadi2::Store::remove(res).exec().waitForFinished(); { Akonadi2::Query query; query.propertyFilter.insert("type", "dummyresource"); - async::SyncListResult result(Akonadi2::Store::load(query)); - result.exec(); - QCOMPARE(result.size(), 0); + // async::SyncListResult result(Akonadi2::Store::load(query)); + auto model = Akonadi2::Store::loadModel(query); + // result.exec(); + // QCOMPARE(result.size(), 0); + // QTRY_COMPARE(result.size(), 0); + QTRY_COMPARE(model->rowCount(QModelIndex()), 0); } } @@ -138,7 +155,6 @@ private Q_SLOTS: query.liveQuery = false; auto model = Akonadi2::Store::loadModel(query); - model->fetchMore(QModelIndex()); QTRY_COMPARE(model->rowCount(), 1); } @@ -155,9 +171,9 @@ private Q_SLOTS: Akonadi2::Query query; query.resources << "dummyresource.instance1"; query.liveQuery = false; + query.parentProperty = "parent"; auto model = Akonadi2::Store::loadModel(query); - model->fetchMore(QModelIndex()); QTRY_COMPARE(model->rowCount(), 1); model->fetchMore(model->index(0, 0)); QTRY_COMPARE(model->rowCount(model->index(0, 0)), 1); diff --git a/tests/dummyresourcebenchmark.cpp b/tests/dummyresourcebenchmark.cpp index 609b8dc..6eaf065 100644 --- a/tests/dummyresourcebenchmark.cpp +++ b/tests/dummyresourcebenchmark.cpp @@ -7,7 +7,6 @@ #include "clientapi.h" #include "commands.h" #include "entitybuffer.h" -#include "synclistresult.h" #include "pipeline.h" #include "log.h" #include "resourceconfig.h" @@ -115,7 +114,6 @@ private Q_SLOTS: query.propertyFilter.insert("uid", "testuid"); auto model = Akonadi2::Store::loadModel(query); - model->fetchMore(QModelIndex()); QTRY_COMPARE(model->rowCount(QModelIndex()), num); } qDebug() << "Query Time: " << time.elapsed() << "/sec " << num*1000/time.elapsed(); -- cgit v1.2.3 From 0118cd09f9a2cc956ae0a07b3ba8ad3c95314cab Mon Sep 17 00:00:00 2001 From: Christian Mollekopf Date: Fri, 27 Nov 2015 17:44:21 +0100 Subject: list and count options for non-gui operations --- examples/client/main.cpp | 28 +++++++++++++++++++++++----- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/examples/client/main.cpp b/examples/client/main.cpp index 794fc58..c75b3ce 100644 --- a/examples/client/main.cpp +++ b/examples/client/main.cpp @@ -68,7 +68,7 @@ public: auto removeButton = new QPushButton(this); removeButton->setText("Remove"); QObject::connect(removeButton, &QPushButton::pressed, [modelView]() { - for (auto index :modelView->selectionModel()->selectedIndexes()) { + for (auto index : modelView->selectionModel()->selectedIndexes()) { auto object = index.data(Akonadi2::Store::DomainObjectRole).value(); Akonadi2::Store::remove(*object).exec(); } @@ -93,6 +93,8 @@ int main(int argc, char *argv[]) QObject::tr("A resource to connect to")); cliOptions.addOption(QCommandLineOption("clear")); cliOptions.addOption(QCommandLineOption("debuglevel")); + cliOptions.addOption(QCommandLineOption("list")); + cliOptions.addOption(QCommandLineOption("count")); cliOptions.addHelpOption(); cliOptions.process(app); QStringList resources = cliOptions.positionalArguments(); @@ -124,11 +126,27 @@ int main(int argc, char *argv[]) } query.syncOnDemand = false; query.processAll = false; - query.liveQuery = true; query.requestedProperties << "name"; auto model = Akonadi2::Store::loadModel(query); - auto view = QSharedPointer >::create(model.data()); - - return app.exec(); + if (cliOptions.isSet("list")) { + query.liveQuery = false; + qDebug() << "Listing"; + QObject::connect(model.data(), &QAbstractItemModel::rowsInserted, [model](const QModelIndex &index, int start, int end) { + for (int i = start; i <= end; i++) { + qDebug() << model->data(model->index(i, 0, index)).toString(); + } + }); + model->fetchMore(QModelIndex()); + return app.exec(); + } else if (cliOptions.isSet("count")) { + query.liveQuery = false; + model->fetchMore(QModelIndex()); + qDebug() << "Counted results " << model->rowCount(QModelIndex()); + } else { + query.liveQuery = true; + auto view = QSharedPointer >::create(model.data()); + return app.exec(); + } + return 0; } -- cgit v1.2.3 From 088d8a40c195ce6dcb91556a17f69d26e5586a3e Mon Sep 17 00:00:00 2001 From: Christian Mollekopf Date: Sat, 28 Nov 2015 12:48:43 +0100 Subject: Removed most uses of SyncListResult and brought back the dummyresourcetest --- tests/CMakeLists.txt | 4 +- tests/clientapitest.cpp | 90 +++++++++++++++++++++------------------------ tests/dummyresourcetest.cpp | 81 ++++++++++++++++++++-------------------- tests/pipelinetest.cpp | 1 - tests/querytest.cpp | 2 - tests/testimplementations.h | 1 - 6 files changed, 85 insertions(+), 94 deletions(-) diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index b26797c..11fe415 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -37,7 +37,7 @@ manual_tests ( auto_tests ( clientapitest storagetest - # dummyresourcetest + dummyresourcetest domainadaptortest messagequeuetest indextest @@ -48,7 +48,7 @@ auto_tests ( querytest ) -# target_link_libraries(dummyresourcetest akonadi2_resource_dummy) +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 d76fac8..e97b2a4 100644 --- a/tests/clientapitest.cpp +++ b/tests/clientapitest.cpp @@ -76,21 +76,20 @@ private Q_SLOTS: Akonadi2::Log::setDebugOutputLevel(Akonadi2::Log::Trace); } - // void testLoad() - // { - // 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; - // - // async::SyncListResult result(Akonadi2::Store::load(query)); - // result.exec(); - // QCOMPARE(result.size(), 1); - // } - // + void testLoad() + { + 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 = Akonadi2::Store::loadModel(query); + QTRY_COMPARE(model->rowCount(QModelIndex()), 1); + } + // //The query provider is supposed to delete itself void testQueryLifetime() { @@ -125,9 +124,7 @@ private Q_SLOTS: { Akonadi2::Query query; query.propertyFilter.insert("type", "dummyresource"); - // async::SyncListResult result(Akonadi2::Store::load(query)); auto model = Akonadi2::Store::loadModel(query); - // result.exec(); QTRY_COMPARE(model->rowCount(QModelIndex()), 1); } @@ -135,11 +132,8 @@ private Q_SLOTS: { Akonadi2::Query query; query.propertyFilter.insert("type", "dummyresource"); - // async::SyncListResult result(Akonadi2::Store::load(query)); auto model = Akonadi2::Store::loadModel(query); - // result.exec(); - // QCOMPARE(result.size(), 0); - // QTRY_COMPARE(result.size(), 0); + //TODO ensure the initial query completed QTRY_COMPARE(model->rowCount(QModelIndex()), 0); } } @@ -179,33 +173,33 @@ private Q_SLOTS: 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(); - // } + 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; + query.parentProperty = "parent"; + + auto model = Akonadi2::Store::loadModel(query); + 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(); + } }; diff --git a/tests/dummyresourcetest.cpp b/tests/dummyresourcetest.cpp index 3b90e6c..d2fcda7 100644 --- a/tests/dummyresourcetest.cpp +++ b/tests/dummyresourcetest.cpp @@ -4,10 +4,10 @@ #include "dummyresource/resourcefactory.h" #include "clientapi.h" -#include "synclistresult.h" #include "commands.h" #include "entitybuffer.h" #include "resourceconfig.h" +#include "modelresult.h" #include "pipeline.h" #include "log.h" @@ -68,10 +68,9 @@ private Q_SLOTS: Akonadi2::Store::synchronize(query).exec().waitForFinished(); query.propertyFilter.insert("uid", "testuid"); - async::SyncListResult result(Akonadi2::Store::load(query)); - result.exec(); - QCOMPARE(result.size(), 1); - auto value = result.first(); + auto model = Akonadi2::Store::loadModel(query); + QTRY_COMPARE(model->rowCount(QModelIndex()), 1); + auto value = model->index(0, 0, QModelIndex()).data(ModelResult::DomainObjectRole).value(); QCOMPARE(value->getProperty("uid").toByteArray(), QByteArray("testuid")); } @@ -95,10 +94,11 @@ private Q_SLOTS: Akonadi2::Store::synchronize(query).exec().waitForFinished(); query.propertyFilter.insert("uid", "testuid"); - async::SyncListResult result(Akonadi2::Store::load(query)); - result.exec(); - QCOMPARE(result.size(), 1); - auto value = result.first(); + + auto model = Akonadi2::Store::loadModel(query); + QTRY_COMPARE(model->rowCount(QModelIndex()), 1); + auto value = model->index(0, 0, QModelIndex()).data(ModelResult::DomainObjectRole).value(); + qDebug() << value->getProperty("uid").toByteArray(); QCOMPARE(value->getProperty("uid").toByteArray(), QByteArray("testuid")); } @@ -124,10 +124,11 @@ private Q_SLOTS: Akonadi2::Store::synchronize(query).exec().waitForFinished(); query.propertyFilter.insert("summary", "summaryValue2"); - async::SyncListResult result(Akonadi2::Store::load(query)); - result.exec(); - QCOMPARE(result.size(), 1); - auto value = result.first(); + + auto model = Akonadi2::Store::loadModel(query); + QTRY_COMPARE(model->rowCount(QModelIndex()), 1); + auto value = model->index(0, 0, QModelIndex()).data(ModelResult::DomainObjectRole).value(); + qDebug() << value->getProperty("uid").toByteArray(); QCOMPARE(value->getProperty("uid").toByteArray(), QByteArray("testuid2")); } @@ -157,10 +158,10 @@ private Q_SLOTS: //Ensure all local data is processed Akonadi2::Store::synchronize(query).exec().waitForFinished(); - async::SyncListResult result(Akonadi2::Store::load(query)); - result.exec(); - QVERIFY(!result.isEmpty()); - auto value = result.first(); + auto model = Akonadi2::Store::loadModel(query); + QTRY_VERIFY(model->rowCount(QModelIndex()) >= 1); + auto value = model->index(0, 0, QModelIndex()).data(ModelResult::DomainObjectRole).value(); + QVERIFY(!value->getProperty("summary").toString().isEmpty()); qDebug() << value->getProperty("summary").toString(); } @@ -175,10 +176,10 @@ private Q_SLOTS: //Ensure all local data is processed Akonadi2::Store::synchronize(query).exec().waitForFinished(); - async::SyncListResult result(Akonadi2::Store::load(query)); - result.exec(); - QVERIFY(!result.isEmpty()); - auto value = result.first(); + auto model = Akonadi2::Store::loadModel(query); + QTRY_VERIFY(model->rowCount(QModelIndex()) >= 1); + auto value = model->index(0, 0, QModelIndex()).data(ModelResult::DomainObjectRole).value(); + QVERIFY(!value->getProperty("subject").toString().isEmpty()); qDebug() << value->getProperty("subject").toString(); } @@ -203,10 +204,10 @@ private Q_SLOTS: //Test create Akonadi2::ApplicationDomain::Event event2; { - async::SyncListResult result(Akonadi2::Store::load(query)); - result.exec(); - QCOMPARE(result.size(), 1); - auto value = result.first(); + auto model = Akonadi2::Store::loadModel(query); + QTRY_COMPARE(model->rowCount(QModelIndex()), 1); + auto value = model->index(0, 0, QModelIndex()).data(ModelResult::DomainObjectRole).value(); + QCOMPARE(value->getProperty("uid").toByteArray(), QByteArray("testuid")); QCOMPARE(value->getProperty("summary").toByteArray(), QByteArray("summaryValue")); event2 = *value; @@ -221,10 +222,10 @@ private Q_SLOTS: //Test modify { - async::SyncListResult result(Akonadi2::Store::load(query)); - result.exec(); - QCOMPARE(result.size(), 1); - auto value = result.first(); + auto model = Akonadi2::Store::loadModel(query); + QTRY_COMPARE(model->rowCount(QModelIndex()), 1); + auto value = model->index(0, 0, QModelIndex()).data(ModelResult::DomainObjectRole).value(); + QCOMPARE(value->getProperty("uid").toByteArray(), QByteArray("testuid")); QCOMPARE(value->getProperty("summary").toByteArray(), QByteArray("summaryValue2")); } @@ -236,9 +237,9 @@ private Q_SLOTS: //Test remove { - async::SyncListResult result(Akonadi2::Store::load(query)); - result.exec(); - QTRY_COMPARE(result.size(), 0); + auto model = Akonadi2::Store::loadModel(query); + //TODO ensure the initial query is done + QTRY_COMPARE(model->rowCount(QModelIndex()), 0); } } @@ -252,9 +253,8 @@ private Q_SLOTS: query.liveQuery = true; query.propertyFilter.insert("uid", "testuid"); - - async::SyncListResult result(Akonadi2::Store::load(query)); - result.exec(); + auto model = Akonadi2::Store::loadModel(query); + //TODO ensure the initial query is done Akonadi2::ApplicationDomain::Event event("org.kde.dummy.instance1"); event.setProperty("uid", "testuid"); @@ -265,8 +265,8 @@ private Q_SLOTS: //Test create Akonadi2::ApplicationDomain::Event event2; { - QTRY_COMPARE(result.size(), 1); - auto value = result.first(); + QTRY_COMPARE(model->rowCount(QModelIndex()), 1); + auto value = model->index(0, 0, QModelIndex()).data(ModelResult::DomainObjectRole).value(); QCOMPARE(value->getProperty("uid").toByteArray(), QByteArray("testuid")); QCOMPARE(value->getProperty("summary").toByteArray(), QByteArray("summaryValue")); event2 = *value; @@ -278,8 +278,9 @@ private Q_SLOTS: //Test modify { - QTRY_COMPARE(result.size(), 1); - auto value = result.first(); + //TODO wait for a change signal + QTRY_COMPARE(model->rowCount(QModelIndex()), 1); + auto value = model->index(0, 0, QModelIndex()).data(ModelResult::DomainObjectRole).value(); QCOMPARE(value->getProperty("uid").toByteArray(), QByteArray("testuid")); QCOMPARE(value->getProperty("summary").toByteArray(), QByteArray("summaryValue2")); } @@ -288,7 +289,7 @@ private Q_SLOTS: //Test remove { - QTRY_COMPARE(result.size(), 0); + QTRY_COMPARE(model->rowCount(QModelIndex()), 0); } } diff --git a/tests/pipelinetest.cpp b/tests/pipelinetest.cpp index 2ede69d..f0fd1a4 100644 --- a/tests/pipelinetest.cpp +++ b/tests/pipelinetest.cpp @@ -12,7 +12,6 @@ #include "deleteentity_generated.h" #include "dummyresource/resourcefactory.h" #include "clientapi.h" -#include "synclistresult.h" #include "commands.h" #include "entitybuffer.h" #include "resourceconfig.h" diff --git a/tests/querytest.cpp b/tests/querytest.cpp index fdfb609..e354272 100644 --- a/tests/querytest.cpp +++ b/tests/querytest.cpp @@ -4,7 +4,6 @@ #include "dummyresource/resourcefactory.h" #include "clientapi.h" -#include "synclistresult.h" #include "commands.h" #include "resourceconfig.h" #include "log.h" @@ -129,7 +128,6 @@ private Q_SLOTS: Akonadi2::Store::synchronize(query).exec().waitForFinished(); auto model = Akonadi2::Store::loadModel(query); - model->fetchMore(QModelIndex()); QTRY_COMPARE(model->rowCount(), 1); auto folderEntity = model->index(0, 0).data(ModelResult::DomainObjectRole).value(); diff --git a/tests/testimplementations.h b/tests/testimplementations.h index c371456..a47a775 100644 --- a/tests/testimplementations.h +++ b/tests/testimplementations.h @@ -23,7 +23,6 @@ #include #include -#include #include #include #include -- cgit v1.2.3 From 4926e7f613ea3e03a2865eec66c6a8c1ec0b6516 Mon Sep 17 00:00:00 2001 From: Christian Mollekopf Date: Sat, 28 Nov 2015 16:07:15 +0100 Subject: Cleanup --- common/clientapi.cpp | 12 +----------- common/clientapi.h | 12 ------------ common/modelresult.cpp | 2 +- common/queryrunner.cpp | 24 ++++++++++++++++++++---- common/queryrunner.h | 1 + common/resultprovider.h | 2 +- examples/client/main.cpp | 4 +--- examples/dummyresource/dummystore.cpp | 2 +- 8 files changed, 26 insertions(+), 33 deletions(-) diff --git a/common/clientapi.cpp b/common/clientapi.cpp index b24dfa8..29b7e68 100644 --- a/common/clientapi.cpp +++ b/common/clientapi.cpp @@ -41,7 +41,7 @@ namespace async { - void run(const std::function &runner) { + static void run(const std::function &runner) { auto timer = new QTimer(); timer->setSingleShot(true); QObject::connect(timer, &QTimer::timeout, [runner, timer]() { @@ -97,15 +97,6 @@ QList Store::getResources(const QList &resourceFilter, c return resources; } -template -QSharedPointer > Store::load(Query query) -{ - auto resultSet = QSharedPointer >::create(); - qWarning() << "Main thread " << QThread::currentThreadId(); - //FIXME remove - return resultSet->emitter(); -} - template QSharedPointer Store::loadModel(Query query) { @@ -213,7 +204,6 @@ KAsync::Job Store::synchronize(const Akonadi2::Query &query) #define REGISTER_TYPE(T) template KAsync::Job Store::remove(const T &domainObject); \ template KAsync::Job Store::create(const T &domainObject); \ template KAsync::Job Store::modify(const T &domainObject); \ - template QSharedPointer > Store::load(Query query); \ template QSharedPointer Store::loadModel(Query query); \ REGISTER_TYPE(ApplicationDomain::Event); diff --git a/common/clientapi.h b/common/clientapi.h index c48c6e9..7fee6ae 100644 --- a/common/clientapi.h +++ b/common/clientapi.h @@ -33,12 +33,6 @@ Q_DECLARE_METATYPE(std::shared_ptr); class QAbstractItemModel; -namespace async { - //This should abstract if we execute from eventloop or in thread. - //It supposed to allow the caller to finish the current method before executing the runner. - void run(const std::function &runner); -} - namespace Akonadi2 { using namespace async; @@ -54,12 +48,6 @@ public: static QString storageLocation(); static QByteArray resourceName(const QByteArray &instanceIdentifier); - /** - * Asynchronusly load a dataset - */ - template - static QSharedPointer > load(Query query); - enum Roles { DomainObjectRole = Qt::UserRole + 1 //Must be the same as in ModelResult }; diff --git a/common/modelresult.cpp b/common/modelresult.cpp index 65eaba9..930048f 100644 --- a/common/modelresult.cpp +++ b/common/modelresult.cpp @@ -141,7 +141,7 @@ void ModelResult::add(const Ptr &value) return; } // qDebug() << "Inserting rows " << index << parent; - beginInsertRows(QModelIndex(), index, index); + beginInsertRows(parent, index, index); mEntities.insert(childId, value); mTree[id].insert(index, childId); mParents.insert(childId, id); diff --git a/common/queryrunner.cpp b/common/queryrunner.cpp index 4159112..3f62f6a 100644 --- a/common/queryrunner.cpp +++ b/common/queryrunner.cpp @@ -21,6 +21,7 @@ #include #include #include +#include #include "commands.h" #include "log.h" #include "storage.h" @@ -45,7 +46,7 @@ static inline ResultSet fullScan(const Akonadi2::Storage::Transaction &transacti qWarning() << "Error during query: " << error.message; }); - Trace() << "Full scan found " << keys.size() << " results"; + Trace() << "Full scan on " << bufferType << " found " << keys.size() << " results"; return ResultSet(keys); } @@ -96,6 +97,12 @@ QueryRunner::QueryRunner(const Akonadi2::Query &query, const Akonadi } } +template +QueryRunner::~QueryRunner() +{ + Trace() << "Stopped query"; +} + template typename Akonadi2::ResultEmitter::Ptr QueryRunner::emitter() { @@ -202,7 +209,6 @@ ResultSet QueryRunner::filterSet(const ResultSet &resultSet, const s readEntity(db, resultSetPtr->id(), [this, filter, callback, initialQuery](const Akonadi2::ApplicationDomain::ApplicationDomainType::Ptr &domainObject, Akonadi2::Operation operation) { //Always remove removals, they probably don't match due to non-available properties if (filter(domainObject) || operation == Akonadi2::Operation_Removal) { - Trace() << "entity is not filtered" << initialQuery; if (initialQuery) { //We're not interested in removals during the initial query if (operation != Akonadi2::Operation_Removal) { @@ -262,16 +268,24 @@ qint64 QueryRunner::load(const Akonadi2::Query &query, const std::fu template qint64 QueryRunner::executeIncrementalQuery(const Akonadi2::Query &query, Akonadi2::ResultProviderInterface &resultProvider) { + QTime time; + time.start(); + const qint64 baseRevision = resultProvider.revision() + 1; Trace() << "Running incremental query " << baseRevision; - return load(query, [&](Akonadi2::Storage::Transaction &transaction, QSet &remainingFilters) -> ResultSet { + auto revision = load(query, [&](Akonadi2::Storage::Transaction &transaction, QSet &remainingFilters) -> ResultSet { return loadIncrementalResultSet(baseRevision, query, transaction, remainingFilters); }, resultProvider, false); + Trace() << "Incremental query took: " << time.elapsed() << " ms"; + return revision; } template qint64 QueryRunner::executeInitialQuery(const Akonadi2::Query &query, const typename DomainType::Ptr &parent, Akonadi2::ResultProviderInterface &resultProvider) { + QTime time; + time.start(); + auto modifiedQuery = query; if (!query.parentProperty.isEmpty()) { if (parent) { @@ -282,9 +296,11 @@ qint64 QueryRunner::executeInitialQuery(const Akonadi2::Query &query modifiedQuery.propertyFilter.insert(query.parentProperty, QVariant()); } } - return load(modifiedQuery, [&](Akonadi2::Storage::Transaction &transaction, QSet &remainingFilters) -> ResultSet { + auto revision = load(modifiedQuery, [&](Akonadi2::Storage::Transaction &transaction, QSet &remainingFilters) -> ResultSet { return loadInitialResultSet(modifiedQuery, transaction, remainingFilters); }, resultProvider, true); + Trace() << "Initial query took: " << time.elapsed() << " ms"; + return revision; } template class QueryRunner; diff --git a/common/queryrunner.h b/common/queryrunner.h index e2af9de..c918dcb 100644 --- a/common/queryrunner.h +++ b/common/queryrunner.h @@ -79,6 +79,7 @@ class QueryRunner : public QueryRunnerBase { public: QueryRunner(const Akonadi2::Query &query, const Akonadi2::ResourceAccessInterface::Ptr &, const QByteArray &instanceIdentifier, const DomainTypeAdaptorFactoryInterface::Ptr &, const QByteArray &bufferType); + virtual ~QueryRunner(); typename Akonadi2::ResultEmitter::Ptr emitter(); diff --git a/common/resultprovider.h b/common/resultprovider.h index 86382ef..6d7867a 100644 --- a/common/resultprovider.h +++ b/common/resultprovider.h @@ -106,7 +106,7 @@ private: public: typedef QSharedPointer > Ptr; - ~ResultProvider() + virtual ~ResultProvider() { } diff --git a/examples/client/main.cpp b/examples/client/main.cpp index c75b3ce..3fa5a4e 100644 --- a/examples/client/main.cpp +++ b/examples/client/main.cpp @@ -77,7 +77,6 @@ public: topLayout->addWidget(titleLabel); topLayout->addWidget(syncButton); topLayout->addWidget(modelView, 10); - model->fetchMore(QModelIndex()); show(); } @@ -127,6 +126,7 @@ int main(int argc, char *argv[]) query.syncOnDemand = false; query.processAll = false; query.requestedProperties << "name"; + query.liveQuery = true; auto model = Akonadi2::Store::loadModel(query); if (cliOptions.isSet("list")) { @@ -137,11 +137,9 @@ int main(int argc, char *argv[]) qDebug() << model->data(model->index(i, 0, index)).toString(); } }); - model->fetchMore(QModelIndex()); return app.exec(); } else if (cliOptions.isSet("count")) { query.liveQuery = false; - model->fetchMore(QModelIndex()); qDebug() << "Counted results " << model->rowCount(QModelIndex()); } else { query.liveQuery = true; diff --git a/examples/dummyresource/dummystore.cpp b/examples/dummyresource/dummystore.cpp index 39ecfe4..458695f 100644 --- a/examples/dummyresource/dummystore.cpp +++ b/examples/dummyresource/dummystore.cpp @@ -64,7 +64,7 @@ QMap > populateMails() QMap > populateFolders() { QMap> content; - for (int i = 0; i < 5000; i++) { + for (int i = 0; i < 5; i++) { content.insert(QString("key%1").arg(i), createFolder(i)); } return content; -- cgit v1.2.3 From 887abffb3f712acaa23eae174d5890f337fe43cb Mon Sep 17 00:00:00 2001 From: Christian Mollekopf Date: Sat, 28 Nov 2015 16:20:38 +0100 Subject: Cleanup --- common/clientapi.h | 5 ----- examples/client/main.cpp | 1 + tests/dummyresourcetest.cpp | 18 +++++++++--------- tests/querytest.cpp | 4 ++-- 4 files changed, 12 insertions(+), 16 deletions(-) diff --git a/common/clientapi.h b/common/clientapi.h index 7fee6ae..04f1305 100644 --- a/common/clientapi.h +++ b/common/clientapi.h @@ -26,17 +26,12 @@ #include #include "query.h" -#include "resultprovider.h" #include "applicationdomaintype.h" -Q_DECLARE_METATYPE(std::shared_ptr); - class QAbstractItemModel; namespace Akonadi2 { -using namespace async; - /** * Store interface used in the client API. */ diff --git a/examples/client/main.cpp b/examples/client/main.cpp index 3fa5a4e..7e69c0a 100644 --- a/examples/client/main.cpp +++ b/examples/client/main.cpp @@ -27,6 +27,7 @@ #include "common/domain/event.h" #include "common/domain/folder.h" #include "common/resourceconfig.h" +#include "common/log.h" #include "console.h" #include diff --git a/tests/dummyresourcetest.cpp b/tests/dummyresourcetest.cpp index d2fcda7..20c725f 100644 --- a/tests/dummyresourcetest.cpp +++ b/tests/dummyresourcetest.cpp @@ -70,7 +70,7 @@ private Q_SLOTS: query.propertyFilter.insert("uid", "testuid"); auto model = Akonadi2::Store::loadModel(query); QTRY_COMPARE(model->rowCount(QModelIndex()), 1); - auto value = model->index(0, 0, QModelIndex()).data(ModelResult::DomainObjectRole).value(); + auto value = model->index(0, 0, QModelIndex()).data(Akonadi2::Store::DomainObjectRole).value(); QCOMPARE(value->getProperty("uid").toByteArray(), QByteArray("testuid")); } @@ -97,7 +97,7 @@ private Q_SLOTS: auto model = Akonadi2::Store::loadModel(query); QTRY_COMPARE(model->rowCount(QModelIndex()), 1); - auto value = model->index(0, 0, QModelIndex()).data(ModelResult::DomainObjectRole).value(); + auto value = model->index(0, 0, QModelIndex()).data(Akonadi2::Store::DomainObjectRole).value(); qDebug() << value->getProperty("uid").toByteArray(); QCOMPARE(value->getProperty("uid").toByteArray(), QByteArray("testuid")); @@ -127,7 +127,7 @@ private Q_SLOTS: auto model = Akonadi2::Store::loadModel(query); QTRY_COMPARE(model->rowCount(QModelIndex()), 1); - auto value = model->index(0, 0, QModelIndex()).data(ModelResult::DomainObjectRole).value(); + auto value = model->index(0, 0, QModelIndex()).data(Akonadi2::Store::DomainObjectRole).value(); qDebug() << value->getProperty("uid").toByteArray(); QCOMPARE(value->getProperty("uid").toByteArray(), QByteArray("testuid2")); @@ -160,7 +160,7 @@ private Q_SLOTS: auto model = Akonadi2::Store::loadModel(query); QTRY_VERIFY(model->rowCount(QModelIndex()) >= 1); - auto value = model->index(0, 0, QModelIndex()).data(ModelResult::DomainObjectRole).value(); + auto value = model->index(0, 0, QModelIndex()).data(Akonadi2::Store::DomainObjectRole).value(); QVERIFY(!value->getProperty("summary").toString().isEmpty()); qDebug() << value->getProperty("summary").toString(); @@ -178,7 +178,7 @@ private Q_SLOTS: auto model = Akonadi2::Store::loadModel(query); QTRY_VERIFY(model->rowCount(QModelIndex()) >= 1); - auto value = model->index(0, 0, QModelIndex()).data(ModelResult::DomainObjectRole).value(); + auto value = model->index(0, 0, QModelIndex()).data(Akonadi2::Store::DomainObjectRole).value(); QVERIFY(!value->getProperty("subject").toString().isEmpty()); qDebug() << value->getProperty("subject").toString(); @@ -206,7 +206,7 @@ private Q_SLOTS: { auto model = Akonadi2::Store::loadModel(query); QTRY_COMPARE(model->rowCount(QModelIndex()), 1); - auto value = model->index(0, 0, QModelIndex()).data(ModelResult::DomainObjectRole).value(); + auto value = model->index(0, 0, QModelIndex()).data(Akonadi2::Store::DomainObjectRole).value(); QCOMPARE(value->getProperty("uid").toByteArray(), QByteArray("testuid")); QCOMPARE(value->getProperty("summary").toByteArray(), QByteArray("summaryValue")); @@ -224,7 +224,7 @@ private Q_SLOTS: { auto model = Akonadi2::Store::loadModel(query); QTRY_COMPARE(model->rowCount(QModelIndex()), 1); - auto value = model->index(0, 0, QModelIndex()).data(ModelResult::DomainObjectRole).value(); + auto value = model->index(0, 0, QModelIndex()).data(Akonadi2::Store::DomainObjectRole).value(); QCOMPARE(value->getProperty("uid").toByteArray(), QByteArray("testuid")); QCOMPARE(value->getProperty("summary").toByteArray(), QByteArray("summaryValue2")); @@ -266,7 +266,7 @@ private Q_SLOTS: Akonadi2::ApplicationDomain::Event event2; { QTRY_COMPARE(model->rowCount(QModelIndex()), 1); - auto value = model->index(0, 0, QModelIndex()).data(ModelResult::DomainObjectRole).value(); + auto value = model->index(0, 0, QModelIndex()).data(Akonadi2::Store::DomainObjectRole).value(); QCOMPARE(value->getProperty("uid").toByteArray(), QByteArray("testuid")); QCOMPARE(value->getProperty("summary").toByteArray(), QByteArray("summaryValue")); event2 = *value; @@ -280,7 +280,7 @@ private Q_SLOTS: { //TODO wait for a change signal QTRY_COMPARE(model->rowCount(QModelIndex()), 1); - auto value = model->index(0, 0, QModelIndex()).data(ModelResult::DomainObjectRole).value(); + auto value = model->index(0, 0, QModelIndex()).data(Akonadi2::Store::DomainObjectRole).value(); QCOMPARE(value->getProperty("uid").toByteArray(), QByteArray("testuid")); QCOMPARE(value->getProperty("summary").toByteArray(), QByteArray("summaryValue2")); } diff --git a/tests/querytest.cpp b/tests/querytest.cpp index e354272..669bf58 100644 --- a/tests/querytest.cpp +++ b/tests/querytest.cpp @@ -108,7 +108,7 @@ private Q_SLOTS: auto model = Akonadi2::Store::loadModel(query); model->fetchMore(QModelIndex()); QTRY_COMPARE(model->rowCount(), 1); - auto folderEntity = model->index(0, 0).data(ModelResult::DomainObjectRole).value(); + auto folderEntity = model->index(0, 0).data(Akonadi2::Store::DomainObjectRole).value(); QVERIFY(!folderEntity->identifier().isEmpty()); } @@ -130,7 +130,7 @@ private Q_SLOTS: auto model = Akonadi2::Store::loadModel(query); QTRY_COMPARE(model->rowCount(), 1); - auto folderEntity = model->index(0, 0).data(ModelResult::DomainObjectRole).value(); + auto folderEntity = model->index(0, 0).data(Akonadi2::Store::DomainObjectRole).value(); QVERIFY(!folderEntity->identifier().isEmpty()); Akonadi2::ApplicationDomain::Folder subfolder("org.kde.dummy.instance1"); -- cgit v1.2.3 From 67d573d98da247d2cd16ce65fdd37457c5ee74ec Mon Sep 17 00:00:00 2001 From: Christian Mollekopf Date: Mon, 30 Nov 2015 10:30:31 +0100 Subject: ModelResult hasChildren, cleanup --- common/modelresult.cpp | 27 +++++++++++++++++++++------ common/modelresult.h | 1 + 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/common/modelresult.cpp b/common/modelresult.cpp index 930048f..582f8ff 100644 --- a/common/modelresult.cpp +++ b/common/modelresult.cpp @@ -44,26 +44,32 @@ template qint64 ModelResult::parentId(const Ptr &value) { if (!mQuery.parentProperty.isEmpty()) { - return qHash(value->getProperty(mQuery.parentProperty).toByteArray()); + const auto property = value->getProperty(mQuery.parentProperty).toByteArray(); + if (!property.isEmpty()) { + return qHash(property); + } } - return qHash(QByteArray()); + return 0; } template int ModelResult::rowCount(const QModelIndex &parent) const { + qDebug() << "row count " << mTree[getIdentifier(parent)].size(); return mTree[getIdentifier(parent)].size(); } template int ModelResult::columnCount(const QModelIndex &parent) const { + qDebug() << "porperty count " << mPropertyColumns.size(); return mPropertyColumns.size(); } template QVariant ModelResult::data(const QModelIndex &index, int role) const { + qDebug() << index; if (role == DomainObjectRole) { Q_ASSERT(mEntities.contains(index.internalId())); return QVariant::fromValue(mEntities.value(index.internalId())); @@ -83,8 +89,8 @@ QVariant ModelResult::data(const QModelIndex &index, int role) const template QModelIndex ModelResult::index(int row, int column, const QModelIndex &parent) const { - auto id = getIdentifier(parent); - auto childId = mTree.value(id).at(row); + const auto id = getIdentifier(parent); + const auto childId = mTree.value(id).at(row); return createIndex(row, column, childId); } @@ -104,6 +110,15 @@ QModelIndex ModelResult::parent(const QModelIndex &index) const return createIndexFromId(parentId); } +template +bool ModelResult::hasChildren(const QModelIndex &parent) const +{ + if (mQuery.parentProperty.isEmpty() && parent.isValid()) { + return false; + } + return QAbstractItemModel::hasChildren(parent); +} + template bool ModelResult::canFetchMore(const QModelIndex &parent) const { @@ -121,8 +136,8 @@ void ModelResult::fetchMore(const QModelIndex &parent) template void ModelResult::add(const Ptr &value) { - auto childId = qHash(value->identifier()); - auto id = parentId(value); + const auto childId = qHash(value->identifier()); + const auto id = parentId(value); //Ignore updates we get before the initial fetch is done if (!mEntityChildrenFetched[id]) { return; diff --git a/common/modelresult.h b/common/modelresult.h index eb6c86b..3ccf629 100644 --- a/common/modelresult.h +++ b/common/modelresult.h @@ -45,6 +45,7 @@ public: QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const; QModelIndex index(int row, int column, const QModelIndex & parent = QModelIndex()) const; QModelIndex parent(const QModelIndex &index) const; + bool hasChildren(const QModelIndex &parent = QModelIndex()) const; bool canFetchMore(const QModelIndex &parent) const; void fetchMore(const QModelIndex &parent); -- cgit v1.2.3 From 0413c13c6a9d2e3f5f5dc018f635f3043f09514b Mon Sep 17 00:00:00 2001 From: Christian Mollekopf Date: Mon, 30 Nov 2015 10:31:00 +0100 Subject: Only install the headers we need. We go rid of large parts of the header entanglements. --- common/CMakeLists.txt | 6 ------ 1 file changed, 6 deletions(-) diff --git a/common/CMakeLists.txt b/common/CMakeLists.txt index be312b9..e56ece9 100644 --- a/common/CMakeLists.txt +++ b/common/CMakeLists.txt @@ -74,12 +74,6 @@ install(FILES clientapi.h domain/applicationdomaintype.h query.h - threadboundary.h - resultprovider.h - facadefactory.h - log.h - listmodelresult.h bufferadaptor.h - facadeinterface.h DESTINATION ${INCLUDE_INSTALL_DIR}/${PROJECT_NAME} COMPONENT Devel ) -- cgit v1.2.3 From bf839f1a38518fd9302f4742ddeac16e891ac408 Mon Sep 17 00:00:00 2001 From: Christian Mollekopf Date: Mon, 30 Nov 2015 10:32:14 +0100 Subject: Debug output --- common/domain/event.cpp | 1 + common/queryrunner.cpp | 5 ++++- common/resourceaccess.cpp | 1 + 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/common/domain/event.cpp b/common/domain/event.cpp index 87e13bc..42c13e2 100644 --- a/common/domain/event.cpp +++ b/common/domain/event.cpp @@ -47,6 +47,7 @@ ResultSet TypeImplementation::queryIndexes(const Akonadi2::Query &query, }); appliedFilters << "uid"; } + Trace() << "Index lookup found " << keys.size() << " keys."; return ResultSet(keys); } diff --git a/common/queryrunner.cpp b/common/queryrunner.cpp index 3f62f6a..bb1127c 100644 --- a/common/queryrunner.cpp +++ b/common/queryrunner.cpp @@ -114,7 +114,9 @@ template void QueryRunner::replaySet(ResultSet &resultSet, Akonadi2::ResultProviderInterface &resultProvider) { // Trace() << "Replay set"; - while (resultSet.next([&resultProvider](const Akonadi2::ApplicationDomain::ApplicationDomainType::Ptr &value, Akonadi2::Operation operation) -> bool { + int counter = 0; + while (resultSet.next([&resultProvider, &counter](const Akonadi2::ApplicationDomain::ApplicationDomainType::Ptr &value, Akonadi2::Operation operation) -> bool { + counter++; switch (operation) { case Akonadi2::Operation_Creation: // Trace() << "Got creation"; @@ -131,6 +133,7 @@ void QueryRunner::replaySet(ResultSet &resultSet, Akonadi2::ResultPr } return true; })){}; + Trace() << "Replayed " << counter << " results"; } template diff --git a/common/resourceaccess.cpp b/common/resourceaccess.cpp index 1b46b82..be25533 100644 --- a/common/resourceaccess.cpp +++ b/common/resourceaccess.cpp @@ -282,6 +282,7 @@ KAsync::Job ResourceAccess::sendCommand(int commandId, flatbuffers::FlatB KAsync::Job ResourceAccess::synchronizeResource(bool sourceSync, bool localSync) { + Trace() << "Sending synchronize command: " << sourceSync << localSync; flatbuffers::FlatBufferBuilder fbb; auto command = Akonadi2::CreateSynchronize(fbb, sourceSync, localSync); Akonadi2::FinishSynchronizeBuffer(fbb, command); -- cgit v1.2.3 From f715898a1b6781e2860727942ce510f324a23c71 Mon Sep 17 00:00:00 2001 From: Christian Mollekopf Date: Mon, 30 Nov 2015 10:32:41 +0100 Subject: Test model signals --- tests/clientapitest.cpp | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/tests/clientapitest.cpp b/tests/clientapitest.cpp index e97b2a4..bd1cccd 100644 --- a/tests/clientapitest.cpp +++ b/tests/clientapitest.cpp @@ -173,6 +173,28 @@ private Q_SLOTS: QTRY_COMPARE(model->rowCount(model->index(0, 0)), 1); } + void testModelSignals() + { + 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; + query.parentProperty = "parent"; + + auto model = Akonadi2::Store::loadModel(query); + QSignalSpy spy(model.data(), SIGNAL(rowsInserted(const QModelIndex &, int, int))); + QVERIFY(spy.isValid()); + model->fetchMore(model->index(0, 0)); + QTRY_VERIFY(spy.count() >= 1); + } + void testModelNestedLive() { auto facade = DummyResourceFacade::registerFacade(); -- cgit v1.2.3 From b5648af02ea7246b41d24e242c5f94e43e43980e Mon Sep 17 00:00:00 2001 From: Christian Mollekopf Date: Mon, 30 Nov 2015 11:09:31 +0100 Subject: Provide status information about children fetch state The fetch state is per parent. --- common/clientapi.h | 3 ++- common/modelresult.cpp | 30 ++++++++++++++++++++---------- common/modelresult.h | 8 ++++++-- common/queryrunner.cpp | 1 + common/resourcefacade.cpp | 2 +- common/resultprovider.h | 30 +++++++++++++++++++++--------- 6 files changed, 51 insertions(+), 23 deletions(-) diff --git a/common/clientapi.h b/common/clientapi.h index 04f1305..8f87562 100644 --- a/common/clientapi.h +++ b/common/clientapi.h @@ -44,7 +44,8 @@ public: static QByteArray resourceName(const QByteArray &instanceIdentifier); enum Roles { - DomainObjectRole = Qt::UserRole + 1 //Must be the same as in ModelResult + DomainObjectRole = Qt::UserRole + 1, //Must be the same as in ModelResult + ChildrenFetchedRole }; /** diff --git a/common/modelresult.cpp b/common/modelresult.cpp index 582f8ff..4def20f 100644 --- a/common/modelresult.cpp +++ b/common/modelresult.cpp @@ -74,6 +74,9 @@ QVariant ModelResult::data(const QModelIndex &index, int role) const Q_ASSERT(mEntities.contains(index.internalId())); return QVariant::fromValue(mEntities.value(index.internalId())); } + if (role == ChildrenFetchedRole) { + return childrenFetched(index); + } if (role == Qt::DisplayRole) { if (index.column() < mPropertyColumns.size()) { Q_ASSERT(mEntities.contains(index.internalId())); @@ -122,8 +125,8 @@ bool ModelResult::hasChildren(const QModelIndex &parent) const template bool ModelResult::canFetchMore(const QModelIndex &parent) const { - qDebug() << "Can fetch more: " << parent << mEntityChildrenFetched.value(parent.internalId()); - return !mEntityChildrenFetched.value(parent.internalId(), false); + qDebug() << "Can fetch more: " << parent << mEntityChildrenFetched.contains(parent.internalId()); + return !mEntityChildrenFetched.contains(parent.internalId()); } template @@ -139,7 +142,8 @@ void ModelResult::add(const Ptr &value) const auto childId = qHash(value->identifier()); const auto id = parentId(value); //Ignore updates we get before the initial fetch is done - if (!mEntityChildrenFetched[id]) { + if (!mEntityChildrenFetched.contains(id)) { + qDebug() << "Children not yet fetched"; return; } auto parent = createIndexFromId(id); @@ -185,7 +189,7 @@ template void ModelResult::fetchEntities(const QModelIndex &parent) { const auto id = getIdentifier(parent); - mEntityChildrenFetched[id] = true; + mEntityChildrenFetched.insert(id); Trace() << "Loading child entities"; loadEntities(parent.data(DomainObjectRole).template value()); } @@ -210,22 +214,28 @@ void ModelResult::setEmitter(const typename Akonadi2::ResultEmitter emitter->onRemoved([this](const Ptr &value) { this->remove(value); }); - emitter->onInitialResultSetComplete([this]() { - }); - emitter->onComplete([this]() { - }); - emitter->onClear([this]() { + emitter->onInitialResultSetComplete([this](const Ptr &parent) { + qint64 parentId = parent ? qHash(parent->identifier()) : 0; + const auto parentIndex = createIndexFromId(parentId); + mEntityChildrenFetchComplete.insert(parentId); + emit dataChanged(parentIndex, parentIndex, QVector() << ChildrenFetchedRole); }); mEmitter = emitter; } +template +bool ModelResult::childrenFetched(const QModelIndex &index) const +{ + return mEntityChildrenFetchComplete.contains(getIdentifier(index)); +} + template void ModelResult::modify(const Ptr &value) { auto childId = qHash(value->identifier()); auto id = parentId(value); //Ignore updates we get before the initial fetch is done - if (!mEntityChildrenFetched[id]) { + if (!mEntityChildrenFetched.contains(id)) { return; } auto parent = createIndexFromId(id); diff --git a/common/modelresult.h b/common/modelresult.h index 3ccf629..700064b 100644 --- a/common/modelresult.h +++ b/common/modelresult.h @@ -33,7 +33,8 @@ class ModelResult : public QAbstractItemModel { public: enum Roles { - DomainObjectRole = Qt::UserRole + 1 + DomainObjectRole = Qt::UserRole + 1, + ChildrenFetchedRole }; ModelResult(const Akonadi2::Query &query, const QList &propertyColumns); @@ -56,6 +57,8 @@ public: void setFetcher(const std::function &fetcher); + bool childrenFetched(const QModelIndex &) const; + private: qint64 parentId(const Ptr &value); QModelIndex createIndexFromId(const qint64 &id) const; @@ -65,7 +68,8 @@ private: QMap mEntities; QMap /* child entity id*/> mTree; QMap mParents; - QMap mEntityChildrenFetched; + QSet mEntityChildrenFetched; + QSet mEntityChildrenFetchComplete; QList mPropertyColumns; Akonadi2::Query mQuery; std::function loadEntities; diff --git a/common/queryrunner.cpp b/common/queryrunner.cpp index bb1127c..e365cfc 100644 --- a/common/queryrunner.cpp +++ b/common/queryrunner.cpp @@ -303,6 +303,7 @@ qint64 QueryRunner::executeInitialQuery(const Akonadi2::Query &query return loadInitialResultSet(modifiedQuery, transaction, remainingFilters); }, resultProvider, true); Trace() << "Initial query took: " << time.elapsed() << " ms"; + resultProvider.initialResultSetComplete(parent); return revision; } diff --git a/common/resourcefacade.cpp b/common/resourcefacade.cpp index 3d207e4..6510c90 100644 --- a/common/resourcefacade.cpp +++ b/common/resourcefacade.cpp @@ -74,7 +74,7 @@ QPair, typename Akonadi2::ResultEmitterinitialResultSetComplete(); + resultProvider->initialResultSetComplete(Akonadi2::ApplicationDomain::AkonadiResource::Ptr()); resultProvider->complete(); }); return qMakePair(job, emitter); diff --git a/common/resultprovider.h b/common/resultprovider.h index 6d7867a..d50f3f6 100644 --- a/common/resultprovider.h +++ b/common/resultprovider.h @@ -50,7 +50,7 @@ public: virtual void add(const T &value) = 0; virtual void modify(const T &value) = 0; virtual void remove(const T &value) = 0; - virtual void initialResultSetComplete() = 0; + virtual void initialResultSetComplete(const T &parent) = 0; virtual void complete() = 0; virtual void clear() = 0; virtual void setFetcher(const std::function &fetcher) = 0; @@ -144,9 +144,15 @@ public: }); } - void initialResultSetComplete() + void initialResultSetComplete(const T &parent) { - callInMainThreadOnEmitter(&ResultEmitter::initialResultSetComplete); + //Because I don't know how to use bind + auto weakEmitter = mResultEmitter; + callInMainThreadOnEmitter([weakEmitter, parent](){ + if (auto strongRef = weakEmitter.toStrongRef()) { + strongRef->initialResultSetComplete(parent); + } + }); } //Called from worker thread @@ -244,7 +250,7 @@ public: removeHandler = handler; } - void onInitialResultSetComplete(const std::function &handler) + void onInitialResultSetComplete(const std::function &handler) { initialResultSetCompleteHandler = handler; } @@ -274,19 +280,25 @@ public: removeHandler(value); } - void initialResultSetComplete() + void initialResultSetComplete(const DomainType &parent) { - initialResultSetCompleteHandler(); + if (initialResultSetCompleteHandler) { + initialResultSetCompleteHandler(parent); + } } void complete() { - completeHandler(); + if (completeHandler) { + completeHandler(); + } } void clear() { - clearHandler(); + if (clearHandler) { + clearHandler(); + } } void setFetcher(const std::function &fetcher) @@ -302,7 +314,7 @@ private: std::function addHandler; std::function modifyHandler; std::function removeHandler; - std::function initialResultSetCompleteHandler; + std::function initialResultSetCompleteHandler; std::function completeHandler; std::function clearHandler; ThreadBoundary mThreadBoundary; -- cgit v1.2.3 From 32a6d2f881bb6d12a4ab685b7bb3fb10c3bdf72c Mon Sep 17 00:00:00 2001 From: Christian Mollekopf Date: Mon, 30 Nov 2015 11:41:09 +0100 Subject: Use the ChildrenFetchedRole --- tests/clientapitest.cpp | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/tests/clientapitest.cpp b/tests/clientapitest.cpp index bd1cccd..5d8cd9f 100644 --- a/tests/clientapitest.cpp +++ b/tests/clientapitest.cpp @@ -30,7 +30,6 @@ public: QPair, typename Akonadi2::ResultEmitter::Ptr > load(const Akonadi2::Query &query) Q_DECL_OVERRIDE { // capturedResultProvider = &resultProvider; - Trace() << "lkjsdflkjsdfljsdfljsdlfj"; auto resultProvider = new Akonadi2::ResultProvider(); resultProvider->onDone([resultProvider]() { @@ -40,7 +39,7 @@ public: //We have to do it this way, otherwise we're not setting the fetcher right auto emitter = resultProvider->emitter(); - resultProvider->setFetcher([query, resultProvider, this](const typename T::Ptr &) { + resultProvider->setFetcher([query, resultProvider, this](const typename T::Ptr &parent) { Trace() << "Running the fetcher"; for (const auto &res : results) { qDebug() << "Parent filter " << query.propertyFilter.value("parent").toByteArray() << res->identifier(); @@ -48,6 +47,7 @@ public: resultProvider->add(res); } } + resultProvider->initialResultSetComplete(parent); }); auto job = KAsync::start([query, resultProvider]() { }); @@ -87,7 +87,8 @@ private Q_SLOTS: query.liveQuery = false; auto model = Akonadi2::Store::loadModel(query); - QTRY_COMPARE(model->rowCount(QModelIndex()), 1); + QTRY_VERIFY(model->data(QModelIndex(), Akonadi2::Store::ChildrenFetchedRole).toBool()); + QCOMPARE(model->rowCount(QModelIndex()), 1); } // //The query provider is supposed to delete itself @@ -168,9 +169,11 @@ private Q_SLOTS: query.parentProperty = "parent"; auto model = Akonadi2::Store::loadModel(query); - QTRY_COMPARE(model->rowCount(), 1); + QTRY_VERIFY(model->data(QModelIndex(), Akonadi2::Store::ChildrenFetchedRole).toBool()); + QCOMPARE(model->rowCount(), 1); model->fetchMore(model->index(0, 0)); - QTRY_COMPARE(model->rowCount(model->index(0, 0)), 1); + QTRY_VERIFY(model->data(model->index(0, 0), Akonadi2::Store::ChildrenFetchedRole).toBool()); + QCOMPARE(model->rowCount(model->index(0, 0)), 1); } void testModelSignals() -- cgit v1.2.3 From bf28c2e3f43038165dc83c10267d103e779b245e Mon Sep 17 00:00:00 2001 From: Christian Mollekopf Date: Mon, 30 Nov 2015 13:45:51 +0100 Subject: ModelResult: return an invalid QModelIndex for the toplevel parent --- common/modelresult.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/common/modelresult.cpp b/common/modelresult.cpp index 4def20f..e2a05f8 100644 --- a/common/modelresult.cpp +++ b/common/modelresult.cpp @@ -100,6 +100,9 @@ QModelIndex ModelResult::index(int row, int column, const QModelIndex &p template QModelIndex ModelResult::createIndexFromId(const qint64 &id) const { + if (id == 0) { + return QModelIndex(); + } auto grandParentId = mParents.value(id, 0); auto row = mTree.value(grandParentId).indexOf(id); return createIndex(row, 0, id); -- cgit v1.2.3 From 0b8850f85f420fcb08643463afe01b026e58dde5 Mon Sep 17 00:00:00 2001 From: Christian Mollekopf Date: Mon, 30 Nov 2015 15:58:23 +0100 Subject: Less debug output --- common/modelresult.cpp | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/common/modelresult.cpp b/common/modelresult.cpp index e2a05f8..c7fcd49 100644 --- a/common/modelresult.cpp +++ b/common/modelresult.cpp @@ -55,21 +55,18 @@ qint64 ModelResult::parentId(const Ptr &value) template int ModelResult::rowCount(const QModelIndex &parent) const { - qDebug() << "row count " << mTree[getIdentifier(parent)].size(); return mTree[getIdentifier(parent)].size(); } template int ModelResult::columnCount(const QModelIndex &parent) const { - qDebug() << "porperty count " << mPropertyColumns.size(); return mPropertyColumns.size(); } template QVariant ModelResult::data(const QModelIndex &index, int role) const { - qDebug() << index; if (role == DomainObjectRole) { Q_ASSERT(mEntities.contains(index.internalId())); return QVariant::fromValue(mEntities.value(index.internalId())); @@ -128,14 +125,12 @@ bool ModelResult::hasChildren(const QModelIndex &parent) const template bool ModelResult::canFetchMore(const QModelIndex &parent) const { - qDebug() << "Can fetch more: " << parent << mEntityChildrenFetched.contains(parent.internalId()); return !mEntityChildrenFetched.contains(parent.internalId()); } template void ModelResult::fetchMore(const QModelIndex &parent) { - qDebug() << "Fetch more: " << parent; fetchEntities(parent); } @@ -146,7 +141,6 @@ void ModelResult::add(const Ptr &value) const auto id = parentId(value); //Ignore updates we get before the initial fetch is done if (!mEntityChildrenFetched.contains(id)) { - qDebug() << "Children not yet fetched"; return; } auto parent = createIndexFromId(id); @@ -178,7 +172,7 @@ void ModelResult::remove(const Ptr &value) auto childId = qHash(value->identifier()); auto id = parentId(value); auto parent = createIndexFromId(id); - qDebug() << "Removed entity" << childId; + // qDebug() << "Removed entity" << childId; auto index = mTree[id].indexOf(qHash(value->identifier())); beginRemoveRows(parent, index, index); mEntities.remove(childId); @@ -218,7 +212,7 @@ void ModelResult::setEmitter(const typename Akonadi2::ResultEmitter this->remove(value); }); emitter->onInitialResultSetComplete([this](const Ptr &parent) { - qint64 parentId = parent ? qHash(parent->identifier()) : 0; + const qint64 parentId = parent ? qHash(parent->identifier()) : 0; const auto parentIndex = createIndexFromId(parentId); mEntityChildrenFetchComplete.insert(parentId); emit dataChanged(parentIndex, parentIndex, QVector() << ChildrenFetchedRole); @@ -242,7 +236,7 @@ void ModelResult::modify(const Ptr &value) return; } auto parent = createIndexFromId(id); - qDebug() << "Modified entity" << childId; + // qDebug() << "Modified entity" << childId; auto i = mTree[id].indexOf(childId); mEntities.remove(childId); mEntities.insert(childId, value); -- cgit v1.2.3 From 0c744f0f14836ee70ca675135a9ca4cef080baa3 Mon Sep 17 00:00:00 2001 From: Christian Mollekopf Date: Mon, 30 Nov 2015 15:58:56 +0100 Subject: Test modifications --- tests/clientapitest.cpp | 62 ++++++++++++++++++++++++++----------------------- 1 file changed, 33 insertions(+), 29 deletions(-) diff --git a/tests/clientapitest.cpp b/tests/clientapitest.cpp index 5d8cd9f..8f956ab 100644 --- a/tests/clientapitest.cpp +++ b/tests/clientapitest.cpp @@ -29,8 +29,6 @@ public: KAsync::Job remove(const T &domainObject) Q_DECL_OVERRIDE { return KAsync::null(); }; QPair, typename Akonadi2::ResultEmitter::Ptr > load(const Akonadi2::Query &query) Q_DECL_OVERRIDE { - // capturedResultProvider = &resultProvider; - auto resultProvider = new Akonadi2::ResultProvider(); resultProvider->onDone([resultProvider]() { Trace() << "Result provider is done"; @@ -51,11 +49,12 @@ public: }); auto job = KAsync::start([query, resultProvider]() { }); + mResultProvider = resultProvider; return qMakePair(job, emitter); } QList results; - // Akonadi2::ResultProviderInterface *capturedResultProvider; + Akonadi2::ResultProviderInterface *mResultProvider; }; @@ -91,26 +90,6 @@ private Q_SLOTS: QCOMPARE(model->rowCount(QModelIndex()), 1); } - // //The query provider is supposed to delete itself - void testQueryLifetime() - { - 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 = true; - - { - auto model = Akonadi2::Store::loadModel(query); - QTRY_COMPARE(model->rowCount(QModelIndex()), 1); - } - //It's running in a separate thread, so we have to wait for a moment until the query provider deletes itself. - // QTRY_VERIFY(!facade->capturedResultProvider); - QTest::qWait(300); - } - //TODO: This test doesn't belong to this testsuite void resourceManagement() { @@ -134,8 +113,8 @@ private Q_SLOTS: Akonadi2::Query query; query.propertyFilter.insert("type", "dummyresource"); auto model = Akonadi2::Store::loadModel(query); - //TODO ensure the initial query completed - QTRY_COMPARE(model->rowCount(QModelIndex()), 0); + QTRY_VERIFY(model->data(QModelIndex(), Akonadi2::Store::ChildrenFetchedRole).toBool()); + QCOMPARE(model->rowCount(QModelIndex()), 0); } } @@ -218,12 +197,37 @@ private Q_SLOTS: model->fetchMore(model->index(0, 0)); QTRY_COMPARE(model->rowCount(model->index(0, 0)), 1); - // auto resultProvider = facade->capturedResultProvider.toStrongRef(); + auto resultProvider = facade->mResultProvider; + + //Test new toplevel folder + { + QSignalSpy rowsInsertedSpy(model.data(), SIGNAL(rowsInserted(const QModelIndex &, int, int))); + auto folder2 = QSharedPointer::create("resource", "id2", 0, QSharedPointer::create()); + resultProvider->add(folder2); + QTRY_COMPARE(model->rowCount(), 2); + QTRY_COMPARE(rowsInsertedSpy.count(), 1); + QCOMPARE(rowsInsertedSpy.at(0).at(0).value(), QModelIndex()); + } - //A modification can also be a move - // resultProvider->modify(); + //Test changed name + { + QSignalSpy dataChanged(model.data(), SIGNAL(dataChanged(const QModelIndex &, const QModelIndex &, const QVector &))); + folder->setProperty("subject", "modifiedSubject"); + resultProvider->modify(folder); + QTRY_COMPARE(model->rowCount(), 2); + QTRY_COMPARE(dataChanged.count(), 1); + } + + //Test removal + { + QSignalSpy rowsRemovedSpy(model.data(), SIGNAL(rowsRemoved(const QModelIndex &, int, int))); + folder->setProperty("subject", "modifiedSubject"); + resultProvider->remove(subfolder); + QTRY_COMPARE(model->rowCount(model->index(0, 0)), 0); + QTRY_COMPARE(rowsRemovedSpy.count(), 1); + } - // resultProvider->remove(); + //TODO: A modification can also be a move } -- cgit v1.2.3 From 6ad307dd846d07f1b55a1679a8d2eb47525af57d Mon Sep 17 00:00:00 2001 From: Christian Mollekopf Date: Mon, 30 Nov 2015 18:46:50 +0100 Subject: example client: slot performance measurements, async commands --- examples/client/main.cpp | 47 +++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 43 insertions(+), 4 deletions(-) diff --git a/examples/client/main.cpp b/examples/client/main.cpp index 7e69c0a..2aeb328 100644 --- a/examples/client/main.cpp +++ b/examples/client/main.cpp @@ -20,6 +20,7 @@ #include #include #include +#include #include "common/clientapi.h" #include "common/resource.h" @@ -36,6 +37,7 @@ #include #include #include +#include template class View : public QWidget @@ -84,9 +86,29 @@ public: }; + +class MyApplication : public QApplication +{ + QElapsedTimer t; +public: + MyApplication(int& argc, char ** argv) : QApplication(argc, argv) { } + virtual ~MyApplication() { } + + virtual bool notify(QObject* receiver, QEvent* event) + { + t.start(); + bool ret = QApplication::notify(receiver, event); + if(t.elapsed() > 3) + qDebug("processing event type %d for object %s took %dms", + (int)event->type(), receiver->objectName().toLocal8Bit().data(), + (int)t.elapsed()); + return ret; + } +}; + int main(int argc, char *argv[]) { - QApplication app(argc, argv); + MyApplication app(argc, argv); QCommandLineParser cliOptions; cliOptions.addPositionalArgument(QObject::tr("[resource]"), @@ -129,19 +151,36 @@ int main(int argc, char *argv[]) query.requestedProperties << "name"; query.liveQuery = true; + QTime time; + time.start(); auto model = Akonadi2::Store::loadModel(query); + qDebug() << "Loaded model in " << time.elapsed() << " ms"; + if (cliOptions.isSet("list")) { query.liveQuery = false; qDebug() << "Listing"; QObject::connect(model.data(), &QAbstractItemModel::rowsInserted, [model](const QModelIndex &index, int start, int end) { for (int i = start; i <= end; i++) { - qDebug() << model->data(model->index(i, 0, index)).toString(); + std::cout << "\tRow " << model->rowCount() << ": " << model->data(model->index(i, 0, index)).toString().toStdString() << std::endl; } }); - return app.exec(); + QObject::connect(model.data(), &QAbstractItemModel::dataChanged, [model, &app](const QModelIndex &, const QModelIndex &, const QVector &roles) { + if (roles.contains(Akonadi2::Store::ChildrenFetchedRole)) { + app.quit(); + } + }); + if (!model->data(QModelIndex(), Akonadi2::Store::ChildrenFetchedRole).toBool()) { + return app.exec(); + } } else if (cliOptions.isSet("count")) { query.liveQuery = false; - qDebug() << "Counted results " << model->rowCount(QModelIndex()); + QObject::connect(model.data(), &QAbstractItemModel::dataChanged, [model, &app](const QModelIndex &, const QModelIndex &, const QVector &roles) { + if (roles.contains(Akonadi2::Store::ChildrenFetchedRole)) { + std::cout << "\tCounted results " << model->rowCount(QModelIndex()); + app.quit(); + } + }); + return app.exec(); } else { query.liveQuery = true; auto view = QSharedPointer >::create(model.data()); -- cgit v1.2.3