diff --git a/autotests/server/itemcreatehandlertest.cpp b/autotests/server/itemcreatehandlertest.cpp index 0104c44e9..e67a88f4f 100644 --- a/autotests/server/itemcreatehandlertest.cpp +++ b/autotests/server/itemcreatehandlertest.cpp @@ -1,906 +1,907 @@ /* Copyright (c) 2014 Daniel Vrátil 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 #include #include #include #include #include "fakeakonadiserver.h" #include "fakeentities.h" #include +#include #include using namespace Akonadi; using namespace Akonadi::Server; Q_DECLARE_METATYPE(PimItem) Q_DECLARE_METATYPE(QVector) Q_DECLARE_METATYPE(QVector) Q_DECLARE_METATYPE(QVector) class ItemCreateHandlerTest : public QObject { Q_OBJECT public: ItemCreateHandlerTest() { // Effectively disable external payload parts, we have a dedicated unit-test // for that const QString serverConfigFile = StandardDirs::serverConfigFile(StandardDirs::ReadWrite); QSettings settings(serverConfigFile, QSettings::IniFormat); settings.setValue(QStringLiteral("General/SizeThreshold"), std::numeric_limits::max()); try { FakeAkonadiServer::instance()->init(); } catch (const FakeAkonadiServerException &e) { qWarning() << e.what(); qFatal("Fake Akonadi Server failed to start up, aborting test"); } } ~ItemCreateHandlerTest() { FakeAkonadiServer::instance()->quit(); } void updatePimItem(PimItem &pimItem, const QString &remoteId, const qint64 size) { pimItem.setRemoteId(remoteId); pimItem.setGid(remoteId); pimItem.setSize(size); } void updateNotifcationEntity(Protocol::ItemChangeNotificationPtr &ntf, const PimItem &pimItem) { Protocol::FetchItemsResponse item; item.setId(pimItem.id()); item.setRemoteId(pimItem.remoteId()); item.setRemoteRevision(pimItem.remoteRevision()); item.setMimeType(pimItem.mimeType().name()); ntf->setItems({std::move(item)}); } struct PartHelper { PartHelper(const QString &type_, const QByteArray &data_, int size_, Part::Storage storage_ = Part::Internal, int version_ = 0) : type(type_) , data(data_) , size(size_) , storage(storage_) , version(version_) { } QString type; QByteArray data; int size; Part::Storage storage; int version; }; void updateParts(QVector &parts, const std::vector &updatedParts) { parts.clear(); Q_FOREACH (const PartHelper &helper, updatedParts) { FakePart part; const QStringList types = helper.type.split(QLatin1Char(':')); Q_ASSERT(types.count() == 2); part.setPartType(PartType(types[1], types[0])); part.setData(helper.data); part.setDatasize(helper.size); part.setStorage(helper.storage); part.setVersion(helper.version); parts << part; } } void updateFlags(QVector &flags, const QStringList &updatedFlags) { flags.clear(); for (const QString &flagName : updatedFlags) { Flag flag; flag.setName(flagName); flags << flag; } } struct TagHelper { TagHelper(const QString &tagType_, const QString &gid_, const QString &remoteId_ = QString()) : tagType(tagType_) , gid(gid_) , remoteId(remoteId_) { } QString tagType; QString gid; QString remoteId; }; void updateTags(QVector &tags, const std::vector &updatedTags) { tags.clear(); Q_FOREACH (const TagHelper &helper, updatedTags) { FakeTag tag; TagType tagType; tagType.setName(helper.tagType); tag.setTagType(tagType); tag.setGid(helper.gid); tag.setRemoteId(helper.remoteId); tags << tag; } } Protocol::CreateItemCommandPtr createCommand(const PimItem &pimItem, const QDateTime &dt, const QSet &parts, qint64 overrideSize = -1) { const qint64 size = overrideSize > -1 ? overrideSize : pimItem.size(); auto cmd = Protocol::CreateItemCommandPtr::create(); cmd->setCollection(Scope(pimItem.collectionId())); cmd->setItemSize(size); cmd->setRemoteId(pimItem.remoteId()); cmd->setRemoteRevision(pimItem.remoteRevision()); cmd->setMimeType(pimItem.mimeType().name()); cmd->setGid(pimItem.gid()); cmd->setDateTime(dt); cmd->setParts(parts); return cmd; } Protocol::FetchItemsResponsePtr createResponse(qint64 expectedId, const PimItem &pimItem, const QDateTime &datetime, const QVector &parts, qint64 overrideSize = -1) { const qint64 size = overrideSize > -1 ? overrideSize : pimItem.size(); auto resp = Protocol::FetchItemsResponsePtr::create(expectedId); resp->setParentId(pimItem.collectionId()); resp->setSize(size); resp->setRemoteId(pimItem.remoteId()); resp->setRemoteRevision(pimItem.remoteRevision()); resp->setMimeType(pimItem.mimeType().name()); resp->setGid(pimItem.gid()); resp->setMTime(datetime); resp->setParts(parts); resp->setAncestors({ Protocol::Ancestor(4, QLatin1String("ColC")) }); return resp; } TestScenario errorResponse(const QString &errorMsg) { auto response = Protocol::CreateItemResponsePtr::create(); response->setError(1, errorMsg); return TestScenario::create(5, TestScenario::ServerCmd, response); } private Q_SLOTS: void testItemCreate_data() { using Notifications = QVector; QTest::addColumn("scenarios"); QTest::addColumn("notifications"); QTest::addColumn("pimItem"); QTest::addColumn >("parts"); QTest::addColumn >("flags"); QTest::addColumn >("tags"); QTest::addColumn("uidnext"); QTest::addColumn("datetime"); QTest::addColumn("expectFail"); TestScenario::List scenarios; auto notification = Protocol::ItemChangeNotificationPtr::create(); qint64 uidnext = 0; QDateTime datetime(QDate(2014, 05, 12), QTime(14, 46, 00), Qt::UTC); PimItem pimItem; QVector parts; QVector flags; QVector tags; pimItem.setCollectionId(4); pimItem.setSize(10); pimItem.setRemoteId(QStringLiteral("TEST-1")); pimItem.setRemoteRevision(QStringLiteral("1")); pimItem.setGid(QStringLiteral("TEST-1")); pimItem.setMimeType(MimeType::retrieveByName(QStringLiteral("application/octet-stream"))); pimItem.setDatetime(datetime); updateParts(parts, { { QLatin1String("PLD:DATA"), "0123456789", 10 } }); notification->setOperation(Protocol::ItemChangeNotification::Add); notification->setParentCollection(4); notification->setResource("akonadi_fake_resource_0"); Protocol::FetchItemsResponse item; item.setId(-1); item.setRemoteId(QStringLiteral("TEST-1")); item.setRemoteRevision(QStringLiteral("1")); item.setMimeType(QStringLiteral("application/octet-stream")); notification->setItems({std::move(item)}); notification->setSessionId(FakeAkonadiServer::instanceName().toLatin1()); uidnext = 13; scenarios << FakeAkonadiServer::loginScenario() << TestScenario::create(5, TestScenario::ClientCmd,createCommand(pimItem, datetime, { "PLD:DATA" })) << TestScenario::create(5, TestScenario::ServerCmd, Protocol::StreamPayloadCommandPtr::create("PLD:DATA", Protocol::StreamPayloadCommand::MetaData)) << TestScenario::create(5, TestScenario::ClientCmd, Protocol::StreamPayloadResponsePtr::create("PLD:DATA", Protocol::PartMetaData("PLD:DATA", 10))) << TestScenario::create(5, TestScenario::ServerCmd, Protocol::StreamPayloadCommandPtr::create("PLD:DATA", Protocol::StreamPayloadCommand::Data)) << TestScenario::create(5, TestScenario::ClientCmd, Protocol::StreamPayloadResponsePtr::create("PLD:DATA", "0123456789")) << TestScenario::create(5, TestScenario::ServerCmd, createResponse(uidnext, pimItem, datetime, { Protocol::StreamPayloadResponse("PLD:DATA", Protocol::PartMetaData("PLD:DATA", 10), "0123456789") })) << TestScenario::create(5, TestScenario::ServerCmd, Protocol::CreateItemResponsePtr::create()); QTest::newRow("single-part") << scenarios << Notifications{ notification } << pimItem << parts << flags << tags << uidnext << datetime << false; notification = Protocol::ItemChangeNotificationPtr::create(*notification); updatePimItem(pimItem, QStringLiteral("TEST-2"), 20); updateParts(parts, { { QLatin1String("PLD:DATA"), "Random Data", 11 }, { QLatin1String("PLD:PLDTEST"), "Test Data", 9 } }); updateNotifcationEntity(notification, pimItem); ++uidnext; scenarios.clear(); scenarios << FakeAkonadiServer::loginScenario() << TestScenario::create(5, TestScenario::ClientCmd, createCommand(pimItem, datetime, { "PLD:DATA", "PLD:PLDTEST" } )) << TestScenario::create(5, TestScenario::ServerCmd, Protocol::StreamPayloadCommandPtr::create("PLD:DATA", Protocol::StreamPayloadCommand::MetaData)) << TestScenario::create(5, TestScenario::ClientCmd, Protocol::StreamPayloadResponsePtr::create("PLD:DATA", Protocol::PartMetaData("PLD:DATA", 11, 0))) << TestScenario::create(5, TestScenario::ServerCmd, Protocol::StreamPayloadCommandPtr::create("PLD:DATA", Protocol::StreamPayloadCommand::Data)) << TestScenario::create(5, TestScenario::ClientCmd, Protocol::StreamPayloadResponsePtr::create("PLD:DATA", "Random Data")) << TestScenario::create(5, TestScenario::ServerCmd, Protocol::StreamPayloadCommandPtr::create("PLD:PLDTEST", Protocol::StreamPayloadCommand::MetaData)) << TestScenario::create(5, TestScenario::ClientCmd, Protocol::StreamPayloadResponsePtr::create("PLD:PLDTEST", Protocol::PartMetaData("PLD:PLDTEST", 9, 0))) << TestScenario::create(5, TestScenario::ServerCmd, Protocol::StreamPayloadCommandPtr::create("PLD:PLDTEST", Protocol::StreamPayloadCommand::Data)) << TestScenario::create(5, TestScenario::ClientCmd, Protocol::StreamPayloadResponsePtr::create("PLD:PLDTEST", "Test Data")) << TestScenario::create(5, TestScenario::ServerCmd, createResponse(uidnext, pimItem, datetime, { Protocol::StreamPayloadResponse("PLD:DATA", Protocol::PartMetaData("PLD:DATA", 11), "Random Data"), Protocol::StreamPayloadResponse("PLD:PLDTEST", Protocol::PartMetaData("PLD:PLDTEST", 9), "Test Data") })) << TestScenario::create(5, TestScenario::ServerCmd, Protocol::CreateItemResponsePtr::create()); QTest::newRow("multi-part") << scenarios << Notifications{ notification } << pimItem << parts << flags << tags << uidnext << datetime << false; TestScenario inScenario, outScenario; { auto cmd = Protocol::CreateItemCommandPtr::create(); cmd->setCollection(Scope(100)); inScenario = TestScenario::create(5, TestScenario::ClientCmd, cmd); } scenarios.clear(); scenarios << FakeAkonadiServer::loginScenario() << inScenario << errorResponse(QStringLiteral("Invalid parent collection")); QTest::newRow("invalid collection") << scenarios << Notifications{} << PimItem() << QVector() << QVector() << QVector() << -1ll << QDateTime() << true; { auto cmd = Protocol::CreateItemCommandPtr::create(); cmd->setCollection(Scope(6)); inScenario = TestScenario::create(5, TestScenario::ClientCmd, cmd); } scenarios.clear(); scenarios << FakeAkonadiServer::loginScenario() << inScenario << errorResponse(QStringLiteral("Cannot append item into virtual collection")); QTest::newRow("virtual collection") << scenarios << Notifications{} << PimItem() << QVector() << QVector() << QVector() << -1ll << QDateTime() << true; updatePimItem(pimItem, QStringLiteral("TEST-3"), 5); updateParts(parts, { { QLatin1String("PLD:DATA"), "12345", 5 } }); updateNotifcationEntity(notification, pimItem); ++uidnext; scenarios.clear(); scenarios << FakeAkonadiServer::loginScenario() << TestScenario::create(5, TestScenario::ClientCmd, createCommand(pimItem, datetime, { "PLD:DATA" }, 1)) << TestScenario::create(5, TestScenario::ServerCmd, Protocol::StreamPayloadCommandPtr::create("PLD:DATA", Protocol::StreamPayloadCommand::MetaData)) << TestScenario::create(5, TestScenario::ClientCmd, Protocol::StreamPayloadResponsePtr::create("PLD:DATA", Protocol::PartMetaData("PLD:DATA", 5))) << TestScenario::create(5, TestScenario::ServerCmd, Protocol::StreamPayloadCommandPtr::create("PLD:DATA", Protocol::StreamPayloadCommand::Data)) << TestScenario::create(5, TestScenario::ClientCmd, Protocol::StreamPayloadResponsePtr::create("PLD:DATA", "12345")) << TestScenario::create(5, TestScenario::ServerCmd, createResponse(uidnext, pimItem, datetime, { Protocol::StreamPayloadResponse("PLD:DATA", Protocol::PartMetaData("PLD:DATA", 5), "12345") })) << TestScenario::create(5, TestScenario::ServerCmd, Protocol::CreateItemResponsePtr::create()); QTest::newRow("mismatch item sizes (smaller)") << scenarios << Notifications{ notification } << pimItem << parts << flags << tags << uidnext << datetime << false; notification = Protocol::ItemChangeNotificationPtr::create(*notification); updatePimItem(pimItem, QStringLiteral("TEST-4"), 10); updateNotifcationEntity(notification, pimItem); ++uidnext; scenarios.clear(); scenarios << FakeAkonadiServer::loginScenario() << TestScenario::create(5, TestScenario::ClientCmd, createCommand(pimItem, datetime, { "PLD:DATA" }, 10)) << TestScenario::create(5, TestScenario::ServerCmd, Protocol::StreamPayloadCommandPtr::create("PLD:DATA", Protocol::StreamPayloadCommand::MetaData)) << TestScenario::create(5, TestScenario::ClientCmd, Protocol::StreamPayloadResponsePtr::create("PLD:DATA", Protocol::PartMetaData("PLD:DATA", 5))) << TestScenario::create(5, TestScenario::ServerCmd, Protocol::StreamPayloadCommandPtr::create("PLD:DATA", Protocol::StreamPayloadCommand::Data)) << TestScenario::create(5, TestScenario::ClientCmd, Protocol::StreamPayloadResponsePtr::create("PLD:DATA", "12345")) << TestScenario::create(5, TestScenario::ServerCmd, createResponse(uidnext, pimItem, datetime, { Protocol::StreamPayloadResponse("PLD:DATA", Protocol::PartMetaData("PLD:DATA", 5), "12345") }, 10)) << TestScenario::create(5, TestScenario::ServerCmd, Protocol::CreateItemResponsePtr::create()); QTest::newRow("mismatch item sizes (bigger)") << scenarios << Notifications{ notification } << pimItem << parts << flags << tags << uidnext << datetime << false; scenarios.clear(); scenarios << FakeAkonadiServer::loginScenario() << TestScenario::create(5, TestScenario::ClientCmd, createCommand(pimItem, datetime, { "PLD:DATA" })) << TestScenario::create(5, TestScenario::ServerCmd, Protocol::StreamPayloadCommandPtr::create("PLD:DATA", Protocol::StreamPayloadCommand::MetaData)) << TestScenario::create(5, TestScenario::ClientCmd, Protocol::StreamPayloadResponsePtr::create("PLD:DATA", Protocol::PartMetaData("PLD:DATA", 5))) << TestScenario::create(5, TestScenario::ServerCmd, Protocol::StreamPayloadCommandPtr::create("PLD:DATA", Protocol::StreamPayloadCommand::Data)) << TestScenario::create(5, TestScenario::ClientCmd, Protocol::StreamPayloadResponsePtr::create("PLD:DATA", "123")) << errorResponse(QStringLiteral("Payload size mismatch")); QTest::newRow("incomplete part data") << scenarios << Notifications{} << PimItem() << QVector() << QVector() << QVector() << -1ll << QDateTime() << true; scenarios.clear(); scenarios << FakeAkonadiServer::loginScenario() << TestScenario::create(5, TestScenario::ClientCmd, createCommand(pimItem, datetime, { "PLD:DATA" })) << TestScenario::create(5, TestScenario::ServerCmd, Protocol::StreamPayloadCommandPtr::create("PLD:DATA", Protocol::StreamPayloadCommand::MetaData)) << TestScenario::create(5, TestScenario::ClientCmd, Protocol::StreamPayloadResponsePtr::create("PLD:DATA", Protocol::PartMetaData("PLD:DATA", 4))) << TestScenario::create(5, TestScenario::ServerCmd, Protocol::StreamPayloadCommandPtr::create("PLD:DATA", Protocol::StreamPayloadCommand::Data)) << TestScenario::create(5, TestScenario::ClientCmd, Protocol::StreamPayloadResponsePtr::create("PLD:DATA", "1234567890")) << errorResponse(QStringLiteral("Payload size mismatch")); QTest::newRow("part data larger than advertised") << scenarios << Notifications{} << PimItem() << QVector() << QVector() << QVector() << -1ll << QDateTime() << true; notification = Protocol::ItemChangeNotificationPtr::create(*notification); updatePimItem(pimItem, QStringLiteral("TEST-5"), 0); updateParts(parts, { { QLatin1String("PLD:DATA"), QByteArray(), 0 } }); updateNotifcationEntity(notification, pimItem); ++uidnext; scenarios.clear(); scenarios << FakeAkonadiServer::loginScenario() << TestScenario::create(5, TestScenario::ClientCmd, createCommand(pimItem, datetime, { "PLD:DATA" })) << TestScenario::create(5, TestScenario::ServerCmd, Protocol::StreamPayloadCommandPtr::create("PLD:DATA", Protocol::StreamPayloadCommand::MetaData)) << TestScenario::create(5, TestScenario::ClientCmd, Protocol::StreamPayloadResponsePtr::create("PLD:DATA", Protocol::PartMetaData("PLD:DATA", 0))) << TestScenario::create(5, TestScenario::ServerCmd, Protocol::StreamPayloadCommandPtr::create("PLD:DATA", Protocol::StreamPayloadCommand::Data)) << TestScenario::create(5, TestScenario::ClientCmd, Protocol::StreamPayloadResponsePtr::create("PLD:DATA", QByteArray())) << TestScenario::create(5, TestScenario::ServerCmd, createResponse(uidnext, pimItem ,datetime, { Protocol::StreamPayloadResponse("PLD:DATA", Protocol::PartMetaData("PLD:DATA", 0), QByteArray()) } )) << TestScenario::create(5, TestScenario::ServerCmd, Protocol::CreateItemResponsePtr::create()); QTest::newRow("empty payload part") << scenarios << Notifications{ notification } << pimItem << parts << flags << tags << uidnext << datetime << false; notification = Protocol::ItemChangeNotificationPtr::create(*notification); updatePimItem(pimItem, QStringLiteral("TEST-8"), 1); updateParts(parts, { { QLatin1String("PLD:DATA"), QByteArray("\0", 1), 1 } }); updateNotifcationEntity(notification, pimItem); ++uidnext; scenarios.clear(); scenarios << FakeAkonadiServer::loginScenario() << TestScenario::create(5, TestScenario::ClientCmd, createCommand(pimItem, datetime, { "PLD:DATA" })) << TestScenario::create(5, TestScenario::ServerCmd, Protocol::StreamPayloadCommandPtr::create("PLD:DATA", Protocol::StreamPayloadCommand::MetaData)) << TestScenario::create(5, TestScenario::ClientCmd, Protocol::StreamPayloadResponsePtr::create("PLD:DATA", Protocol::PartMetaData("PLD:DATA", 1))) << TestScenario::create(5, TestScenario::ServerCmd, Protocol::StreamPayloadCommandPtr::create("PLD:DATA", Protocol::StreamPayloadCommand::Data)) << TestScenario::create(5, TestScenario::ClientCmd, Protocol::StreamPayloadResponsePtr::create("PLD:DATA", QByteArray("\0", 1))) << TestScenario::create(5, TestScenario::ServerCmd, createResponse(uidnext, pimItem, datetime, { Protocol::StreamPayloadResponse("PLD:DATA", Protocol::PartMetaData("PLD:DATA", 1), QByteArray("\0", 1)) })) << TestScenario::create(5, TestScenario::ServerCmd, Protocol::CreateItemResponsePtr::create()); QTest::newRow("part data will null character") << scenarios << Notifications{ notification } << pimItem << parts << flags << tags << uidnext << datetime << false; const QString utf8String = QStringLiteral("äöüß@€µøđ¢©®"); notification = Protocol::ItemChangeNotificationPtr::create(*notification); updatePimItem(pimItem, QStringLiteral("TEST-9"), utf8String.toUtf8().size()); updateParts(parts, { { QLatin1String("PLD:DATA"), utf8String.toUtf8(), utf8String.toUtf8().size() } }); updateNotifcationEntity(notification, pimItem); ++uidnext; scenarios.clear(); scenarios << FakeAkonadiServer::loginScenario() << TestScenario::create(5, TestScenario::ClientCmd, createCommand(pimItem, datetime, { "PLD:DATA" })) << TestScenario::create(5, TestScenario::ServerCmd, Protocol::StreamPayloadCommandPtr::create("PLD:DATA", Protocol::StreamPayloadCommand::MetaData)) << TestScenario::create(5, TestScenario::ClientCmd, Protocol::StreamPayloadResponsePtr::create("PLD:DATA", Protocol::PartMetaData("PLD:DATA", parts.first().datasize()))) << TestScenario::create(5, TestScenario::ServerCmd, Protocol::StreamPayloadCommandPtr::create("PLD:DATA", Protocol::StreamPayloadCommand::Data)) << TestScenario::create(5, TestScenario::ClientCmd, Protocol::StreamPayloadResponsePtr::create("PLD:DATA", utf8String.toUtf8())) << TestScenario::create(5, TestScenario::ServerCmd, createResponse(uidnext, pimItem, datetime, { Protocol::StreamPayloadResponse("PLD:DATA", Protocol::PartMetaData("PLD:DATA", utf8String.toUtf8().size()), utf8String.toUtf8()) })) << TestScenario::create(5, TestScenario::ServerCmd, Protocol::CreateItemResponsePtr::create()); QTest::newRow("utf8 part data") << scenarios << Notifications{ notification } << pimItem << parts << flags << tags << uidnext << datetime << false; const QByteArray hugeData = QByteArray("a").repeated(1 << 20); notification = Protocol::ItemChangeNotificationPtr::create(*notification); updatePimItem(pimItem, QStringLiteral("TEST-10"), 1 << 20); updateParts(parts, { { QLatin1String("PLD:DATA"), hugeData, 1 << 20 } }); updateNotifcationEntity(notification, pimItem); ++uidnext; scenarios.clear(); scenarios << FakeAkonadiServer::loginScenario() << TestScenario::create(5, TestScenario::ClientCmd, createCommand(pimItem, datetime, { "PLD:DATA" })) << TestScenario::create(5, TestScenario::ServerCmd, Protocol::StreamPayloadCommandPtr::create("PLD:DATA", Protocol::StreamPayloadCommand::MetaData)) << TestScenario::create(5, TestScenario::ClientCmd, Protocol::StreamPayloadResponsePtr::create("PLD:DATA", Protocol::PartMetaData("PLD:DATA", parts.first().datasize()))) << TestScenario::create(5, TestScenario::ServerCmd, Protocol::StreamPayloadCommandPtr::create("PLD:DATA", Protocol::StreamPayloadCommand::Data)) << TestScenario::create(5, TestScenario::ClientCmd, Protocol::StreamPayloadResponsePtr::create("PLD:DATA", hugeData)) << TestScenario::create(5, TestScenario::ServerCmd, createResponse(uidnext, pimItem ,datetime, { Protocol::StreamPayloadResponse("PLD:DATA", Protocol::PartMetaData("PLD:DATA", parts.first().datasize()), hugeData) })) << TestScenario::create(5, TestScenario::ServerCmd, Protocol::CreateItemResponsePtr::create()); QTest::newRow("huge part data") << scenarios << Notifications{ notification } << pimItem << parts << flags << tags << uidnext << datetime << false; const QByteArray dataWithNewLines = "Bernard, Bernard, Bernard, Bernard, look, look Bernard!\nWHAT!!!!!!!\nI'm a prostitute robot from the future!"; notification = Protocol::ItemChangeNotificationPtr::create(*notification); updatePimItem(pimItem, QStringLiteral("TEST-11"), dataWithNewLines.size()); updateParts(parts, { { QLatin1String("PLD:DATA"), dataWithNewLines, dataWithNewLines.size() } }); updateNotifcationEntity(notification, pimItem); ++uidnext; scenarios.clear(); scenarios << FakeAkonadiServer::loginScenario() << TestScenario::create(5, TestScenario::ClientCmd, createCommand(pimItem, datetime, { "PLD:DATA" })) << TestScenario::create(5, TestScenario::ServerCmd, Protocol::StreamPayloadCommandPtr::create("PLD:DATA", Protocol::StreamPayloadCommand::MetaData)) << TestScenario::create(5, TestScenario::ClientCmd, Protocol::StreamPayloadResponsePtr::create("PLD:DATA", Protocol::PartMetaData("PLD:DATA", parts.first().datasize()))) << TestScenario::create(5, TestScenario::ServerCmd, Protocol::StreamPayloadCommandPtr::create("PLD:DATA", Protocol::StreamPayloadCommand::Data)) << TestScenario::create(5, TestScenario::ClientCmd, Protocol::StreamPayloadResponsePtr::create("PLD:DATA", dataWithNewLines)) << TestScenario::create(5, TestScenario::ServerCmd, createResponse(uidnext, pimItem, datetime, { Protocol::StreamPayloadResponse("PLD:DATA", Protocol::PartMetaData("PLD:DATA", dataWithNewLines.size()), dataWithNewLines) })) << TestScenario::create(5, TestScenario::ServerCmd, Protocol::CreateItemResponsePtr::create()); QTest::newRow("data with newlines") << scenarios << Notifications{ notification } << pimItem << parts << flags << tags << uidnext << datetime << false; const QByteArray lotsOfNewlines = QByteArray("\n").repeated(1 << 20); notification = Protocol::ItemChangeNotificationPtr::create(*notification); updatePimItem(pimItem, QStringLiteral("TEST-12"), lotsOfNewlines.size()); updateParts(parts, { { QLatin1String("PLD:DATA"), lotsOfNewlines, lotsOfNewlines.size() } }); updateNotifcationEntity(notification, pimItem); ++uidnext; scenarios.clear(); scenarios << FakeAkonadiServer::loginScenario() << TestScenario::create(5, TestScenario::ClientCmd, createCommand(pimItem, datetime, { "PLD:DATA" })) << TestScenario::create(5, TestScenario::ServerCmd, Protocol::StreamPayloadCommandPtr::create("PLD:DATA", Protocol::StreamPayloadCommand::MetaData)) << TestScenario::create(5, TestScenario::ClientCmd, Protocol::StreamPayloadResponsePtr::create("PLD:DATA", Protocol::PartMetaData("PLD:DATA", parts.first().datasize()))) << TestScenario::create(5, TestScenario::ServerCmd, Protocol::StreamPayloadCommandPtr::create("PLD:DATA", Protocol::StreamPayloadCommand::Data)) << TestScenario::create(5, TestScenario::ClientCmd, Protocol::StreamPayloadResponsePtr::create("PLD:DATA", lotsOfNewlines)) << TestScenario::create(5, TestScenario::ServerCmd, createResponse(uidnext, pimItem, datetime, { Protocol::StreamPayloadResponse("PLD:DATA", Protocol::PartMetaData("PLD:DATA", parts.first().datasize()), lotsOfNewlines) })) << TestScenario::create(5, TestScenario::ServerCmd, Protocol::CreateItemResponsePtr::create()); QTest::newRow("data with lots of newlines") << scenarios << Notifications{ notification } << pimItem << parts << flags << tags << uidnext << datetime << false; notification = Protocol::ItemChangeNotificationPtr::create(*notification); updatePimItem(pimItem, QStringLiteral("TEST-13"), 20); updateParts(parts, { { QLatin1String("PLD:NEWPARTTYPE1"), "0123456789", 10 }, { QLatin1String("PLD:NEWPARTTYPE2"), "9876543210", 10 } }); updateNotifcationEntity(notification, pimItem); ++uidnext; scenarios.clear(); scenarios << FakeAkonadiServer::loginScenario() << TestScenario::create(5, TestScenario::ClientCmd, createCommand(pimItem, datetime, { "PLD:NEWPARTTYPE1", "PLD:NEWPARTTYPE2" })) << TestScenario::create(5, TestScenario::ServerCmd, Protocol::StreamPayloadCommandPtr::create("PLD:NEWPARTTYPE2", Protocol::StreamPayloadCommand::MetaData)) << TestScenario::create(5, TestScenario::ClientCmd, Protocol::StreamPayloadResponsePtr::create("PLD:NEWPARTTYPE2", Protocol::PartMetaData("PLD:NEWPARTTYPE2", 10))) << TestScenario::create(5, TestScenario::ServerCmd, Protocol::StreamPayloadCommandPtr::create("PLD:NEWPARTTYPE2", Protocol::StreamPayloadCommand::Data)) << TestScenario::create(5, TestScenario::ClientCmd, Protocol::StreamPayloadResponsePtr::create("PLD:NEWPARTTYPE2", "9876543210")) << TestScenario::create(5, TestScenario::ServerCmd, Protocol::StreamPayloadCommandPtr::create("PLD:NEWPARTTYPE1", Protocol::StreamPayloadCommand::MetaData)) << TestScenario::create(5, TestScenario::ClientCmd, Protocol::StreamPayloadResponsePtr::create("PLD:NEWPARTTYPE1", Protocol::PartMetaData("PLD:NEWPARTTYPE1", 10))) << TestScenario::create(5, TestScenario::ServerCmd, Protocol::StreamPayloadCommandPtr::create("PLD:NEWPARTTYPE1", Protocol::StreamPayloadCommand::Data)) << TestScenario::create(5, TestScenario::ClientCmd, Protocol::StreamPayloadResponsePtr::create("PLD:NEWPARTTYPE1", "0123456789")) << TestScenario::create(5, TestScenario::ServerCmd, createResponse(uidnext, pimItem, datetime, { Protocol::StreamPayloadResponse("PLD:NEWPARTTYPE2", Protocol::PartMetaData("PLD:NEWPARTTYPE2", 10), "9876543210"), Protocol::StreamPayloadResponse("PLD:NEWPARTTYPE1", Protocol::PartMetaData("PLD:NEWPARTTYPE1", 10), "0123456789") })) << TestScenario::create(5, TestScenario::ServerCmd, Protocol::CreateItemResponsePtr::create()); QTest::newRow("non-existent part types") << scenarios << Notifications{ notification } << pimItem << parts << flags << tags << uidnext << datetime << false; notification = Protocol::ItemChangeNotificationPtr::create(*notification); updatePimItem(pimItem, QStringLiteral("TEST-14"), 0); updateParts(parts, {}); updateFlags(flags, QStringList() << QStringLiteral("\\SEEN") << QStringLiteral("\\RANDOM")); updateNotifcationEntity(notification, pimItem); ++uidnext; { auto cmd = createCommand(pimItem, datetime, {}); cmd->setFlags({ "\\SEEN", "\\RANDOM" }); inScenario = TestScenario::create(5, TestScenario::ClientCmd, cmd); auto rsp = createResponse(uidnext, pimItem, datetime, {}); rsp->setFlags({ "\\SEEN", "\\RANDOM" }); outScenario = TestScenario::create(5, TestScenario::ServerCmd, rsp); } scenarios.clear(); scenarios << FakeAkonadiServer::loginScenario() << inScenario << outScenario << TestScenario::create(5, TestScenario::ServerCmd, Protocol::CreateItemResponsePtr::create()); QTest::newRow("item with flags") << scenarios << Notifications{ notification } << pimItem << parts << flags << tags << uidnext << datetime << false; notification = Protocol::ItemChangeNotificationPtr::create(*notification); updatePimItem(pimItem, QStringLiteral("TEST-15"), 0); updateFlags(flags, {}); updateTags(tags, { { QLatin1String("PLAIN"), QLatin1String("TAG-1") }, { QLatin1String("PLAIN"), QLatin1String("TAG-2") } }); updateNotifcationEntity(notification, pimItem); ++uidnext; { auto cmd = createCommand(pimItem, datetime, {}); cmd->setTags(Scope(Scope::Gid, { QLatin1String("TAG-1"), QLatin1String("TAG-2") })); inScenario = TestScenario::create(5, TestScenario::ClientCmd, cmd); auto rsp = createResponse(uidnext, pimItem, datetime, {}); rsp->setTags({ Protocol::FetchTagsResponse(2, "TAG-1", "PLAIN"), Protocol::FetchTagsResponse(3, "TAG-2", "PLAIN") }); outScenario = TestScenario::create(5, TestScenario::ServerCmd, rsp); } scenarios.clear(); scenarios << FakeAkonadiServer::loginScenario() << inScenario << outScenario << TestScenario::create(5, TestScenario::ServerCmd, Protocol::CreateItemResponsePtr::create()); QTest::newRow("item with non-existent tags (GID)") << scenarios << Notifications{ notification } << pimItem << parts << flags << tags << uidnext << datetime << false; notification = Protocol::ItemChangeNotificationPtr::create(*notification); updatePimItem(pimItem, QStringLiteral("TEST-16"), 0); updateTags(tags, { { QLatin1String("PLAIN"), QLatin1String("TAG-3") }, { QLatin1String("PLAIN"), QLatin1String("TAG-4") } }); updateNotifcationEntity(notification, pimItem); ++uidnext; { auto cmd = createCommand(pimItem, datetime, {}); cmd->setTags(Scope(Scope::Rid, { QLatin1String("TAG-3"), QLatin1String("TAG-4") })); inScenario = TestScenario::create(5, TestScenario::ClientCmd, cmd); auto rsp = createResponse(uidnext, pimItem, datetime, {}); rsp->setTags({ Protocol::FetchTagsResponse(4, "TAG-3", "PLAIN"), Protocol::FetchTagsResponse(5, "TAG-4", "PLAIN") }); outScenario = TestScenario::create(5, TestScenario::ServerCmd, rsp); } scenarios.clear(); scenarios << FakeAkonadiServer::loginScenario() << FakeAkonadiServer::selectResourceScenario(QStringLiteral("akonadi_fake_resource_0")) << inScenario << outScenario << TestScenario::create(5, TestScenario::ServerCmd, Protocol::CreateItemResponsePtr::create()); QTest::newRow("item with non-existent tags (RID)") << scenarios << Notifications{ notification } << pimItem << parts << flags << tags << uidnext << datetime << false; notification = Protocol::ItemChangeNotificationPtr::create(*notification); updatePimItem(pimItem, QStringLiteral("TEST-17"), 0); updateNotifcationEntity(notification, pimItem); updateTags(tags, { { QLatin1String("PLAIN"), QLatin1String("TAG-1") }, { QLatin1String("PLAIN"), QLatin1String("TAG-2") } }); ++uidnext; { auto cmd = createCommand(pimItem, datetime, {}); cmd->setTags(Scope(Scope::Rid, { QLatin1String("TAG-1"), QLatin1String("TAG-2") })); inScenario = TestScenario::create(5, TestScenario::ClientCmd, cmd); auto rsp = createResponse(uidnext, pimItem, datetime, {}); rsp->setTags({ Protocol::FetchTagsResponse(2, "TAG-1", "PLAIN"), Protocol::FetchTagsResponse(3, "TAG-2", "PLAIN") }); outScenario = TestScenario::create(5, TestScenario::ServerCmd, rsp); } scenarios.clear(); scenarios << FakeAkonadiServer::loginScenario() << FakeAkonadiServer::selectResourceScenario(QStringLiteral("akonadi_fake_resource_0")) << inScenario << outScenario << TestScenario::create(5, TestScenario::ServerCmd, Protocol::CreateItemResponsePtr::create()); QTest::newRow("item with existing tags (RID)") << scenarios << Notifications{ notification } << pimItem << parts << flags << tags << uidnext << datetime << false; notification = Protocol::ItemChangeNotificationPtr::create(*notification); updatePimItem(pimItem, QStringLiteral("TEST-18"), 0); updateNotifcationEntity(notification, pimItem); updateTags(tags, { { QLatin1String("PLAIN"), QLatin1String("TAG-3") }, { QLatin1String("PLAIN"), QLatin1String("TAG-4") } }); ++uidnext; { auto cmd = createCommand(pimItem, datetime, {}); cmd->setTags(Scope(Scope::Gid, { QLatin1String("TAG-3"), QLatin1String("TAG-4") })); inScenario = TestScenario::create(5, TestScenario::ClientCmd, cmd); auto rsp = createResponse(uidnext, pimItem, datetime, {}); rsp->setTags({ Protocol::FetchTagsResponse(4, "TAG-3", "PLAIN"), Protocol::FetchTagsResponse(5, "TAG-4", "PLAIN") }); outScenario = TestScenario::create(5, TestScenario::ServerCmd, rsp); } scenarios.clear(); scenarios << FakeAkonadiServer::loginScenario() << inScenario << outScenario << TestScenario::create(5, TestScenario::ServerCmd, Protocol::CreateItemResponsePtr::create()); QTest::newRow("item with existing tags (GID)") << scenarios << Notifications{ notification } << pimItem << parts << flags << tags << uidnext << datetime << false; notification = Protocol::ItemChangeNotificationPtr::create(*notification); updatePimItem(pimItem, QStringLiteral("TEST-19"), 0); updateFlags(flags, QStringList() << QStringLiteral("\\SEEN") << QStringLiteral("$FLAG")); updateTags(tags, { { QLatin1String("PLAIN"), QLatin1String("TAG-1") }, { QLatin1String("PLAIN"), QLatin1String("TAG-2") } }); updateNotifcationEntity(notification, pimItem); ++uidnext; { auto cmd = createCommand(pimItem, datetime, {}); cmd->setTags(Scope(Scope::Gid, { QLatin1String("TAG-1"), QLatin1String("TAG-2") })); cmd->setFlags({ "\\SEEN", "$FLAG" }); inScenario = TestScenario::create(5, TestScenario::ClientCmd, cmd); auto rsp = createResponse(uidnext, pimItem, datetime, {}); rsp->setTags({ Protocol::FetchTagsResponse(2, "TAG-1", "PLAIN"), Protocol::FetchTagsResponse(3, "TAG-2", "PLAIN") }); rsp->setFlags({ "\\SEEN", "$FLAG" }); outScenario = TestScenario::create(5, TestScenario::ServerCmd, rsp); } scenarios.clear(); scenarios << FakeAkonadiServer::loginScenario() << inScenario << outScenario << TestScenario::create(5, TestScenario::ServerCmd, Protocol::CreateItemResponsePtr::create()); QTest::newRow("item with flags and tags") << scenarios << Notifications{ notification } << pimItem << parts << flags << tags << uidnext << datetime << false; notification = Protocol::ItemChangeNotificationPtr::create(*notification); updatePimItem(pimItem, QStringLiteral("TEST-20"), 0); updateFlags(flags, {}); updateTags(tags, { { QLatin1String("PLAIN"), utf8String } }); updateNotifcationEntity(notification, pimItem); ++uidnext; { auto cmd = createCommand(pimItem, datetime, {}); cmd->setTags(Scope(Scope::Gid, { utf8String })); inScenario = TestScenario::create(5, TestScenario::ClientCmd, cmd); auto rsp = createResponse(uidnext, pimItem, datetime, {}); rsp->setTags({ Protocol::FetchTagsResponse(6, utf8String.toUtf8(), "PLAIN") }); outScenario = TestScenario::create(5, TestScenario::ServerCmd, rsp); } scenarios.clear(); scenarios << FakeAkonadiServer::loginScenario() << inScenario << outScenario << TestScenario::create(5, TestScenario::ServerCmd, Protocol::CreateItemResponsePtr::create()); QTest::newRow("item with UTF-8 tag") << scenarios << Notifications{ notification }<< pimItem << parts << flags << tags << uidnext << datetime << false; notification = Protocol::ItemChangeNotificationPtr::create(*notification); updatePimItem(pimItem, QStringLiteral("TEST-21"), 0); updateFlags(flags, {}); updateTags(tags, {}); pimItem.setGid(QStringLiteral("GID-21")); updateNotifcationEntity(notification, pimItem); scenarios = FakeAkonadiServer::loginScenario(); // Create a normal item with RID { ++uidnext; auto cmd = createCommand(pimItem, datetime, {}); scenarios << TestScenario::create(5, TestScenario::ClientCmd, cmd); auto rsp = createResponse(uidnext, pimItem, datetime, {}); scenarios << TestScenario::create(5, TestScenario::ServerCmd, rsp) << TestScenario::create(5, TestScenario::ServerCmd, Protocol::CreateItemResponsePtr::create()); } // Create the same item again (no merging, so it will just be created) { ++uidnext; auto cmd = createCommand(pimItem, datetime, {}); scenarios << TestScenario::create(6, TestScenario::ClientCmd, cmd); auto rsp = createResponse(uidnext, pimItem, datetime, {}); scenarios << TestScenario::create(6, TestScenario::ServerCmd, rsp) << TestScenario::create(6, TestScenario::ServerCmd, Protocol::CreateItemResponsePtr::create()); } // Now try to create the item once again, but in merge mode, we should fail now { ++uidnext; auto cmd = createCommand(pimItem, datetime, {}); cmd->setMergeModes(Protocol::CreateItemCommand::RemoteID); scenarios << TestScenario::create(7, TestScenario::ClientCmd, cmd); auto rsp = Protocol::CreateItemResponsePtr::create(); rsp->setError(1, QStringLiteral("Multiple merge candidates")); scenarios << TestScenario::create(7, TestScenario::ServerCmd, rsp); } Notifications notifications = { notification, Protocol::ItemChangeNotificationPtr::create(*notification) }; QTest::newRow("multiple merge candidates (RID)") << scenarios << notifications << pimItem << parts << flags << tags << uidnext << datetime << true; notification = Protocol::ItemChangeNotificationPtr::create(*notification); updatePimItem(pimItem, QStringLiteral("TEST-22"), 0); pimItem.setGid(QStringLiteral("GID-22")); updateNotifcationEntity(notification, pimItem); scenarios = FakeAkonadiServer::loginScenario(); // Create a normal item with GID { // Don't increase uidnext, we will reuse the one from previous test, // since that did not actually create a new Item auto cmd = createCommand(pimItem, datetime, {}); scenarios << TestScenario::create(5, TestScenario::ClientCmd, cmd); auto rsp = createResponse(uidnext, pimItem, datetime, {}); scenarios << TestScenario::create(5, TestScenario::ServerCmd, rsp) << TestScenario::create(5, TestScenario::ServerCmd, Protocol::CreateItemResponsePtr::create()); } // Create the same item again (no merging, so it will just be created) { ++uidnext; auto cmd = createCommand(pimItem, datetime, {}); scenarios << TestScenario::create(6, TestScenario::ClientCmd, cmd); auto rsp = createResponse(uidnext, pimItem, datetime, {}); scenarios << TestScenario::create(6, TestScenario::ServerCmd, rsp) << TestScenario::create(6, TestScenario::ServerCmd, Protocol::CreateItemResponsePtr::create()); } // Now try to create the item once again, but in merge mode, we should fail now { ++uidnext; auto cmd = createCommand(pimItem, datetime, {}); cmd->setMergeModes(Protocol::CreateItemCommand::GID); scenarios << TestScenario::create(7, TestScenario::ClientCmd, cmd); auto rsp = Protocol::CreateItemResponsePtr::create(); rsp->setError(1, QStringLiteral("Multiple merge candidates")); scenarios << TestScenario::create(7, TestScenario::ServerCmd, rsp); } notifications = { notification, Protocol::ItemChangeNotificationPtr::create(*notification) }; QTest::newRow("multiple merge candidates (GID)") << scenarios << notifications << pimItem << parts << flags << tags << uidnext << datetime << true; notification = Protocol::ItemChangeNotificationPtr::create(*notification); updatePimItem(pimItem, QStringLiteral("TEST-23"), 0); pimItem.setGid(QString()); updateNotifcationEntity(notification, pimItem); scenarios = FakeAkonadiServer::loginScenario(); // Create a normal item with RID, but with empty GID { // Don't increase uidnext, we will reuse the one from previous test, // since that did not actually create a new Item auto cmd = createCommand(pimItem, datetime, {}); scenarios << TestScenario::create(5, TestScenario::ClientCmd, cmd); auto rsp = createResponse(uidnext, pimItem, datetime, {}); scenarios << TestScenario::create(5, TestScenario::ServerCmd, rsp) << TestScenario::create(5, TestScenario::ServerCmd, Protocol::CreateItemResponsePtr::create()); } // Merge by GID - should not create a new Item but actually merge by RID, // since an item with matching RID but empty GID exists { ++uidnext; pimItem.setGid(QStringLiteral("GID-23")); auto cmd = createCommand(pimItem, datetime, {}); cmd->setMergeModes(Protocol::CreateItemCommand::GID); scenarios << TestScenario::create(6, TestScenario::ClientCmd, cmd); auto rsp = createResponse(uidnext, pimItem, datetime, {}); scenarios << TestScenario::create(6, TestScenario::ServerCmd, rsp) << TestScenario::create(6, TestScenario::ServerCmd, Protocol::CreateItemResponsePtr::create()); } notifications = { notification, Protocol::ItemChangeNotificationPtr::create(*notification) }; QTest::newRow("merge into empty GID if RID matches") << scenarios << notifications << pimItem << parts << flags << tags << uidnext << datetime << false; } void testItemCreate() { QFETCH(TestScenario::List, scenarios); QFETCH(QVector, notifications); QFETCH(PimItem, pimItem); QFETCH(QVector, parts); QFETCH(QVector, flags); QFETCH(QVector, tags); QFETCH(qint64, uidnext); QFETCH(bool, expectFail); FakeAkonadiServer::instance()->setScenarios(scenarios); FakeAkonadiServer::instance()->runTest(); auto notificationSpy = FakeAkonadiServer::instance()->notificationSpy(); QCOMPARE(notificationSpy->count(), notifications.count()); for (int i = 0; i < notifications.count(); ++i) { const auto incomingNtfs = notificationSpy->at(i).first().value(); QCOMPARE(incomingNtfs.count(), 1); const auto itemNotification = incomingNtfs.at(0).staticCast(); QVERIFY(AkTest::compareNotifications(itemNotification, notifications.at(i), QFlag(AkTest::NtfAll & ~ AkTest::NtfEntities))); QCOMPARE(itemNotification->items().count(), notifications.at(i)->items().count()); } const PimItem actualItem = PimItem::retrieveById(uidnext); if (expectFail) { QVERIFY(!actualItem.isValid()); } else { QVERIFY(actualItem.isValid()); QCOMPARE(actualItem.remoteId(), pimItem.remoteId()); QCOMPARE(actualItem.remoteRevision(), pimItem.remoteRevision()); QCOMPARE(actualItem.gid(), pimItem.gid()); QCOMPARE(actualItem.size(), pimItem.size()); QCOMPARE(actualItem.datetime(), pimItem.datetime()); QCOMPARE(actualItem.collectionId(), pimItem.collectionId()); QCOMPARE(actualItem.mimeTypeId(), pimItem.mimeTypeId()); - const QList actualFlags = actualItem.flags().toList(); + const auto actualFlags = actualItem.flags() | toQList; QCOMPARE(actualFlags.count(), flags.count()); Q_FOREACH (const Flag &flag, flags) { const QList::const_iterator actualFlagIter = std::find_if(actualFlags.constBegin(), actualFlags.constEnd(), [flag](Flag const & actualFlag) { return flag.name() == actualFlag.name(); }); QVERIFY(actualFlagIter != actualFlags.constEnd()); const Flag actualFlag = *actualFlagIter; QVERIFY(actualFlag.isValid()); } - const QList actualTags = actualItem.tags().toList(); + const auto actualTags = actualItem.tags() | toQList; QCOMPARE(actualTags.count(), tags.count()); Q_FOREACH (const FakeTag &tag, tags) { const QList::const_iterator actualTagIter = std::find_if(actualTags.constBegin(), actualTags.constEnd(), [tag](Tag const & actualTag) { return tag.gid() == actualTag.gid(); }); QVERIFY(actualTagIter != actualTags.constEnd()); const Tag actualTag = *actualTagIter; QVERIFY(actualTag.isValid()); QCOMPARE(actualTag.tagType().name(), tag.tagType().name()); QCOMPARE(actualTag.gid(), tag.gid()); if (!tag.remoteId().isEmpty()) { SelectQueryBuilder qb; qb.addValueCondition(TagRemoteIdResourceRelation::resourceIdFullColumnName(), Query::Equals, QLatin1String("akonadi_fake_resource_0")); qb.addValueCondition(TagRemoteIdResourceRelation::tagIdColumn(), Query::Equals, actualTag.id()); QVERIFY(qb.exec()); QCOMPARE(qb.result().size(), 1); QCOMPARE(qb.result()[0].remoteId(), tag.remoteId()); } } - const QList actualParts = actualItem.parts().toList(); + const auto actualParts = actualItem.parts() | toQList; QCOMPARE(actualParts.count(), parts.count()); Q_FOREACH (const FakePart &part, parts) { const QList::const_iterator actualPartIter = std::find_if(actualParts.constBegin(), actualParts.constEnd(), [part](Part const & actualPart) { return part.partType().ns() == actualPart.partType().ns() && part.partType().name() == actualPart.partType().name(); }); QVERIFY(actualPartIter != actualParts.constEnd()); const Part actualPart = *actualPartIter; QVERIFY(actualPart.isValid()); QCOMPARE(QString::fromUtf8(actualPart.data()), QString::fromUtf8(part.data())); QCOMPARE(actualPart.data(), part.data()); QCOMPARE(actualPart.datasize(), part.datasize()); QCOMPARE(actualPart.storage(), part.storage()); } } } }; AKTEST_FAKESERVER_MAIN(ItemCreateHandlerTest) #include "itemcreatehandlertest.moc" diff --git a/src/core/monitor.cpp b/src/core/monitor.cpp index 11eefe0f4..e4804dccf 100644 --- a/src/core/monitor.cpp +++ b/src/core/monitor.cpp @@ -1,384 +1,386 @@ /* Copyright (c) 2006 - 2007 Volker Krause 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 "monitor.h" #include "monitor_p.h" #include "changemediator_p.h" #include "collectionfetchscope.h" #include "itemfetchjob.h" #include "session.h" +#include + #include using namespace Akonadi; Monitor::Monitor(QObject *parent) : QObject(parent) , d_ptr(new MonitorPrivate(nullptr, this)) { d_ptr->init(); d_ptr->connectToNotificationManager(); ChangeMediator::registerMonitor(this); } //@cond PRIVATE Monitor::Monitor(MonitorPrivate *d, QObject *parent) : QObject(parent) , d_ptr(d) { d_ptr->init(); d_ptr->connectToNotificationManager(); ChangeMediator::registerMonitor(this); } //@endcond Monitor::~Monitor() { ChangeMediator::unregisterMonitor(this); delete d_ptr; } void Monitor::setCollectionMonitored(const Collection &collection, bool monitored) { Q_D(Monitor); if (!d->collections.contains(collection) && monitored) { d->collections << collection; d->pendingModification.startMonitoringCollection(collection.id()); d->scheduleSubscriptionUpdate(); } else if (!monitored) { if (d->collections.removeAll(collection)) { d->pendingModification.stopMonitoringCollection(collection.id()); d->scheduleSubscriptionUpdate(); } } emit collectionMonitored(collection, monitored); } void Monitor::setItemMonitored(const Item &item, bool monitored) { Q_D(Monitor); if (!d->items.contains(item.id()) && monitored) { d->items.insert(item.id()); d->pendingModification.startMonitoringItem(item.id()); d->scheduleSubscriptionUpdate(); } else if (!monitored) { if (d->items.remove(item.id())) { d->pendingModification.stopMonitoringItem(item.id()); d->scheduleSubscriptionUpdate(); } } emit itemMonitored(item, monitored); } void Monitor::setResourceMonitored(const QByteArray &resource, bool monitored) { Q_D(Monitor); if (!d->resources.contains(resource) && monitored) { d->resources.insert(resource); d->pendingModification.startMonitoringResource(resource); d->scheduleSubscriptionUpdate(); } else if (!monitored) { if (d->resources.remove(resource)) { d->pendingModification.stopMonitoringResource(resource); d->scheduleSubscriptionUpdate(); } } emit resourceMonitored(resource, monitored); } void Monitor::setMimeTypeMonitored(const QString &mimetype, bool monitored) { Q_D(Monitor); if (!d->mimetypes.contains(mimetype) && monitored) { d->mimetypes.insert(mimetype); d->pendingModification.startMonitoringMimeType(mimetype); d->scheduleSubscriptionUpdate(); } else if (!monitored) { if (d->mimetypes.remove(mimetype)) { d->pendingModification.stopMonitoringMimeType(mimetype); d->scheduleSubscriptionUpdate(); } } emit mimeTypeMonitored(mimetype, monitored); } void Monitor::setTagMonitored(const Akonadi::Tag &tag, bool monitored) { Q_D(Monitor); if (!d->tags.contains(tag.id()) && monitored) { d->tags.insert(tag.id()); d->pendingModification.startMonitoringTag(tag.id()); d->scheduleSubscriptionUpdate(); } else if (!monitored) { if (d->tags.remove(tag.id())) { d->pendingModification.stopMonitoringTag(tag.id()); d->scheduleSubscriptionUpdate(); } } emit tagMonitored(tag, monitored); } void Monitor::setTypeMonitored(Monitor::Type type, bool monitored) { Q_D(Monitor); if (!d->types.contains(type) && monitored) { d->types.insert(type); d->pendingModification.startMonitoringType(MonitorPrivate::monitorTypeToProtocol(type)); d->scheduleSubscriptionUpdate(); } else if (!monitored) { if (d->types.remove(type)) { d->pendingModification.stopMonitoringType(MonitorPrivate::monitorTypeToProtocol(type)); d->scheduleSubscriptionUpdate(); } } emit typeMonitored(type, monitored); } void Akonadi::Monitor::setAllMonitored(bool monitored) { Q_D(Monitor); if (d->monitorAll == monitored) { return; } d->monitorAll = monitored; d->pendingModification.setAllMonitored(monitored); d->scheduleSubscriptionUpdate(); emit allMonitored(monitored); } void Monitor::setExclusive(bool exclusive) { Q_D(Monitor); d->exclusive = exclusive; d->pendingModification.setIsExclusive(exclusive); d->scheduleSubscriptionUpdate(); } bool Monitor::exclusive() const { Q_D(const Monitor); return d->exclusive; } void Monitor::ignoreSession(Session *session) { Q_D(Monitor); if (!d->sessions.contains(session->sessionId())) { d->sessions << session->sessionId(); connect(session, SIGNAL(destroyed(QObject*)), this, SLOT(slotSessionDestroyed(QObject*))); d->pendingModification.startIgnoringSession(session->sessionId()); d->scheduleSubscriptionUpdate(); } } void Monitor::fetchCollection(bool enable) { Q_D(Monitor); d->fetchCollection = enable; } void Monitor::fetchCollectionStatistics(bool enable) { Q_D(Monitor); d->fetchCollectionStatistics = enable; } void Monitor::setItemFetchScope(const ItemFetchScope &fetchScope) { Q_D(Monitor); d->mItemFetchScope = fetchScope; d->pendingModificationChanges |= Protocol::ModifySubscriptionCommand::ItemFetchScope; d->scheduleSubscriptionUpdate(); } ItemFetchScope &Monitor::itemFetchScope() { Q_D(Monitor); d->pendingModificationChanges |= Protocol::ModifySubscriptionCommand::ItemFetchScope; d->scheduleSubscriptionUpdate(); return d->mItemFetchScope; } void Monitor::fetchChangedOnly(bool enable) { Q_D(Monitor); d->mFetchChangedOnly = enable; } void Monitor::setCollectionFetchScope(const CollectionFetchScope &fetchScope) { Q_D(Monitor); d->mCollectionFetchScope = fetchScope; d->pendingModificationChanges |= Protocol::ModifySubscriptionCommand::CollectionFetchScope; d->scheduleSubscriptionUpdate(); } CollectionFetchScope &Monitor::collectionFetchScope() { Q_D(Monitor); d->pendingModificationChanges |= Protocol::ModifySubscriptionCommand::CollectionFetchScope; d->scheduleSubscriptionUpdate(); return d->mCollectionFetchScope; } void Monitor::setTagFetchScope(const TagFetchScope &fetchScope) { Q_D(Monitor); d->mTagFetchScope = fetchScope; d->pendingModificationChanges |= Protocol::ModifySubscriptionCommand::TagFetchScope; d->scheduleSubscriptionUpdate(); } TagFetchScope &Monitor::tagFetchScope() { Q_D(Monitor); d->pendingModificationChanges |= Protocol::ModifySubscriptionCommand::TagFetchScope; d->scheduleSubscriptionUpdate(); return d->mTagFetchScope; } Akonadi::Collection::List Monitor::collectionsMonitored() const { Q_D(const Monitor); return d->collections; } QVector Monitor::itemsMonitoredEx() const { Q_D(const Monitor); QVector result; result.reserve(d->items.size()); std::copy(d->items.begin(), d->items.end(), std::back_inserter(result)); return result; } int Monitor::numItemsMonitored() const { Q_D(const Monitor); return d->items.size(); } QVector Monitor::tagsMonitored() const { Q_D(const Monitor); QVector result; result.reserve(d->tags.size()); std::copy(d->tags.begin(), d->tags.end(), std::back_inserter(result)); return result; } QVector Monitor::typesMonitored() const { Q_D(const Monitor); QVector result; result.reserve(d->types.size()); std::copy(d->types.begin(), d->types.end(), std::back_inserter(result)); return result; } QStringList Monitor::mimeTypesMonitored() const { Q_D(const Monitor); - return d->mimetypes.toList(); + return d->mimetypes | toQList; } int Monitor::numMimeTypesMonitored() const { Q_D(const Monitor); return d->mimetypes.count(); } QList Monitor::resourcesMonitored() const { Q_D(const Monitor); - return d->resources.toList(); + return d->resources | toQList; } int Monitor::numResourcesMonitored() const { Q_D(const Monitor); return d->resources.count(); } bool Monitor::isAllMonitored() const { Q_D(const Monitor); return d->monitorAll; } void Monitor::setSession(Akonadi::Session *session) { Q_D(Monitor); if (session == d->session) { return; } if (!session) { d->session = Session::defaultSession(); } else { d->session = session; } d->itemCache->setSession(d->session); d->collectionCache->setSession(d->session); d->tagCache->setSession(d->session); // Reconnect with a new session d->connectToNotificationManager(); } Session *Monitor::session() const { Q_D(const Monitor); return d->session; } void Monitor::setCollectionMoveTranslationEnabled(bool enabled) { Q_D(Monitor); d->collectionMoveTranslationEnabled = enabled; } void Monitor::connectNotify(const QMetaMethod &signal) { Q_D(Monitor); d->updateListeners(signal, MonitorPrivate::AddListener); } void Monitor::disconnectNotify(const QMetaMethod &signal) { Q_D(Monitor); d->updateListeners(signal, MonitorPrivate::RemoveListener); } #include "moc_monitor.cpp" diff --git a/src/core/monitor_p.cpp b/src/core/monitor_p.cpp index f6c2e4b7b..4856174d8 100644 --- a/src/core/monitor_p.cpp +++ b/src/core/monitor_p.cpp @@ -1,1402 +1,1404 @@ /* Copyright (c) 2007 Tobias Koenig 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. */ // @cond PRIVATE #include "monitor_p.h" #include "collectionfetchjob.h" #include "collectionstatistics.h" #include "itemfetchjob.h" #include "notificationmanagerinterface.h" #include "session.h" #include "changemediator_p.h" #include "vectorhelper.h" #include "akonadicore_debug.h" #include "notificationsubscriber.h" #include "changenotification.h" #include "protocolhelper_p.h" +#include + #include using namespace Akonadi; class operation; static const int PipelineSize = 5; MonitorPrivate::MonitorPrivate(ChangeNotificationDependenciesFactory *dependenciesFactory_, Monitor *parent) : q_ptr(parent) , dependenciesFactory(dependenciesFactory_ ? dependenciesFactory_ : new ChangeNotificationDependenciesFactory) , ntfConnection(nullptr) , monitorAll(false) , exclusive(false) , mFetchChangedOnly(false) , session(Session::defaultSession()) , collectionCache(nullptr) , itemCache(nullptr) , tagCache(nullptr) , mCommandBuffer(parent, "handleCommands") , pendingModificationChanges(Protocol::ModifySubscriptionCommand::None) , pendingModificationTimer(nullptr) , monitorReady(false) , fetchCollection(false) , fetchCollectionStatistics(false) , collectionMoveTranslationEnabled(true) , useRefCounting(false) { } MonitorPrivate::~MonitorPrivate() { disconnectFromNotificationManager(); delete dependenciesFactory; delete collectionCache; delete itemCache; delete tagCache; } void MonitorPrivate::init() { // needs to be at least 3x pipeline size for the collection move case collectionCache = dependenciesFactory->createCollectionCache(3 * PipelineSize, session); // needs to be at least 1x pipeline size itemCache = dependenciesFactory->createItemListCache(PipelineSize, session); // 20 tags looks like a reasonable amount to keep around tagCache = dependenciesFactory->createTagListCache(20, session); QObject::connect(collectionCache, SIGNAL(dataAvailable()), q_ptr, SLOT(dataAvailable())); QObject::connect(itemCache, SIGNAL(dataAvailable()), q_ptr, SLOT(dataAvailable())); QObject::connect(tagCache, SIGNAL(dataAvailable()), q_ptr, SLOT(dataAvailable())); QObject::connect(ServerManager::self(), SIGNAL(stateChanged(Akonadi::ServerManager::State)), q_ptr, SLOT(serverStateChanged(Akonadi::ServerManager::State))); statisticsCompressionTimer.setSingleShot(true); statisticsCompressionTimer.setInterval(500); QObject::connect(&statisticsCompressionTimer, SIGNAL(timeout()), q_ptr, SLOT(slotFlushRecentlyChangedCollections())); } bool MonitorPrivate::connectToNotificationManager() { if (ntfConnection) { ntfConnection->deleteLater(); ntfConnection = nullptr; } if (!session) { return false; } ntfConnection = dependenciesFactory->createNotificationConnection(session, &mCommandBuffer); if (!ntfConnection) { return false; } slotUpdateSubscription(); ntfConnection->reconnect(); return true; } void MonitorPrivate::disconnectFromNotificationManager() { if (ntfConnection) { ntfConnection->disconnect(q_ptr); dependenciesFactory->destroyNotificationConnection(session, ntfConnection.data()); } } void MonitorPrivate::serverStateChanged(ServerManager::State state) { if (state == ServerManager::Running) { connectToNotificationManager(); } } void MonitorPrivate::invalidateCollectionCache(qint64 id) { collectionCache->update(id, mCollectionFetchScope); } void MonitorPrivate::invalidateItemCache(qint64 id) { itemCache->update({ id }, mItemFetchScope); // Also invalidate content of all any pending notification for given item for (auto it = pendingNotifications.begin(), end = pendingNotifications.end(); it != end; ++it) { if ((*it)->type() == Protocol::Command::ItemChangeNotification) { auto &ntf = Protocol::cmdCast(*it); const auto items = ntf.items(); if (std::any_of(items.cbegin(), items.cend(), [id](const Protocol::FetchItemsResponse &r) { return r.id() == id; })) { ntf.setMustRetrieve(true); } } } } void MonitorPrivate::invalidateTagCache(qint64 id) { tagCache->update({ id }, mTagFetchScope); } int MonitorPrivate::pipelineSize() const { return PipelineSize; } void MonitorPrivate::scheduleSubscriptionUpdate() { if (pendingModificationTimer || !monitorReady) { return; } pendingModificationTimer = new QTimer(); pendingModificationTimer->setSingleShot(true); pendingModificationTimer->setInterval(0); pendingModificationTimer->start(); q_ptr->connect(pendingModificationTimer, SIGNAL(timeout()), q_ptr, SLOT(slotUpdateSubscription())); } void MonitorPrivate::slotUpdateSubscription() { if (pendingModificationTimer) { pendingModificationTimer->stop(); std::exchange(pendingModificationTimer, nullptr)->deleteLater(); } if (pendingModificationChanges & Protocol::ModifySubscriptionCommand::ItemFetchScope) { pendingModification.setItemFetchScope(ProtocolHelper::itemFetchScopeToProtocol(mItemFetchScope)); } if (pendingModificationChanges & Protocol::ModifySubscriptionCommand::CollectionFetchScope) { pendingModification.setCollectionFetchScope(ProtocolHelper::collectionFetchScopeToProtocol(mCollectionFetchScope)); } if (pendingModificationChanges & Protocol::ModifySubscriptionCommand::TagFetchScope) { pendingModification.setTagFetchScope(ProtocolHelper::tagFetchScopeToProtocol(mTagFetchScope)); } pendingModificationChanges = Protocol::ModifySubscriptionCommand::None; if (ntfConnection) { ntfConnection->sendCommand(3, Protocol::ModifySubscriptionCommandPtr::create(pendingModification)); pendingModification = Protocol::ModifySubscriptionCommand(); } } bool MonitorPrivate::isLazilyIgnored(const Protocol::ChangeNotificationPtr &msg, bool allowModifyFlagsConversion) const { if (msg->type() == Protocol::Command::CollectionChangeNotification) { // Lazy fetching can only affects items. return false; } if (msg->type() == Protocol::Command::TagChangeNotification) { const auto op = Protocol::cmdCast(msg).operation(); return ((op == Protocol::TagChangeNotification::Add && !hasListeners(&Monitor::tagAdded)) || (op == Protocol::TagChangeNotification::Modify && !hasListeners(&Monitor::tagChanged)) || (op == Protocol::TagChangeNotification::Remove && !hasListeners(&Monitor::tagRemoved))); } if (!fetchCollectionStatistics && msg->type() == Protocol::Command::ItemChangeNotification) { const auto &itemNtf = Protocol::cmdCast(msg); const auto op = itemNtf.operation(); if ((op == Protocol::ItemChangeNotification::Add && !hasListeners(&Monitor::itemAdded)) || (op == Protocol::ItemChangeNotification::Remove && !hasListeners(&Monitor::itemRemoved) && !hasListeners(&Monitor::itemsRemoved)) || (op == Protocol::ItemChangeNotification::Modify && !hasListeners(&Monitor::itemChanged)) || (op == Protocol::ItemChangeNotification::ModifyFlags && !hasListeners(&Monitor::itemsFlagsChanged) // Newly delivered ModifyFlags notifications will be converted to // itemChanged(item, "FLAGS") for legacy clients. && (!allowModifyFlagsConversion || !hasListeners(&Monitor::itemChanged))) || (op == Protocol::ItemChangeNotification::ModifyTags && !hasListeners(&Monitor::itemsTagsChanged)) || (op == Protocol::ItemChangeNotification::Move && !hasListeners(&Monitor::itemMoved) && !hasListeners(&Monitor::itemsMoved)) || (op == Protocol::ItemChangeNotification::Link && !hasListeners(&Monitor::itemLinked) && !hasListeners(&Monitor::itemsLinked)) || (op == Protocol::ItemChangeNotification::Unlink && !hasListeners(&Monitor::itemUnlinked) && !hasListeners(&Monitor::itemsUnlinked))) { return true; } if (!useRefCounting) { return false; } const Collection::Id parentCollectionId = itemNtf.parentCollection(); if ((op == Protocol::ItemChangeNotification::Add) || (op == Protocol::ItemChangeNotification::Remove) || (op == Protocol::ItemChangeNotification::Modify) || (op == Protocol::ItemChangeNotification::ModifyFlags) || (op == Protocol::ItemChangeNotification::ModifyTags) || (op == Protocol::ItemChangeNotification::Link) || (op == Protocol::ItemChangeNotification::Unlink)) { if (isMonitored(parentCollectionId)) { return false; } } if (op == Protocol::ItemChangeNotification::Move) { if (!isMonitored(parentCollectionId) && !isMonitored(itemNtf.parentDestCollection())) { return true; } // We can't ignore the move. It must be transformed later into a removal or insertion. return false; } return true; } return false; } void MonitorPrivate::checkBatchSupport(const Protocol::ChangeNotificationPtr &msg, bool &needsSplit, bool &batchSupported) const { if (msg->type() != Protocol::Command::ItemChangeNotification) { needsSplit = false; batchSupported = false; return; } const auto &itemNtf = Protocol::cmdCast(msg); const bool isBatch = (itemNtf.items().count() > 1); switch (itemNtf.operation()) { case Protocol::ItemChangeNotification::Add: needsSplit = isBatch; batchSupported = false; return; case Protocol::ItemChangeNotification::Modify: needsSplit = isBatch; batchSupported = false; return; case Protocol::ItemChangeNotification::ModifyFlags: batchSupported = hasListeners(&Monitor::itemsFlagsChanged); needsSplit = isBatch && !batchSupported && hasListeners(&Monitor::itemChanged); return; case Protocol::ItemChangeNotification::ModifyTags: // Tags were added after batch notifications, so they are always supported batchSupported = true; needsSplit = false; return; case Protocol::ItemChangeNotification::ModifyRelations: // Relations were added after batch notifications, so they are always supported batchSupported = true; needsSplit = false; return; case Protocol::ItemChangeNotification::Move: needsSplit = isBatch && hasListeners(&Monitor::itemMoved); batchSupported = hasListeners(&Monitor::itemsMoved); return; case Protocol::ItemChangeNotification::Remove: needsSplit = isBatch && hasListeners(&Monitor::itemRemoved); batchSupported = hasListeners(&Monitor::itemsRemoved); return; case Protocol::ItemChangeNotification::Link: needsSplit = isBatch && hasListeners(&Monitor::itemLinked); batchSupported = hasListeners(&Monitor::itemsLinked); return; case Protocol::ItemChangeNotification::Unlink: needsSplit = isBatch && hasListeners(&Monitor::itemUnlinked); batchSupported = hasListeners(&Monitor::itemsUnlinked); return; default: needsSplit = isBatch; batchSupported = false; qCDebug(AKONADICORE_LOG) << "Unknown operation type" << itemNtf.operation() << "in item change notification"; return; } } Protocol::ChangeNotificationList MonitorPrivate::splitMessage(const Protocol::ItemChangeNotification &msg, bool legacy) const { Protocol::ChangeNotificationList list; Protocol::ItemChangeNotification baseMsg; baseMsg.setSessionId(msg.sessionId()); if (legacy && msg.operation() == Protocol::ItemChangeNotification::ModifyFlags) { baseMsg.setOperation(Protocol::ItemChangeNotification::Modify); baseMsg.setItemParts(QSet() << "FLAGS"); } else { baseMsg.setOperation(msg.operation()); baseMsg.setItemParts(msg.itemParts()); } baseMsg.setParentCollection(msg.parentCollection()); baseMsg.setParentDestCollection(msg.parentDestCollection()); baseMsg.setResource(msg.resource()); baseMsg.setDestinationResource(msg.destinationResource()); baseMsg.setAddedFlags(msg.addedFlags()); baseMsg.setRemovedFlags(msg.removedFlags()); baseMsg.setAddedTags(msg.addedTags()); baseMsg.setRemovedTags(msg.removedTags()); const auto items = msg.items(); list.reserve(items.count()); for (const auto &item : items) { auto copy = Protocol::ItemChangeNotificationPtr::create(baseMsg); copy->setItems({Protocol::FetchItemsResponse(item)}); list.push_back(std::move(copy)); } return list; } bool MonitorPrivate::fetchCollections() const { return fetchCollection; } bool MonitorPrivate::fetchItems() const { return !mItemFetchScope.isEmpty(); } bool MonitorPrivate::ensureDataAvailable(const Protocol::ChangeNotificationPtr &msg) { if (msg->type() == Protocol::Command::TagChangeNotification) { const auto tagMsg = Protocol::cmdCast(msg); if (tagMsg.metadata().contains("FETCH_TAG")) { if (!tagCache->ensureCached({ tagMsg.tag().id() }, mTagFetchScope)) { return false; } } return true; } if (msg->type() == Protocol::Command::RelationChangeNotification) { return true; } if (msg->type() == Protocol::Command::SubscriptionChangeNotification) { return true; } if (msg->type() == Protocol::Command::DebugChangeNotification) { return true; } if (msg->type() == Protocol::Command::CollectionChangeNotification && Protocol::cmdCast(msg).operation() == Protocol::CollectionChangeNotification::Remove) { // For collection removals the collection is gone already, so we can't fetch it, // but we have to at least obtain the ancestor chain. const qint64 parentCollection = Protocol::cmdCast(msg).parentCollection(); return parentCollection <= -1 || collectionCache->ensureCached(parentCollection, mCollectionFetchScope); } bool allCached = true; if (fetchCollections()) { const qint64 parentCollection = (msg->type() == Protocol::Command::ItemChangeNotification) ? Protocol::cmdCast(msg).parentCollection() : (msg->type() == Protocol::Command::CollectionChangeNotification) ? Protocol::cmdCast(msg).parentCollection() : -1; if (parentCollection > -1 && !collectionCache->ensureCached(parentCollection, mCollectionFetchScope)) { allCached = false; } qint64 parentDestCollection = -1; if ((msg->type() == Protocol::Command::ItemChangeNotification) && (Protocol::cmdCast(msg).operation() == Protocol::ItemChangeNotification::Move)) { parentDestCollection = Protocol::cmdCast(msg).parentDestCollection(); } else if ((msg->type() == Protocol::Command::CollectionChangeNotification) && (Protocol::cmdCast(msg).operation() == Protocol::CollectionChangeNotification::Move)) { parentDestCollection = Protocol::cmdCast(msg).parentDestCollection(); } if (parentDestCollection > -1 && !collectionCache->ensureCached(parentDestCollection, mCollectionFetchScope)) { allCached = false; } } if (msg->isRemove()) { return allCached; } if (msg->type() == Protocol::Command::ItemChangeNotification && fetchItems()) { const auto &itemNtf = Protocol::cmdCast(msg); if (mFetchChangedOnly && (itemNtf.operation() == Protocol::ItemChangeNotification::Modify || itemNtf.operation() == Protocol::ItemChangeNotification::ModifyFlags)) { const auto changedParts = itemNtf.itemParts(); const auto requestedParts = mItemFetchScope.payloadParts(); const auto requestedAttrs = mItemFetchScope.attributes(); QSet missingParts, missingAttributes; for (const QByteArray &part : changedParts) { const auto partName = part.mid(4); if (part.startsWith("PLD:") && //krazy:exclude=strings since QByteArray (!mItemFetchScope.fullPayload() || !requestedParts.contains(partName))) { missingParts.insert(partName); } else if (part.startsWith("ATR:") && //krazy:exclude=strings since QByteArray (!mItemFetchScope.allAttributes() || !requestedAttrs.contains(partName))) { missingAttributes.insert(partName); } } if (!missingParts.isEmpty() || !missingAttributes.isEmpty()) { ItemFetchScope scope(mItemFetchScope); scope.fetchFullPayload(false); for (const auto &part : requestedParts) { scope.fetchPayloadPart(part, false); } for (const auto &attr : requestedAttrs) { scope.fetchAttribute(attr, false); } for (const auto &part : missingParts) { scope.fetchPayloadPart(part, true); } for (const auto &attr : missingAttributes) { scope.fetchAttribute(attr, true); } if (!itemCache->ensureCached(Protocol::ChangeNotification::itemsToUids(itemNtf.items()), scope)) { return false; } } return allCached; } // Make sure all tags for ModifyTags operation are in cache too if (itemNtf.operation() == Protocol::ItemChangeNotification::ModifyTags) { - if (!tagCache->ensureCached((itemNtf.addedTags() + itemNtf.removedTags()).toList(), mTagFetchScope)) { + if (!tagCache->ensureCached((itemNtf.addedTags() + itemNtf.removedTags()) | toQList, mTagFetchScope)) { return false; } } if (itemNtf.metadata().contains("FETCH_ITEM") || itemNtf.mustRetrieve()) { if (!itemCache->ensureCached(Protocol::ChangeNotification::itemsToUids(itemNtf.items()), mItemFetchScope)) { return false; } } return allCached; } else if (msg->type() == Protocol::Command::CollectionChangeNotification && fetchCollections()) { const auto &colMsg = Protocol::cmdCast(msg); if (colMsg.metadata().contains("FETCH_COLLECTION")) { if (!collectionCache->ensureCached(colMsg.collection().id(), mCollectionFetchScope)) { return false; } } return allCached; } return allCached; } bool MonitorPrivate::emitNotification(const Protocol::ChangeNotificationPtr &msg) { bool someoneWasListening = false; if (msg->type() == Protocol::Command::TagChangeNotification) { const auto &tagNtf = Protocol::cmdCast(msg); const bool fetched = tagNtf.metadata().contains("FETCH_TAG"); Tag tag; if (fetched) { const auto tags = tagCache->retrieve({ tagNtf.tag().id() }); tag = tags.isEmpty() ? Tag() : tags.at(0); } else { tag = ProtocolHelper::parseTag(tagNtf.tag()); } someoneWasListening = emitTagNotification(tagNtf, tag); } else if (msg->type() == Protocol::Command::RelationChangeNotification) { const auto &relNtf = Protocol::cmdCast(msg); const Relation rel = ProtocolHelper::parseRelationFetchResult(relNtf.relation()); someoneWasListening = emitRelationNotification(relNtf, rel); } else if (msg->type() == Protocol::Command::CollectionChangeNotification) { const auto &colNtf = Protocol::cmdCast(msg); const Collection parent = collectionCache->retrieve(colNtf.parentCollection()); Collection destParent; if (colNtf.operation() == Protocol::CollectionChangeNotification::Move) { destParent = collectionCache->retrieve(colNtf.parentDestCollection()); } const bool fetched = colNtf.metadata().contains("FETCH_COLLECTION"); //For removals this will retrieve an invalid collection. We'll deal with that in emitCollectionNotification const Collection col = fetched ? collectionCache->retrieve(colNtf.collection().id()) : ProtocolHelper::parseCollection(colNtf.collection(), true); //It is possible that the retrieval fails also in the non-removal case (e.g. because the item was meanwhile removed while //the changerecorder stored the notification or the notification was in the queue). In order to drop such invalid notifications we have to ignore them. if (col.isValid() || colNtf.operation() == Protocol::CollectionChangeNotification::Remove || !fetchCollections()) { someoneWasListening = emitCollectionNotification(colNtf, col, parent, destParent); } } else if (msg->type() == Protocol::Command::ItemChangeNotification) { const auto &itemNtf = Protocol::cmdCast(msg); const Collection parent = collectionCache->retrieve(itemNtf.parentCollection()); Collection destParent; if (itemNtf.operation() == Protocol::ItemChangeNotification::Move) { destParent = collectionCache->retrieve(itemNtf.parentDestCollection()); } const bool fetched = itemNtf.metadata().contains("FETCH_ITEM") || itemNtf.mustRetrieve(); //For removals this will retrieve an empty set. We'll deal with that in emitItemNotification Item::List items; if (fetched && fetchItems()) { items = itemCache->retrieve(Protocol::ChangeNotification::itemsToUids(itemNtf.items())); } else { const auto ntfItems = itemNtf.items(); items.reserve(ntfItems.size()); for (const auto &ntfItem : ntfItems) { items.push_back(ProtocolHelper::parseItemFetchResult(ntfItem, &mItemFetchScope)); } } //It is possible that the retrieval fails also in the non-removal case (e.g. because the item was meanwhile removed while //the changerecorder stored the notification or the notification was in the queue). In order to drop such invalid notifications we have to ignore them. if (!items.isEmpty() || itemNtf.operation() == Protocol::ItemChangeNotification::Remove || !fetchItems()) { someoneWasListening = emitItemsNotification(itemNtf, items, parent, destParent); } } else if (msg->type() == Protocol::Command::SubscriptionChangeNotification) { const auto &subNtf = Protocol::cmdCast(msg); NotificationSubscriber subscriber; subscriber.setSubscriber(subNtf.subscriber()); subscriber.setSessionId(subNtf.sessionId()); subscriber.setMonitoredCollections(subNtf.collections()); subscriber.setMonitoredItems(subNtf.items()); subscriber.setMonitoredTags(subNtf.tags()); QSet monitorTypes; Q_FOREACH (auto type, subNtf.types()) { if (type == Protocol::ModifySubscriptionCommand::NoType) { continue; } monitorTypes.insert([](Protocol::ModifySubscriptionCommand::ChangeType type) { switch (type) { case Protocol::ModifySubscriptionCommand::ItemChanges: return Monitor::Items; case Protocol::ModifySubscriptionCommand::CollectionChanges: return Monitor::Collections; case Protocol::ModifySubscriptionCommand::TagChanges: return Monitor::Tags; case Protocol::ModifySubscriptionCommand::RelationChanges: return Monitor::Relations; case Protocol::ModifySubscriptionCommand::SubscriptionChanges: return Monitor::Subscribers; case Protocol::ModifySubscriptionCommand::ChangeNotifications: return Monitor::Notifications; default: Q_ASSERT(false); return Monitor::Items; //unreachable } }(type)); } subscriber.setMonitoredTypes(monitorTypes); subscriber.setMonitoredMimeTypes(subNtf.mimeTypes()); subscriber.setMonitoredResources(subNtf.resources()); subscriber.setIgnoredSessions(subNtf.ignoredSessions()); subscriber.setIsAllMonitored(subNtf.allMonitored()); subscriber.setIsExclusive(subNtf.exclusive()); subscriber.setItemFetchScope(ProtocolHelper::parseItemFetchScope(subNtf.itemFetchScope())); subscriber.setCollectionFetchScope(ProtocolHelper::parseCollectionFetchScope(subNtf.collectionFetchScope())); someoneWasListening = emitSubscriptionChangeNotification(subNtf, subscriber); } else if (msg->type() == Protocol::Command::DebugChangeNotification) { const auto &changeNtf = Protocol::cmdCast(msg); ChangeNotification notification; notification.setListeners(changeNtf.listeners()); notification.setTimestamp(QDateTime::fromMSecsSinceEpoch(changeNtf.timestamp())); notification.setNotification(changeNtf.notification()); switch (changeNtf.notification()->type()) { case Protocol::Command::ItemChangeNotification: notification.setType(ChangeNotification::Items); break; case Protocol::Command::CollectionChangeNotification: notification.setType(ChangeNotification::Collection); break; case Protocol::Command::TagChangeNotification: notification.setType(ChangeNotification::Tag); break; case Protocol::Command::RelationChangeNotification: notification.setType(ChangeNotification::Relation); break; case Protocol::Command::SubscriptionChangeNotification: notification.setType(ChangeNotification::Subscription); break; default: Q_ASSERT(false); // huh? return false; } someoneWasListening = emitDebugChangeNotification(changeNtf, notification); } return someoneWasListening; } void MonitorPrivate::updatePendingStatistics(const Protocol::ChangeNotificationPtr &msg) { if (msg->type() == Protocol::Command::ItemChangeNotification) { const auto &itemNtf = Protocol::cmdCast(msg); notifyCollectionStatisticsWatchers(itemNtf.parentCollection(), itemNtf.resource()); // FIXME use the proper resource of the target collection, for cross resource moves notifyCollectionStatisticsWatchers(itemNtf.parentDestCollection(), itemNtf.destinationResource()); } else if (msg->type() == Protocol::Command::CollectionChangeNotification) { const auto &colNtf = Protocol::cmdCast(msg); if (colNtf.operation() == Protocol::CollectionChangeNotification::Remove) { // no need for statistics updates anymore recentlyChangedCollections.remove(colNtf.collection().id()); } } } void MonitorPrivate::slotSessionDestroyed(QObject *object) { Session *objectSession = qobject_cast(object); if (objectSession) { sessions.removeAll(objectSession->sessionId()); pendingModification.stopIgnoringSession(objectSession->sessionId()); scheduleSubscriptionUpdate(); } } void MonitorPrivate::slotStatisticsChangedFinished(KJob *job) { if (job->error()) { qCWarning(AKONADICORE_LOG) << "Error on fetching collection statistics: " << job->errorText(); } else { CollectionStatisticsJob *statisticsJob = static_cast(job); Q_ASSERT(statisticsJob->collection().isValid()); emit q_ptr->collectionStatisticsChanged(statisticsJob->collection().id(), statisticsJob->statistics()); } } void MonitorPrivate::slotFlushRecentlyChangedCollections() { for (Collection::Id collection : qAsConst(recentlyChangedCollections)) { Q_ASSERT(collection >= 0); if (fetchCollectionStatistics) { fetchStatistics(collection); } else { static const CollectionStatistics dummyStatistics; emit q_ptr->collectionStatisticsChanged(collection, dummyStatistics); } } recentlyChangedCollections.clear(); } int MonitorPrivate::translateAndCompress(QQueue ¬ificationQueue, const Protocol::ChangeNotificationPtr &msg) { // Always handle tags and relations if (msg->type() == Protocol::Command::TagChangeNotification || msg->type() == Protocol::Command::RelationChangeNotification) { notificationQueue.enqueue(msg); return 1; } // We have to split moves into insert or remove if the source or destination // is not monitored. if (!msg->isMove()) { notificationQueue.enqueue(msg); return 1; } bool sourceWatched = false; bool destWatched = false; if (msg->type() == Protocol::Command::ItemChangeNotification) { const auto &itemNtf = Protocol::cmdCast(msg); if (useRefCounting) { sourceWatched = isMonitored(itemNtf.parentCollection()); destWatched = isMonitored(itemNtf.parentDestCollection()); } else { if (!resources.isEmpty()) { sourceWatched = resources.contains(itemNtf.resource()); destWatched = isMoveDestinationResourceMonitored(itemNtf); } if (!sourceWatched) { sourceWatched = isCollectionMonitored(itemNtf.parentCollection()); } if (!destWatched) { destWatched = isCollectionMonitored(itemNtf.parentDestCollection()); } } } else if (msg->type() == Protocol::Command::CollectionChangeNotification) { const auto &colNtf = Protocol::cmdCast(msg); if (!resources.isEmpty()) { sourceWatched = resources.contains(colNtf.resource()); destWatched = isMoveDestinationResourceMonitored(colNtf); } if (!sourceWatched) { sourceWatched = isCollectionMonitored(colNtf.parentCollection()); } if (!destWatched) { destWatched = isCollectionMonitored(colNtf.parentDestCollection()); } } else { Q_ASSERT(false); return 0; } if (!sourceWatched && !destWatched) { return 0; } if ((sourceWatched && destWatched) || (!collectionMoveTranslationEnabled && msg->type() == Protocol::Command::CollectionChangeNotification)) { notificationQueue.enqueue(msg); return 1; } if (sourceWatched) { if (msg->type() == Protocol::Command::ItemChangeNotification) { auto removalMessage = Protocol::ItemChangeNotificationPtr::create( Protocol::cmdCast(msg)); removalMessage->setOperation(Protocol::ItemChangeNotification::Remove); removalMessage->setParentDestCollection(-1); notificationQueue.enqueue(removalMessage); return 1; } else { auto removalMessage = Protocol::CollectionChangeNotificationPtr::create( Protocol::cmdCast(msg)); removalMessage->setOperation(Protocol::CollectionChangeNotification::Remove); removalMessage->setParentDestCollection(-1); notificationQueue.enqueue(removalMessage); return 1; } } // Transform into an insertion if (msg->type() == Protocol::Command::ItemChangeNotification) { auto insertionMessage = Protocol::ItemChangeNotificationPtr::create( Protocol::cmdCast(msg)); insertionMessage->setOperation(Protocol::ItemChangeNotification::Add); insertionMessage->setParentCollection(insertionMessage->parentDestCollection()); insertionMessage->setParentDestCollection(-1); // We don't support batch insertion, so we have to do it one by one const auto split = splitMessage(*insertionMessage, false); for (const Protocol::ChangeNotificationPtr &insertion : split) { notificationQueue.enqueue(insertion); } return split.count(); } else if (msg->type() == Protocol::Command::CollectionChangeNotification) { auto insertionMessage = Protocol::CollectionChangeNotificationPtr::create( Protocol::cmdCast(msg)); insertionMessage->setOperation(Protocol::CollectionChangeNotification::Add); insertionMessage->setParentCollection(insertionMessage->parentDestCollection()); insertionMessage->setParentDestCollection(-1); notificationQueue.enqueue(insertionMessage); return 1; } Q_ASSERT(false); return 0; } void MonitorPrivate::handleCommands() { Q_Q(Monitor); CommandBufferLocker lock(&mCommandBuffer); CommandBufferNotifyBlocker notify(&mCommandBuffer); while (!mCommandBuffer.isEmpty()) { const auto cmd = mCommandBuffer.dequeue(); lock.unlock(); const auto command = cmd.command; if (command->isResponse()) { switch (command->type()) { case Protocol::Command::Hello: { qCDebug(AKONADICORE_LOG) << q_ptr << "Connected to notification bus"; QByteArray subname; if (!q->objectName().isEmpty()) { subname = q->objectName().toLatin1(); } else { subname = session->sessionId(); } subname += " - " + QByteArray::number(quintptr(q)); qCDebug(AKONADICORE_LOG) << q_ptr << "Subscribing as \"" << subname << "\""; auto subCmd = Protocol::CreateSubscriptionCommandPtr::create(subname, session->sessionId()); ntfConnection->sendCommand(2, subCmd); break; } case Protocol::Command::CreateSubscription: { auto msubCmd = Protocol::ModifySubscriptionCommandPtr::create(); for (const auto &col : qAsConst(collections)) { msubCmd->startMonitoringCollection(col.id()); } for (const auto &res : qAsConst(resources)) { msubCmd->startMonitoringResource(res); } for (auto itemId : qAsConst(items)) { msubCmd->startMonitoringItem(itemId); } for (auto tagId : qAsConst(tags)) { msubCmd->startMonitoringTag(tagId); } for (auto type : qAsConst(types)) { msubCmd->startMonitoringType(monitorTypeToProtocol(type)); } for (const auto &mimetype : qAsConst(mimetypes)) { msubCmd->startMonitoringMimeType(mimetype); } for (const auto &session : qAsConst(sessions)) { msubCmd->startIgnoringSession(session); } msubCmd->setAllMonitored(monitorAll); msubCmd->setIsExclusive(exclusive); msubCmd->setItemFetchScope(ProtocolHelper::itemFetchScopeToProtocol(mItemFetchScope)); msubCmd->setCollectionFetchScope(ProtocolHelper::collectionFetchScopeToProtocol(mCollectionFetchScope)); msubCmd->setTagFetchScope(ProtocolHelper::tagFetchScopeToProtocol(mTagFetchScope)); pendingModification = Protocol::ModifySubscriptionCommand(); ntfConnection->sendCommand(3, msubCmd); break; } case Protocol::Command::ModifySubscription: // TODO: Handle errors if (!monitorReady) { monitorReady = true; Q_EMIT q_ptr->monitorReady(); } break; default: qCWarning(AKONADICORE_LOG) << "Received an unexpected response on Notification stream: " << Protocol::debugString(command); break; } } else { switch (command->type()) { case Protocol::Command::ItemChangeNotification: case Protocol::Command::CollectionChangeNotification: case Protocol::Command::TagChangeNotification: case Protocol::Command::RelationChangeNotification: case Protocol::Command::SubscriptionChangeNotification: case Protocol::Command::DebugChangeNotification: slotNotify(command.staticCast()); break; default: qCWarning(AKONADICORE_LOG) << "Received an unexpected message on Notification stream:" << Protocol::debugString(command); break; } } lock.relock(); } notify.unblock(); lock.unlock(); } /* server notification --> ?accepted --> pendingNotifications --> ?dataAvailable --> emit | | x --> discard x --> pipeline fetchJobDone --> pipeline ?dataAvailable --> emit */ void MonitorPrivate::slotNotify(const Protocol::ChangeNotificationPtr &msg) { int appendedMessages = 0; int modifiedMessages = 0; int erasedMessages = 0; invalidateCaches(msg); updatePendingStatistics(msg); bool needsSplit = true; bool supportsBatch = false; if (isLazilyIgnored(msg, true)) { return; } checkBatchSupport(msg, needsSplit, supportsBatch); const bool isModifyFlags = (msg->type() == Protocol::Command::ItemChangeNotification && Protocol::cmdCast(msg).operation() == Protocol::ItemChangeNotification::ModifyFlags); if (supportsBatch || (!needsSplit && !supportsBatch && !isModifyFlags) || msg->type() == Protocol::Command::CollectionChangeNotification) { // Make sure the batch msg is always queued before the split notifications const int oldSize = pendingNotifications.size(); const int appended = translateAndCompress(pendingNotifications, msg); if (appended > 0) { appendedMessages += appended; } else { ++modifiedMessages; } // translateAndCompress can remove an existing "modify" when msg is a "delete". // Or it can merge two ModifyFlags and return false. // We need to detect such removals, for ChangeRecorder. if (pendingNotifications.count() != oldSize + appended) { ++erasedMessages; // this count isn't exact, but it doesn't matter } } else if (needsSplit) { // If it's not queued at least make sure we fetch all the items from split // notifications in one go. if (msg->type() == Protocol::Command::ItemChangeNotification) { const auto items = Protocol::cmdCast(msg).items(); itemCache->ensureCached(Protocol::ChangeNotification::itemsToUids(items), mItemFetchScope); } } // if the message contains more items, but we need to emit single-item notification, // split the message into one message per item and queue them // if the message contains only one item, but batches are not supported // (and thus neither is flagsModified), splitMessage() will convert the // notification to regular Modify with "FLAGS" part changed if (needsSplit || (!needsSplit && !supportsBatch && isModifyFlags)) { // Make sure inter-resource move notifications are translated into // Add/Remove notifications if (msg->type() == Protocol::Command::ItemChangeNotification) { const auto &itemNtf = Protocol::cmdCast(msg); if (itemNtf.operation() == Protocol::ItemChangeNotification::Move && itemNtf.resource() != itemNtf.destinationResource()) { if (needsSplit) { const Protocol::ChangeNotificationList split = splitMessage(itemNtf, !supportsBatch); for (const auto &splitMsg : split) { appendedMessages += translateAndCompress(pendingNotifications, splitMsg); } } else { appendedMessages += translateAndCompress(pendingNotifications, msg); } } else { const Protocol::ChangeNotificationList split = splitMessage(itemNtf, !supportsBatch); - pendingNotifications << split.toList(); + pendingNotifications << (split | toQList); appendedMessages += split.count(); } } } // tell ChangeRecorder (even if 0 appended, the compression could have made changes to existing messages) if (appendedMessages > 0 || modifiedMessages > 0 || erasedMessages > 0) { if (erasedMessages > 0) { notificationsErased(); } else { notificationsEnqueued(appendedMessages); } } dispatchNotifications(); } void MonitorPrivate::flushPipeline() { while (!pipeline.isEmpty()) { const auto msg = pipeline.head(); if (ensureDataAvailable(msg)) { // dequeue should be before emit, otherwise stuff might happen (like dataAvailable // being called again) and we end up dequeuing an empty pipeline pipeline.dequeue(); emitNotification(msg); } else { break; } } } void MonitorPrivate::dataAvailable() { flushPipeline(); dispatchNotifications(); } void MonitorPrivate::dispatchNotifications() { // Note that this code is not used in a ChangeRecorder (pipelineSize==0) while (pipeline.size() < pipelineSize() && !pendingNotifications.isEmpty()) { const auto msg = pendingNotifications.dequeue(); const bool avail = ensureDataAvailable(msg); if (avail && pipeline.isEmpty()) { emitNotification(msg); } else { pipeline.enqueue(msg); } } } static Relation::List extractRelations(const QSet &rels) { Relation::List relations; if (rels.isEmpty()) { return relations; } relations.reserve(rels.size()); for (const auto &rel : rels) { relations.push_back(Relation(rel.type.toLatin1(), Akonadi::Item(rel.leftId), Akonadi::Item(rel.rightId))); } return relations; } bool MonitorPrivate::emitItemsNotification(const Protocol::ItemChangeNotification &msg_, const Item::List &items, const Collection &collection, const Collection &collectionDest) { Protocol::ItemChangeNotification msg = msg_; Collection col = collection; Collection colDest = collectionDest; if (!col.isValid()) { col = Collection(msg.parentCollection()); col.setResource(QString::fromUtf8(msg.resource())); } if (!colDest.isValid()) { colDest = Collection(msg.parentDestCollection()); // HACK: destination resource is delivered in the parts field... if (!msg.itemParts().isEmpty()) { colDest.setResource(QString::fromLatin1(*(msg.itemParts().cbegin()))); } } const QSet addedFlags = msg.addedFlags(); const QSet removedFlags = msg.removedFlags(); Relation::List addedRelations, removedRelations; if (msg.operation() == Protocol::ItemChangeNotification::ModifyRelations) { addedRelations = extractRelations(msg.addedRelations()); removedRelations = extractRelations(msg.removedRelations()); } Tag::List addedTags, removedTags; if (msg.operation() == Protocol::ItemChangeNotification::ModifyTags) { - addedTags = tagCache->retrieve(msg.addedTags().toList()); - removedTags = tagCache->retrieve(msg.removedTags().toList()); + addedTags = tagCache->retrieve(msg.addedTags() | toQList); + removedTags = tagCache->retrieve(msg.removedTags() | toQList); } Item::List its = items; for (auto it = its.begin(), end = its.end(); it != end; ++it) { if (msg.operation() == Protocol::ItemChangeNotification::Move) { it->setParentCollection(colDest); } else { it->setParentCollection(col); } } bool handled = false; switch (msg.operation()) { case Protocol::ItemChangeNotification::Add: return emitToListeners(&Monitor::itemAdded, its.first(), col); case Protocol::ItemChangeNotification::Modify: return emitToListeners(&Monitor::itemChanged, its.first(), msg.itemParts()); case Protocol::ItemChangeNotification::ModifyFlags: return emitToListeners(&Monitor::itemsFlagsChanged, its, msg.addedFlags(), msg.removedFlags()); case Protocol::ItemChangeNotification::Move: handled |= emitToListeners(&Monitor::itemMoved, its.first(), col, colDest); handled |= emitToListeners(&Monitor::itemsMoved, its, col, colDest); return handled; case Protocol::ItemChangeNotification::Remove: handled |= emitToListeners(&Monitor::itemRemoved, its.first()); handled |= emitToListeners(&Monitor::itemsRemoved, its); return handled; case Protocol::ItemChangeNotification::Link: handled |= emitToListeners(&Monitor::itemLinked, its.first(), col); handled |= emitToListeners(&Monitor::itemsLinked, its, col); return handled; case Protocol::ItemChangeNotification::Unlink: handled |= emitToListeners(&Monitor::itemUnlinked, its.first(), col); handled |= emitToListeners(&Monitor::itemsUnlinked, its, col); return handled; case Protocol::ItemChangeNotification::ModifyTags: - return emitToListeners(&Monitor::itemsTagsChanged, its, Akonadi::vectorToSet(addedTags), Akonadi::vectorToSet(removedTags)); + return emitToListeners(&Monitor::itemsTagsChanged, its, addedTags | toQSet, removedTags | toQSet); case Protocol::ItemChangeNotification::ModifyRelations: return emitToListeners(&Monitor::itemsRelationsChanged, its, addedRelations, removedRelations); default: qCDebug(AKONADICORE_LOG) << "Unknown operation type" << msg.operation() << "in item change notification"; return false; } } bool MonitorPrivate::emitCollectionNotification(const Protocol::CollectionChangeNotification &msg, const Collection &col, const Collection &par, const Collection &dest) { Collection parent = par; if (!parent.isValid()) { parent = Collection(msg.parentCollection()); } Collection destination = dest; if (!destination.isValid()) { destination = Collection(msg.parentDestCollection()); } Collection collection = col; Q_ASSERT(collection.isValid()); if (!collection.isValid()) { qCWarning(AKONADICORE_LOG) << "Failed to get valid Collection for a Collection change!"; return true; // prevent Monitor disconnecting from a signal } if (msg.operation() == Protocol::CollectionChangeNotification::Move) { collection.setParentCollection(destination); } else { collection.setParentCollection(parent); } bool handled = false; switch (msg.operation()) { case Protocol::CollectionChangeNotification::Add: return emitToListeners(&Monitor::collectionAdded, collection, parent); case Protocol::CollectionChangeNotification::Modify: handled |= emitToListeners(QOverload::of(&Monitor::collectionChanged), collection); handled |= emitToListeners(QOverload &>::of(&Monitor::collectionChanged), collection, msg.changedParts()); return handled; case Protocol::CollectionChangeNotification::Move: return emitToListeners(&Monitor::collectionMoved, collection, parent, destination); case Protocol::CollectionChangeNotification::Remove: return emitToListeners(&Monitor::collectionRemoved, collection); case Protocol::CollectionChangeNotification::Subscribe: // ### why?? return !monitorAll && emitToListeners(&Monitor::collectionSubscribed, collection, parent); case Protocol::CollectionChangeNotification::Unsubscribe: // ### why?? return !monitorAll && emitToListeners(&Monitor::collectionUnsubscribed, collection); default: qCDebug(AKONADICORE_LOG) << "Unknown operation type" << msg.operation() << "in collection change notification"; return false; } } bool MonitorPrivate::emitTagNotification(const Protocol::TagChangeNotification &msg, const Tag &tag) { Q_UNUSED(msg); switch (msg.operation()) { case Protocol::TagChangeNotification::Add: return emitToListeners(&Monitor::tagAdded, tag); case Protocol::TagChangeNotification::Modify: return emitToListeners(&Monitor::tagChanged, tag); case Protocol::TagChangeNotification::Remove: return emitToListeners(&Monitor::tagRemoved, tag); default: qCDebug(AKONADICORE_LOG) << "Unknown operation type" << msg.operation() << "in tag change notification"; return false; } } bool MonitorPrivate::emitRelationNotification(const Protocol::RelationChangeNotification &msg, const Relation &relation) { if (!relation.isValid()) { return false; } switch (msg.operation()) { case Protocol::RelationChangeNotification::Add: return emitToListeners(&Monitor::relationAdded, relation); case Protocol::RelationChangeNotification::Remove: return emitToListeners(&Monitor::relationRemoved, relation); default: qCDebug(AKONADICORE_LOG) << "Unknown operation type" << msg.operation() << "in tag change notification"; return false; } } bool MonitorPrivate::emitSubscriptionChangeNotification(const Protocol::SubscriptionChangeNotification &msg, const Akonadi::NotificationSubscriber &subscriber) { if (!subscriber.isValid()) { return false; } switch (msg.operation()) { case Protocol::SubscriptionChangeNotification::Add: return emitToListeners(&Monitor::notificationSubscriberAdded, subscriber); case Protocol::SubscriptionChangeNotification::Modify: return emitToListeners(&Monitor::notificationSubscriberChanged, subscriber); case Protocol::SubscriptionChangeNotification::Remove: return emitToListeners(&Monitor::notificationSubscriberRemoved, subscriber); default: qCDebug(AKONADICORE_LOG) << "Unknown operation type" << msg.operation() << "in subscription change notification"; return false; } } bool MonitorPrivate::emitDebugChangeNotification(const Protocol::DebugChangeNotification &msg, const ChangeNotification &ntf) { Q_UNUSED(msg); if (!ntf.isValid()) { return false; } return emitToListeners(&Monitor::debugNotification, ntf); } void MonitorPrivate::invalidateCaches(const Protocol::ChangeNotificationPtr &msg) { // remove invalidates // modify removes the cache entry, as we need to re-fetch // And subscription modify the visibility of the collection by the collectionFetchScope. switch (msg->type()) { case Protocol::Command::CollectionChangeNotification: { const auto &colNtf = Protocol::cmdCast(msg); switch (colNtf.operation()) { case Protocol::CollectionChangeNotification::Modify: case Protocol::CollectionChangeNotification::Move: case Protocol::CollectionChangeNotification::Subscribe: collectionCache->update(colNtf.collection().id(), mCollectionFetchScope); break; case Protocol::CollectionChangeNotification::Remove: collectionCache->invalidate(colNtf.collection().id()); break; default: break; } } break; case Protocol::Command::ItemChangeNotification: { const auto &itemNtf = Protocol::cmdCast(msg); switch (itemNtf.operation()) { case Protocol::ItemChangeNotification::Modify: case Protocol::ItemChangeNotification::ModifyFlags: case Protocol::ItemChangeNotification::ModifyTags: case Protocol::ItemChangeNotification::ModifyRelations: case Protocol::ItemChangeNotification::Move: itemCache->update(Protocol::ChangeNotification::itemsToUids(itemNtf.items()), mItemFetchScope); break; case Protocol::ItemChangeNotification::Remove: itemCache->invalidate(Protocol::ChangeNotification::itemsToUids(itemNtf.items())); break; default: break; } } break; case Protocol::Command::TagChangeNotification: { const auto &tagNtf = Protocol::cmdCast(msg); switch (tagNtf.operation()) { case Protocol::TagChangeNotification::Modify: tagCache->update({ tagNtf.tag().id() }, mTagFetchScope); break; case Protocol::TagChangeNotification::Remove: tagCache->invalidate({ tagNtf.tag().id() }); break; default: break; } } break; default: break; } } void MonitorPrivate::invalidateCache(const Collection &col) { collectionCache->update(col.id(), mCollectionFetchScope); } void MonitorPrivate::ref(Collection::Id id) { if (!refCountMap.contains(id)) { refCountMap.insert(id, 0); } ++refCountMap[id]; if (m_buffer.isBuffered(id)) { m_buffer.purge(id); } } Akonadi::Collection::Id MonitorPrivate::deref(Collection::Id id) { Q_ASSERT(refCountMap.contains(id)); if (--refCountMap[id] == 0) { refCountMap.remove(id); return m_buffer.buffer(id); } return -1; } void MonitorPrivate::PurgeBuffer::purge(Collection::Id id) { m_buffer.removeOne(id); } Akonadi::Collection::Id MonitorPrivate::PurgeBuffer::buffer(Collection::Id id) { // Ensure that we don't put a duplicate @p id into the buffer. purge(id); Collection::Id bumpedId = -1; if (m_buffer.size() == MAXBUFFERSIZE) { bumpedId = m_buffer.dequeue(); purge(bumpedId); } m_buffer.enqueue(id); return bumpedId; } int MonitorPrivate::PurgeBuffer::buffersize() { return MAXBUFFERSIZE; } bool MonitorPrivate::isMonitored(Collection::Id colId) const { if (!useRefCounting) { return true; } return refCountMap.contains(colId) || m_buffer.isBuffered(colId); } void MonitorPrivate::notifyCollectionStatisticsWatchers(Collection::Id collection, const QByteArray &resource) { if (collection > 0 && (monitorAll || isCollectionMonitored(collection) || resources.contains(resource))) { recentlyChangedCollections.insert(collection); if (!statisticsCompressionTimer.isActive()) { statisticsCompressionTimer.start(); } } } Protocol::ModifySubscriptionCommand::ChangeType MonitorPrivate::monitorTypeToProtocol(Monitor::Type type) { switch (type) { case Monitor::Collections: return Protocol::ModifySubscriptionCommand::CollectionChanges; case Monitor::Items: return Protocol::ModifySubscriptionCommand::ItemChanges; case Monitor::Tags: return Protocol::ModifySubscriptionCommand::TagChanges; case Monitor::Relations: return Protocol::ModifySubscriptionCommand::RelationChanges; case Monitor::Subscribers: return Protocol::ModifySubscriptionCommand::SubscriptionChanges; case Monitor::Notifications: return Protocol::ModifySubscriptionCommand::ChangeNotifications; default: Q_ASSERT(false); return Protocol::ModifySubscriptionCommand::NoType; } } void MonitorPrivate::updateListeners(const QMetaMethod &signal, ListenerAction action) { #define UPDATE_LISTENERS(sig) \ if (signal == QMetaMethod::fromSignal(sig)) { \ updateListener(sig, action); \ return; \ } UPDATE_LISTENERS(&Monitor::itemChanged) UPDATE_LISTENERS(&Monitor::itemChanged) UPDATE_LISTENERS(&Monitor::itemsFlagsChanged) UPDATE_LISTENERS(&Monitor::itemsTagsChanged) UPDATE_LISTENERS(&Monitor::itemsRelationsChanged) UPDATE_LISTENERS(&Monitor::itemMoved) UPDATE_LISTENERS(&Monitor::itemsMoved) UPDATE_LISTENERS(&Monitor::itemAdded) UPDATE_LISTENERS(&Monitor::itemRemoved) UPDATE_LISTENERS(&Monitor::itemsRemoved) UPDATE_LISTENERS(&Monitor::itemLinked) UPDATE_LISTENERS(&Monitor::itemsLinked) UPDATE_LISTENERS(&Monitor::itemUnlinked) UPDATE_LISTENERS(&Monitor::itemsUnlinked) UPDATE_LISTENERS(&Monitor::collectionAdded) UPDATE_LISTENERS(QOverload::of(&Monitor::collectionChanged)) UPDATE_LISTENERS((QOverload &>::of(&Monitor::collectionChanged))) UPDATE_LISTENERS(&Monitor::collectionMoved) UPDATE_LISTENERS(&Monitor::collectionRemoved) UPDATE_LISTENERS(&Monitor::collectionSubscribed) UPDATE_LISTENERS(&Monitor::collectionUnsubscribed) UPDATE_LISTENERS(&Monitor::collectionStatisticsChanged) UPDATE_LISTENERS(&Monitor::tagAdded) UPDATE_LISTENERS(&Monitor::tagChanged) UPDATE_LISTENERS(&Monitor::tagRemoved) UPDATE_LISTENERS(&Monitor::relationAdded) UPDATE_LISTENERS(&Monitor::relationRemoved) UPDATE_LISTENERS(&Monitor::notificationSubscriberAdded) UPDATE_LISTENERS(&Monitor::notificationSubscriberChanged) UPDATE_LISTENERS(&Monitor::notificationSubscriberRemoved) UPDATE_LISTENERS(&Monitor::debugNotification) #undef UPDATE_LISTENERS } // @endcond diff --git a/src/server/aggregatedfetchscope.cpp b/src/server/aggregatedfetchscope.cpp index ead89de01..36884d7cf 100644 --- a/src/server/aggregatedfetchscope.cpp +++ b/src/server/aggregatedfetchscope.cpp @@ -1,621 +1,621 @@ /* Copyright (c) 2017 Daniel Vrátil 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 "aggregatedfetchscope.h" #include -#include +#include #include #include #define LOCKED_D(name) \ Q_D(name); \ QMutexLocker lock(&d->lock); namespace Akonadi { namespace Server { class AggregatedFetchScopePrivate { public: AggregatedFetchScopePrivate() : lock(QMutex::Recursive) // recursive so that we can call our own getters/setters {} inline void addToSet(const QByteArray &value, QSet &set, QHash &count) { auto it = count.find(value); if (it == count.end()) { it = count.insert(value, 0); set.insert(value); } ++(*it); } inline void removeFromSet(const QByteArray &value, QSet &set, QHash &count) { auto it = count.find(value); if (it == count.end()) { return; } if (--(*it) == 0) { count.erase(it); set.remove(value); } } inline void updateBool(bool newValue, int &store) { store += newValue ? 1 : -1; } inline void applySet(const QSet &oldSet, const QSet &newSet, QSet &set, QHash &count) { const auto added = newSet - oldSet; for (const auto &value : added) { addToSet(value, set, count); } const auto removed = oldSet - newSet; for (const auto &value : removed) { removeFromSet(value, set, count); } } public: mutable QMutex lock; }; class AggregatedCollectionFetchScopePrivate : public AggregatedFetchScopePrivate { public: QSet attrs; QHash attrsCount; int fetchIdOnly = 0; int fetchStats = 0; }; class AggregatedTagFetchScopePrivate : public AggregatedFetchScopePrivate { public: QSet attrs; QHash attrsCount; int fetchIdOnly = 0; int fetchRemoteId = 0; int fetchAllAttributes = 0; }; class AggregatedItemFetchScopePrivate : public AggregatedFetchScopePrivate { public: mutable Protocol::ItemFetchScope mCachedScope; mutable bool mCachedScopeValid = false; // use std::optional for mCachedScope QSet parts; QHash partsCount; QSet tags; QHash tagsCount; int ancestors[3] = { 0, 0, 0 }; // 3 = size of AncestorDepth enum int cacheOnly = 0; int fullPayload = 0; int allAttributes = 0; int fetchSize = 0; int fetchMTime = 0; int fetchRRev = 0; int ignoreErrors = 0; int fetchFlags = 0; int fetchRID = 0; int fetchGID = 0; int fetchTags = 0; int fetchRelations = 0; int fetchVRefs = 0; }; } // namespace Server } // namespace Akonadi using namespace Akonadi; using namespace Akonadi::Protocol; using namespace Akonadi::Server; AggregatedCollectionFetchScope::AggregatedCollectionFetchScope() : d_ptr(new AggregatedCollectionFetchScopePrivate) { } AggregatedCollectionFetchScope::~AggregatedCollectionFetchScope() { delete d_ptr; } void AggregatedCollectionFetchScope::apply(const Protocol::CollectionFetchScope &oldScope, const Protocol::CollectionFetchScope &newScope) { LOCKED_D(AggregatedCollectionFetchScope) if (newScope.includeStatistics() != oldScope.includeStatistics()) { setFetchStatistics(newScope.includeStatistics()); } if (newScope.fetchIdOnly() != oldScope.fetchIdOnly()) { setFetchIdOnly(newScope.fetchIdOnly()); } if (newScope.attributes() != oldScope.attributes()) { d->applySet(oldScope.attributes(), newScope.attributes(), d->attrs, d->attrsCount); } } QSet AggregatedCollectionFetchScope::attributes() const { LOCKED_D(const AggregatedCollectionFetchScope) return d->attrs; } void AggregatedCollectionFetchScope::addAttribute(const QByteArray &attribute) { LOCKED_D(AggregatedCollectionFetchScope) d->addToSet(attribute, d->attrs, d->attrsCount); } void AggregatedCollectionFetchScope::removeAttribute(const QByteArray &attribute) { LOCKED_D(AggregatedCollectionFetchScope) d->removeFromSet(attribute, d->attrs, d->attrsCount); } bool AggregatedCollectionFetchScope::fetchIdOnly() const { LOCKED_D(const AggregatedCollectionFetchScope) // Aggregation: we can return true only if everyone wants fetchIdOnly, // otherwise there's at least one subscriber who wants everything return d->fetchIdOnly == 0; } void AggregatedCollectionFetchScope::setFetchIdOnly(bool fetchIdOnly) { LOCKED_D(AggregatedCollectionFetchScope) d->updateBool(fetchIdOnly, d->fetchIdOnly); } bool AggregatedCollectionFetchScope::fetchStatistics() const { LOCKED_D(const AggregatedCollectionFetchScope); // Aggregation: return true if at least one subscriber wants stats return d->fetchStats > 0; } void AggregatedCollectionFetchScope::setFetchStatistics(bool fetchStats) { LOCKED_D(AggregatedCollectionFetchScope); d->updateBool(fetchStats, d->fetchStats); } AggregatedItemFetchScope::AggregatedItemFetchScope() : d_ptr(new AggregatedItemFetchScopePrivate) { } AggregatedItemFetchScope::~AggregatedItemFetchScope() { delete d_ptr; } void AggregatedItemFetchScope::apply(const Protocol::ItemFetchScope &oldScope, const Protocol::ItemFetchScope &newScope) { LOCKED_D(AggregatedItemFetchScope); - const auto newParts = vectorToSet(newScope.requestedParts()); - const auto oldParts = vectorToSet(oldScope.requestedParts()); + const auto newParts = newScope.requestedParts() | toQSet; + const auto oldParts = oldScope.requestedParts() | toQSet; if (newParts != oldParts) { d->applySet(oldParts, newParts, d->parts, d->partsCount); } if (newScope.ancestorDepth() != oldScope.ancestorDepth()) { updateAncestorDepth(oldScope.ancestorDepth(), newScope.ancestorDepth()); } if (newScope.cacheOnly() != oldScope.cacheOnly()) { setCacheOnly(newScope.cacheOnly()); } if (newScope.fullPayload() != oldScope.fullPayload()) { setFullPayload(newScope.fullPayload()); } if (newScope.allAttributes() != oldScope.allAttributes()) { setAllAttributes(newScope.allAttributes()); } if (newScope.fetchSize() != oldScope.fetchSize()) { setFetchSize(newScope.fetchSize()); } if (newScope.fetchMTime() != oldScope.fetchMTime()) { setFetchMTime(newScope.fetchMTime()); } if (newScope.fetchRemoteRevision() != oldScope.fetchRemoteRevision()) { setFetchRemoteRevision(newScope.fetchRemoteRevision()); } if (newScope.ignoreErrors() != oldScope.ignoreErrors()) { setIgnoreErrors(newScope.ignoreErrors()); } if (newScope.fetchFlags() != oldScope.fetchFlags()) { setFetchFlags(newScope.fetchFlags()); } if (newScope.fetchRemoteId() != oldScope.fetchRemoteId()) { setFetchRemoteId(newScope.fetchRemoteId()); } if (newScope.fetchGID() != oldScope.fetchGID()) { setFetchGID(newScope.fetchGID()); } if (newScope.fetchTags() != oldScope.fetchTags()) { setFetchTags(newScope.fetchTags()); } if (newScope.fetchRelations() != oldScope.fetchRelations()) { setFetchRelations(newScope.fetchRelations()); } if (newScope.fetchVirtualReferences() != oldScope.fetchVirtualReferences()) { setFetchVirtualReferences(newScope.fetchVirtualReferences()); } d->mCachedScopeValid = false; } ItemFetchScope AggregatedItemFetchScope::toFetchScope() const { LOCKED_D(const AggregatedItemFetchScope); if (d->mCachedScopeValid) { return d->mCachedScope; } d->mCachedScope = ItemFetchScope(); - d->mCachedScope.setRequestedParts(setToVector(d->parts)); + d->mCachedScope.setRequestedParts(d->parts | toQVector); d->mCachedScope.setAncestorDepth(ancestorDepth()); d->mCachedScope.setFetch(ItemFetchScope::CacheOnly, cacheOnly()); d->mCachedScope.setFetch(ItemFetchScope::FullPayload, fullPayload()); d->mCachedScope.setFetch(ItemFetchScope::AllAttributes, allAttributes()); d->mCachedScope.setFetch(ItemFetchScope::Size, fetchSize()); d->mCachedScope.setFetch(ItemFetchScope::MTime, fetchMTime()); d->mCachedScope.setFetch(ItemFetchScope::RemoteRevision, fetchRemoteRevision()); d->mCachedScope.setFetch(ItemFetchScope::IgnoreErrors, ignoreErrors()); d->mCachedScope.setFetch(ItemFetchScope::Flags, fetchFlags()); d->mCachedScope.setFetch(ItemFetchScope::RemoteID, fetchRemoteId()); d->mCachedScope.setFetch(ItemFetchScope::GID, fetchGID()); d->mCachedScope.setFetch(ItemFetchScope::Tags, fetchTags()); d->mCachedScope.setFetch(ItemFetchScope::Relations, fetchRelations()); d->mCachedScope.setFetch(ItemFetchScope::VirtReferences, fetchVirtualReferences()); d->mCachedScopeValid = true; return d->mCachedScope; } QSet AggregatedItemFetchScope::requestedParts() const { LOCKED_D(const AggregatedItemFetchScope) return d->parts; } void AggregatedItemFetchScope::addRequestedPart(const QByteArray &part) { LOCKED_D(AggregatedItemFetchScope) d->addToSet(part, d->parts, d->partsCount); } void AggregatedItemFetchScope::removeRequestedPart(const QByteArray &part) { LOCKED_D(AggregatedItemFetchScope) d->removeFromSet(part, d->parts, d->partsCount); } ItemFetchScope::AncestorDepth AggregatedItemFetchScope::ancestorDepth() const { LOCKED_D(const AggregatedItemFetchScope) // Aggregation: return the largest depth with at least one subscriber if (d->ancestors[ItemFetchScope::AllAncestors] > 0) { return ItemFetchScope::AllAncestors; } else if (d->ancestors[ItemFetchScope::ParentAncestor] > 0) { return ItemFetchScope::ParentAncestor; } else { return ItemFetchScope::NoAncestor; } } void AggregatedItemFetchScope::updateAncestorDepth(ItemFetchScope::AncestorDepth oldDepth, ItemFetchScope::AncestorDepth newDepth) { LOCKED_D(AggregatedItemFetchScope) if (d->ancestors[oldDepth] > 0) { --d->ancestors[oldDepth]; } ++d->ancestors[newDepth]; } bool AggregatedItemFetchScope::cacheOnly() const { LOCKED_D(const AggregatedItemFetchScope) // Aggregation: we can return true only if everyone wants cached data only, // otherwise there's at least one subscriber who wants uncached data return d->cacheOnly == 0; } void AggregatedItemFetchScope::setCacheOnly(bool cacheOnly) { LOCKED_D(AggregatedItemFetchScope) d->updateBool(cacheOnly, d->cacheOnly); } bool AggregatedItemFetchScope::fullPayload() const { LOCKED_D(const AggregatedItemFetchScope) // Aggregation: return true if there's at least one subscriber who wants the // full payload return d->fullPayload > 0; } void AggregatedItemFetchScope::setFullPayload(bool fullPayload) { LOCKED_D(AggregatedItemFetchScope) d->updateBool(fullPayload, d->fullPayload); } bool AggregatedItemFetchScope::allAttributes() const { LOCKED_D(const AggregatedItemFetchScope) // Aggregation: return true if there's at least one subscriber who wants // all attributes return d->allAttributes > 0; } void AggregatedItemFetchScope::setAllAttributes(bool allAttributes) { LOCKED_D(AggregatedItemFetchScope) d->updateBool(allAttributes, d->allAttributes); } bool AggregatedItemFetchScope::fetchSize() const { LOCKED_D(const AggregatedItemFetchScope) // Aggregation: return true if there's at least one subscriber who wants size return d->fetchSize > 0; } void AggregatedItemFetchScope::setFetchSize(bool fetchSize) { LOCKED_D(AggregatedItemFetchScope) d->updateBool(fetchSize, d->fetchSize); } bool AggregatedItemFetchScope::fetchMTime() const { LOCKED_D(const AggregatedItemFetchScope) // Aggregation: return true if there's at least one subscriber who wants mtime return d->fetchMTime > 0; } void AggregatedItemFetchScope::setFetchMTime(bool fetchMTime) { LOCKED_D(AggregatedItemFetchScope) d->updateBool(fetchMTime, d->fetchMTime); } bool AggregatedItemFetchScope::fetchRemoteRevision() const { LOCKED_D(const AggregatedItemFetchScope) // Aggregation: return true if there's at least one subscriber who wants rrev return d->fetchRRev > 0; } void AggregatedItemFetchScope::setFetchRemoteRevision(bool remoteRevision) { LOCKED_D(AggregatedItemFetchScope) d->updateBool(remoteRevision, d->fetchRRev); } bool AggregatedItemFetchScope::ignoreErrors() const { LOCKED_D(const AggregatedItemFetchScope) // Aggregation: return true only if everyone wants to ignore errors, otherwise // there's at least one subscriber who does not want to ignore them return d->ignoreErrors == 0; } void AggregatedItemFetchScope::setIgnoreErrors(bool ignoreErrors) { LOCKED_D(AggregatedItemFetchScope) d->updateBool(ignoreErrors, d->ignoreErrors); } bool AggregatedItemFetchScope::fetchFlags() const { LOCKED_D(const AggregatedItemFetchScope) // Aggregation: return true if there's at least one subscriber who wants flags return d->fetchFlags > 0; } void AggregatedItemFetchScope::setFetchFlags(bool fetchFlags) { LOCKED_D(AggregatedItemFetchScope) d->updateBool(fetchFlags, d->fetchFlags); } bool AggregatedItemFetchScope::fetchRemoteId() const { LOCKED_D(const AggregatedItemFetchScope) // Aggregation: return true if there's at least one subscriber who wants RID return d->fetchRID > 0; } void AggregatedItemFetchScope::setFetchRemoteId(bool fetchRemoteId) { LOCKED_D(AggregatedItemFetchScope) d->updateBool(fetchRemoteId, d->fetchRID); } bool AggregatedItemFetchScope::fetchGID() const { LOCKED_D(const AggregatedItemFetchScope) // Aggregation: return true if there's at least one subscriber who wants GID return d->fetchGID > 0; } void AggregatedItemFetchScope::setFetchGID(bool fetchGid) { LOCKED_D(AggregatedItemFetchScope) d->updateBool(fetchGid, d->fetchGID); } bool AggregatedItemFetchScope::fetchTags() const { LOCKED_D(const AggregatedItemFetchScope) // Aggregation: return true if there's at least one subscriber who wants tags return d->fetchTags > 0; } void AggregatedItemFetchScope::setFetchTags(bool fetchTags) { LOCKED_D(AggregatedItemFetchScope) d->updateBool(fetchTags, d->fetchTags); } bool AggregatedItemFetchScope::fetchRelations() const { LOCKED_D(const AggregatedItemFetchScope) // Aggregation: return true if there's at least one subscriber who wants relations return d->fetchRelations > 0; } void AggregatedItemFetchScope::setFetchRelations(bool fetchRelations) { LOCKED_D(AggregatedItemFetchScope) d->updateBool(fetchRelations, d->fetchRelations); } bool AggregatedItemFetchScope::fetchVirtualReferences() const { LOCKED_D(const AggregatedItemFetchScope) // Aggregation: return true if there's at least one subscriber who wants vrefs return d->fetchVRefs > 0; } void AggregatedItemFetchScope::setFetchVirtualReferences(bool fetchVRefs) { LOCKED_D(AggregatedItemFetchScope) d->updateBool(fetchVRefs, d->fetchVRefs); } AggregatedTagFetchScope::AggregatedTagFetchScope() : d_ptr(new AggregatedTagFetchScopePrivate) { } AggregatedTagFetchScope::~AggregatedTagFetchScope() { delete d_ptr; } void AggregatedTagFetchScope::apply(const Protocol::TagFetchScope &oldScope, const Protocol::TagFetchScope &newScope) { LOCKED_D(AggregatedTagFetchScope) if (newScope.fetchIdOnly() != oldScope.fetchIdOnly()) { setFetchIdOnly(newScope.fetchIdOnly()); } if (newScope.fetchRemoteID() != oldScope.fetchRemoteID()) { setFetchRemoteId(newScope.fetchRemoteID()); } if (newScope.fetchAllAttributes() != oldScope.fetchAllAttributes()) { setFetchAllAttributes(newScope.fetchAllAttributes()); } if (newScope.attributes() != oldScope.attributes()) { d->applySet(oldScope.attributes(), newScope.attributes(), d->attrs, d->attrsCount); } } Protocol::TagFetchScope AggregatedTagFetchScope::toFetchScope() const { Protocol::TagFetchScope tfs; tfs.setFetchIdOnly(fetchIdOnly()); tfs.setFetchRemoteID(fetchRemoteId()); tfs.setFetchAllAttributes(fetchAllAttributes()); tfs.setAttributes(attributes()); return tfs; } bool AggregatedTagFetchScope::fetchIdOnly() const { LOCKED_D(const AggregatedTagFetchScope) // Aggregation: we can return true only if everyone wants fetchIdOnly, // otherwise there's at least one subscriber who wants everything return d->fetchIdOnly == 0; } void AggregatedTagFetchScope::setFetchIdOnly(bool fetchIdOnly) { LOCKED_D(AggregatedTagFetchScope) d->updateBool(fetchIdOnly, d->fetchIdOnly); } bool AggregatedTagFetchScope::fetchRemoteId() const { LOCKED_D(const AggregatedTagFetchScope) return d->fetchRemoteId > 0; } void AggregatedTagFetchScope::setFetchRemoteId(bool fetchRemoteId) { LOCKED_D(AggregatedTagFetchScope) d->updateBool(fetchRemoteId, d->fetchRemoteId); } bool AggregatedTagFetchScope::fetchAllAttributes() const { LOCKED_D(const AggregatedTagFetchScope) return d->fetchAllAttributes > 0; } void AggregatedTagFetchScope::setFetchAllAttributes(bool fetchAllAttributes) { LOCKED_D(AggregatedTagFetchScope) d->updateBool(fetchAllAttributes, d->fetchAllAttributes); } QSet AggregatedTagFetchScope::attributes() const { LOCKED_D(const AggregatedTagFetchScope) return d->attrs; } void AggregatedTagFetchScope::addAttribute(const QByteArray &attribute) { LOCKED_D(AggregatedTagFetchScope) d->addToSet(attribute, d->attrs, d->attrsCount); } void AggregatedTagFetchScope::removeAttribute(const QByteArray &attribute) { LOCKED_D(AggregatedTagFetchScope) d->removeFromSet(attribute, d->attrs, d->attrsCount); } #undef LOCKED_D diff --git a/src/server/handler/collectioncreatehandler.cpp b/src/server/handler/collectioncreatehandler.cpp index d23cad013..70bfebb0e 100644 --- a/src/server/handler/collectioncreatehandler.cpp +++ b/src/server/handler/collectioncreatehandler.cpp @@ -1,134 +1,134 @@ /*************************************************************************** * Copyright (C) 2006 by Ingo Kloecker * * * * This program 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 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 Library 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 "collectioncreatehandler.h" #include "connection.h" #include "handlerhelper.h" #include "storage/datastore.h" #include "storage/transaction.h" #include "storage/selectquerybuilder.h" #include using namespace Akonadi; using namespace Akonadi::Server; bool CollectionCreateHandler::parseStream() { const auto &cmd = Protocol::cmdCast(m_command); if (cmd.name().isEmpty()) { return failureResponse(QStringLiteral("Invalid collection name")); } Collection parent; qint64 resourceId = 0; bool forceVirtual = false; MimeType::List parentContentTypes; // Invalid or empty scope means we refer to root collection if (cmd.parent().scope() != Scope::Invalid && !cmd.parent().isEmpty()) { parent = HandlerHelper::collectionFromScope(cmd.parent(), connection()); if (!parent.isValid()) { return failureResponse(QStringLiteral("Invalid parent collection")); } // check if parent can contain a sub-folder parentContentTypes = parent.mimeTypes(); bool found = false, foundVirtual = false; for (const MimeType &mt : qAsConst(parentContentTypes)) { const QString mtName{mt.name()}; if (mtName == QLatin1String("inode/directory")) { found = true; } else if (mtName == QLatin1String("application/x-vnd.akonadi.collection.virtual")) { foundVirtual = true; } if (found && foundVirtual) { break; } } if (!found && !foundVirtual) { return failureResponse(QStringLiteral("Parent collection can not contain sub-collections")); } // If only virtual collections are supported, force every new collection to // be virtual. Otherwise depend on VIRTUAL attribute in the command if (foundVirtual && !found) { forceVirtual = true; } // inherit resource resourceId = parent.resourceId(); } else { const QString sessionId = QString::fromUtf8(connection()->sessionId()); Resource res = Resource::retrieveByName(sessionId); if (!res.isValid()) { return failureResponse(QStringLiteral("Cannot create top-level collection")); } resourceId = res.id(); } Collection collection; if (parent.isValid()) { collection.setParentId(parent.id()); } collection.setName(cmd.name()); collection.setResourceId(resourceId); collection.setRemoteId(cmd.remoteId()); collection.setRemoteRevision(cmd.remoteRevision()); collection.setIsVirtual(cmd.isVirtual() || forceVirtual); collection.setEnabled(cmd.enabled()); collection.setSyncPref(static_cast(cmd.syncPref())); collection.setDisplayPref(static_cast(cmd.displayPref())); collection.setIndexPref(static_cast(cmd.indexPref())); const Protocol::CachePolicy &cp = cmd.cachePolicy(); collection.setCachePolicyCacheTimeout(cp.cacheTimeout()); collection.setCachePolicyCheckInterval(cp.checkInterval()); collection.setCachePolicyInherit(cp.inherit()); collection.setCachePolicyLocalParts(cp.localParts().join(QLatin1Char(' '))); collection.setCachePolicySyncOnDemand(cp.syncOnDemand()); DataStore *db = connection()->storageBackend(); Transaction transaction(db, QStringLiteral("CREATE")); QStringList effectiveMimeTypes = cmd.mimeTypes(); if (effectiveMimeTypes.isEmpty()) { effectiveMimeTypes.reserve(parentContentTypes.count()); - for (const MimeType &mt : qAsConst(parentContentTypes)) { - effectiveMimeTypes << mt.name(); - } + std::transform(parentContentTypes.cbegin(), parentContentTypes.cend(), + std::back_inserter(effectiveMimeTypes), + std::bind(&MimeType::name, std::placeholders::_1)); } if (!db->appendCollection(collection, effectiveMimeTypes, cmd.attributes())) { return failureResponse(QStringLiteral("Could not create collection ") % cmd.name() % QStringLiteral(", resourceId: ") % QString::number(resourceId)); } if (!transaction.commit()) { return failureResponse(QStringLiteral("Unable to commit transaction.")); } db->activeCachePolicy(collection); sendResponse( HandlerHelper::fetchCollectionsResponse(collection)); return successResponse(); } diff --git a/src/server/storage/itemretriever.cpp b/src/server/storage/itemretriever.cpp index 88b3d9e27..422000f47 100644 --- a/src/server/storage/itemretriever.cpp +++ b/src/server/storage/itemretriever.cpp @@ -1,425 +1,425 @@ /* Copyright (c) 2009 Volker Krause Copyright (c) 2010 Milian Wolff 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 "itemretriever.h" #include "connection.h" #include "storage/datastore.h" #include "storage/itemqueryhelper.h" #include "storage/itemretrievalmanager.h" #include "storage/itemretrievalrequest.h" #include "storage/parthelper.h" #include "storage/parttypehelper.h" #include "storage/querybuilder.h" #include "storage/selectquerybuilder.h" #include "utils.h" - +#include #include #include #include "akonadiserver_debug.h" using namespace Akonadi; using namespace Akonadi::Server; ItemRetriever::ItemRetriever(Connection *connection) : mScope() , mConnection(connection) , mFullPayload(false) , mRecursive(false) , mCanceled(false) { connect(mConnection, &Connection::disconnected, this, [this]() { mCanceled = true; }); } Connection *ItemRetriever::connection() const { return mConnection; } void ItemRetriever::setRetrieveParts(const QVector &parts) { mParts = parts; std::sort(mParts.begin(), mParts.end()); mParts.erase(std::unique(mParts.begin(), mParts.end()), mParts.end()); // HACK, we need a full payload available flag in PimItem if (mFullPayload && !mParts.contains(AKONADI_PARAM_PLD_RFC822)) { mParts.append(AKONADI_PARAM_PLD_RFC822); } } void ItemRetriever::setItemSet(const ImapSet &set, const Collection &collection) { mItemSet = set; mCollection = collection; } void ItemRetriever::setItemSet(const ImapSet &set, bool isUid) { Q_ASSERT(mConnection); if (!isUid && mConnection->context()->collectionId() >= 0) { setItemSet(set, mConnection->context()->collection()); } else { setItemSet(set); } } void ItemRetriever::setItem(const Entity::Id &id) { ImapSet set; set.add(ImapInterval(id, id)); mItemSet = set; mCollection = Collection(); } void ItemRetriever::setRetrieveFullPayload(bool fullPayload) { mFullPayload = fullPayload; // HACK, we need a full payload available flag in PimItem if (fullPayload && !mParts.contains(AKONADI_PARAM_PLD_RFC822)) { mParts.append(AKONADI_PARAM_PLD_RFC822); } } void ItemRetriever::setCollection(const Collection &collection, bool recursive) { mCollection = collection; mItemSet = ImapSet(); mRecursive = recursive; } void ItemRetriever::setScope(const Scope &scope) { mScope = scope; } Scope ItemRetriever::scope() const { return mScope; } void ItemRetriever::setChangedSince(const QDateTime &changedSince) { mChangedSince = changedSince; } QVector ItemRetriever::retrieveParts() const { return mParts; } enum QueryColumns { PimItemIdColumn, CollectionIdColumn, ResourceIdColumn, PartTypeNameColumn, PartDatasizeColumn }; QSqlQuery ItemRetriever::buildQuery() const { QueryBuilder qb(PimItem::tableName()); qb.addJoin(QueryBuilder::InnerJoin, Collection::tableName(), PimItem::collectionIdFullColumnName(), Collection::idFullColumnName()); qb.addJoin(QueryBuilder::LeftJoin, Part::tableName(), PimItem::idFullColumnName(), Part::pimItemIdFullColumnName()); Query::Condition partTypeJoinCondition; partTypeJoinCondition.addColumnCondition(Part::partTypeIdFullColumnName(), Query::Equals, PartType::idFullColumnName()); if (!mFullPayload && !mParts.isEmpty()) { partTypeJoinCondition.addCondition(PartTypeHelper::conditionFromFqNames(mParts)); } partTypeJoinCondition.addValueCondition(PartType::nsFullColumnName(), Query::Equals, QStringLiteral("PLD")); qb.addJoin(QueryBuilder::LeftJoin, PartType::tableName(), partTypeJoinCondition); qb.addColumn(PimItem::idFullColumnName()); qb.addColumn(PimItem::collectionIdFullColumnName()); qb.addColumn(Collection::resourceIdFullColumnName()); qb.addColumn(PartType::nameFullColumnName()); qb.addColumn(Part::datasizeFullColumnName()); if (!mItemSet.isEmpty() || mCollection.isValid()) { ItemQueryHelper::itemSetToQuery(mItemSet, qb, mCollection); } else { ItemQueryHelper::scopeToQuery(mScope, mConnection->context(), qb); } // prevent a resource to trigger item retrieval from itself if (mConnection) { const Resource res = Resource::retrieveByName(QString::fromUtf8(mConnection->sessionId())); if (res.isValid()) { qb.addValueCondition(Collection::resourceIdFullColumnName(), Query::NotEquals, res.id()); } } if (mChangedSince.isValid()) { qb.addValueCondition(PimItem::datetimeFullColumnName(), Query::GreaterOrEqual, mChangedSince.toUTC()); } qb.addSortColumn(PimItem::idFullColumnName(), Query::Ascending); if (!qb.exec()) { mLastError = "Unable to retrieve items"; throw ItemRetrieverException(mLastError); } qb.query().next(); return qb.query(); } namespace { static bool hasAllParts(ItemRetrievalRequest *req, const QSet &availableParts) { for (const auto &part : qAsConst(req->parts)) { if (!availableParts.contains(part)) { return false; } } return true; } } bool ItemRetriever::exec() { if (mParts.isEmpty() && !mFullPayload) { return true; } verifyCache(); QSqlQuery query = buildQuery(); QByteArrayList parts; for (const QByteArray &part : qAsConst(mParts)) { if (part.startsWith(AKONADI_PARAM_PLD)) { parts << part.mid(4); } } QHash resourceIdNameCache; QVector requests; QHash colRequests; QHash itemRequests; QVector readyItems; qint64 prevPimItemId = -1; QSet availableParts; ItemRetrievalRequest *lastRequest = nullptr; while (query.isValid()) { const qint64 pimItemId = query.value(PimItemIdColumn).toLongLong(); const qint64 collectionId = query.value(CollectionIdColumn).toLongLong(); const qint64 resourceId = query.value(ResourceIdColumn).toLongLong(); const auto itemIter = itemRequests.constFind(pimItemId); if (Q_UNLIKELY(mCanceled)) { return false; } if (pimItemId == prevPimItemId) { if (query.value(PartTypeNameColumn).isNull()) { // This is not the first part of the Item we saw, but LEFT JOIN PartTable // returned a null row - that means the row is an ATR part // which we don't care about query.next(); continue; } } else { if (lastRequest) { if (hasAllParts(lastRequest, availableParts)) { // We went through all parts of a single item, if we have all // parts available in the DB and they are not expired, then // exclude this item from the retrieval lastRequest->ids.removeOne(prevPimItemId); itemRequests.remove(prevPimItemId); readyItems.push_back(prevPimItemId); } } availableParts.clear(); prevPimItemId = pimItemId; } if (itemIter != itemRequests.constEnd()) { lastRequest = *itemIter; } else { lastRequest = colRequests.value(collectionId); if (!lastRequest || lastRequest->ids.size() > 100) { lastRequest = new ItemRetrievalRequest(); lastRequest->ids.push_back(pimItemId); auto resIter = resourceIdNameCache.find(resourceId); if (resIter == resourceIdNameCache.end()) { resIter = resourceIdNameCache.insert(resourceId, Resource::retrieveById(resourceId).name()); } lastRequest->resourceId = *resIter; lastRequest->parts = parts; colRequests.insert(collectionId, lastRequest); itemRequests.insert(pimItemId, lastRequest); requests << lastRequest; } else { lastRequest->ids.push_back(pimItemId); itemRequests.insert(pimItemId, lastRequest); } } if (query.value(PartTypeNameColumn).isNull()) { // LEFT JOIN did not find anything, retrieve all parts query.next(); continue; } qint64 datasize = query.value(PartDatasizeColumn).toLongLong(); const QByteArray partName = Utils::variantToByteArray(query.value(PartTypeNameColumn)); Q_ASSERT(!partName.startsWith(AKONADI_PARAM_PLD)); if (datasize <= 0) { // request update for this part if (mFullPayload && !lastRequest->parts.contains(partName)) { lastRequest->parts << partName; } } else { // add the part to list of available parts, we will compare it with // the list of request parts once we handle all parts of this item availableParts.insert(partName); } query.next(); } query.finish(); // Post-check in case we only queried one item thus did not reach the check // at the beginning of the while() loop above if (lastRequest && hasAllParts(lastRequest, availableParts)) { lastRequest->ids.removeOne(prevPimItemId); readyItems.push_back(prevPimItemId); // No need to update the hashtable at this point } //qCDebug(AKONADISERVER_LOG) << "Closing queries and sending out requests."; query.finish(); if (!readyItems.isEmpty()) { - Q_EMIT itemsRetrieved(readyItems.toList()); + Q_EMIT itemsRetrieved(readyItems | toQList); } QEventLoop eventLoop; connect(ItemRetrievalManager::instance(), &ItemRetrievalManager::requestFinished, this, [&](ItemRetrievalRequest *finishedRequest) { if (requests.removeOne(finishedRequest)) { if (mCanceled) { eventLoop.exit(1); } else if (!finishedRequest->errorMsg.isEmpty()) { mLastError = finishedRequest->errorMsg.toUtf8(); eventLoop.exit(1); } else { Q_EMIT itemsRetrieved(finishedRequest->ids); if (requests.isEmpty()) { eventLoop.quit(); } } } }, Qt::UniqueConnection); connect(mConnection, &Connection::connectionClosing, &eventLoop, [&eventLoop]() { eventLoop.exit(1); }); auto it = requests.begin(); while (it != requests.end()) { auto request = (*it); if ((!mFullPayload && request->parts.isEmpty()) || request->ids.isEmpty()) { it = requests.erase(it); delete request; continue; } // TODO: how should we handle retrieval errors here? so far they have been ignored, // which makes sense in some cases, do we need a command parameter for this? try { // Request is deleted inside ItemRetrievalManager, so we need to take // a copy here //const auto ids = request->ids; ItemRetrievalManager::instance()->requestItemDelivery(request); } catch (const ItemRetrieverException &e) { qCCritical(AKONADISERVER_LOG) << e.type() << ": " << e.what(); mLastError = e.what(); return false; } ++it; } if (!requests.isEmpty()) { if (eventLoop.exec()) { return false; } } // retrieve items in child collections if requested bool result = true; if (mRecursive && mCollection.isValid()) { Q_FOREACH (const Collection &col, mCollection.children()) { ItemRetriever retriever(mConnection); retriever.setCollection(col, mRecursive); retriever.setRetrieveParts(mParts); retriever.setRetrieveFullPayload(mFullPayload); connect(&retriever, &ItemRetriever::itemsRetrieved, this, &ItemRetriever::itemsRetrieved); result = retriever.exec(); if (!result) { break; } } } return result; } void ItemRetriever::verifyCache() { if (!connection() || !connection()->verifyCacheOnRetrieval()) { return; } SelectQueryBuilder qb; qb.addJoin(QueryBuilder::InnerJoin, PimItem::tableName(), Part::pimItemIdFullColumnName(), PimItem::idFullColumnName()); qb.addValueCondition(Part::storageFullColumnName(), Query::Equals, Part::External); qb.addValueCondition(Part::dataFullColumnName(), Query::IsNot, QVariant()); if (mScope.scope() != Scope::Invalid) { ItemQueryHelper::scopeToQuery(mScope, mConnection->context(), qb); } else { ItemQueryHelper::itemSetToQuery(mItemSet, qb, mCollection); } if (!qb.exec()) { mLastError = QByteArrayLiteral("Unable to query parts."); throw ItemRetrieverException(mLastError); } const Part::List externalParts = qb.result(); for (Part part : externalParts) { PartHelper::verify(part); } } QByteArray ItemRetriever::lastError() const { return mLastError; } diff --git a/src/server/storage/notificationcollector.cpp b/src/server/storage/notificationcollector.cpp index a32595e8c..097ea6ddd 100644 --- a/src/server/storage/notificationcollector.cpp +++ b/src/server/storage/notificationcollector.cpp @@ -1,623 +1,625 @@ /* Copyright (c) 2006 - 2007 Volker Krause 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 "notificationcollector.h" #include "storage/datastore.h" #include "storage/entity.h" #include "storage/collectionstatistics.h" #include "handlerhelper.h" #include "cachecleaner.h" #include "intervalcheck.h" #include "search/searchmanager.h" #include "akonadi.h" #include "notificationmanager.h" #include "aggregatedfetchscope.h" #include "selectquerybuilder.h" #include "handler/itemfetchhelper.h" #include "connection.h" +#include "shared/akranges.h" #include "akonadiserver_debug.h" #include using namespace Akonadi; using namespace Akonadi::Server; NotificationCollector::NotificationCollector(DataStore *db) : mDb(db) { QObject::connect(db, &DataStore::transactionCommitted, [this]() { if (!mIgnoreTransactions) { dispatchNotifications(); } }); QObject::connect(db, &DataStore::transactionRolledBack, [this]() { if (!mIgnoreTransactions) { clear(); } }); } void NotificationCollector::itemAdded(const PimItem &item, bool seen, const Collection &collection, const QByteArray &resource) { SearchManager::instance()->scheduleSearchUpdate(); CollectionStatistics::self()->itemAdded(collection, item.size(), seen); itemNotification(Protocol::ItemChangeNotification::Add, item, collection, Collection(), resource); } void NotificationCollector::itemChanged(const PimItem &item, const QSet &changedParts, const Collection &collection, const QByteArray &resource) { SearchManager::instance()->scheduleSearchUpdate(); itemNotification(Protocol::ItemChangeNotification::Modify, item, collection, Collection(), resource, changedParts); } void NotificationCollector::itemsFlagsChanged(const PimItem::List &items, const QSet &addedFlags, const QSet &removedFlags, const Collection &collection, const QByteArray &resource) { int seenCount = (addedFlags.contains(AKONADI_FLAG_SEEN) || addedFlags.contains(AKONADI_FLAG_IGNORED) ? items.count() : 0); seenCount -= (removedFlags.contains(AKONADI_FLAG_SEEN) || removedFlags.contains(AKONADI_FLAG_IGNORED) ? items.count() : 0); CollectionStatistics::self()->itemsSeenChanged(collection, seenCount); itemNotification(Protocol::ItemChangeNotification::ModifyFlags, items, collection, Collection(), resource, QSet(), addedFlags, removedFlags); } void NotificationCollector::itemsTagsChanged(const PimItem::List &items, const QSet &addedTags, const QSet &removedTags, const Collection &collection, const QByteArray &resource) { itemNotification(Protocol::ItemChangeNotification::ModifyTags, items, collection, Collection(), resource, QSet(), QSet(), QSet(), addedTags, removedTags); } void NotificationCollector::itemsRelationsChanged(const PimItem::List &items, const Relation::List &addedRelations, const Relation::List &removedRelations, const Collection &collection, const QByteArray &resource) { itemNotification(Protocol::ItemChangeNotification::ModifyRelations, items, collection, Collection(), resource, QSet(), QSet(), QSet(), QSet(), QSet(), addedRelations, removedRelations); } void NotificationCollector::itemsMoved(const PimItem::List &items, const Collection &collectionSrc, const Collection &collectionDest, const QByteArray &sourceResource) { SearchManager::instance()->scheduleSearchUpdate(); itemNotification(Protocol::ItemChangeNotification::Move, items, collectionSrc, collectionDest, sourceResource); } void NotificationCollector::itemsRemoved(const PimItem::List &items, const Collection &collection, const QByteArray &resource) { itemNotification(Protocol::ItemChangeNotification::Remove, items, collection, Collection(), resource); } void NotificationCollector::itemsLinked(const PimItem::List &items, const Collection &collection) { itemNotification(Protocol::ItemChangeNotification::Link, items, collection, Collection(), QByteArray()); } void NotificationCollector::itemsUnlinked(const PimItem::List &items, const Collection &collection) { itemNotification(Protocol::ItemChangeNotification::Unlink, items, collection, Collection(), QByteArray()); } void NotificationCollector::collectionAdded(const Collection &collection, const QByteArray &resource) { if (auto cleaner = AkonadiServer::instance()->cacheCleaner()) { cleaner->collectionAdded(collection.id()); } if (auto checker = AkonadiServer::instance()->intervalChecker()) { checker->collectionAdded(collection.id()); } collectionNotification(Protocol::CollectionChangeNotification::Add, collection, collection.parentId(), -1, resource); } void NotificationCollector::collectionChanged(const Collection &collection, const QList &changes, const QByteArray &resource) { if (auto cleaner = AkonadiServer::instance()->cacheCleaner()) { cleaner->collectionChanged(collection.id()); } if (auto checker = AkonadiServer::instance()->intervalChecker()) { checker->collectionChanged(collection.id()); } if (changes.contains(AKONADI_PARAM_ENABLED) || changes.contains(AKONADI_PARAM_REFERENCED)) { CollectionStatistics::self()->invalidateCollection(collection); } - collectionNotification(Protocol::CollectionChangeNotification::Modify, collection, collection.parentId(), -1, resource, changes.toSet()); + collectionNotification(Protocol::CollectionChangeNotification::Modify, collection, collection.parentId(), + -1, resource, changes | toQSet); } void NotificationCollector::collectionMoved(const Collection &collection, const Collection &source, const QByteArray &resource, const QByteArray &destResource) { if (auto cleaner = AkonadiServer::instance()->cacheCleaner()) { cleaner->collectionChanged(collection.id()); } if (auto checker = AkonadiServer::instance()->intervalChecker()) { checker->collectionChanged(collection.id()); } collectionNotification(Protocol::CollectionChangeNotification::Move, collection, source.id(), collection.parentId(), resource, QSet(), destResource); } void NotificationCollector::collectionRemoved(const Collection &collection, const QByteArray &resource) { if (auto cleaner = AkonadiServer::instance()->cacheCleaner()) { cleaner->collectionRemoved(collection.id()); } if (auto checker = AkonadiServer::instance()->intervalChecker()) { checker->collectionRemoved(collection.id()); } CollectionStatistics::self()->invalidateCollection(collection); collectionNotification(Protocol::CollectionChangeNotification::Remove, collection, collection.parentId(), -1, resource); } void NotificationCollector::collectionSubscribed(const Collection &collection, const QByteArray &resource) { if (auto cleaner = AkonadiServer::instance()->cacheCleaner()) { cleaner->collectionAdded(collection.id()); } if (auto checker = AkonadiServer::instance()->intervalChecker()) { checker->collectionAdded(collection.id()); } collectionNotification(Protocol::CollectionChangeNotification::Subscribe, collection, collection.parentId(), -1, resource, QSet()); } void NotificationCollector::collectionUnsubscribed(const Collection &collection, const QByteArray &resource) { if (auto cleaner = AkonadiServer::instance()->cacheCleaner()) { cleaner->collectionRemoved(collection.id()); } if (auto checker = AkonadiServer::instance()->intervalChecker()) { checker->collectionRemoved(collection.id()); } CollectionStatistics::self()->invalidateCollection(collection); collectionNotification(Protocol::CollectionChangeNotification::Unsubscribe, collection, collection.parentId(), -1, resource, QSet()); } void NotificationCollector::tagAdded(const Tag &tag) { tagNotification(Protocol::TagChangeNotification::Add, tag); } void NotificationCollector::tagChanged(const Tag &tag) { tagNotification(Protocol::TagChangeNotification::Modify, tag); } void NotificationCollector::tagRemoved(const Tag &tag, const QByteArray &resource, const QString &remoteId) { tagNotification(Protocol::TagChangeNotification::Remove, tag, resource, remoteId); } void NotificationCollector::relationAdded(const Relation &relation) { relationNotification(Protocol::RelationChangeNotification::Add, relation); } void NotificationCollector::relationRemoved(const Relation &relation) { relationNotification(Protocol::RelationChangeNotification::Remove, relation); } void NotificationCollector::clear() { mNotifications.clear(); } void NotificationCollector::setConnection(Connection *connection) { mConnection = connection; } void NotificationCollector::itemNotification(Protocol::ItemChangeNotification::Operation op, const PimItem &item, const Collection &collection, const Collection &collectionDest, const QByteArray &resource, const QSet &parts) { PimItem::List items; items << item; itemNotification(op, items, collection, collectionDest, resource, parts); } void NotificationCollector::itemNotification(Protocol::ItemChangeNotification::Operation op, const PimItem::List &items, const Collection &collection, const Collection &collectionDest, const QByteArray &resource, const QSet &parts, const QSet &addedFlags, const QSet &removedFlags, const QSet &addedTags, const QSet &removedTags, const Relation::List &addedRelations, const Relation::List &removedRelations) { QMap > vCollections; if ((op == Protocol::ItemChangeNotification::Modify) || (op == Protocol::ItemChangeNotification::ModifyFlags) || (op == Protocol::ItemChangeNotification::ModifyTags) || (op == Protocol::ItemChangeNotification::ModifyRelations)) { vCollections = DataStore::self()->virtualCollections(items); } auto msg = Protocol::ItemChangeNotificationPtr::create(); if (mConnection) { msg->setSessionId(mConnection->sessionId()); } msg->setOperation(op); msg->setItemParts(parts); msg->setAddedFlags(addedFlags); msg->setRemovedFlags(removedFlags); msg->setAddedTags(addedTags); msg->setRemovedTags(removedTags); if (!addedRelations.isEmpty()) { QSet rels; Q_FOREACH (const Relation &rel, addedRelations) { rels.insert(Protocol::ItemChangeNotification::Relation(rel.leftId(), rel.rightId(), rel.relationType().name())); } msg->setAddedRelations(rels); } if (!removedRelations.isEmpty()) { QSet rels; Q_FOREACH (const Relation &rel, removedRelations) { rels.insert(Protocol::ItemChangeNotification::Relation(rel.leftId(), rel.rightId(), rel.relationType().name())); } msg->setRemovedRelations(rels); } if (collectionDest.isValid()) { QByteArray destResourceName; destResourceName = collectionDest.resource().name().toLatin1(); msg->setDestinationResource(destResourceName); } msg->setParentDestCollection(collectionDest.id()); QVector ntfItems; Q_FOREACH (const PimItem &item, items) { Protocol::FetchItemsResponse i; i.setId(item.id()); i.setRemoteId(item.remoteId()); i.setRemoteRevision(item.remoteRevision()); i.setMimeType(item.mimeType().name()); ntfItems.push_back(std::move(i)); } /* Notify all virtual collections the items are linked to. */ QHash virtItems; for (const auto &ntfItem : ntfItems) { virtItems.insert(ntfItem.id(), std::move(ntfItem)); } auto iter = vCollections.constBegin(), endIter = vCollections.constEnd(); for (; iter != endIter; ++iter) { auto copy = Protocol::ItemChangeNotificationPtr::create(*msg); QVector items; items.reserve(iter->size()); for (const auto &item : qAsConst(*iter)) { items.append(virtItems.value(item.id())); } copy->setItems(items); copy->setParentCollection(iter.key()); copy->setResource(resource); CollectionStatistics::self()->invalidateCollection(Collection::retrieveById(iter.key())); dispatchNotification(copy); } msg->setItems(ntfItems); Collection col; if (!collection.isValid()) { msg->setParentCollection(items.first().collection().id()); col = items.first().collection(); } else { msg->setParentCollection(collection.id()); col = collection; } QByteArray res = resource; if (res.isEmpty()) { if (col.resourceId() <= 0) { col = Collection::retrieveById(col.id()); } res = col.resource().name().toLatin1(); } msg->setResource(res); // Add and ModifyFlags are handled incrementally // (see itemAdded() and itemsFlagsChanged()) if (msg->operation() != Protocol::ItemChangeNotification::Add && msg->operation() != Protocol::ItemChangeNotification::ModifyFlags) { CollectionStatistics::self()->invalidateCollection(col); } dispatchNotification(msg); } void NotificationCollector::collectionNotification(Protocol::CollectionChangeNotification::Operation op, const Collection &collection, Collection::Id source, Collection::Id destination, const QByteArray &resource, const QSet &changes, const QByteArray &destResource) { auto msg = Protocol::CollectionChangeNotificationPtr::create(); msg->setOperation(op); if (mConnection) { msg->setSessionId(mConnection->sessionId()); } msg->setParentCollection(source); msg->setParentDestCollection(destination); msg->setDestinationResource(destResource); msg->setChangedParts(changes); auto msgCollection = HandlerHelper::fetchCollectionsResponse(collection); if (auto mgr = AkonadiServer::instance()->notificationManager()) { auto fetchScope = mgr->collectionFetchScope(); // Make sure we have all the data if (!fetchScope->fetchIdOnly() && msgCollection.name().isEmpty()) { const auto col = Collection::retrieveById(msgCollection.id()); const auto mts = col.mimeTypes(); QStringList mimeTypes; mimeTypes.reserve(mts.size()); for (const auto &mt : mts) { mimeTypes.push_back(mt.name()); } msgCollection = HandlerHelper::fetchCollectionsResponse(col, {}, false, 0, {}, {}, false, mimeTypes); } // Get up-to-date statistics if (fetchScope->fetchStatistics()) { Collection col; col.setId(msgCollection.id()); const auto stats = CollectionStatistics::self()->statistics(col); msgCollection.setStatistics(Protocol::FetchCollectionStatsResponse(stats.count, stats.count - stats.read, stats.size)); } // Get attributes const auto requestedAttrs = fetchScope->attributes(); auto msgColAttrs = msgCollection.attributes(); // TODO: This assumes that we have either none or all attributes in msgCollection if (msgColAttrs.isEmpty() && !requestedAttrs.isEmpty()) { SelectQueryBuilder qb; qb.addColumn(CollectionAttribute::typeFullColumnName()); qb.addColumn(CollectionAttribute::valueFullColumnName()); qb.addValueCondition(CollectionAttribute::collectionIdFullColumnName(), Query::Equals, msgCollection.id()); Query::Condition cond(Query::Or); for (const auto &attr : requestedAttrs) { cond.addValueCondition(CollectionAttribute::typeFullColumnName(), Query::Equals, attr); } qb.addCondition(cond); if (!qb.exec()) { qCWarning(AKONADISERVER_LOG) << "NotificationCollector failed to query attributes for Collection" << collection.name() << "(ID" << collection.id() << ")"; } const auto attrs = qb.result(); for (const auto &attr : attrs) { msgColAttrs.insert(attr.type(), attr.value()); } msgCollection.setAttributes(msgColAttrs); } } msg->setCollection(std::move(msgCollection)); if (!collection.enabled()) { msg->addMetadata("DISABLED"); } QByteArray res = resource; if (res.isEmpty()) { res = collection.resource().name().toLatin1(); } msg->setResource(res); dispatchNotification(msg); } void NotificationCollector::tagNotification(Protocol::TagChangeNotification::Operation op, const Tag &tag, const QByteArray &resource, const QString &remoteId) { auto msg = Protocol::TagChangeNotificationPtr::create(); msg->setOperation(op); if (mConnection) { msg->setSessionId(mConnection->sessionId()); } msg->setResource(resource); Protocol::FetchTagsResponse msgTag; msgTag.setId(tag.id()); msgTag.setRemoteId(remoteId.toUtf8()); if (auto mgr = AkonadiServer::instance()->notificationManager()) { auto fetchScope = mgr->tagFetchScope(); if (!fetchScope->fetchIdOnly() && msgTag.gid().isEmpty()) { msgTag = HandlerHelper::fetchTagsResponse(Tag::retrieveById(msgTag.id()), fetchScope->toFetchScope(), mConnection); } const auto requestedAttrs = fetchScope->attributes(); auto msgTagAttrs = msgTag.attributes(); if (msgTagAttrs.isEmpty() && !requestedAttrs.isEmpty()) { SelectQueryBuilder qb; qb.addColumn(TagAttribute::typeFullColumnName()); qb.addColumn(TagAttribute::valueFullColumnName()); qb.addValueCondition(TagAttribute::tagIdFullColumnName(), Query::Equals, msgTag.id()); Query::Condition cond(Query::Or); for (const auto &attr : requestedAttrs) { cond.addValueCondition(TagAttribute::typeFullColumnName(), Query::Equals, attr); } qb.addCondition(cond); if (!qb.exec()) { qCWarning(AKONADISERVER_LOG) << "NotificationCollection failed to query attributes for Tag" << tag.id(); } const auto attrs = qb.result(); for (const auto &attr : attrs) { msgTagAttrs.insert(attr.type(), attr.value()); } msgTag.setAttributes(msgTagAttrs); } } msg->setTag(std::move(msgTag)); dispatchNotification(msg); } void NotificationCollector::relationNotification(Protocol::RelationChangeNotification::Operation op, const Relation &relation) { auto msg = Protocol::RelationChangeNotificationPtr::create(); msg->setOperation(op); if (mConnection) { msg->setSessionId(mConnection->sessionId()); } msg->setRelation(HandlerHelper::fetchRelationsResponse(relation)); dispatchNotification(msg); } void NotificationCollector::completeNotification(const Protocol::ChangeNotificationPtr &changeMsg) { if (changeMsg->type() == Protocol::Command::ItemChangeNotification) { const auto msg = changeMsg.staticCast(); const auto mgr = AkonadiServer::instance()->notificationManager(); if (mgr && msg->operation() != Protocol::ItemChangeNotification::Remove) { if (mDb->inTransaction()) { qCWarning(AKONADISERVER_LOG) << "NotificationCollector requested FetchHelper from within a transaction." << "Aborting since this would deadlock!"; return; } auto fetchScope = mgr->itemFetchScope(); // NOTE: Checking and retrieving missing elements for each Item manually // here would require a complex code (and I'm too lazy), so instead we simply // feed the Items to FetchHelper and retrieve them all with the setup from // the aggregated fetch scope. The worst case is that we re-fetch everything // we already have, but that's stil better than the pre-ntf-payload situation QVector ids; const auto items = msg->items(); ids.reserve(items.size()); bool allHaveRID = true; for (const auto &item : items) { ids.push_back(item.id()); allHaveRID &= !item.remoteId().isEmpty(); } // FetchHelper may trigger ItemRetriever, which needs RemoteID. If we // dont have one (maybe because the Resource has not stored it yet, // we emit a notification without it and leave it up to the Monitor // to retrieve the Item on demand - we should have a RID stored in // Akonadi by then. if (mConnection && (allHaveRID || msg->operation() != Protocol::ItemChangeNotification::Add)) { // Prevent transactions inside FetchHelper to recursively call our slot QScopedValueRollback ignoreTransactions(mIgnoreTransactions); mIgnoreTransactions = true; CommandContext context; auto itemFetchScope = fetchScope->toFetchScope(); auto tagFetchScope = mgr->tagFetchScope()->toFetchScope(); itemFetchScope.setFetch(Protocol::ItemFetchScope::CacheOnly); ItemFetchHelper helper(mConnection, &context, Scope(ids), itemFetchScope, tagFetchScope); // The Item was just changed, which means the atime was // updated, no need to do it again a couple milliseconds later. helper.disableATimeUpdates(); QVector fetchedItems; auto callback = [&fetchedItems](Protocol::FetchItemsResponse &&cmd) { fetchedItems.push_back(std::move(cmd)); }; if (helper.fetchItems(std::move(callback))) { msg->setItems(fetchedItems); } else { qCWarning(AKONADISERVER_LOG) << "NotificationCollector railed to retrieve Items for notification!"; } } else { QVector fetchedItems; for (const auto &item : items) { Protocol::FetchItemsResponse resp; resp.setId(item.id()); resp.setRevision(item.revision()); resp.setMimeType(item.mimeType()); resp.setParentId(item.parentId()); resp.setGid(item.gid()); resp.setSize(item.size()); resp.setMTime(item.mTime()); resp.setFlags(item.flags()); fetchedItems.push_back(std::move(resp)); } msg->setItems(fetchedItems); msg->setMustRetrieve(true); } } } } void NotificationCollector::dispatchNotification(const Protocol::ChangeNotificationPtr &msg) { if (!mDb || mDb->inTransaction()) { if (msg->type() == Protocol::Command::CollectionChangeNotification) { Protocol::CollectionChangeNotification::appendAndCompress(mNotifications, msg); } else { mNotifications.append(msg); } } else { completeNotification(msg); notify({msg}); } } void NotificationCollector::dispatchNotifications() { if (!mNotifications.isEmpty()) { for (auto &ntf : mNotifications) { completeNotification(ntf); } notify(std::move(mNotifications)); clear(); } } void NotificationCollector::notify(Protocol::ChangeNotificationList msgs) { if (auto mgr = AkonadiServer::instance()->notificationManager()) { QMetaObject::invokeMethod(mgr, "slotNotify", Qt::QueuedConnection, Q_ARG(Akonadi::Protocol::ChangeNotificationList, msgs)); } } diff --git a/src/server/storage/querybuilder.cpp b/src/server/storage/querybuilder.cpp index 8b9f11675..dea6817ed 100644 --- a/src/server/storage/querybuilder.cpp +++ b/src/server/storage/querybuilder.cpp @@ -1,634 +1,635 @@ /* Copyright (c) 2007 - 2012 Volker Krause 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 "querybuilder.h" #include "akonadiserver_debug.h" - #ifndef QUERYBUILDER_UNITTEST #include "storage/datastore.h" #include "storage/querycache.h" #include "storage/storagedebugger.h" #endif +#include + #include #include #include using namespace Akonadi::Server; static QLatin1String compareOperatorToString(Query::CompareOperator op) { switch (op) { case Query::Equals: return QLatin1String(" = "); case Query::NotEquals: return QLatin1String(" <> "); case Query::Is: return QLatin1String(" IS "); case Query::IsNot: return QLatin1String(" IS NOT "); case Query::Less: return QLatin1String(" < "); case Query::LessOrEqual: return QLatin1String(" <= "); case Query::Greater: return QLatin1String(" > "); case Query::GreaterOrEqual: return QLatin1String(" >= "); case Query::In: return QLatin1String(" IN "); case Query::NotIn: return QLatin1String(" NOT IN "); case Query::Like: return QLatin1String(" LIKE "); } Q_ASSERT_X(false, "QueryBuilder::compareOperatorToString()", "Unknown compare operator."); return QLatin1String(""); } static QLatin1String logicOperatorToString(Query::LogicOperator op) { switch (op) { case Query::And: return QLatin1String(" AND "); case Query::Or: return QLatin1String(" OR "); } Q_ASSERT_X(false, "QueryBuilder::logicOperatorToString()", "Unknown logic operator."); return QLatin1String(""); } static QLatin1String sortOrderToString(Query::SortOrder order) { switch (order) { case Query::Ascending: return QLatin1String(" ASC"); case Query::Descending: return QLatin1String(" DESC"); } Q_ASSERT_X(false, "QueryBuilder::sortOrderToString()", "Unknown sort order."); return QLatin1String(""); } static void appendJoined(QString *statement, const QStringList &strings, const QLatin1String &glue = QLatin1String(", ")) { for (int i = 0, c = strings.size(); i < c; ++i) { *statement += strings.at(i); if (i + 1 < c) { *statement += glue; } } } QueryBuilder::QueryBuilder(const QString &table, QueryBuilder::QueryType type) : mTable(table) #ifndef QUERYBUILDER_UNITTEST , mDatabaseType(DbType::type(DataStore::self()->database())) , mQuery(DataStore::self()->database()) #else , mDatabaseType(DbType::Unknown) #endif , mType(type) , mIdentificationColumn() , mLimit(-1) , mDistinct(false) { static const QString defaultIdColumn = QStringLiteral("id"); mIdentificationColumn = defaultIdColumn; } void QueryBuilder::setDatabaseType(DbType::Type type) { mDatabaseType = type; } void QueryBuilder::addJoin(JoinType joinType, const QString &table, const Query::Condition &condition) { Q_ASSERT((joinType == InnerJoin && (mType == Select || mType == Update)) || (joinType == LeftJoin && mType == Select)); if (mJoinedTables.contains(table)) { // InnerJoin is more restrictive than a LeftJoin, hence use that in doubt mJoins[table].first = qMin(joinType, mJoins.value(table).first); mJoins[table].second.addCondition(condition); } else { mJoins[table] = qMakePair(joinType, condition); mJoinedTables << table; } } void QueryBuilder::addJoin(JoinType joinType, const QString &table, const QString &col1, const QString &col2) { Query::Condition condition; condition.addColumnCondition(col1, Query::Equals, col2); addJoin(joinType, table, condition); } void QueryBuilder::addValueCondition(const QString &column, Query::CompareOperator op, const QVariant &value, ConditionType type) { Q_ASSERT(type == WhereCondition || (type == HavingCondition && mType == Select)); mRootCondition[type].addValueCondition(column, op, value); } void QueryBuilder::addColumnCondition(const QString &column, Query::CompareOperator op, const QString &column2, ConditionType type) { Q_ASSERT(type == WhereCondition || (type == HavingCondition && mType == Select)); mRootCondition[type].addColumnCondition(column, op, column2); } QSqlQuery &QueryBuilder::query() { return mQuery; } void QueryBuilder::sqliteAdaptUpdateJoin(Query::Condition &condition) { // FIXME: This does not cover all cases by far. It however can handle most // (probably all) of the update-join queries we do in Akonadi and convert them // properly into a SQLite-compatible query. Better than nothing ;-) if (!condition.mSubConditions.isEmpty()) { for (int i = condition.mSubConditions.count() - 1; i >= 0; --i) { sqliteAdaptUpdateJoin(condition.mSubConditions[i]); } return; } QString table; if (condition.mColumn.contains(QLatin1Char('.'))) { table = condition.mColumn.left(condition.mColumn.indexOf(QLatin1Char('.'))); } else { return; } if (!mJoinedTables.contains(table)) { return; } const QPair joinCondition = mJoins.value(table); QueryBuilder qb(table, Select); qb.addColumn(condition.mColumn); qb.addCondition(joinCondition.second); // Convert the subquery to string condition.mColumn.reserve(1024); condition.mColumn.resize(0); condition.mColumn += QLatin1String("( "); qb.buildQuery(&condition.mColumn); condition.mColumn += QLatin1String(" )"); } void QueryBuilder::buildQuery(QString *statement) { // we add the ON conditions of Inner Joins in a Update query here // but don't want to change the mRootCondition on each exec(). Query::Condition whereCondition = mRootCondition[WhereCondition]; switch (mType) { case Select: // Enable forward-only on all SELECT queries, since we never need to // iterate backwards. This is a memory optimization. mQuery.setForwardOnly(true); *statement += QLatin1String("SELECT "); if (mDistinct) { *statement += QLatin1String("DISTINCT "); } Q_ASSERT_X(mColumns.count() > 0, "QueryBuilder::exec()", "No columns specified"); appendJoined(statement, mColumns); *statement += QLatin1String(" FROM "); *statement += mTable; for (const QString &joinedTable : qAsConst(mJoinedTables)) { const QPair &join = mJoins.value(joinedTable); switch (join.first) { case LeftJoin: *statement += QLatin1String(" LEFT JOIN "); break; case InnerJoin: *statement += QLatin1String(" INNER JOIN "); break; } *statement += joinedTable; *statement += QLatin1String(" ON "); buildWhereCondition(statement, join.second); } break; case Insert: { *statement += QLatin1String("INSERT INTO "); *statement += mTable; *statement += QLatin1String(" ("); for (int i = 0, c = mColumnValues.size(); i < c; ++i) { *statement += mColumnValues.at(i).first; if (i + 1 < c) { *statement += QLatin1String(", "); } } *statement += QLatin1String(") VALUES ("); for (int i = 0, c = mColumnValues.size(); i < c; ++i) { bindValue(statement, mColumnValues.at(i).second); if (i + 1 < c) { *statement += QLatin1String(", "); } } *statement += QLatin1Char(')'); if (mDatabaseType == DbType::PostgreSQL && !mIdentificationColumn.isEmpty()) { *statement += QLatin1String(" RETURNING ") + mIdentificationColumn; } break; } case Update: { // put the ON condition into the WHERE part of the UPDATE query if (mDatabaseType != DbType::Sqlite) { for (const QString &table : qAsConst(mJoinedTables)) { const QPair< JoinType, Query::Condition > &join = mJoins.value(table); Q_ASSERT(join.first == InnerJoin); whereCondition.addCondition(join.second); } } else { // Note: this will modify the whereCondition sqliteAdaptUpdateJoin(whereCondition); } *statement += QLatin1String("UPDATE "); *statement += mTable; if (mDatabaseType == DbType::MySQL && !mJoinedTables.isEmpty()) { // for mysql we list all tables directly *statement += QLatin1String(", "); appendJoined(statement, mJoinedTables); } *statement += QLatin1String(" SET "); Q_ASSERT_X(mColumnValues.count() >= 1, "QueryBuilder::exec()", "At least one column needs to be changed"); for (int i = 0, c = mColumnValues.size(); i < c; ++i) { const QPair &p = mColumnValues.at(i); *statement += p.first; *statement += QLatin1String(" = "); bindValue(statement, p.second); if (i + 1 < c) { *statement += QLatin1String(", "); } } if (mDatabaseType == DbType::PostgreSQL && !mJoinedTables.isEmpty()) { // PSQL have this syntax // FROM t1 JOIN t2 JOIN ... *statement += QLatin1String(" FROM "); appendJoined(statement, mJoinedTables, QLatin1String(" JOIN ")); } break; } case Delete: *statement += QLatin1String("DELETE FROM "); *statement += mTable; break; default: Q_ASSERT_X(false, "QueryBuilder::exec()", "Unknown enum value"); } if (!whereCondition.isEmpty()) { *statement += QLatin1String(" WHERE "); buildWhereCondition(statement, whereCondition); } if (!mGroupColumns.isEmpty()) { *statement += QLatin1String(" GROUP BY "); appendJoined(statement, mGroupColumns); } if (!mRootCondition[HavingCondition].isEmpty()) { *statement += QLatin1String(" HAVING "); buildWhereCondition(statement, mRootCondition[HavingCondition]); } if (!mSortColumns.isEmpty()) { Q_ASSERT_X(mType == Select, "QueryBuilder::exec()", "Order statements are only valid for SELECT queries"); *statement += QLatin1String(" ORDER BY "); for (int i = 0, c = mSortColumns.size(); i < c; ++i) { const QPair &order = mSortColumns.at(i); *statement += order.first; *statement += sortOrderToString(order.second); if (i + 1 < c) { *statement += QLatin1String(", "); } } } if (mLimit > 0) { *statement += QLatin1Literal(" LIMIT ") + QString::number(mLimit); } if (mType == Select && mForUpdate) { if (mDatabaseType == DbType::Sqlite) { // SQLite does not support SELECT ... FOR UPDATE syntax, because it does // table-level locking } else { *statement += QLatin1Literal(" FOR UPDATE"); } } } bool QueryBuilder::retryLastTransaction(bool rollback) { #ifndef QUERYBUILDER_UNITTEST mQuery = DataStore::self()->retryLastTransaction(rollback); return !mQuery.lastError().isValid(); #else Q_UNUSED(rollback); return true; #endif } bool QueryBuilder::exec() { QString statement; statement.reserve(1024); buildQuery(&statement); #ifndef QUERYBUILDER_UNITTEST auto query = QueryCache::query(statement); if (query.has_value()) { mQuery = std::move(*query); } else { mQuery.clear(); mQuery.prepare(statement); QueryCache::insert(statement, mQuery); } //too heavy debug info but worths to have from time to time //qCDebug(AKONADISERVER_LOG) << "Executing query" << statement; bool isBatch = false; for (int i = 0; i < mBindValues.count(); ++i) { mQuery.bindValue(QLatin1Char(':') + QString::number(i), mBindValues[i]); if (!isBatch && static_cast(mBindValues[i].type()) == QMetaType::QVariantList) { isBatch = true; } //qCDebug(AKONADISERVER_LOG) << QString::fromLatin1( ":%1" ).arg( i ) << mBindValues[i]; } bool ret; if (StorageDebugger::instance()->isSQLDebuggingEnabled()) { QTime t; t.start(); if (isBatch) { ret = mQuery.execBatch(); } else { ret = mQuery.exec(); } StorageDebugger::instance()->queryExecuted(reinterpret_cast(DataStore::self()), mQuery, t.elapsed()); } else { StorageDebugger::instance()->incSequence(); if (isBatch) { ret = mQuery.execBatch(); } else { ret = mQuery.exec(); } } // Add the query to DataStore so that we can replay it in case transaction deadlocks. // The method does nothing when this query is not executed within a transaction. // We don't care whether the query was successful or not. In case of error, the caller // will rollback the transaction anyway, and all cached queries will be removed. DataStore::self()->addQueryToTransaction(statement, mBindValues, isBatch); if (!ret) { // Handle transaction deadlocks and timeouts by attempting to replay the transaction. if (mDatabaseType == DbType::PostgreSQL) { const QString dbError = mQuery.lastError().databaseText(); if (dbError.contains(QLatin1String("40P01" /* deadlock_detected */))) { qCWarning(AKONADISERVER_LOG) << "QueryBuilder::exec(): database reported transaction deadlock, retrying transaction"; qCWarning(AKONADISERVER_LOG) << mQuery.lastError().text(); return retryLastTransaction(); } } else if (mDatabaseType == DbType::MySQL) { const QString lastErrorStr = mQuery.lastError().nativeErrorCode(); const int error = lastErrorStr.isEmpty() ? -1 : lastErrorStr.toInt(); if (error == 1213 /* ER_LOCK_DEADLOCK */) { qCWarning(AKONADISERVER_LOG) << "QueryBuilder::exec(): database reported transaction deadlock, retrying transaction"; qCWarning(AKONADISERVER_LOG) << mQuery.lastError().text(); return retryLastTransaction(); } else if (error == 1205 /* ER_LOCK_WAIT_TIMEOUT */) { qCWarning(AKONADISERVER_LOG) << "QueryBuilder::exec(): database reported transaction timeout, retrying transaction"; qCWarning(AKONADISERVER_LOG) << mQuery.lastError().text(); return retryLastTransaction(); } } else if (mDatabaseType == DbType::Sqlite && !DbType::isSystemSQLite(DataStore::self()->database())) { const QString lastErrorStr = mQuery.lastError().nativeErrorCode(); const int error = lastErrorStr.isEmpty() ? -1 : lastErrorStr.toInt(); if (error == 6 /* SQLITE_LOCKED */) { qCWarning(AKONADISERVER_LOG) << "QueryBuilder::exec(): database reported transaction deadlock, retrying transaction"; qCWarning(AKONADISERVER_LOG) << mQuery.lastError().text(); return retryLastTransaction(true); } else if (error == 5 /* SQLITE_BUSY */) { qCWarning(AKONADISERVER_LOG) << "QueryBuilder::exec(): database reported transaction timeout, retrying transaction"; qCWarning(AKONADISERVER_LOG) << mQuery.lastError().text(); return retryLastTransaction(true); } } else if (mDatabaseType == DbType::Sqlite) { // We can't have a transaction deadlock in SQLite when using driver shipped // with Qt, because it does not support concurrent transactions and DataStore // serializes them through a global lock. } qCCritical(AKONADISERVER_LOG) << "DATABASE ERROR:"; qCCritical(AKONADISERVER_LOG) << " Error code:" << mQuery.lastError().nativeErrorCode(); qCCritical(AKONADISERVER_LOG) << " DB error: " << mQuery.lastError().databaseText(); qCCritical(AKONADISERVER_LOG) << " Error text:" << mQuery.lastError().text(); qCCritical(AKONADISERVER_LOG) << " Values:" << mQuery.boundValues(); qCCritical(AKONADISERVER_LOG) << " Query:" << statement; return false; } #else mStatement = statement; #endif return true; } void QueryBuilder::addColumns(const QStringList &cols) { mColumns << cols; } void QueryBuilder::addColumn(const QString &col) { mColumns << col; } void QueryBuilder::addColumn(const Query::Case &caseStmt) { QString query; buildCaseStatement(&query, caseStmt); mColumns.append(query); } void QueryBuilder::addAggregation(const QString &col, const QString &aggregate) { mColumns.append(aggregate + QLatin1Char('(') + col + QLatin1Char(')')); } void QueryBuilder::addAggregation(const Query::Case &caseStmt, const QString &aggregate) { QString query(aggregate + QLatin1Char('(')); buildCaseStatement(&query, caseStmt); query += QLatin1Char(')'); mColumns.append(query); } void QueryBuilder::bindValue(QString *query, const QVariant &value) { mBindValues << value; *query += QLatin1Char(':') + QString::number(mBindValues.count() - 1); } void QueryBuilder::buildWhereCondition(QString *query, const Query::Condition &cond) { if (!cond.isEmpty()) { *query += QLatin1String("( "); const QLatin1String glue = logicOperatorToString(cond.mCombineOp); const Query::Condition::List &subConditions = cond.subConditions(); for (int i = 0, c = subConditions.size(); i < c; ++i) { buildWhereCondition(query, subConditions.at(i)); if (i + 1 < c) { *query += glue; } } *query += QLatin1String(" )"); } else { *query += cond.mColumn; *query += compareOperatorToString(cond.mCompareOp); if (cond.mComparedColumn.isEmpty()) { if (cond.mComparedValue.isValid()) { if (cond.mComparedValue.canConvert(QVariant::List)) { *query += QLatin1String("( "); const QVariantList &entries = cond.mComparedValue.toList(); Q_ASSERT_X(!entries.isEmpty(), "QueryBuilder::buildWhereCondition()", "No values given for IN condition."); for (int i = 0, c = entries.size(); i < c; ++i) { bindValue(query, entries.at(i)); if (i + 1 < c) { *query += QLatin1String(", "); } } *query += QLatin1String(" )"); } else { bindValue(query, cond.mComparedValue); } } else { *query += QLatin1String("NULL"); } } else { *query += cond.mComparedColumn; } } } void QueryBuilder::buildCaseStatement(QString *query, const Query::Case &caseStmt) { *query += QLatin1String("CASE "); Q_FOREACH (const auto &whenThen, caseStmt.mWhenThen) { *query += QLatin1String("WHEN "); buildWhereCondition(query, whenThen.first); // When *query += QLatin1String(" THEN ") + whenThen.second; // then } if (!caseStmt.mElse.isEmpty()) { *query += QLatin1String(" ELSE ") + caseStmt.mElse; } *query += QLatin1String(" END"); } void QueryBuilder::setSubQueryMode(Query::LogicOperator op, ConditionType type) { Q_ASSERT(type == WhereCondition || (type == HavingCondition && mType == Select)); mRootCondition[type].setSubQueryMode(op); } void QueryBuilder::addCondition(const Query::Condition &condition, ConditionType type) { Q_ASSERT(type == WhereCondition || (type == HavingCondition && mType == Select)); mRootCondition[type].addCondition(condition); } void QueryBuilder::addSortColumn(const QString &column, Query::SortOrder order) { mSortColumns << qMakePair(column, order); } void QueryBuilder::addGroupColumn(const QString &column) { Q_ASSERT(mType == Select); mGroupColumns << column; } void QueryBuilder::addGroupColumns(const QStringList &columns) { Q_ASSERT(mType == Select); mGroupColumns += columns; } void QueryBuilder::setColumnValue(const QString &column, const QVariant &value) { mColumnValues << qMakePair(column, value); } void QueryBuilder::setDistinct(bool distinct) { mDistinct = distinct; } void QueryBuilder::setLimit(int limit) { mLimit = limit; } void QueryBuilder::setIdentificationColumn(const QString &column) { mIdentificationColumn = column; } qint64 QueryBuilder::insertId() { if (mDatabaseType == DbType::PostgreSQL) { query().next(); if (mIdentificationColumn.isEmpty()) { return 0; // FIXME: Does this make sense? } return query().record().value(mIdentificationColumn).toLongLong(); } else { const QVariant v = query().lastInsertId(); if (!v.isValid()) { return -1; } bool ok; const qint64 insertId = v.toLongLong(&ok); if (!ok) { return -1; } return insertId; } return -1; } void QueryBuilder::setForUpdate(bool forUpdate) { mForUpdate = forUpdate; } diff --git a/src/server/utils.h b/src/server/utils.h index 6b614475e..fe268b0d3 100644 --- a/src/server/utils.h +++ b/src/server/utils.h @@ -1,136 +1,125 @@ /* * Copyright (C) 2010 Tobias Koenig * * 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. * */ #ifndef UTILS_H #define UTILS_H #include #include #include #include #include "storage/datastore.h" #include "storage/dbtype.h" namespace Akonadi { namespace Server { namespace Utils { /** * Converts a QVariant to a QString depending on its internal type. */ static inline QString variantToString(const QVariant &variant) { if (variant.type() == QVariant::String) { return variant.toString(); } else if (variant.type() == QVariant::ByteArray) { return QString::fromUtf8(variant.toByteArray()); } else { qWarning("Unable to convert variant of type %s to QString", variant.typeName()); Q_ASSERT(false); return QString(); } } /** * Converts a QVariant to a QByteArray depending on its internal type. */ static inline QByteArray variantToByteArray(const QVariant &variant) { if (variant.type() == QVariant::String) { return variant.toString().toUtf8(); } else if (variant.type() == QVariant::ByteArray) { return variant.toByteArray(); } else { qWarning("Unable to convert variant of type %s to QByteArray", variant.typeName()); Q_ASSERT(false); return QByteArray(); } } static inline QDateTime variantToDateTime(const QVariant &variant) { if (variant.canConvert(QVariant::DateTime)) { // MySQL and SQLite backends read the datetime from the database and // assume it's local time. We stored it as UTC though, so we just need // to change the interpretation in QDateTime. // PostgreSQL on the other hand reads the datetime and assumes it's // UTC(?) and converts it to local time via QDateTime::toLocalTime(), // so we need to convert it back to UTC manually. switch (DbType::type(DataStore::self()->database())) { case DbType::MySQL: case DbType::Sqlite: { QDateTime dt = variant.toDateTime(); dt.setTimeSpec(Qt::UTC); return dt; } case DbType::PostgreSQL: return variant.toDateTime().toUTC(); default: Q_UNREACHABLE(); } } else { qWarning("Unable to convert variant of type %s to QDateTime", variant.typeName()); Q_ASSERT(false); return QDateTime(); } } -template -static inline QSet vectorToSet(const QVector &v) -{ - QSet set; - set.reserve(v.size()); - for (const T &t : v) { - set.insert(t); - } - return set; -} - /** * Returns the socket @p directory that is passed to this method or the one * the user has overwritten via the config file. */ QString preferredSocketDirectory(const QString &directory); /** * Returns name of filesystem that @p directory is stored on. This * only works on Linux and returns empty string on other platforms or when it's * unable to detect the filesystem. */ QString getDirectoryFileSystem(const QString &directory); /** * Disables filesystem copy-on-write feature on given file or directory. * Only works on Linux and does nothing on other platforms. * * It was tested only with Btrfs but in theory can be called on any FS that * supports NOCOW. */ void disableCoW(const QString &path); } // namespace Utils } // namespace Server } // namespace Akonadi #endif diff --git a/src/shared/akranges.h b/src/shared/akranges.h new file mode 100644 index 000000000..fd91f453f --- /dev/null +++ b/src/shared/akranges.h @@ -0,0 +1,110 @@ +/* + Copyright (C) 2018 Daniel Vrátil + + 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. +*/ + +#ifndef AKONADI_AKRANGES_H +#define AKONADI_AKRANGES_H + +#include +#include + +#include +#include + +namespace Akonadi { + +namespace detail { + +struct ToQVector +{ + template using Container = QVector; +}; + +struct ToQSet +{ + template using Container = QSet; +}; + +struct ToQList +{ + template using Container = QList; +}; + +template +OutContainer copyContainer(const InContainer &in, std::true_type) +{ + OutContainer rv; + rv.reserve(in.size()); + std::copy(std::begin(in), std::end(in), std::back_inserter(rv)); + return rv; +} + +template +OutContainer copyContainer(const InContainer &in, std::false_type) +{ + OutContainer rv; + for (const auto &v : in) { + rv.insert(v); // can't use std::inserter on QSet, sadly :/ + } + return rv; +} + +template +using void_type = void; + +template class, typename = void_type<>> +struct has_method: std::false_type +{}; + +template class Op> +struct has_method>>: std::true_type +{}; + +template +using push_back = decltype(std::declval().push_back({})); + +} // namespace detail + +} // namespace Akonadi + +// Magic to pipe container with a toQFoo object as a conversion +template> +auto operator|(const InContainer &in, const OutFn &) -> OutContainer +{ + static_assert(std::is_same::value, + "We can only convert container types, not the value types."); + static_assert(!std::is_same::value, + "Wait, are you trying to convert a container to the same type?"); + + return Akonadi::detail::copyContainer( + in, Akonadi::detail::has_method{}); +} + + +namespace Akonadi { + +static constexpr auto toQVector = detail::ToQVector{}; +static constexpr auto toQSet = detail::ToQSet{}; +static constexpr auto toQList = detail::ToQList{}; + +} // namespace Akonadi + + +#endif