diff --git a/autotests/libs/collectionattributetest.cpp b/autotests/libs/collectionattributetest.cpp index fe5048648..9c79cafda 100644 --- a/autotests/libs/collectionattributetest.cpp +++ b/autotests/libs/collectionattributetest.cpp @@ -1,260 +1,260 @@ /* Copyright (c) 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 "collectionattributetest.h" #include "collectionpathresolver.h" #include "collection.h" #include "attributefactory.h" #include "collectioncreatejob.h" #include "collectiondeletejob.h" #include "collectionfetchjob.h" #include "collectionmodifyjob.h" #include "collectionrightsattribute_p.h" #include "control.h" #include "collectionidentificationattribute.h" #include "qtest_akonadi.h" using namespace Akonadi; QTEST_AKONADIMAIN(CollectionAttributeTest) class TestAttribute : public Attribute { public: TestAttribute() : Attribute() { } TestAttribute(const QByteArray &data) : mData(data) { } TestAttribute *clone() const override { return new TestAttribute(mData); } QByteArray type() const override { return "TESTATTRIBUTE"; } QByteArray serialized() const override { return mData; } void deserialize(const QByteArray &data) override { mData = data; } private: QByteArray mData; }; static int parentColId = -1; void CollectionAttributeTest::initTestCase() { AkonadiTest::checkTestIsIsolated(); Control::start(); AttributeFactory::registerAttribute(); CollectionPathResolver *resolver = new CollectionPathResolver(QStringLiteral("res3"), this); AKVERIFYEXEC(resolver); parentColId = resolver->collection(); QVERIFY(parentColId > 0); } void CollectionAttributeTest::testAttributes_data() { QTest::addColumn("attr1"); QTest::addColumn("attr2"); QTest::newRow("basic") << QByteArray("foo") << QByteArray("bar"); QTest::newRow("empty1") << QByteArray("") << QByteArray("non-empty"); #if 0 // This one is failing on the CI with SQLite. Can't reproduce locally and // it works with other DB backends, so I have no idea what is going on... QTest::newRow("empty2") << QByteArray("non-empty") << QByteArray(""); #endif QTest::newRow("space") << QByteArray("foo bar") << QByteArray("bar foo"); QTest::newRow("newline") << QByteArray("\n") << QByteArray("no newline"); QTest::newRow("newline2") << QByteArray(" \\\n\\\nnn") << QByteArray("no newline"); QTest::newRow("cr") << QByteArray("\r") << QByteArray("\\\r\n"); QTest::newRow("quotes") << QByteArray("\"quoted \\ test\"") << QByteArray("single \" quote \\"); QTest::newRow("parenthesis") << QByteArray(")") << QByteArray("("); QTest::newRow("binary") << QByteArray("\000") << QByteArray("\001"); } void CollectionAttributeTest::testAttributes() { QFETCH(QByteArray, attr1); QFETCH(QByteArray, attr2); struct Cleanup { Cleanup(const Collection &col) : m_col(col) {} ~Cleanup() { // cleanup CollectionDeleteJob *del = new CollectionDeleteJob(m_col); AKVERIFYEXEC(del); } Collection m_col; }; // add a custom attribute TestAttribute *attr = new TestAttribute(); attr->deserialize(attr1); Collection col; col.setName(QStringLiteral("attribute test")); col.setParentCollection(Collection(parentColId)); col.addAttribute(attr); CollectionCreateJob *create = new CollectionCreateJob(col, this); AKVERIFYEXEC(create); col = create->collection(); QVERIFY(col.isValid()); Cleanup cleanup(col); attr = col.attribute(); QVERIFY(attr != nullptr); QCOMPARE(attr->serialized(), QByteArray(attr1)); CollectionFetchJob *list = new CollectionFetchJob(col, CollectionFetchJob::Base, this); AKVERIFYEXEC(list); QCOMPARE(list->collections().count(), 1); - col = list->collections().first(); + col = list->collections().at(0); QVERIFY(col.isValid()); attr = col.attribute(); QVERIFY(attr != nullptr); QCOMPARE(attr->serialized(), QByteArray(attr1)); TestAttribute *attrB = new TestAttribute(); attrB->deserialize(attr2); col.addAttribute(attrB); attrB = col.attribute(); QVERIFY(attrB != nullptr); QCOMPARE(attrB->serialized(), QByteArray(attr2)); attrB->deserialize(attr1); col.addAttribute(attrB); attrB = col.attribute(); QVERIFY(attrB != nullptr); QCOMPARE(attrB->serialized(), QByteArray(attr1)); // this will mark the attribute as modified in the storage, but should not create trouble further down QVERIFY(!col.attribute("does_not_exist")); // modify a custom attribute col.attribute(Collection::AddIfMissing)->deserialize(attr2); CollectionModifyJob *modify = new CollectionModifyJob(col, this); AKVERIFYEXEC(modify); list = new CollectionFetchJob(col, CollectionFetchJob::Base, this); AKVERIFYEXEC(list); QCOMPARE(list->collections().count(), 1); - col = list->collections().first(); + col = list->collections().at(0); QVERIFY(col.isValid()); attr = col.attribute(); QVERIFY(attr != nullptr); QCOMPARE(attr->serialized(), QByteArray(attr2)); // delete a custom attribute col.removeAttribute(); modify = new CollectionModifyJob(col, this); AKVERIFYEXEC(modify); list = new CollectionFetchJob(col, CollectionFetchJob::Base, this); AKVERIFYEXEC(list); QCOMPARE(list->collections().count(), 1); - col = list->collections().first(); + col = list->collections().at(0); QVERIFY(col.isValid()); attr = col.attribute(); QVERIFY(attr == nullptr); // Give the knut resource a bit of time to modify the collection and add a remote ID (after adding) // and reparent attributes (after modifying). // Otherwise we can delete it faster than it can do that, and we end up with a confusing warning // "No such collection" from the resource's modify job. QTest::qWait(100); // ideally we'd loop over "fetch and check there's a remote id" } void CollectionAttributeTest::testDefaultAttributes() { Collection col; QCOMPARE(col.attributes().count(), 0); Attribute *attr = AttributeFactory::createAttribute("TYPE"); QVERIFY(attr); attr->deserialize("VALUE"); col.addAttribute(attr); QCOMPARE(col.attributes().count(), 1); QVERIFY(col.hasAttribute("TYPE")); QCOMPARE(col.attribute("TYPE")->serialized(), QByteArray("VALUE")); } void CollectionAttributeTest::testCollectionRightsAttribute() { CollectionRightsAttribute attribute; Collection::Rights rights; QCOMPARE(attribute.rights(), rights); for (int mask = 0; mask <= Collection::AllRights; ++mask) { rights = Collection::AllRights; rights &= mask; QCOMPARE(rights, mask); attribute.setRights(rights); QCOMPARE(attribute.rights(), rights); QByteArray data = attribute.serialized(); attribute.deserialize(data); QCOMPARE(attribute.rights(), rights); } } void CollectionAttributeTest::testCollectionIdentificationAttribute() { QByteArray id("identifier"); QByteArray ns("namespace"); CollectionIdentificationAttribute attribute(id, ns); QCOMPARE(attribute.identifier(), id); QCOMPARE(attribute.collectionNamespace(), ns); QByteArray result = attribute.serialized(); CollectionIdentificationAttribute parsed; parsed.deserialize(result); qDebug() << parsed.identifier() << parsed.collectionNamespace() << result; QCOMPARE(parsed.identifier(), id); QCOMPARE(parsed.collectionNamespace(), ns); } void CollectionAttributeTest::testDetach() { // GIVEN a collection with an attribute Collection col; col.attribute(Akonadi::Collection::AddIfMissing); Collection col2 = col; // and a copy, so that non-const access detaches // WHEN TestAttribute *attr = col2.attribute(Akonadi::Collection::AddIfMissing); TestAttribute *attr2 = col2.attribute(); // THEN QCOMPARE(attr, attr2); } diff --git a/autotests/private/akdbustest.cpp b/autotests/private/akdbustest.cpp index 3b2ecb741..aed447cd5 100644 --- a/autotests/private/akdbustest.cpp +++ b/autotests/private/akdbustest.cpp @@ -1,137 +1,137 @@ /* Copyright (c) 2011 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 #include #include #include using namespace Akonadi; Q_DECLARE_METATYPE(DBus::AgentType) class DBusTest : public QObject { Q_OBJECT private Q_SLOTS: void testServiceName() { akTestSetInstanceIdentifier(QString()); QCOMPARE(DBus::serviceName(DBus::Server), QLatin1String("org.freedesktop.Akonadi")); akTestSetInstanceIdentifier(QStringLiteral("foo")); QCOMPARE(DBus::serviceName(DBus::Server), QLatin1String("org.freedesktop.Akonadi.foo")); } void testParseAgentServiceName_data() { QTest::addColumn("instanceId"); QTest::addColumn("serviceName"); QTest::addColumn("agentId"); QTest::addColumn("agentType"); QTest::addColumn("valid"); // generic invalid QTest::newRow("empty") << QString() << QString() << QString() << DBus::Unknown << false; QTest::newRow("wrong base") << QString() << "org.freedesktop.Agent.foo" << QString() << DBus::Unknown << false; QTest::newRow("wrong type") << QString() << "org.freedesktop.Akonadi.Randomizer.akonadi_maildir_resource_0" << QString() << DBus::Unknown << false; QTest::newRow("too long") << QString() << "org.freedesktop.Akonadi.Agent.akonadi_maildir_resource_0.foo.bar" << QString() << DBus::Unknown << false; // single instance cases QTest::newRow("agent, no multi-instance") << QString() << "org.freedesktop.Akonadi.Agent.akonadi_maildir_resource_0" << "akonadi_maildir_resource_0" << DBus::Agent << true; QTest::newRow("resource, no multi-instance") << QString() << "org.freedesktop.Akonadi.Resource.akonadi_maildir_resource_0" << "akonadi_maildir_resource_0" << DBus::Resource << true; QTest::newRow("preproc, no multi-instance") << QString() << "org.freedesktop.Akonadi.Preprocessor.akonadi_maildir_resource_0" << "akonadi_maildir_resource_0" << DBus::Preprocessor << true; QTest::newRow("multi-instance name in single-instance setup") << QString() << "org.freedesktop.Akonadi.Agent.akonadi_maildir_resource_0.foo" << QString() << DBus::Unknown << false; // multi-instance cases QTest::newRow("agent, multi-instance") << "foo" << "org.freedesktop.Akonadi.Agent.akonadi_maildir_resource_0.foo" << "akonadi_maildir_resource_0" << DBus::Agent << true; QTest::newRow("resource, multi-instance") << "foo" << "org.freedesktop.Akonadi.Resource.akonadi_maildir_resource_0.foo" << "akonadi_maildir_resource_0" << DBus::Resource << true; QTest::newRow("preproc, multi-instance") << "foo" << "org.freedesktop.Akonadi.Preprocessor.akonadi_maildir_resource_0.foo" << "akonadi_maildir_resource_0" << DBus::Preprocessor << true; QTest::newRow("single-instance name in multi-instance setup") << "foo" << "org.freedesktop.Akonadi.Agent.akonadi_maildir_resource_0" << QString() << DBus::Unknown << false; } void testParseAgentServiceName() { QFETCH(QString, instanceId); QFETCH(QString, serviceName); QFETCH(QString, agentId); QFETCH(DBus::AgentType, agentType); QFETCH(bool, valid); akTestSetInstanceIdentifier(instanceId); const auto service = DBus::parseAgentServiceName(serviceName); QCOMPARE(service.has_value(), valid); if (service.has_value()) { QCOMPARE(service->identifier, agentId); QCOMPARE(service->agentType, agentType); } } void testAgentServiceName() { akTestSetInstanceIdentifier(QString()); - QCOMPARE(DBus::agentServiceName(QLatin1String("akonadi_maildir_resource_0"), DBus::Agent), QLatin1String("org.freedesktop.Akonadi.Agent.akonadi_maildir_resource_0")); + QCOMPARE(DBus::agentServiceName(QStringLiteral("akonadi_maildir_resource_0"), DBus::Agent), QStringLiteral("org.freedesktop.Akonadi.Agent.akonadi_maildir_resource_0")); akTestSetInstanceIdentifier(QStringLiteral("foo")); - QCOMPARE(DBus::agentServiceName(QLatin1String("akonadi_maildir_resource_0"), DBus::Agent), QLatin1String("org.freedesktop.Akonadi.Agent.akonadi_maildir_resource_0.foo")); + QCOMPARE(DBus::agentServiceName(QStringLiteral("akonadi_maildir_resource_0"), DBus::Agent), QStringLiteral("org.freedesktop.Akonadi.Agent.akonadi_maildir_resource_0.foo")); } void testParseInstanceIdentifier_data() { QTest::addColumn("serviceName"); QTest::addColumn("hasInstance"); QTest::newRow("server") << QStringLiteral("org.freedesktop.Akonadi") << false; QTest::newRow("server instance") << QStringLiteral("org.freedesktop.Akonadi.instance") << true; QTest::newRow("control") << QStringLiteral("org.freedesktop.Akonadi.Control") << false; QTest::newRow("control instance") << QStringLiteral("org.freedesktop.Akonadi.Control.instance") << true; QTest::newRow("control lock") << QStringLiteral("org.freedesktop.Akonadi.Control.lock") << false; QTest::newRow("control lock instance") << QStringLiteral("org.freedesktop.Akonadi.Control.lock.instance") << true; QTest::newRow("janitor") << QStringLiteral("org.freedesktop.Akonadi.Janitor") << false; QTest::newRow("janitor instance") << QStringLiteral("org.freedesktop.Akonadi.Janitor.instance") << true; QTest::newRow("agentserver") << QStringLiteral("org.freedesktop.Akonadi.AgentServer") << false; QTest::newRow("agentserver instance") << QStringLiteral("org.freedesktop.Akonadi.AgentServer.instance") << true; QTest::newRow("upgrading") << QStringLiteral("org.freedesktop.Akonadi.upgrading") << false; QTest::newRow("upgrading instance") << QStringLiteral("org.freedesktop.Akonadi.upgrading.instance") << true; QTest::newRow("agent") << QStringLiteral("org.freedesktop.Akonadi.Agent.akonadi_agent_identifier") << false; QTest::newRow("agent instance") << QStringLiteral("org.freedesktop.Akonadi.Agent.akonadi_agent_identifier.instance") << true; QTest::newRow("resource") << QStringLiteral("org.freedesktop.Akonadi.Resource.akonadi_resource_identifier") << false; QTest::newRow("resource instance") << QStringLiteral("org.freedesktop.Akonadi.Resource.akonadi_resource_identifier.instance") << true; QTest::newRow("preprocessor") << QStringLiteral("org.freedesktop.Akonadi.Preprocessor.akonadi_preprocessor_identifier") << false; QTest::newRow("preprocessor instance") << QStringLiteral("org.freedesktop.Akonadi.Preprocessor.akonadi_preprocessor_identifier.instance") << true; } void testParseInstanceIdentifier() { QFETCH(QString, serviceName); QFETCH(bool, hasInstance); const auto identifier = DBus::parseInstanceIdentifier(serviceName); QCOMPARE(identifier.has_value(), hasInstance); if (hasInstance) { QCOMPARE(identifier.value(), QStringLiteral("instance")); } } }; AKTEST_MAIN(DBusTest) #include "akdbustest.moc" diff --git a/autotests/shared/akrangestest.cpp b/autotests/shared/akrangestest.cpp index 0baf49fb0..5aadad836 100644 --- a/autotests/shared/akrangestest.cpp +++ b/autotests/shared/akrangestest.cpp @@ -1,422 +1,423 @@ /* 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. */ #include #include #include #include using namespace AkRanges; namespace { int transformFreeFunc(int i) { return i * 2; } struct TransformHelper { public: static int transform(int i) { return transformFreeFunc(i); } int operator()(int i) const { return transformFreeFunc(i); } }; bool filterFreeFunc(int i) { return i % 2 == 0; } struct FilterHelper { public: static bool filter(int i) { return filterFreeFunc(i); } bool operator()(int i) { return filterFreeFunc(i); } }; } class AkRangesTest : public QObject { Q_OBJECT private Q_SLOTS: void initTestCase() { qSetGlobalQHashSeed(0); } void testTraits() { QVERIFY(AkTraits::isAppendable>); QVERIFY(!AkTraits::isInsertable>); QVERIFY(AkTraits::isReservable>); QVERIFY(!AkTraits::isAppendable>); QVERIFY(AkTraits::isInsertable>); QVERIFY(AkTraits::isReservable>); QVERIFY(!AkTraits::isAppendable); QVERIFY(!AkTraits::isInsertable); QVERIFY(AkTraits::isReservable); } void testContainerConversion() { { QVector in = { 1, 2, 3, 4, 5 }; QCOMPARE(in | Actions::toQList, in.toList()); QCOMPARE(in | Actions::toQList | Actions::toQVector, in); - QCOMPARE(in | Actions::toQSet, in.toList().toSet()); + QCOMPARE(in | Actions::toQSet, in.toList().toSet()); // clazy:exclude=container-anti-pattern } { QList in = { 1, 2, 3, 4, 5 }; QCOMPARE(in | Actions::toQVector, in.toVector()); QCOMPARE(in | Actions::toQVector | Actions::toQList, in); QCOMPARE(in | Actions::toQSet, in.toSet()); } } void testAssociativeContainerConversion() { QVector> in = {{1, QStringLiteral("One")}, {2, QStringLiteral("Two")}, {3, QStringLiteral("Three")}}; QMap out = {{1, QStringLiteral("One")}, {2, QStringLiteral("Two")}, {3, QStringLiteral("Three")}}; QCOMPARE(in | Actions::toQMap, out); } void testRangeConversion() { { QList in = { 1, 2, 3, 4, 5 }; AkRanges::detail::Range::const_iterator> range(in.cbegin(), in.cend()); QCOMPARE(range | Actions::toQVector, QVector::fromList(in)); } { QVector in = { 1, 2, 3, 4, 5 }; AkRanges::detail::Range::const_iterator> range(in.cbegin(), in.cend()); QCOMPARE(range | Actions::toQList, in.toList()); } { QVector> in = {{1, QStringLiteral("One")}, {2, QStringLiteral("Two")}, {3, QStringLiteral("Three")}}; QMap out = {{1, QStringLiteral("One")}, {2, QStringLiteral("Two")}, {3, QStringLiteral("Three")}}; AkRanges::detail::Range>::const_iterator> range(in.cbegin(), in.cend()); QCOMPARE(range | Actions::toQMap, out); } } void testTransform() { QList in = { 1, 2, 3, 4, 5 }; QList out = { 2, 4, 6, 8, 10 }; QCOMPARE(in | Views::transform([](int i) { return i * 2; }) | Actions::toQList, out); QCOMPARE(in | Views::transform(transformFreeFunc) | Actions::toQList, out); QCOMPARE(in | Views::transform(&TransformHelper::transform) | Actions::toQList, out); QCOMPARE(in | Views::transform(TransformHelper()) | Actions::toQList, out); } private: class CopyCounter { public: CopyCounter() = default; CopyCounter(const CopyCounter &other) : copyCount(other.copyCount + 1), transformed(other.transformed) {} CopyCounter(CopyCounter &&other) = default; CopyCounter &operator=(const CopyCounter &other) { copyCount = other.copyCount + 1; transformed = other.transformed; return *this; } CopyCounter &operator=(CopyCounter &&other) = default; + ~CopyCounter() = default; int copyCount = 0; bool transformed = false; }; private Q_SLOTS: void testTransformCopyCount() { { QList in = { {} }; // 1st copy (QList::append()) QList out = in | Views::transform([](const auto &c) { CopyCounter r(c); // 2nd copy (expected) r.transformed = true; return r; }) | Actions::toQList; // 3rd copy (QList::append()) QCOMPARE(out.size(), in.size()); QCOMPARE(out[0].copyCount, 3); QCOMPARE(out[0].transformed, true); } { QVector in(1); // construct vector of one element, so no copying // occurs at initialization QVector out = in | Views::transform([](const auto &c) { CopyCounter r(c); // 1st copy r.transformed = true; return r; }) | Actions::toQVector; QCOMPARE(out.size(), in.size()); QCOMPARE(out[0].copyCount, 1); QCOMPARE(out[0].transformed, true); } } void testTransformConvert() { { QList in = { 1, 2, 3, 4, 5 }; QVector out = { 2, 4, 6, 8, 10 }; QCOMPARE(in | Views::transform([](int i) { return i * 2; }) | Actions::toQVector, out); } { QVector in = { 1, 2, 3, 4, 5 }; QList out = { 2, 4, 6, 8, 10 }; QCOMPARE(in | Views::transform([](int i) { return i * 2; }) | Actions::toQList, out); } } void testCreateRange() { { QList in = { 1, 2, 3, 4, 5, 6 }; QList out = { 3, 4, 5 }; QCOMPARE(Views::range(in.begin() + 2, in.begin() + 5) | Actions::toQList, out); } } void testRangeWithTransform() { { QList in = { 1, 2, 3, 4, 5, 6 }; QList out = { 6, 8, 10 }; QCOMPARE(Views::range(in.begin() + 2, in.begin() + 5) | Views::transform([](int i) { return i * 2; }) | Actions::toQList, out); } } void testTransformType() { { QStringList in = { QStringLiteral("foo"), QStringLiteral("foobar"), QStringLiteral("foob") }; QList out = { 3, 6, 4 }; QCOMPARE(in | Views::transform([](const auto &str) { return str.size(); }) | Actions::toQList, out); } } void testFilter() { { QList in = { 1, 2, 3, 4, 5, 6, 7, 8 }; QList out = { 2, 4, 6, 8 }; QCOMPARE(in | Views::filter([](int i) { return i % 2 == 0; }) | Actions::toQList, out); QCOMPARE(in | Views::filter(filterFreeFunc) | Actions::toQList, out); QCOMPARE(in | Views::filter(&FilterHelper::filter) | Actions::toQList, out); QCOMPARE(in | Views::filter(FilterHelper()) | Actions::toQList, out); } } void testFilterTransform() { { QStringList in = { QStringLiteral("foo"), QStringLiteral("foobar"), QStringLiteral("foob") }; QList out = { 6 }; QCOMPARE(in | Views::transform(&QString::size) | Views::filter([](int i) { return i > 5; }) | Actions::toQList, out); QCOMPARE(in | Views::filter([](const auto &str) { return str.size() > 5; }) | Views::transform(&QString::size) | Actions::toQList, out); } } void testTemporaryContainer() { const auto func = []{ QStringList rv; for (int i = 0; i < 5; i++) { rv.push_back(QString::number(i)); } return rv; }; { QList out = { 0, 2, 4 }; QCOMPARE(func() | Views::transform([](const auto &str) { return str.toInt(); }) | Views::filter([](int i) { return i % 2 == 0; }) | Actions::toQList, out); } { QList out = { 0, 2, 4 }; QCOMPARE(func() | Views::filter([](const auto &v) { return v.toInt() % 2 == 0; }) | Views::transform([](const auto &str) { return str.toInt(); }) | Actions::toQList, out); } } void testTemporaryRange() { const auto func = []{ QStringList rv; for (int i = 0; i < 5; ++i) { rv.push_back(QString::number(i)); } return rv | Views::transform([](const auto &str) { return str.toInt(); }); }; QList out = { 1, 3 }; QCOMPARE(func() | Views::filter([](int i) { return i % 2 == 1; }) | Actions::toQList, out); } private: struct ForEachCallable { public: ForEachCallable(QList &out) : mOut(out) {} void operator()(int i) { mOut.push_back(i); } static void append(int i) { sOut.push_back(i); } static void clear() { sOut.clear(); } static QList sOut; private: QList &mOut; }; private Q_SLOTS: void testForEach() { const QList in = { 1, 2, 3, 4, 5, 6 }; { QList out; in | Actions::forEach([&out](int v) { out.push_back(v); }); QCOMPARE(out, in); } { QList out; in | Actions::forEach(ForEachCallable(out)); QCOMPARE(out, in); } { ForEachCallable::clear(); in | Actions::forEach(&ForEachCallable::append); QCOMPARE(ForEachCallable::sOut, in); } { QList out; QCOMPARE(in | Actions::forEach([&out](int v) { out.push_back(v); }) | Views::filter([](int v){ return v % 2 == 0; }) | Views::transform([](int v) { return v * 2; }) | Actions::toQList, QList({ 4, 8, 12 })); QCOMPARE(out, in); } } private: template class Container> void testKeysValuesHelper() { const Container in = { { 1, QStringLiteral("1") }, { 2, QStringLiteral("2") }, { 3, QStringLiteral("3") } }; { const QList out = { 1, 2, 3 }; QCOMPARE(out, in | Views::keys | Actions::toQList); } { const QStringList out = { QStringLiteral("1"), QStringLiteral("2"), QStringLiteral("3") }; QCOMPARE(out, in | Views::values | Actions::toQList); } } private Q_SLOTS: void testKeysValues() { testKeysValuesHelper(); testKeysValuesHelper(); } void testAll() { const QList vals = { 2, 4, 6, 8, 10 }; QVERIFY(vals | Actions::all([](int v) { return v % 2 == 0; })); QVERIFY(!(vals | Actions::all([](int v) { return v % 2 == 1; }))); } void testAny() { const QList vals = { 1, 3, 5, 7, 9 }; QVERIFY(vals | Actions::any([](int v) { return v % 2 == 1; })); QVERIFY(!(vals | Actions::any([](int v) { return v % 2 == 0; }))); } void testNone() { const QList vals = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; QVERIFY(vals | Views::filter([](int i) { return i % 2 == 0; }) | Actions::none([](int i) { return i % 2 == 1; })); QVERIFY(!(vals | Views::filter([](int i) { return i % 2 == 0; }) | Actions::none([](int i) { return i % 2 == 0; }))); } }; QList AkRangesTest::ForEachCallable::sOut; QTEST_GUILESS_MAIN(AkRangesTest) #include "akrangestest.moc" diff --git a/src/akonadicontrol/agentmanager.h b/src/akonadicontrol/agentmanager.h index 68a9b13d5..8389a99ee 100644 --- a/src/akonadicontrol/agentmanager.h +++ b/src/akonadicontrol/agentmanager.h @@ -1,391 +1,390 @@ /*************************************************************************** * Copyright (C) 2006 by Tobias Koenig * * Copyright (c) 2007 Volker Krause * * * * 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. * ***************************************************************************/ #ifndef AGENTMANAGER_H #define AGENTMANAGER_H #include #include #include "agenttype.h" #include "agentinstance.h" class QDir; namespace Akonadi { class ProcessControl; } /** * The agent manager has knowledge about all available agents (it scans * for .desktop files in the agent directory) and the available configured * instances. */ class AgentManager : public QObject, protected QDBusContext { Q_OBJECT Q_CLASSINFO("D-Bus Interface", "org.freedesktop.Akonadi.AgentManager") public: /** * Creates a new agent manager. * * @param parent The parent object. */ explicit AgentManager(bool verbose, QObject *parent = nullptr); /** * Destroys the agent manager. */ ~AgentManager(); /** * Called by the crash handler and dtor to terminate * the child processes. */ void cleanup(); -public Q_SLOTS: /** * Returns the list of identifiers of all available * agent types. */ QStringList agentTypes() const; /** * Returns the i18n'ed name of the agent type for * the given @p identifier. */ QString agentName(const QString &identifier) const; /** * Returns the i18n'ed comment of the agent type for * the given @p identifier.. */ QString agentComment(const QString &identifier) const; /** * Returns the icon name of the agent type for the * given @p identifier. */ QString agentIcon(const QString &identifier) const; /** * Returns a list of supported mimetypes of the agent type * for the given @p identifier. */ QStringList agentMimeTypes(const QString &identifier) const; /** * Returns a list of supported capabilities of the agent type * for the given @p identifier. */ QStringList agentCapabilities(const QString &identifier) const; /** * Returns a list of Custom added propeties of the agent type * for the given @p identifier * @since 1.11 */ QVariantMap agentCustomProperties(const QString &identifier) const; /** * Creates a new agent of the given agent type @p identifier. * * @return The identifier of the new agent if created successfully, * an empty string otherwise. * The identifier consists of two parts, the type of the * agent and an unique instance number, and looks like * the following: 'file_1' or 'imap_267'. */ QString createAgentInstance(const QString &identifier); /** * Removes the agent with the given @p identifier. */ void removeAgentInstance(const QString &identifier); /** * Returns the type of the agent instance with the given @p identifier. */ QString agentInstanceType(const QString &identifier); /** * Returns the list of identifiers of configured instances. */ QStringList agentInstances() const; /** * Returns the current status code of the agent with the given @p identifier. */ int agentInstanceStatus(const QString &identifier) const; /** * Returns the i18n'ed description of the current status of the agent with * the given @p identifier. */ QString agentInstanceStatusMessage(const QString &identifier) const; /** * Returns the current progress of the agent with the given @p identifier * in percentage. */ uint agentInstanceProgress(const QString &identifier) const; /** * Returns the i18n'ed description of the current progress of the agent with * the given @p identifier. */ QString agentInstanceProgressMessage(const QString &identifier) const; /** * Sets the @p name of the agent instance with the given @p identifier. */ void setAgentInstanceName(const QString &identifier, const QString &name); /** * Returns the name of the agent instance with the given @p identifier. */ QString agentInstanceName(const QString &identifier) const; /** * Triggers the agent instance with the given @p identifier to show * its configuration dialog. * @param windowId Parent window id for the configuration dialog. */ void agentInstanceConfigure(const QString &identifier, qlonglong windowId); /** * Triggers the agent instance with the given @p identifier to start * synchronization. */ void agentInstanceSynchronize(const QString &identifier); /** Trigger a synchronization of the collection tree by the given resource agent. @param identifier The resource agent identifier. */ void agentInstanceSynchronizeCollectionTree(const QString &identifier); /** Trigger a synchronization of the given collection by its owning resource agent. */ void agentInstanceSynchronizeCollection(const QString &identifier, qint64 collection); /** Trigger a synchronization of the given collection by its owning resource agent. @param recursive set it true to have sub-collection synchronized as well */ void agentInstanceSynchronizeCollection(const QString &identifier, qint64 collection, bool recursive); /** * Trigger a synchronization of tags by the given resource agent. * @param identifier The resource agent identifier. */ void agentInstanceSynchronizeTags(const QString &identifier); /** * Trigger a synchronization of relations by the given resource agent. * @param identifier The resource agent identifier. */ void agentInstanceSynchronizeRelations(const QString &identifier); /** Returns if the agent instance @p identifier is in online mode. */ bool agentInstanceOnline(const QString &identifier); /** Sets agent instance @p identifier to online or offline mode. */ void setAgentInstanceOnline(const QString &identifier, bool state); /** Restarts the agent instance @p identifier. This is supposed to be used as a development aid and not something to use during normal operations. */ void restartAgentInstance(const QString &identifier); /** * Add a persistent search to remote search agents. */ void addSearch(const QString &query, const QString &queryLanguage, qint64 resultCollectionId); /** * Removes a persistent search for the given result collection. */ void removeSearch(quint64 resultCollectionId); Q_SIGNALS: /** * This signal is emitted whenever a new agent type was installed on the system. * * @param agentType The identifier of the new agent type. */ void agentTypeAdded(const QString &agentType); /** * This signal is emitted whenever an agent type was removed from the system. * * @param agentType The identifier of the removed agent type. */ void agentTypeRemoved(const QString &agentType); /** * This signal is emitted whenever a new agent instance was created. * * @param agentIdentifier The identifier of the new agent instance. */ void agentInstanceAdded(const QString &agentIdentifier); /** * This signal is emitted whenever an agent instance was removed. * * @param agentIdentifier The identifier of the removed agent instance. */ void agentInstanceRemoved(const QString &agentIdentifier); /** * This signal is emitted whenever the status of an agent instance has * changed. * * @param agentIdentifier The identifier of the agent that has changed. * @param status The new status code. * @param message The i18n'ed description of the new status. */ void agentInstanceStatusChanged(const QString &agentIdentifier, int status, const QString &message); /** * This signal is emitted whenever the status of an agent instance has * changed. * * @param agentIdentifier The identifier of the agent that has changed. * @param status The object that describes the status change. */ void agentInstanceAdvancedStatusChanged(const QString &agentIdentifier, const QVariantMap &status); /** * This signal is emitted whenever the progress of an agent instance has * changed. * * @param agentIdentifier The identifier of the agent that has changed. * @param progress The new progress in percentage. * @param message The i18n'ed description of the new progress. */ void agentInstanceProgressChanged(const QString &agentIdentifier, uint progress, const QString &message); /** * This signal is emitted whenever an agent instance raised a warning. * * @param agentIdentifier The identifier of the agent instance. * @param message The i18n'ed warning message. */ void agentInstanceWarning(const QString &agentIdentifier, const QString &message); /** * This signal is emitted whenever an agent instance raised an error. * * @param agentIdentifier The identifier of the agent instance. * @param message The i18n'ed error message. */ void agentInstanceError(const QString &agentIdentifier, const QString &message); /** * This signal is emitted whenever the name of the agent instance has changed. * * @param agentIdentifier The identifier of the agent that has changed. * @param name The new name of the agent instance. */ void agentInstanceNameChanged(const QString &agentIdentifier, const QString &name); /** * Emitted when the online state of an agent changed. */ void agentInstanceOnlineChanged(const QString &agentIdentifier, bool state); private Q_SLOTS: void updatePluginInfos(); void serviceOwnerChanged(const QString &name, const QString &oldOwner, const QString &newOwner); void agentExeChanged(const QString &fileName); private: /** * Returns the list of directory paths where the .desktop files * for the plugins are located. */ static QStringList pluginInfoPathList(); /** * Loads the internal state from config file. */ void load(); /** * Saves internal state to the config file. */ void save(); /** * Reads the plugin information from directory. */ void readPluginInfos(); /** * Reads the plugin information from directory. * * @param directory the directory to get plugin information from */ void readPluginInfos(const QDir &directory); AgentInstance::Ptr createAgentInstance(const AgentType &type); bool checkAgentInterfaces(const QString &identifier, const QString &method) const; bool checkInstance(const QString &identifier) const; bool checkResourceInterface(const QString &identifier, const QString &method) const; bool checkAgentExists(const QString &identifier) const; void ensureAutoStart(const AgentType &info); void continueStartup(); void registerAgentAtServer(const QString &agentIdentifier, const AgentType &type); private: /** * The map which stores the .desktop file * entries for every agent type. * * Key is the agent type (e.g. 'file' or 'imap'). */ QHash mAgents; /** * The map which stores the active instances. * * Key is the instance identifier. */ QHash mAgentInstances; std::unique_ptr mAgentServer; std::unique_ptr mStorageController; bool mAgentServerEnabled = false; bool mVerbose = false; friend class AgentInstance; }; #endif diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 915a19bc3..bc86fd465 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -1,349 +1,351 @@ configure_file(akonaditests_export.h.in "${CMAKE_CURRENT_BINARY_DIR}/akonaditests_export.h") set(akonadicore_base_SRCS agentconfigurationbase.cpp agentconfigurationfactorybase.cpp agentconfigurationmanager.cpp agentinstance.cpp agentmanager.cpp agenttype.cpp asyncselectionhandler.cpp attribute.cpp attributefactory.cpp attributestorage.cpp braveheart.cpp cachepolicy.cpp changemediator_p.cpp changenotification.cpp changenotificationdependenciesfactory.cpp changerecorder.cpp changerecorder_p.cpp changerecorderjournal.cpp connection.cpp collection.cpp collectioncolorattribute.cpp collectionfetchscope.cpp collectionpathresolver.cpp collectionquotaattribute.cpp collectionquotaattribute.cpp collectionrightsattribute.cpp collectionstatistics.cpp collectionsync.cpp conflicthandler.cpp collectionidentificationattribute.cpp control.cpp entityannotationsattribute.cpp entitycache.cpp entitydeletedattribute.cpp entitydeletedattribute.cpp entitydisplayattribute.cpp entityhiddenattribute.cpp exception.cpp favoritecollectionattribute.cpp firstrun.cpp gidextractor.cpp indexpolicyattribute.cpp item.cpp itemchangelog.cpp itemfetchscope.cpp itemmonitor.cpp itemserializer.cpp itemserializerplugin.cpp itemsync.cpp mimetypechecker.cpp monitor.cpp monitor_p.cpp notificationsource_p.cpp notificationsubscriber.cpp partfetcher.cpp pastehelper.cpp persistentsearchattribute.cpp pluginloader.cpp protocolhelper.cpp remotelog.cpp relation.cpp relationsync.cpp searchquery.cpp servermanager.cpp session.cpp sessionthread.cpp specialcollectionattribute.cpp specialcollections.cpp tag.cpp tagattribute.cpp tagfetchscope.cpp tagsync.cpp trashsettings.cpp typepluginloader.cpp ) ecm_generate_headers(AkonadiCore_base_HEADERS HEADER_NAMES AbstractDifferencesReporter AgentConfigurationBase AgentConfigurationFactoryBase AgentInstance AgentManager AgentType Attribute AttributeFactory CachePolicy ChangeNotification ChangeRecorder Collection CollectionColorAttribute CollectionFetchScope CollectionQuotaAttribute CollectionStatistics CollectionUtils CollectionIdentificationAttribute Control DifferencesAlgorithmInterface EntityAnnotationsAttribute EntityDeletedAttribute EntityDisplayAttribute EntityHiddenAttribute ExceptionBase FavoriteCollectionAttribute GidExtractorInterface IndexPolicyAttribute Item ItemFetchScope ItemMonitor ItemSerializerPlugin ItemSync MimeTypeChecker NotificationSubscriber Monitor PartFetcher PersistentSearchAttribute Relation SearchQuery ServerManager Session SpecialCollections SpecialCollectionAttribute Supertrait Tag TagAttribute TagFetchScope TrashSettings CollectionPathResolver REQUIRED_HEADERS AkonadiCore_base_HEADERS ) set(akonadicore_models_SRCS models/agentfilterproxymodel.cpp models/agentinstancemodel.cpp models/agenttypemodel.cpp models/collectionfilterproxymodel.cpp models/entitymimetypefiltermodel.cpp models/entityorderproxymodel.cpp models/entityrightsfiltermodel.cpp models/entitytreemodel.cpp models/entitytreemodel_p.cpp models/favoritecollectionsmodel.cpp models/recursivecollectionfilterproxymodel.cpp models/selectionproxymodel.cpp models/statisticsproxymodel.cpp models/subscriptionmodel.cpp models/tagmodel.cpp models/tagmodel_p.cpp models/trashfilterproxymodel.cpp ) ecm_generate_headers(AkonadiCore_models_HEADERS HEADER_NAMES AgentFilterProxyModel AgentInstanceModel AgentTypeModel CollectionFilterProxyModel EntityMimeTypeFilterModel EntityOrderProxyModel EntityRightsFilterModel EntityTreeModel FavoriteCollectionsModel RecursiveCollectionFilterProxyModel SelectionProxyModel StatisticsProxyModel TagModel TrashFilterProxyModel REQUIRED_HEADERS AkonadiCore_models_HEADERS RELATIVE models ) set(akonadicore_jobs_SRCS jobs/agentinstancecreatejob.cpp jobs/collectionattributessynchronizationjob.cpp jobs/collectioncopyjob.cpp jobs/collectioncreatejob.cpp jobs/collectiondeletejob.cpp jobs/collectionfetchjob.cpp jobs/collectionmodifyjob.cpp jobs/collectionmovejob.cpp jobs/collectionstatisticsjob.cpp jobs/invalidatecachejob.cpp jobs/itemcopyjob.cpp jobs/itemcreatejob.cpp jobs/itemdeletejob.cpp jobs/itemfetchjob.cpp jobs/itemmodifyjob.cpp jobs/itemmovejob.cpp jobs/itemsearchjob.cpp jobs/job.cpp jobs/kjobprivatebase.cpp jobs/linkjob.cpp jobs/recursiveitemfetchjob.cpp jobs/resourceselectjob.cpp jobs/resourcesynchronizationjob.cpp jobs/relationfetchjob.cpp jobs/relationcreatejob.cpp jobs/relationdeletejob.cpp jobs/searchcreatejob.cpp jobs/searchresultjob.cpp jobs/specialcollectionsdiscoveryjob.cpp jobs/specialcollectionshelperjobs.cpp jobs/specialcollectionsrequestjob.cpp jobs/subscriptionjob.cpp jobs/tagcreatejob.cpp jobs/tagdeletejob.cpp jobs/tagfetchjob.cpp jobs/tagmodifyjob.cpp jobs/transactionjobs.cpp jobs/transactionsequence.cpp jobs/trashjob.cpp jobs/trashrestorejob.cpp jobs/unlinkjob.cpp ) ecm_generate_headers(AkonadiCore_jobs_HEADERS HEADER_NAMES AgentInstanceCreateJob CollectionAttributesSynchronizationJob CollectionCopyJob CollectionCreateJob CollectionDeleteJob CollectionFetchJob CollectionModifyJob CollectionMoveJob CollectionStatisticsJob ItemCopyJob ItemCreateJob ItemDeleteJob ItemFetchJob ItemModifyJob ItemMoveJob ItemSearchJob Job LinkJob RecursiveItemFetchJob ResourceSynchronizationJob RelationFetchJob RelationCreateJob RelationDeleteJob SearchCreateJob SpecialCollectionsDiscoveryJob SpecialCollectionsRequestJob TagCreateJob TagDeleteJob TagFetchJob TagModifyJob TransactionJobs TransactionSequence TrashJob TrashRestoreJob UnlinkJob REQUIRED_HEADERS AkonadiCore_jobs_HEADERS RELATIVE jobs ) set(akonadicore_dbus_xml ${Akonadi_SOURCE_DIR}/src/interfaces/org.freedesktop.Akonadi.NotificationManager.xml) qt5_add_dbus_interface(akonadicore_dbus_SRCS ${akonadicore_dbus_xml} notificationmanagerinterface) set(akonadicore_dbus_xml ${Akonadi_SOURCE_DIR}/src/interfaces/org.freedesktop.Akonadi.NotificationSource.xml) set_source_files_properties(${akonadicore_dbus_xml} PROPERTIES INCLUDE "${Akonadi_SOURCE_DIR}/src/private/protocol_p.h" ) qt5_add_dbus_interface(akonadicore_dbus_SRCS ${akonadicore_dbus_xml} notificationsourceinterface) qt5_add_dbus_interfaces(akonadicore_dbus_SRCS ${Akonadi_SOURCE_DIR}/src/interfaces/org.freedesktop.Akonadi.AgentManager.xml) qt5_add_dbus_interfaces(akonadicore_dbus_SRCS ${Akonadi_SOURCE_DIR}/src/interfaces/org.freedesktop.Akonadi.Tracer.xml) qt5_add_dbus_interfaces(akonadicore_dbus_SRCS ${Akonadi_SOURCE_DIR}/src/interfaces/org.freedesktop.Akonadi.Agent.Control.xml) +qt5_add_dbus_interfaces(akonadicore_dbus_SRCS + ${Akonadi_SOURCE_DIR}/src/interfaces/org.freedesktop.Akonadi.Resource.xml) set(akonadicore_SRCS ${akonadicore_base_SRCS} ${akonadicore_jobs_SRCS} ${akonadicore_models_SRCS} ${akonadicore_dbus_SRCS} ) ecm_qt_declare_logging_category(akonadicore_SRCS HEADER akonadicore_debug.h IDENTIFIER AKONADICORE_LOG CATEGORY_NAME org.kde.pim.akonadicore DESCRIPTION "akonadi (Akonadi Core Library)" OLD_CATEGORY_NAMES log_akonadicore EXPORT AKONADI ) add_library(KF5AkonadiCore ${akonadicore_SRCS}) generate_export_header(KF5AkonadiCore BASE_NAME akonadicore) add_library(KF5::AkonadiCore ALIAS KF5AkonadiCore) target_include_directories(KF5AkonadiCore INTERFACE "$") target_include_directories(KF5AkonadiCore PUBLIC "$") target_include_directories(KF5AkonadiCore PUBLIC "$") target_include_directories(KF5AkonadiCore PUBLIC "$") kde_target_enable_exceptions(KF5AkonadiCore PUBLIC) target_link_libraries(KF5AkonadiCore PUBLIC KF5::CoreAddons # for KJob KF5::ItemModels Qt5::Gui # for QColor PRIVATE Qt5::Network Qt5::Widgets KF5::AkonadiPrivate KF5::I18n KF5::IconThemes KF5::ConfigCore KF5AkonadiPrivate akonadi_shared ) set_target_properties(KF5AkonadiCore PROPERTIES VERSION ${AKONADI_VERSION_STRING} SOVERSION ${AKONADI_SOVERSION} EXPORT_NAME AkonadiCore ) ecm_generate_pri_file(BASE_NAME AkonadiCore LIB_NAME KF5AkonadiCore DEPS "ItemModels CoreAddons" FILENAME_VAR PRI_FILENAME ) install(TARGETS KF5AkonadiCore EXPORT KF5AkonadiTargets ${KF5_INSTALL_TARGETS_DEFAULT_ARGS} ) install(FILES ${CMAKE_CURRENT_BINARY_DIR}/akonadicore_export.h ${AkonadiCore_base_HEADERS} ${AkonadiCore_models_HEADERS} ${AkonadiCore_jobs_HEADERS} ${AkonadiCore_HEADERS} qtest_akonadi.h itempayloadinternals_p.h ${Akonadi_BINARY_DIR}/config-akonadi.h DESTINATION ${KDE_INSTALL_INCLUDEDIR_KF5}/AkonadiCore COMPONENT Devel ) install(FILES ${PRI_FILENAME} DESTINATION ${ECM_MKSPECS_INSTALL_DIR} ) install( FILES kcfg2dbus.xsl DESTINATION ${KDE_INSTALL_DATADIR_KF5}/akonadi ) diff --git a/src/core/abstractdifferencesreporter.h b/src/core/abstractdifferencesreporter.h index 37cc704ad..4eb5a0420 100644 --- a/src/core/abstractdifferencesreporter.h +++ b/src/core/abstractdifferencesreporter.h @@ -1,144 +1,148 @@ /* Copyright (c) 2010 KDAB Author: 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 ABSTRACTDIFFERENCESREPORTER_P_H #define ABSTRACTDIFFERENCESREPORTER_P_H namespace Akonadi { /** * @short An interface to report differences between two arbitrary objects. * * This interface can be used to report differences between two arbitrary objects * by describing a virtual table with three columns. The first column contains the name * of the property that is compared, the second column the property value of the one * object and the third column the property of the other object. * * The rows of this table can have different modes: * @li NormalMode The left and right columns show the same property values. * @li ConflictMode The left and right columns show conflicting property values. * @li AdditionalLeftMode The left column contains a property value that is not available in the right column. * @li AdditionalRightMode The right column contains a property value that is not available in the left column. * * Example: * * @code * // add differences of a contact * const KContacts::Addressee contact1 = ... * const KContacts::Addressee contact2 = ... * * AbstractDifferencesReporter *reporter = ... * reporter->setPropertyNameTitle( i18n( "Contact fields" ) ); * reporter->setLeftPropertyValueTitle( i18n( "Changed Contact" ) ); * reporter->setRightPropertyValueTitle( i18n( "Conflicting Contact" ) ); * * // check given name * if ( contact1.givenName() != contact2.givenName() ) * reporter->addProperty( AbstractDifferencesReporter::ConflictMode, i18n( "Given Name" ), * contact1.givenName(), contact2.givenName() ); * else * reporter->addProperty( AbstractDifferencesReporter::NormalMode, i18n( "Given Name" ), * contact1.givenName(), contact2.givenName() ); * * // check family name * if ( contact1.familyName() != contact2.familyName() ) * reporter->addProperty( AbstractDifferencesReporter::ConflictMode, i18n( "Family Name" ), * contact1.givenName(), contact2.givenName() ); * else * reporter->addProperty( AbstractDifferencesReporter::NormalMode, i18n( "Family Name" ), * contact1.givenName(), contact2.givenName() ); * * // check emails * const QStringList leftEmails = contact1.emails(); * const QStringList rightEmails = contact2.emails(); * * for ( const QString &leftEmail : leftEmails ) { * if ( rightEmails.contains( leftEmail ) ) * reporter->addProperty( AbstractDifferencesReporter::NormalMode, i18n( "Email" ), * leftEmail, leftEmail ); * else * reporter->addProperty( AbstractDifferencesReporter::AdditionalLeftMode, i18n( "Email" ), * leftEmail, QString() ); * } * * for( const QString &rightEmail : rightEmails ) { * if ( !leftEmails.contains( rightEmail ) ) * reporter->addProperty( AbstractDifferencesReporter::AdditionalRightMode, i18n( "Email" ), * QString(), rightEmail ); * } * * @endcode * * @author Tobias Koenig * @since 4.6 */ class AbstractDifferencesReporter { public: /** * Describes the property modes. */ enum Mode { NormalMode, ///< The left and right column show the same property values. ConflictMode, ///< The left and right column show conflicting property values. AdditionalLeftMode, ///< The left column contains a property value that is not available in the right column. AdditionalRightMode ///< The right column contains a property value that is not available in the left column. }; /** * Destroys the abstract differences reporter. */ - virtual ~AbstractDifferencesReporter() - { - } + virtual ~AbstractDifferencesReporter() = default; /** * Sets the @p title of the property name column. */ virtual void setPropertyNameTitle(const QString &title) = 0; /** * Sets the @p title of the column that shows the property values * of the left object. */ virtual void setLeftPropertyValueTitle(const QString &title) = 0; /** * Sets the @p title of the column that shows the property values * of the right object. */ virtual void setRightPropertyValueTitle(const QString &title) = 0; /** * Adds a new property entry to the table. * * @param mode Describes the mode of the property. If mode is AdditionalLeftMode or AdditionalRightMode, rightValue resp. leftValue * should be QString(). * @param name The user visible name of the property. * @param leftValue The user visible property value of the left object. * @param rightValue The user visible property value of the right object. */ virtual void addProperty(Mode mode, const QString &name, const QString &leftValue, const QString &rightValue) = 0; + +protected: + explicit AbstractDifferencesReporter() = default; + +private: + Q_DISABLE_COPY_MOVE(AbstractDifferencesReporter); }; } #endif diff --git a/src/core/agentconfigurationbase.cpp b/src/core/agentconfigurationbase.cpp index 6ca7da0ac..5c06b9c8d 100644 --- a/src/core/agentconfigurationbase.cpp +++ b/src/core/agentconfigurationbase.cpp @@ -1,115 +1,113 @@ /* 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. */ #include "agentconfigurationbase.h" #include "agentmanager.h" #include "akonadicore_debug.h" #include #include #include namespace Akonadi { class Q_DECL_HIDDEN AgentConfigurationBase::Private { public: Private(const KSharedConfigPtr &config, QWidget *parentWidget, const QVariantList &args) : config(config) , parentWidget(parentWidget) { Q_ASSERT(args.size() >= 1); if (args.empty()) { qCCritical(AKONADICORE_LOG, "AgentConfigurationBase instantiated with invalid arguments"); return; } identifier = args.at(0).toString(); } KSharedConfigPtr config; QString identifier; QScopedPointer aboutData; QWidget *parentWidget = nullptr; }; } using namespace Akonadi; AgentConfigurationBase::AgentConfigurationBase(const KSharedConfigPtr &config, QWidget *parentWidget, const QVariantList &args) : QObject(reinterpret_cast(parentWidget)) , d(new Private(config, parentWidget, args)) { } AgentConfigurationBase::~AgentConfigurationBase() { } KSharedConfigPtr AgentConfigurationBase::config() const { return d->config; } QString AgentConfigurationBase::identifier() const { return d->identifier; } void AgentConfigurationBase::load() { d->config->reparseConfiguration(); } bool AgentConfigurationBase::save() const { d->config->sync(); d->config->reparseConfiguration(); return true; } QWidget *AgentConfigurationBase::parentWidget() const { return d->parentWidget; } void AgentConfigurationBase::setKAboutData(const KAboutData &aboutData) { d->aboutData.reset(new KAboutData(aboutData)); } KAboutData *AgentConfigurationBase::aboutData() const { return d->aboutData.data(); } QSize AgentConfigurationBase::restoreDialogSize() const { return {}; } -void AgentConfigurationBase::saveDialogSize(const QSize &size) -{ - Q_UNUSED(size); -} +void AgentConfigurationBase::saveDialogSize(QSize) +{} QDialogButtonBox::StandardButtons AgentConfigurationBase::standardButtons() const { return QDialogButtonBox::Ok | QDialogButtonBox::Apply | QDialogButtonBox::Cancel; } diff --git a/src/core/agentconfigurationbase.h b/src/core/agentconfigurationbase.h index 609e9554e..f1f3b4303 100644 --- a/src/core/agentconfigurationbase.h +++ b/src/core/agentconfigurationbase.h @@ -1,172 +1,172 @@ /* 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_AGENTCONFIGURATIONBASE_H #define AKONADI_AGENTCONFIGURATIONBASE_H #include "akonadicore_export.h" #include "agentconfigurationfactorybase.h" #include #include #include class KAboutData; namespace Akonadi { /** * @brief Base class for configuration UI for Akonadi agents * * Each agent that has a graphical configuration should subclass this class * and create its configuration UI there. * * The subclass must reimplement load() and save() virtual methods which * are called automatically. The load() method is called on start to initialize * widgets (thus subclasses don't need to call it themselves) or when user * clicks a "Reset" button. The save() method is called whenever user decides to * save changes. * * Since each Akonadi agent instance has its own configuration file whose * location and name is opaque to the implementation, config() method can be * used to get access to the current configuration object. * * The widget will not run in the same process as the Akonadi agent, thus all * communication with the resource (if needed) should be done over DBus. The * identifier of the instance currently being configured is accessible from the * identifier() method. * * There is no need to signal back to the resource when configuration is changed. * When save() is called and the dialog is destroyed, Akonadi will automatically * call AgentBase::reconfigure() in the respective Akonadi agent instance. * * It is guaranteed that only a single instance of the configuration dialog for * given agent will be opened at the same time. * * Subclasses of ConfigurationBase must be registered as Akonadi plugins using * AKONADI_AGENTCONFIG_FACTORY macro. * * The metadata JSON file then must contain the following values: * @code * { * "X-Akonadi-PluginType": "AgentConfig", * "X-Akonadi-Library": "exampleresourceconfig", * "X-Akonadi-AgentConfig-Type": "akonadi_example_resource" * } * @endcode * * The @p X-Akonadi-Library value must match the name of the plugin binary without * the (optional) "lib" prefix and file extension. The @p X-Akonadi-AgentConfig-Type * value must match the name of the @p X-Akonadi-Identifier value from the agent's * desktop file. * * The plugin binary should be installed into akonadi/config subdirectory in one * of the paths search by QCoreApplication::libraryPaths(). */ class AKONADICORE_EXPORT AgentConfigurationBase : public QObject { Q_OBJECT public: /** * Creates a new AgentConfigurationBase objects. * * The @p parentWidget should be used as a parent widget for the configuration * widgets. * * Subclasses must provide a constructor with this exact signature. */ explicit AgentConfigurationBase(const KSharedConfigPtr &config, QWidget *parentWidget, const QVariantList &args); ~AgentConfigurationBase() override; /** * Reimplement to load settings from the configuration object into widgets. * * @warning Always call the base class implementation at the beginning of * your overridden method! * * @see config(), save() */ virtual void load(); /** * Reimplement to save new settings into the configuration object. * * Return true if the configuration has been successfully saved and should * be applied to the agent, return false otherwise. * * @warning Always remember call the base class implementation at the end * of your overridden method! * * @see config(), load() */ virtual bool save() const; /** * Returns about data for the currently configured component. * * May return a null pointer. */ KAboutData *aboutData() const; /** * Reimplement to restore dialog size. */ virtual QSize restoreDialogSize() const; /** * Reimplement to save dialog size. */ - virtual void saveDialogSize(const QSize &size); + virtual void saveDialogSize(QSize size); virtual QDialogButtonBox::StandardButtons standardButtons() const; protected: QWidget *parentWidget() const; /** * Returns KConfig object belonging to the current Akonadi agent instance. */ KSharedConfigPtr config() const; /** * Returns identifier of the Akonadi agent instance currently being configured. */ QString identifier() const; /** * When KAboutData is provided the dialog will also contain KHelpMenu with * access to user help etc. */ void setKAboutData(const KAboutData &aboutData); Q_SIGNALS: void enableOkButton(bool enabled); private: class Private; friend class Private; QScopedPointer d; }; } // namespace #endif // AKONADI_AGENTCONFIGURATIONBASE_H diff --git a/src/core/agentmanager.cpp b/src/core/agentmanager.cpp index f336566e4..f0ec051e1 100644 --- a/src/core/agentmanager.cpp +++ b/src/core/agentmanager.cpp @@ -1,434 +1,421 @@ /* Copyright (c) 2006-2008 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. */ #include "agentmanager.h" #include "agentmanager_p.h" #include "agenttype_p.h" #include "agentinstance_p.h" #include #include "servermanager.h" #include "collection.h" #include "shared/akranges.h" #include #include using namespace Akonadi; using namespace AkRanges; // @cond PRIVATE AgentInstance AgentManagerPrivate::createInstance(const AgentType &type) { const QString &identifier = mManager->createAgentInstance(type.identifier()); if (identifier.isEmpty()) { return AgentInstance(); } return fillAgentInstanceLight(identifier); } void AgentManagerPrivate::agentTypeAdded(const QString &identifier) { // Ignore agent types we already know about, for example because we called // readAgentTypes before. if (mTypes.contains(identifier)) { return; } if (mTypes.isEmpty()) { // The Akonadi ServerManager assumes that the server is up and running as soon // as it knows about at least one agent type. // If we Q_EMIT the typeAdded() signal here, it therefore thinks the server is // running. However, the AgentManager does not know about all agent types yet, // as the server might still have pending agentTypeAdded() signals, even though // it internally knows all agent types already. // This can cause situations where the client gets told by the ServerManager that // the server is running, yet the client will encounter an error because the // AgentManager doesn't know all types yet. // // Therefore, we read all agent types from the server here so they are known. readAgentTypes(); } const AgentType type = fillAgentType(identifier); if (type.isValid()) { mTypes.insert(identifier, type); Q_EMIT mParent->typeAdded(type); } } void AgentManagerPrivate::agentTypeRemoved(const QString &identifier) { if (!mTypes.contains(identifier)) { return; } const AgentType type = mTypes.take(identifier); Q_EMIT mParent->typeRemoved(type); } void AgentManagerPrivate::agentInstanceAdded(const QString &identifier) { const AgentInstance instance = fillAgentInstance(identifier); if (instance.isValid()) { // It is possible that this function is called when the instance is already // in our list we filled initially in the constructor. // This happens when the constructor is called during Akonadi startup, when // the agent processes are not fully loaded and have no D-Bus interface yet. // The server-side agent manager then emits the instance added signal when // the D-Bus interface for the agent comes up. // In this case, we simply notify that the instance status has changed. const bool newAgentInstance = !mInstances.contains(identifier); if (newAgentInstance) { mInstances.insert(identifier, instance); Q_EMIT mParent->instanceAdded(instance); } else { mInstances.remove(identifier); mInstances.insert(identifier, instance); Q_EMIT mParent->instanceStatusChanged(instance); } } } void AgentManagerPrivate::agentInstanceRemoved(const QString &identifier) { if (!mInstances.contains(identifier)) { return; } const AgentInstance instance = mInstances.take(identifier); Q_EMIT mParent->instanceRemoved(instance); } void AgentManagerPrivate::agentInstanceStatusChanged(const QString &identifier, int status, const QString &msg) { if (!mInstances.contains(identifier)) { return; } AgentInstance &instance = mInstances[identifier]; instance.d->mStatus = status; instance.d->mStatusMessage = msg; Q_EMIT mParent->instanceStatusChanged(instance); } void AgentManagerPrivate::agentInstanceProgressChanged(const QString &identifier, uint progress, const QString &msg) { if (!mInstances.contains(identifier)) { return; } AgentInstance &instance = mInstances[identifier]; instance.d->mProgress = progress; if (!msg.isEmpty()) { instance.d->mStatusMessage = msg; } Q_EMIT mParent->instanceProgressChanged(instance); } void AgentManagerPrivate::agentInstanceWarning(const QString &identifier, const QString &msg) { if (!mInstances.contains(identifier)) { return; } AgentInstance &instance = mInstances[identifier]; Q_EMIT mParent->instanceWarning(instance, msg); } void AgentManagerPrivate::agentInstanceError(const QString &identifier, const QString &msg) { if (!mInstances.contains(identifier)) { return; } AgentInstance &instance = mInstances[identifier]; Q_EMIT mParent->instanceError(instance, msg); } void AgentManagerPrivate::agentInstanceOnlineChanged(const QString &identifier, bool state) { if (!mInstances.contains(identifier)) { return; } AgentInstance &instance = mInstances[identifier]; instance.d->mIsOnline = state; Q_EMIT mParent->instanceOnline(instance, state); } void AgentManagerPrivate::agentInstanceNameChanged(const QString &identifier, const QString &name) { if (!mInstances.contains(identifier)) { return; } AgentInstance &instance = mInstances[identifier]; instance.d->mName = name; Q_EMIT mParent->instanceNameChanged(instance); } void AgentManagerPrivate::readAgentTypes() { const QDBusReply types = mManager->agentTypes(); if (types.isValid()) { const QStringList lst = types.value(); for (const QString &type : lst) { const AgentType agentType = fillAgentType(type); if (agentType.isValid()) { mTypes.insert(type, agentType); Q_EMIT mParent->typeAdded(agentType); } } } } void AgentManagerPrivate::readAgentInstances() { const QDBusReply instances = mManager->agentInstances(); if (instances.isValid()) { const QStringList lst = instances.value(); for (const QString &instance : lst) { const AgentInstance agentInstance = fillAgentInstance(instance); if (agentInstance.isValid()) { mInstances.insert(instance, agentInstance); Q_EMIT mParent->instanceAdded(agentInstance); } } } } AgentType AgentManagerPrivate::fillAgentType(const QString &identifier) const { AgentType type; type.d->mIdentifier = identifier; type.d->mName = mManager->agentName(identifier); type.d->mDescription = mManager->agentComment(identifier); type.d->mIconName = mManager->agentIcon(identifier); type.d->mMimeTypes = mManager->agentMimeTypes(identifier); type.d->mCapabilities = mManager->agentCapabilities(identifier); type.d->mCustomProperties = mManager->agentCustomProperties(identifier); return type; } void AgentManagerPrivate::setName(const AgentInstance &instance, const QString &name) { mManager->setAgentInstanceName(instance.identifier(), name); } void AgentManagerPrivate::setOnline(const AgentInstance &instance, bool state) { mManager->setAgentInstanceOnline(instance.identifier(), state); } void AgentManagerPrivate::configure(const AgentInstance &instance, QWidget *parent) { qlonglong winId = 0; if (parent) { winId = static_cast(parent->window()->winId()); } mManager->agentInstanceConfigure(instance.identifier(), winId); } void AgentManagerPrivate::synchronize(const AgentInstance &instance) { mManager->agentInstanceSynchronize(instance.identifier()); } void AgentManagerPrivate::synchronizeCollectionTree(const AgentInstance &instance) { mManager->agentInstanceSynchronizeCollectionTree(instance.identifier()); } void AgentManagerPrivate::synchronizeTags(const AgentInstance &instance) { mManager->agentInstanceSynchronizeTags(instance.identifier()); } void AgentManagerPrivate::synchronizeRelations(const AgentInstance &instance) { mManager->agentInstanceSynchronizeRelations(instance.identifier()); } AgentInstance AgentManagerPrivate::fillAgentInstance(const QString &identifier) const { AgentInstance instance; const QString agentTypeIdentifier = mManager->agentInstanceType(identifier); if (!mTypes.contains(agentTypeIdentifier)) { return instance; } instance.d->mType = mTypes.value(agentTypeIdentifier); instance.d->mIdentifier = identifier; instance.d->mName = mManager->agentInstanceName(identifier); instance.d->mStatus = mManager->agentInstanceStatus(identifier); instance.d->mStatusMessage = mManager->agentInstanceStatusMessage(identifier); instance.d->mProgress = mManager->agentInstanceProgress(identifier); instance.d->mIsOnline = mManager->agentInstanceOnline(identifier); return instance; } AgentInstance AgentManagerPrivate::fillAgentInstanceLight(const QString &identifier) const { AgentInstance instance; const QString agentTypeIdentifier = mManager->agentInstanceType(identifier); Q_ASSERT_X(mTypes.contains(agentTypeIdentifier), "fillAgentInstanceLight", "Requests non-existing agent type"); instance.d->mType = mTypes.value(agentTypeIdentifier); instance.d->mIdentifier = identifier; return instance; } void AgentManagerPrivate::createDBusInterface() { mTypes.clear(); mInstances.clear(); - delete mManager; - mManager = new org::freedesktop::Akonadi::AgentManager(ServerManager::serviceName(ServerManager::Control), + using AgentManagerIface = org::freedesktop::Akonadi::AgentManager; + mManager = std::make_unique(ServerManager::serviceName(ServerManager::Control), QStringLiteral("/AgentManager"), QDBusConnection::sessionBus(), mParent); - QObject::connect(mManager, SIGNAL(agentTypeAdded(QString)), - mParent, SLOT(agentTypeAdded(QString))); - QObject::connect(mManager, SIGNAL(agentTypeRemoved(QString)), - mParent, SLOT(agentTypeRemoved(QString))); - QObject::connect(mManager, SIGNAL(agentInstanceAdded(QString)), - mParent, SLOT(agentInstanceAdded(QString))); - QObject::connect(mManager, SIGNAL(agentInstanceRemoved(QString)), - mParent, SLOT(agentInstanceRemoved(QString))); - QObject::connect(mManager, SIGNAL(agentInstanceStatusChanged(QString,int,QString)), - mParent, SLOT(agentInstanceStatusChanged(QString,int,QString))); - QObject::connect(mManager, SIGNAL(agentInstanceProgressChanged(QString,uint,QString)), - mParent, SLOT(agentInstanceProgressChanged(QString,uint,QString))); - QObject::connect(mManager, SIGNAL(agentInstanceNameChanged(QString,QString)), - mParent, SLOT(agentInstanceNameChanged(QString,QString))); - QObject::connect(mManager, SIGNAL(agentInstanceWarning(QString,QString)), - mParent, SLOT(agentInstanceWarning(QString,QString))); - QObject::connect(mManager, SIGNAL(agentInstanceError(QString,QString)), - mParent, SLOT(agentInstanceError(QString,QString))); - QObject::connect(mManager, SIGNAL(agentInstanceOnlineChanged(QString,bool)), - mParent, SLOT(agentInstanceOnlineChanged(QString,bool))); + connect(mManager.get(), &AgentManagerIface::agentTypeAdded, this, &AgentManagerPrivate::agentTypeAdded); + connect(mManager.get(), &AgentManagerIface::agentTypeRemoved, this, &AgentManagerPrivate::agentTypeRemoved); + connect(mManager.get(), &AgentManagerIface::agentInstanceAdded, this, &AgentManagerPrivate::agentInstanceAdded); + connect(mManager.get(), &AgentManagerIface::agentInstanceRemoved, this, &AgentManagerPrivate::agentInstanceRemoved); + connect(mManager.get(), &AgentManagerIface::agentInstanceStatusChanged, this, &AgentManagerPrivate::agentInstanceStatusChanged); + connect(mManager.get(), &AgentManagerIface::agentInstanceProgressChanged, this, &AgentManagerPrivate::agentInstanceProgressChanged); + connect(mManager.get(), &AgentManagerIface::agentInstanceNameChanged, this, &AgentManagerPrivate::agentInstanceNameChanged); + connect(mManager.get(), &AgentManagerIface::agentInstanceWarning, this, &AgentManagerPrivate::agentInstanceWarning); + connect(mManager.get(), &AgentManagerIface::agentInstanceError, this, &AgentManagerPrivate::agentInstanceError); + connect(mManager.get(), &AgentManagerIface::agentInstanceOnlineChanged, this, &AgentManagerPrivate::agentInstanceOnlineChanged); if (mManager->isValid()) { readAgentTypes(); readAgentInstances(); } } AgentManager *AgentManagerPrivate::mSelf = nullptr; AgentManager::AgentManager() : QObject(nullptr) , d(new AgentManagerPrivate(this)) { // needed for queued connections on our signals qRegisterMetaType(); qRegisterMetaType(); d->createDBusInterface(); d->mServiceWatcher = std::make_unique( ServerManager::serviceName(ServerManager::Control), QDBusConnection::sessionBus(), QDBusServiceWatcher::WatchForRegistration); connect(d->mServiceWatcher.get(), &QDBusServiceWatcher::serviceRegistered, this, [this]() { if (d->mTypes.isEmpty()) { // just to be safe d->readAgentTypes(); } if (d->mInstances.isEmpty()) { d->readAgentInstances(); } }); } // @endcond -AgentManager::~AgentManager() -{ - delete d; -} +AgentManager::~AgentManager() = default; AgentManager *AgentManager::self() { if (!AgentManagerPrivate::mSelf) { AgentManagerPrivate::mSelf = new AgentManager(); } return AgentManagerPrivate::mSelf; } AgentType::List AgentManager::types() const { // Maybe the Control process is up and ready but we haven't been to the event loop yet so // QDBusServiceWatcher hasn't notified us yet. // In that case make sure to do it here, to avoid going into Broken state. if (d->mTypes.isEmpty()) { d->readAgentTypes(); } return d->mTypes | Views::values | Actions::toQVector; } AgentType AgentManager::type(const QString &identifier) const { return d->mTypes.value(identifier); } AgentInstance::List AgentManager::instances() const { return d->mInstances | Views::values | Actions::toQVector; } AgentInstance AgentManager::instance(const QString &identifier) const { return d->mInstances.value(identifier); } void AgentManager::removeInstance(const AgentInstance &instance) { d->mManager->removeAgentInstance(instance.identifier()); } void AgentManager::synchronizeCollection(const Collection &collection) { synchronizeCollection(collection, false); } void AgentManager::synchronizeCollection(const Collection &collection, bool recursive) { const QString resId = collection.resource(); Q_ASSERT(!resId.isEmpty()); d->mManager->agentInstanceSynchronizeCollection(resId, collection.id(), recursive); } #include "moc_agentmanager.cpp" diff --git a/src/core/agentmanager.h b/src/core/agentmanager.h index 89108fd96..3a1b425c9 100644 --- a/src/core/agentmanager.h +++ b/src/core/agentmanager.h @@ -1,221 +1,212 @@ /* Copyright (c) 2006-2008 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 AKONADI_AGENTMANAGER_H #define AKONADI_AGENTMANAGER_H #include "akonadicore_export.h" #include "agenttype.h" #include "agentinstance.h" #include +#include + namespace Akonadi { class AgentManagerPrivate; class Collection; /** * @short Provides an interface to retrieve agent types and manage agent instances. * * This singleton class can be used to create or remove agent instances or trigger * synchronization of collections. Furthermore it provides information about status * changes of the agents. * * @code * * Akonadi::AgentManager *manager = Akonadi::AgentManager::self(); * * Akonadi::AgentType::List types = manager->types(); * for ( const Akonadi::AgentType& type : types ) { * qDebug() << "Type:" << type.name() << type.description(); * } * * @endcode * * @author Tobias Koenig */ class AKONADICORE_EXPORT AgentManager : public QObject { friend class AgentInstance; friend class AgentInstanceCreateJobPrivate; friend class AgentManagerPrivate; Q_OBJECT public: /** * Returns the global instance of the agent manager. */ static AgentManager *self(); /** * Destroys the agent manager. */ ~AgentManager(); /** * Returns the list of all available agent types. */ Q_REQUIRED_RESULT AgentType::List types() const; /** * Returns the agent type with the given @p identifier or * an invalid agent type if the identifier does not exist. */ Q_REQUIRED_RESULT AgentType type(const QString &identifier) const; /** * Returns the list of all available agent instances. */ Q_REQUIRED_RESULT AgentInstance::List instances() const; /** * Returns the agent instance with the given @p identifier or * an invalid agent instance if the identifier does not exist. * * Note that because a resource is a special case of an agent, the * identifier of a resource is the same as that of its agent instance. * @param identifier identifier to choose the agent instance */ Q_REQUIRED_RESULT AgentInstance instance(const QString &identifier) const; /** * Removes the given agent @p instance. */ void removeInstance(const AgentInstance &instance); /** * Trigger a synchronization of the given collection by its owning resource agent. * * @param collection The collection to synchronize. */ void synchronizeCollection(const Collection &collection); /** * Trigger a synchronization of the given collection by its owning resource agent. * * @param collection The collection to synchronize. * @param recursive If true, the sub-collections are also synchronized * * @since 4.6 */ void synchronizeCollection(const Collection &collection, bool recursive); Q_SIGNALS: /** * This signal is emitted whenever a new agent type was installed on the system. * * @param type The new agent type. */ void typeAdded(const Akonadi::AgentType &type); /** * This signal is emitted whenever an agent type was removed from the system. * * @param type The removed agent type. */ void typeRemoved(const Akonadi::AgentType &type); /** * This signal is emitted whenever a new agent instance was created. * * @param instance The new agent instance. */ void instanceAdded(const Akonadi::AgentInstance &instance); /** * This signal is emitted whenever an agent instance was removed. * * @param instance The removed agent instance. */ void instanceRemoved(const Akonadi::AgentInstance &instance); /** * This signal is emitted whenever the status of an agent instance has * changed. * * @param instance The agent instance that status has changed. */ void instanceStatusChanged(const Akonadi::AgentInstance &instance); /** * This signal is emitted whenever the progress of an agent instance has * changed. * * @param instance The agent instance that progress has changed. */ void instanceProgressChanged(const Akonadi::AgentInstance &instance); /** * This signal is emitted whenever the name of the agent instance has changed. * * @param instance The agent instance that name has changed. */ void instanceNameChanged(const Akonadi::AgentInstance &instance); /** * This signal is emitted whenever the agent instance raised an error. * * @param instance The agent instance that raised the error. * @param message The i18n'ed error message. */ void instanceError(const Akonadi::AgentInstance &instance, const QString &message); /** * This signal is emitted whenever the agent instance raised a warning. * * @param instance The agent instance that raised the warning. * @param message The i18n'ed warning message. */ void instanceWarning(const Akonadi::AgentInstance &instance, const QString &message); /** * This signal is emitted whenever the online state of an agent changed. * * @param instance The agent instance that changed its online state. * @param online The new online state. * @since 4.2 */ void instanceOnline(const Akonadi::AgentInstance &instance, bool online); private: //@cond PRIVATE - AgentManager(); - - AgentManagerPrivate *const d; - - Q_PRIVATE_SLOT(d, void agentTypeAdded(const QString &)) - Q_PRIVATE_SLOT(d, void agentTypeRemoved(const QString &)) - Q_PRIVATE_SLOT(d, void agentInstanceAdded(const QString &)) - Q_PRIVATE_SLOT(d, void agentInstanceRemoved(const QString &)) - Q_PRIVATE_SLOT(d, void agentInstanceStatusChanged(const QString &, int, const QString &)) - Q_PRIVATE_SLOT(d, void agentInstanceProgressChanged(const QString &, uint, const QString &)) - Q_PRIVATE_SLOT(d, void agentInstanceNameChanged(const QString &, const QString &)) - Q_PRIVATE_SLOT(d, void agentInstanceWarning(const QString &, const QString &)) - Q_PRIVATE_SLOT(d, void agentInstanceError(const QString &, const QString &)) - Q_PRIVATE_SLOT(d, void agentInstanceOnlineChanged(const QString &, bool)) + explicit AgentManager(); + + std::unique_ptr const d; //@endcond }; } #endif diff --git a/src/core/agentmanager_p.h b/src/core/agentmanager_p.h index 4772acd86..c9e54360d 100644 --- a/src/core/agentmanager_p.h +++ b/src/core/agentmanager_p.h @@ -1,111 +1,111 @@ /* Copyright (c) 2006-2008 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 AKONADI_AGENTMANAGER_P_H #define AKONADI_AGENTMANAGER_P_H #include "agentmanagerinterface.h" #include "agenttype.h" #include "agentinstance.h" #include #include class QDBusServiceWatcher; namespace Akonadi { class AgentManager; /** * @internal */ -class AgentManagerPrivate +class AgentManagerPrivate : public QObject { friend class AgentManager; + Q_OBJECT public: explicit AgentManagerPrivate(AgentManager *parent) : mParent(parent) - { - } + {} /* * Used by AgentInstanceCreateJob */ AgentInstance createInstance(const AgentType &type); void agentTypeAdded(const QString &identifier); void agentTypeRemoved(const QString &identifier); void agentInstanceAdded(const QString &identifier); void agentInstanceRemoved(const QString &identifier); void agentInstanceStatusChanged(const QString &identifier, int status, const QString &msg); void agentInstanceProgressChanged(const QString &identifier, uint progress, const QString &msg); void agentInstanceNameChanged(const QString &identifier, const QString &name); void agentInstanceWarning(const QString &identifier, const QString &msg); void agentInstanceError(const QString &identifier, const QString &msg); void agentInstanceOnlineChanged(const QString &identifier, bool state); /** * Reads the information about all known agent types from the serverside * agent manager and updates mTypes, like agentTypeAdded() does. * * This will not remove agents from the internal map that are no longer on * the server. */ void readAgentTypes(); /** * Reads the information about all known agent instances from the server. If AgentManager * is created before the Akonadi.Control interface is registered, the agent * instances aren't immediately found then. */ void readAgentInstances(); void setName(const AgentInstance &instance, const QString &name); void setOnline(const AgentInstance &instance, bool state); void configure(const AgentInstance &instance, QWidget *parent); void synchronize(const AgentInstance &instance); void synchronizeCollectionTree(const AgentInstance &instance); void synchronizeTags(const AgentInstance &instance); void synchronizeRelations(const AgentInstance &instance); void createDBusInterface(); AgentType fillAgentType(const QString &identifier) const; AgentInstance fillAgentInstance(const QString &identifier) const; AgentInstance fillAgentInstanceLight(const QString &identifier) const; static AgentManager *mSelf; AgentManager *mParent = nullptr; - org::freedesktop::Akonadi::AgentManager *mManager = nullptr; + std::unique_ptr mManager; QHash mTypes; QHash mInstances; std::unique_ptr mServiceWatcher; }; } #endif diff --git a/src/core/attribute.h b/src/core/attribute.h index 6af463a32..e438a98d7 100644 --- a/src/core/attribute.h +++ b/src/core/attribute.h @@ -1,179 +1,183 @@ /* Copyright (c) 2006 - 2008 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. */ #ifndef AKONADI_ATTRIBUTE_H #define AKONADI_ATTRIBUTE_H #include "akonadicore_export.h" #include namespace Akonadi { /** * @short Provides interface for custom attributes for Entity. * * This class is an interface for custom attributes, that can be stored * in an entity. Attributes should be meta data, e.g. ACLs, quotas etc. * that are not part of the entities' data itself. * * Note that attributes are per user, i.e. when an attribute is added to * an entity, it only applies to the current user. * * To provide custom attributes, you have to subclass from this interface * and reimplement the pure virtual methods. * * @code * * class SecrecyAttribute : public Akonadi::Attribute * { * public: * enum Secrecy * { * Public, * Private, * Confidential * }; * * SecrecyAttribute( Secrecy secrecy = Public ) * : mSecrecy( secrecy ) * { * } * * void setSecrecy( Secrecy secrecy ) * { * mSecrecy = secrecy; * } * * Secrecy secrecy() const * { * return mSecrecy; * } * * virtual QByteArray type() const * { * return "secrecy"; * } * * virtual Attribute* clone() const * { * return new SecrecyAttribute( mSecrecy ); * } * * virtual QByteArray serialized() const * { * switch ( mSecrecy ) { * case Public: return "public"; break; * case Private: return "private"; break; * case Confidential: return "confidential"; break; * } * } * * virtual void deserialize( const QByteArray &data ) * { * if ( data == "public" ) * mSecrecy = Public; * else if ( data == "private" ) * mSecrecy = Private; * else if ( data == "confidential" ) * mSecrecy = Confidential; * } * } * * @endcode * * Additionally, you need to register your attribute with Akonadi::AttributeFactory * for automatic deserialization during retrieving of collections or items: * * @code * AttributeFactory::registerAttribute(); * @endcode * * Third party attributes need to be registered once by each application that uses * them. So the above snippet needs to be in the resource that adds the attribute, * and each application that uses the resource. This may be simplified in the future. * * The custom attributes can be used in the following way: * * @code * * Akonadi::Item item( "text/directory" ); * SecrecyAttribute* attr = item.attribute( Item::AddIfMissing ); * attr->setSecrecy( SecrecyAttribute::Confidential ); * * @endcode * * and * * @code * * Akonadi::Item item = ... * * if ( item.hasAttribute() ) { * SecrecyAttribute *attr = item.attribute(); * * SecrecyAttribute::Secrecy secrecy = attr->secrecy(); * ... * } * @endcode * * @author Volker Krause */ -class AKONADICORE_EXPORT Attribute +class AKONADICORE_EXPORT Attribute // clazy:exclude=copyable-polymorphic { public: /** * Describes a list of attributes. */ typedef QList List; /** * Returns the type of the attribute. */ virtual QByteArray type() const = 0; /** * Destroys this attribute. */ virtual ~Attribute(); /** * Creates a copy of this attribute. */ virtual Attribute *clone() const = 0; /** * Returns a QByteArray representation of the attribute which will be * storaged. This can be raw binary data, no encoding needs to be applied. */ virtual QByteArray serialized() const = 0; /** * Sets the data of this attribute, using the same encoding * as returned by toByteArray(). * * @param data The encoded attribute data. */ virtual void deserialize(const QByteArray &data) = 0; + +protected: + explicit Attribute() = default; + Q_DISABLE_COPY_MOVE(Attribute) }; } #endif diff --git a/src/core/braveheart.cpp b/src/core/braveheart.cpp index 6487de29d..88dec9fc6 100644 --- a/src/core/braveheart.cpp +++ b/src/core/braveheart.cpp @@ -1,79 +1,79 @@ /* Copyright (c) 2016 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 #ifdef HAVE_MALLOC_TRIM #include #include #include #include #include namespace Akonadi { class Braveheart { private: static void sonOfScotland() { Q_ASSERT(qApp->thread() == QThread::currentThread()); if (!qApp->property("__Akonadi__Braveheart").isNull()) { // One Scottish warrior is enough.... return; } auto freedom = new QTimer(qApp); QObject::connect(freedom, &QTimer::timeout, freedom, []() { // They may take our lives, but they will never // take our memory! malloc_trim(50 * 1024 * 1024); }); // Fight for freedom every 15 minutes freedom->start(15 * 60 * 1000); qApp->setProperty("__Akonadi__Braveheart", true); } public: explicit Braveheart() { qAddPreRoutine([]() { if (qApp->thread() != QThread::currentThread()) { QTimer::singleShot(0, qApp, sonOfScotland); } else { sonOfScotland(); } }); } }; namespace { -Braveheart Wallace; +Braveheart Wallace; // clazy:exclude=non-pod-global-static } } // namespace Akonadi #endif // HAVE_MALLOC_TRIM diff --git a/src/core/changemediator_p.cpp b/src/core/changemediator_p.cpp index 8a595ddbe..c1f76f490 100644 --- a/src/core/changemediator_p.cpp +++ b/src/core/changemediator_p.cpp @@ -1,98 +1,103 @@ /* Copyright (c) 2011 Tobias Koenig Copyright (c) 2011 Stephen Kelly 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 "changemediator_p.h" #include #include "collection.h" #include "item.h" using namespace Akonadi; -class GlobalChangeMediator : public ChangeMediator {}; +class GlobalChangeMediator : public ChangeMediator +{ + Q_OBJECT +}; + Q_GLOBAL_STATIC(GlobalChangeMediator, s_globalChangeMediator) ChangeMediator *ChangeMediator::instance() { if (s_globalChangeMediator.isDestroyed()) { return nullptr; } else { return s_globalChangeMediator; } } ChangeMediator::ChangeMediator(QObject *parent) : QObject(parent) { if (auto app = QCoreApplication::instance(); app != nullptr) { this->moveToThread(app->thread()); } } /* static */ void ChangeMediator::registerMonitor(QObject *monitor) { QMetaObject::invokeMethod(instance(), [monitor]() { instance()->m_monitors.push_back(monitor); }); } /* static */ void ChangeMediator::unregisterMonitor(QObject *monitor) { QMetaObject::invokeMethod(instance(), [monitor]() { instance()->m_monitors.removeAll(monitor); }); } /* static */ void ChangeMediator::invalidateCollection(const Akonadi::Collection &collection) { QMetaObject::invokeMethod(instance(), [colId = collection.id()]() { - for (auto monitor : instance()->m_monitors) { + for (auto *monitor : qAsConst(instance()->m_monitors)) { const bool ok = QMetaObject::invokeMethod(monitor, "invalidateCollectionCache", Q_ARG(qint64, colId)); Q_ASSERT(ok); Q_UNUSED(ok); } }); } /* static */ void ChangeMediator::invalidateItem(const Akonadi::Item &item) { QMetaObject::invokeMethod(instance(), [itemId = item.id()]() { - for (auto monitor : instance()->m_monitors) { + for (auto monitor : qAsConst(instance()->m_monitors)) { const bool ok = QMetaObject::invokeMethod(monitor, "invalidateItemCache", Q_ARG(qint64, itemId)); Q_ASSERT(ok); Q_UNUSED(ok); } }); } /* static */ void ChangeMediator::invalidateTag(const Tag &tag) { QMetaObject::invokeMethod(instance(), [tagId = tag.id()]() { - for (auto monitor : instance()->m_monitors) { + for (auto monitor : qAsConst(instance()->m_monitors)) { const bool ok = QMetaObject::invokeMethod(monitor, "invalidateTagCache", Q_ARG(qint64, tagId)); Q_ASSERT(ok); Q_UNUSED(ok); } }); } +#include "changemediator_p.moc" diff --git a/src/core/changemediator_p.h b/src/core/changemediator_p.h index b6d4ad963..44cc9ed2d 100644 --- a/src/core/changemediator_p.h +++ b/src/core/changemediator_p.h @@ -1,58 +1,59 @@ /* Copyright (c) 2011 Tobias Koenig Copyright (c) 2011 Stephen Kelly 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_CHANGEMEDIATOR_P_H #define AKONADI_CHANGEMEDIATOR_P_H #include #include namespace Akonadi { class Job; class JobPrivate; class Collection; class Item; class Tag; class ChangeMediator : public QObject { Q_OBJECT public: static ChangeMediator *instance(); static void registerMonitor(QObject *monitor); static void unregisterMonitor(QObject *monitor); static void invalidateCollection(const Akonadi::Collection &collection); static void invalidateItem(const Akonadi::Item &item); static void invalidateTag(const Akonadi::Tag &tag); protected: explicit ChangeMediator(QObject *parent = nullptr); + Q_DISABLE_COPY_MOVE(ChangeMediator) QList m_monitors; }; } #endif diff --git a/src/core/changenotificationdependenciesfactory_p.h b/src/core/changenotificationdependenciesfactory_p.h index 6ff5902f8..51ec8cd57 100644 --- a/src/core/changenotificationdependenciesfactory_p.h +++ b/src/core/changenotificationdependenciesfactory_p.h @@ -1,57 +1,60 @@ /* Copyright (c) 2011 Stephen Kelly 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 CHANGENOTIFICATIONDEPENDENCIESFACTORY_P_H #define CHANGENOTIFICATIONDEPENDENCIESFACTORY_P_H #include "session.h" #include "entitycache_p.h" namespace Akonadi { class Connection; class CommandBuffer; /** * This class exists so that we can create a fake notification source in * unit tests. */ class AKONADI_TESTS_EXPORT ChangeNotificationDependenciesFactory { public: + explicit ChangeNotificationDependenciesFactory() = default; virtual ~ChangeNotificationDependenciesFactory() = default; virtual Connection *createNotificationConnection(Session *parent, CommandBuffer *commandBuffer); virtual void destroyNotificationConnection(Session *parent, Connection *connection); virtual QObject *createChangeMediator(QObject *parent); virtual Akonadi::CollectionCache *createCollectionCache(int maxCapacity, Session *session); virtual Akonadi::ItemCache *createItemCache(int maxCapacity, Session *session); virtual Akonadi::ItemListCache *createItemListCache(int maxCapacity, Session *session); virtual Akonadi::TagListCache *createTagListCache(int maxCapacity, Session *session); protected: + Q_DISABLE_COPY_MOVE(ChangeNotificationDependenciesFactory) + void addConnection(Session *session, Connection *connection); }; } #endif diff --git a/src/core/changerecorder.cpp b/src/core/changerecorder.cpp index d372f34cc..bf0adaae3 100644 --- a/src/core/changerecorder.cpp +++ b/src/core/changerecorder.cpp @@ -1,127 +1,128 @@ /* Copyright (c) 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 "changerecorder.h" #include "changerecorder_p.h" #include using namespace Akonadi; ChangeRecorder::ChangeRecorder(QObject *parent) : Monitor(new ChangeRecorderPrivate(nullptr, this), parent) { } ChangeRecorder::ChangeRecorder(ChangeRecorderPrivate *privateclass, QObject *parent) : Monitor(privateclass, parent) { } ChangeRecorder::~ChangeRecorder() { } void ChangeRecorder::setConfig(QSettings *settings) { Q_D(ChangeRecorder); if (settings) { d->settings = settings; Q_ASSERT(d->pendingNotifications.isEmpty()); d->loadNotifications(); } else if (d->settings) { if (d->enableChangeRecording) { d->saveNotifications(); } d->settings = settings; } } void ChangeRecorder::replayNext() { Q_D(ChangeRecorder); if (!d->enableChangeRecording) { return; } if (!d->pendingNotifications.isEmpty()) { const auto msg = d->pendingNotifications.head(); if (d->ensureDataAvailable(msg)) { d->emitNotification(msg); } else if (d->translateAndCompress(d->pipeline, msg)) { // The msg is now in both pipeline and pendingNotifications. // When data is available, MonitorPrivate::flushPipeline will emitNotification. // When changeProcessed is called, we'll finally remove it from pendingNotifications. } else { // In the case of a move where both source and destination are // ignored, we ignore the message and process the next one. d->dequeueNotification(); - return replayNext(); + replayNext(); + return; } } else { // This is necessary when none of the notifications were accepted / processed // above, and so there is no one to call changeProcessed() and the ChangeReplay task // will be stuck forever in the ResourceScheduler. Q_EMIT nothingToReplay(); } } bool ChangeRecorder::isEmpty() const { Q_D(const ChangeRecorder); return d->pendingNotifications.isEmpty(); } void ChangeRecorder::changeProcessed() { Q_D(ChangeRecorder); if (!d->enableChangeRecording) { return; } // changerecordertest.cpp calls changeProcessed after receiving nothingToReplay, // so test for emptiness. Not sure real code does this though. // Q_ASSERT( !d->pendingNotifications.isEmpty() ) if (!d->pendingNotifications.isEmpty()) { d->dequeueNotification(); } } void ChangeRecorder::setChangeRecordingEnabled(bool enable) { Q_D(ChangeRecorder); if (d->enableChangeRecording == enable) { return; } d->enableChangeRecording = enable; if (enable) { d->m_needFullSave = true; d->notificationsLoaded(); } else { d->dispatchNotifications(); } } QString Akonadi::ChangeRecorder::dumpNotificationListToString() const { Q_D(const ChangeRecorder); return d->dumpNotificationListToString(); } diff --git a/src/core/collection.cpp b/src/core/collection.cpp index b075a8601..0a4f3c626 100644 --- a/src/core/collection.cpp +++ b/src/core/collection.cpp @@ -1,444 +1,444 @@ /* 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 "collection.h" #include "collection_p.h" #include "attributefactory.h" #include "cachepolicy.h" #include "collectionrightsattribute_p.h" #include "collectionstatistics.h" #include "entitydisplayattribute.h" #include #include #include #include #include #include using namespace Akonadi; Q_GLOBAL_STATIC(Akonadi::Collection, s_defaultParentCollection) uint Akonadi::qHash(const Akonadi::Collection &collection) { return ::qHash(collection.id()); } /** * Helper method for assignment operator and copy constructor. */ static void assignCollectionPrivate(QSharedDataPointer &one, const QSharedDataPointer &other) { // We can't simply do one = other here, we have to use a temp. // Otherwise ProtocolHelperTest::testParentCollectionAfterCollectionParsing() // will break. // // The reason are assignments like // col = col.parentCollection() // // Here, parentCollection() actually returns a reference to a pointer owned // by col. So when col (or rather, it's private class) is deleted, the pointer // to the parent collection and therefore the reference becomes invalid. // // With a single-line assignment here, the parent collection would be deleted // before it is assigned, and therefore the resulting object would point to // uninitalized memory. QSharedDataPointer temp = other; one = temp; } class CollectionRoot : public Collection { public: CollectionRoot() : Collection(0) { setContentMimeTypes({ Collection::mimeType() }); // The root collection is read-only for the users setRights(Collection::ReadOnly); } }; Q_GLOBAL_STATIC(CollectionRoot, s_root) Collection::Collection() : d_ptr(new CollectionPrivate) { static int lastId = -1; d_ptr->mId = lastId--; } Collection::Collection(Id id) : d_ptr(new CollectionPrivate(id)) { } Collection::Collection(const Collection &other) { assignCollectionPrivate(d_ptr, other.d_ptr); } Collection::Collection(Collection &&) = default; Collection::~Collection() = default; void Collection::setId(Collection::Id identifier) { d_ptr->mId = identifier; } Collection::Id Collection::id() const { return d_ptr->mId; } void Collection::setRemoteId(const QString &id) { d_ptr->mRemoteId = id; } QString Collection::remoteId() const { return d_ptr->mRemoteId; } void Collection::setRemoteRevision(const QString &revision) { d_ptr->mRemoteRevision = revision; } QString Collection::remoteRevision() const { return d_ptr->mRemoteRevision; } bool Collection::isValid() const { return (d_ptr->mId >= 0); } bool Collection::operator==(const Collection &other) const { // Invalid collections are the same, no matter what their internal ID is return (!isValid() && !other.isValid()) || (d_ptr->mId == other.d_ptr->mId); } bool Akonadi::Collection::operator!=(const Collection &other) const { return (isValid() || other.isValid()) && (d_ptr->mId != other.d_ptr->mId); } Collection &Collection ::operator=(const Collection &other) { if (this != &other) { assignCollectionPrivate(d_ptr, other.d_ptr); } return *this; } bool Akonadi::Collection::operator<(const Collection &other) const { return d_ptr->mId < other.d_ptr->mId; } void Collection::addAttribute(Attribute *attr) { d_ptr->mAttributeStorage.addAttribute(attr); } void Collection::removeAttribute(const QByteArray &type) { d_ptr->mAttributeStorage.removeAttribute(type); } bool Collection::hasAttribute(const QByteArray &type) const { return d_ptr->mAttributeStorage.hasAttribute(type); } Attribute::List Collection::attributes() const { return d_ptr->mAttributeStorage.attributes(); } void Akonadi::Collection::clearAttributes() { - return d_ptr->mAttributeStorage.clearAttributes(); + d_ptr->mAttributeStorage.clearAttributes(); } Attribute *Collection::attribute(const QByteArray &type) { markAttributeModified(type); return d_ptr->mAttributeStorage.attribute(type); } const Attribute *Collection::attribute(const QByteArray &type) const { return d_ptr->mAttributeStorage.attribute(type); } Collection &Collection::parentCollection() { if (!d_ptr->mParent) { d_ptr->mParent.reset(new Collection()); } return *d_ptr->mParent.get(); } Collection Collection::parentCollection() const { if (!d_ptr->mParent) { return *(s_defaultParentCollection); } else { return *d_ptr->mParent.get(); } } void Collection::setParentCollection(const Collection &parent) { d_ptr->mParent.reset(new Collection(parent)); } QString Collection::name() const { return d_ptr->name; } QString Collection::displayName() const { const EntityDisplayAttribute *const attr = attribute(); const QString displayName = attr ? attr->displayName() : QString(); return !displayName.isEmpty() ? displayName : d_ptr->name; } void Collection::setName(const QString &name) { d_ptr->name = name; } Collection::Rights Collection::rights() const { if (const auto attr = attribute()) { return attr->rights(); } else { return AllRights; } } void Collection::setRights(Rights rights) { attribute(AddIfMissing)->setRights(rights); } QStringList Collection::contentMimeTypes() const { return d_ptr->contentTypes; } void Collection::setContentMimeTypes(const QStringList &types) { if (d_ptr->contentTypes != types) { d_ptr->contentTypes = types; d_ptr->contentTypesChanged = true; } } QUrl Collection::url(UrlType type) const { QUrlQuery query; query.addQueryItem(QStringLiteral("collection"), QString::number(id())); if (type == UrlWithName) { query.addQueryItem(QStringLiteral("name"), name()); } QUrl url; url.setScheme(QStringLiteral("akonadi")); url.setQuery(query); return url; } Collection Collection::fromUrl(const QUrl &url) { if (url.scheme() != QLatin1String("akonadi")) { return Collection(); } const QString colStr = QUrlQuery(url).queryItemValue(QStringLiteral("collection")); bool ok = false; Collection::Id colId = colStr.toLongLong(&ok); if (!ok) { return Collection(); } if (colId == 0) { return Collection::root(); } return Collection(colId); } Collection Collection::root() { return *s_root; } QString Collection::mimeType() { return QStringLiteral("inode/directory"); } QString Akonadi::Collection::virtualMimeType() { return QStringLiteral("application/x-vnd.akonadi.collection.virtual"); } QString Collection::resource() const { return d_ptr->resource; } void Collection::setResource(const QString &resource) { d_ptr->resource = resource; } QDebug operator <<(QDebug d, const Akonadi::Collection &collection) { return d << "Collection ID:" << collection.id() << " remote ID:" << collection.remoteId() << '\n' << " name:" << collection.name() << '\n' << " url:" << collection.url() << '\n' << " parent:" << collection.parentCollection().id() << collection.parentCollection().remoteId() << '\n' << " resource:" << collection.resource() << '\n' << " rights:" << collection.rights() << '\n' << " contents mime type:" << collection.contentMimeTypes() << '\n' << " isVirtual:" << collection.isVirtual() << '\n' << " " << collection.cachePolicy() << '\n' << " " << collection.statistics(); } CollectionStatistics Collection::statistics() const { return d_ptr->statistics; } void Collection::setStatistics(const CollectionStatistics &statistics) { d_ptr->statistics = statistics; } CachePolicy Collection::cachePolicy() const { return d_ptr->cachePolicy; } void Collection::setCachePolicy(const CachePolicy &cachePolicy) { d_ptr->cachePolicy = cachePolicy; d_ptr->cachePolicyChanged = true; } bool Collection::isVirtual() const { return d_ptr->isVirtual; } void Akonadi::Collection::setVirtual(bool isVirtual) { d_ptr->isVirtual = isVirtual; } void Collection::setEnabled(bool enabled) { d_ptr->enabledChanged = true; d_ptr->enabled = enabled; } bool Collection::enabled() const { return d_ptr->enabled; } void Collection::setLocalListPreference(Collection::ListPurpose purpose, Collection::ListPreference preference) { switch (purpose) { case ListDisplay: d_ptr->displayPreference = preference; break; case ListSync: d_ptr->syncPreference = preference; break; case ListIndex: d_ptr->indexPreference = preference; break; } d_ptr->listPreferenceChanged = true; } Collection::ListPreference Collection::localListPreference(Collection::ListPurpose purpose) const { switch (purpose) { case ListDisplay: return d_ptr->displayPreference; case ListSync: return d_ptr->syncPreference; case ListIndex: return d_ptr->indexPreference; } return ListDefault; } bool Collection::shouldList(Collection::ListPurpose purpose) const { if (localListPreference(purpose) == ListDefault) { return enabled(); } return (localListPreference(purpose) == ListEnabled); } void Collection::setShouldList(ListPurpose purpose, bool list) { if (localListPreference(purpose) == ListDefault) { setEnabled(list); } else { setLocalListPreference(purpose, list ? ListEnabled : ListDisabled); } } void Collection::setKeepLocalChanges(const QSet &parts) { d_ptr->keepLocalChanges = parts; } QSet Collection::keepLocalChanges() const { return d_ptr->keepLocalChanges; } void Collection::markAttributeModified(const QByteArray &type) { d_ptr->mAttributeStorage.markAttributeModified(type); } diff --git a/src/core/collectionsync.cpp b/src/core/collectionsync.cpp index 65f75a4e2..bc2296945 100644 --- a/src/core/collectionsync.cpp +++ b/src/core/collectionsync.cpp @@ -1,866 +1,862 @@ /* Copyright (c) 2007, 2009 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 "collectionsync_p.h" #include "collection.h" #include "akonadicore_debug.h" #include "collectioncreatejob.h" #include "collectiondeletejob.h" #include "collectionfetchjob.h" #include "collectionmodifyjob.h" #include "collectionfetchscope.h" #include "collectionmovejob.h" #include "cachepolicy.h" #include #include #include #include using namespace Akonadi; static const char CONTENTMIMETYPES[] = "CONTENTMIMETYPES"; static const char ROOTPARENTRID[] = "AKONADI_ROOT_COLLECTION"; class RemoteId { public: explicit RemoteId() { } explicit inline RemoteId(const QStringList &ridChain): ridChain(ridChain) { } explicit inline RemoteId(const QString &rid) { ridChain.append(rid); } inline bool isAbsolute() const { return ridChain.last() == QString::fromLatin1(ROOTPARENTRID); } inline bool isEmpty() const { return ridChain.isEmpty(); } inline bool operator==(const RemoteId &other) const { return ridChain == other.ridChain; } QStringList ridChain; static RemoteId rootRid; }; RemoteId RemoteId::rootRid = RemoteId(QStringList() << QString::fromLatin1(ROOTPARENTRID)); Q_DECLARE_METATYPE(RemoteId) uint qHash(const RemoteId &rid) { uint hash = 0; for (QStringList::ConstIterator iter = rid.ridChain.constBegin(), end = rid.ridChain.constEnd(); iter != end; ++iter) { hash += qHash(*iter); } return hash; } inline bool operator<(const RemoteId &r1, const RemoteId &r2) { if (r1.ridChain.length() == r2.ridChain.length()) { QStringList::ConstIterator it1 = r1.ridChain.constBegin(), end1 = r1.ridChain.constEnd(), it2 = r2.ridChain.constBegin(); while (it1 != end1) { if ((*it1) == (*it2)) { ++it1; ++it2; continue; } return (*it1) < (*it2); } } else { return r1.ridChain.length() < r2.ridChain.length(); } return false; } QDebug operator<<(QDebug s, const RemoteId &rid) { s.nospace() << "RemoteId(" << rid.ridChain << ")"; return s; } /** * @internal */ class CollectionSync::Private { public: Private(CollectionSync *parent) : q(parent) , pendingJobs(0) , progress(0) , currentTransaction(nullptr) , incremental(false) , streaming(false) , hierarchicalRIDs(false) , localListDone(false) , deliveryDone(false) , akonadiRootCollection(Collection::root()) , resultEmitted(false) { } ~Private() { } RemoteId remoteIdForCollection(const Collection &collection) const { if (collection == Collection::root()) { return RemoteId::rootRid; } if (!hierarchicalRIDs) { return RemoteId(collection.remoteId()); } RemoteId rid; Collection parent = collection; while (parent.isValid() || !parent.remoteId().isEmpty()) { QString prid = parent.remoteId(); if (prid.isEmpty() && parent.isValid()) { prid = uidRidMap.value(parent.id()); } if (prid.isEmpty()) { break; } rid.ridChain.append(prid); parent = parent.parentCollection(); if (parent == akonadiRootCollection) { rid.ridChain.append(QString::fromLatin1(ROOTPARENTRID)); break; } } return rid; } void addRemoteColection(const Collection &collection, bool removed = false) { QHash &map = (removed ? removedRemoteCollections : remoteCollections); const Collection parentCollection = collection.parentCollection(); if (parentCollection.remoteId() == akonadiRootCollection.remoteId() || parentCollection.id() == akonadiRootCollection.id()) { Collection c2(collection); c2.setParentCollection(akonadiRootCollection); map[RemoteId::rootRid].append(c2); } else { Q_ASSERT(!parentCollection.remoteId().isEmpty()); map[remoteIdForCollection(parentCollection)].append(collection); } } /* Compares collections by remoteId and falls back to name comparison in case * local collection does not have remoteId (which can happen in some cases) */ bool matchLocalAndRemoteCollection(const Collection &local, const Collection &remote) { if (!local.remoteId().isEmpty()) { return local.remoteId() == remote.remoteId(); } else { return local.name() == remote.name(); } } void localCollectionsReceived(const Akonadi::Collection::List &localCols) { for (const Akonadi::Collection &collection : localCols) { const RemoteId parentRid = remoteIdForCollection(collection.parentCollection()); localCollections[parentRid] += collection; } } void processCollections(const RemoteId &parentRid) { Collection::List remoteChildren = remoteCollections.value(parentRid); Collection::List removedChildren = removedRemoteCollections.value(parentRid); Collection::List localChildren = localCollections.value(parentRid); // Iterate over the list of local children of localParent Collection::List::Iterator localIter, localEnd, removedIter, removedEnd, remoteIter, remoteEnd; for (localIter = localChildren.begin(), localEnd = localChildren.end(); localIter != localEnd;) { const Collection localCollection = *localIter; bool matched = false; uidRidMap.insert(localIter->id(), localIter->remoteId()); // Try to map removed remote collections (from incremental sync) to local collections for (removedIter = removedChildren.begin(), removedEnd = removedChildren.end(); removedIter != removedEnd;) { Collection removedCollection = *removedIter; if (matchLocalAndRemoteCollection(localCollection, removedCollection)) { matched = true; if (!localCollection.remoteId().isEmpty()) { localCollectionsToRemove.append(localCollection); } // Remove the matched removed collection from the list so that // we don't have to iterate over it again next time. removedIter = removedChildren.erase(removedIter); removedEnd = removedChildren.end(); break; } else { // Keep looking ++removedIter; } } if (matched) { // Remove the matched local collection from the list, because we // have already put it into localCollectionsToRemove localIter = localChildren.erase(localIter); localEnd = localChildren.end(); continue; } // Try to find a matching collection in the list of remote children for (remoteIter = remoteChildren.begin(), remoteEnd = remoteChildren.end(); !matched && remoteIter != remoteEnd;) { Collection remoteCollection = *remoteIter; // Yay, we found a match! if (matchLocalAndRemoteCollection(localCollection, remoteCollection)) { matched = true; // "Virtual" flag cannot be updated: we need to recreate // the collection from scratch. if (localCollection.isVirtual() != remoteCollection.isVirtual()) { // Mark the local collection and all its children for deletion and re-creation QList> parents = {{localCollection,remoteCollection}}; while (!parents.empty()) { auto parent = parents.takeFirst(); qCDebug(AKONADICORE_LOG) << "Local collection " << parent.first.name() << " will be recreated"; localCollectionsToRemove.push_back(parent.first); remoteCollectionsToCreate.push_back(parent.second); for (auto it = localChildren.begin(), end = localChildren.end(); it != end;) { if (it->parentCollection() == parent.first) { Collection remoteParent; auto remoteIt = std::find_if(remoteChildren.begin(), remoteChildren.end(), std::bind(&CollectionSync::Private::matchLocalAndRemoteCollection, this, parent.first, std::placeholders::_1)); if (remoteIt != remoteChildren.end()) { remoteParent = *remoteIt; remoteEnd = remoteChildren.erase(remoteIt); } parents.push_back({*it, remoteParent}); it = localChildren.erase(it); localEnd = end = localChildren.end(); } else { ++it; } } } } else if (collectionNeedsUpdate(localCollection, remoteCollection)) { // We need to store both local and remote collections, so that // we can copy over attributes to be preserved remoteCollectionsToUpdate.append(qMakePair(localCollection, remoteCollection)); } else { // Collections are the same, no need to update anything } // Remove the matched remote collection from the list so that // in the end we are left with list of collections that don't // exist locally (i.e. new collections) remoteIter = remoteChildren.erase(remoteIter); remoteEnd = remoteChildren.end(); break; } else { // Keep looking ++remoteIter; } } if (matched) { // Remove the matched local collection from the list so that // in the end we are left with list of collections that don't // exist remotely (i.e. removed collections) localIter = localChildren.erase(localIter); localEnd = localChildren.end(); } else { ++localIter; } } if (!removedChildren.isEmpty()) { removedRemoteCollections[parentRid] = removedChildren; } else { removedRemoteCollections.remove(parentRid); } if (!remoteChildren.isEmpty()) { remoteCollections[parentRid] = remoteChildren; } else { remoteCollections.remove(parentRid); } if (!localChildren.isEmpty()) { localCollections[parentRid] = localChildren; } else { localCollections.remove(parentRid); } } void processLocalCollections(const RemoteId &parentRid, const Collection &parentCollection) { const Collection::List originalChildren = localCollections.value(parentRid); processCollections(parentRid); const Collection::List remoteChildren = remoteCollections.take(parentRid); const Collection::List localChildren = localCollections.take(parentRid); // At this point remoteChildren contains collections that don't exist locally yet if (!remoteChildren.isEmpty()) { for (Collection c : remoteChildren) { c.setParentCollection(parentCollection); remoteCollectionsToCreate.append(c); } } // At this point localChildren contains collections that don't exist remotely anymore if (!localChildren.isEmpty() && !incremental) { for (const auto &c : localChildren) { if (!c.remoteId().isEmpty()) { localCollectionsToRemove.push_back(c); } } } // Recurse into children for (const Collection &c : originalChildren) { processLocalCollections(remoteIdForCollection(c), c); } } void localCollectionFetchResult(KJob *job) { if (job->error()) { return; // handled by the base class } processLocalCollections(RemoteId::rootRid, akonadiRootCollection); localListDone = true; execute(); } bool ignoreAttributeChanges(const Akonadi::Collection &col, const QByteArray &attribute) const { return (keepLocalChanges.contains(attribute) || col.keepLocalChanges().contains(attribute)); } /** Checks if the given localCollection and remoteCollection are different */ bool collectionNeedsUpdate(const Collection &localCollection, const Collection &remoteCollection) const { if (!ignoreAttributeChanges(remoteCollection, CONTENTMIMETYPES)) { if (localCollection.contentMimeTypes().size() != remoteCollection.contentMimeTypes().size()) { return true; } else { for (int i = 0, total = remoteCollection.contentMimeTypes().size(); i < total; ++i) { const QString &m = remoteCollection.contentMimeTypes().at(i); if (!localCollection.contentMimeTypes().contains(m)) { return true; } } } } if (localCollection.parentCollection().remoteId() != remoteCollection.parentCollection().remoteId()) { return true; } if (localCollection.name() != remoteCollection.name()) { return true; } if (localCollection.remoteId() != remoteCollection.remoteId()) { return true; } if (localCollection.remoteRevision() != remoteCollection.remoteRevision()) { return true; } if (!(localCollection.cachePolicy() == remoteCollection.cachePolicy())) { return true; } if (localCollection.enabled() != remoteCollection.enabled()) { return true; } // CollectionModifyJob adds the remote attributes to the local collection const Akonadi::Attribute::List lstAttr = remoteCollection.attributes(); for (const Attribute *attr : lstAttr) { const Attribute *localAttr = localCollection.attribute(attr->type()); if (localAttr && ignoreAttributeChanges(remoteCollection, attr->type())) { continue; } // The attribute must both exist and have equal contents if (!localAttr || localAttr->serialized() != attr->serialized()) { return true; } } return false; } void createLocalCollections() { if (remoteCollectionsToCreate.isEmpty()) { updateLocalCollections(); return; } Collection::List::Iterator iter, end; for (iter = remoteCollectionsToCreate.begin(), end = remoteCollectionsToCreate.end(); iter != end;) { const Collection col = *iter; const Collection parentCollection = col.parentCollection(); // The parent already exists locally if (parentCollection == akonadiRootCollection || parentCollection.id() > 0) { ++pendingJobs; CollectionCreateJob *create = new CollectionCreateJob(col, currentTransaction); - connect(create, SIGNAL(result(KJob*)), - q, SLOT(createLocalCollectionResult(KJob*))); + QObject::connect(create, &KJob::result, q, [this](KJob *job) { createLocalCollectionResult(job); }); // Commit transaction after every 100 collections are created, // otherwise it overloads database journal and things get veeery slow if (pendingJobs % 100 == 0) { currentTransaction->commit(); createTransaction(); } iter = remoteCollectionsToCreate.erase(iter); end = remoteCollectionsToCreate.end(); } else { // Skip the collection, we'll try again once we create all the other // collection we already have a parent for ++iter; } } } void createLocalCollectionResult(KJob *job) { --pendingJobs; if (job->error()) { return; // handled by the base class } q->setProcessedAmount(KJob::Bytes, ++progress); const Collection newLocal = static_cast(job)->collection(); uidRidMap.insert(newLocal.id(), newLocal.remoteId()); const RemoteId newLocalRID = remoteIdForCollection(newLocal); // See if there are any pending collections that this collection is parent of and // update them if so Collection::List::Iterator iter, end; for (iter = remoteCollectionsToCreate.begin(), end = remoteCollectionsToCreate.end(); iter != end; ++iter) { const Collection parentCollection = iter->parentCollection(); if (parentCollection != akonadiRootCollection && parentCollection.id() <= 0) { const RemoteId remoteRID = remoteIdForCollection(*iter); if (remoteRID.isAbsolute()) { if (newLocalRID == remoteIdForCollection(*iter)) { iter->setParentCollection(newLocal); } } else if (!hierarchicalRIDs) { if (remoteRID.ridChain.startsWith(parentCollection.remoteId())) { iter->setParentCollection(newLocal); } } } } // Enqueue all pending remote collections that are children of the just-created // collection Collection::List collectionsToCreate = remoteCollections.take(newLocalRID); if (collectionsToCreate.isEmpty() && !hierarchicalRIDs) { collectionsToCreate = remoteCollections.take(RemoteId(newLocal.remoteId())); } for (Collection col : qAsConst(collectionsToCreate)) { col.setParentCollection(newLocal); remoteCollectionsToCreate.append(col); } // If there are still any collections to create left, try if we just created // a parent for any of them if (!remoteCollectionsToCreate.isEmpty()) { createLocalCollections(); } else if (pendingJobs == 0) { Q_ASSERT(remoteCollectionsToCreate.isEmpty()); if (!remoteCollections.isEmpty()) { currentTransaction->rollback(); q->setError(Unknown); q->setErrorText(i18n("Found unresolved orphan collections")); qCWarning(AKONADICORE_LOG) << "found unresolved orphan collection"; emitResult(); return; } currentTransaction->commit(); createTransaction(); // Otherwise move to next task: updating existing collections updateLocalCollections(); } /* * else if (!remoteCollections.isEmpty()) { currentTransaction->rollback(); q->setError(Unknown); q->setErrorText(i18n("Incomplete collection tree")); emitResult(); return; } */ } /** Performs a local update for the given node pair. */ void updateLocalCollections() { if (remoteCollectionsToUpdate.isEmpty()) { deleteLocalCollections(); return; } typedef QPair CollectionPair; for (const CollectionPair &pair : qAsConst(remoteCollectionsToUpdate)) { const Collection local = pair.first; const Collection remote = pair.second; Collection upd(remote); Q_ASSERT(!upd.remoteId().isEmpty()); Q_ASSERT(currentTransaction); upd.setId(local.id()); if (ignoreAttributeChanges(remote, CONTENTMIMETYPES)) { upd.setContentMimeTypes(local.contentMimeTypes()); } Q_FOREACH (Attribute *remoteAttr, upd.attributes()) { if (ignoreAttributeChanges(remote, remoteAttr->type()) && local.hasAttribute(remoteAttr->type())) { //We don't want to overwrite the attribute changes with the defaults provided by the resource. const Attribute *localAttr = local.attribute(remoteAttr->type()); upd.removeAttribute(localAttr->type()); upd.addAttribute(localAttr->clone()); } } // ### HACK to work around the implicit move attempts of CollectionModifyJob // which we do explicitly below Collection c(upd); c.setParentCollection(local.parentCollection()); ++pendingJobs; CollectionModifyJob *mod = new CollectionModifyJob(c, currentTransaction); - connect(mod, SIGNAL(result(KJob*)), q, SLOT(updateLocalCollectionResult(KJob*))); + QObject::connect(mod, &KJob::result, q, [this](KJob *job) { updateLocalCollectionResult(job); }); // detecting moves is only possible with global RIDs if (!hierarchicalRIDs) { if (remote.parentCollection().isValid() && remote.parentCollection().id() != local.parentCollection().id()) { ++pendingJobs; CollectionMoveJob *move = new CollectionMoveJob(upd, remote.parentCollection(), currentTransaction); - connect(move, SIGNAL(result(KJob*)), q, SLOT(updateLocalCollectionResult(KJob*))); + QObject::connect(move, &KJob::result, q, [this](KJob *job) { updateLocalCollectionResult(job); }); } } } } void updateLocalCollectionResult(KJob *job) { --pendingJobs; if (job->error()) { return; // handled by the base class } if (qobject_cast(job)) { q->setProcessedAmount(KJob::Bytes, ++progress); } // All updates are done, time to move on to next task: deletion if (pendingJobs == 0) { currentTransaction->commit(); createTransaction(); deleteLocalCollections(); } } void deleteLocalCollections() { if (localCollectionsToRemove.isEmpty()) { done(); return; } for (const Collection &col : qAsConst(localCollectionsToRemove)) { Q_ASSERT(!col.remoteId().isEmpty()); // empty RID -> stuff we haven't even written to the remote side yet ++pendingJobs; Q_ASSERT(currentTransaction); CollectionDeleteJob *job = new CollectionDeleteJob(col, currentTransaction); - connect(job, SIGNAL(result(KJob*)), q, SLOT(deleteLocalCollectionsResult(KJob*))); + connect(job, &KJob::result, q, [this](KJob *job) { deleteLocalCollectionsResult(job); }); // It can happen that the groupware servers report us deleted collections // twice, in this case this collection delete job will fail on the second try. // To avoid a rollback of the complete transaction we gracefully allow the job // to fail :) currentTransaction->setIgnoreJobFailure(job); } } void deleteLocalCollectionsResult(KJob *) { --pendingJobs; q->setProcessedAmount(KJob::Bytes, ++progress); if (pendingJobs == 0) { currentTransaction->commit(); currentTransaction = nullptr; done(); } } void done() { if (currentTransaction) { //This can trigger a direct call of transactionSequenceResult currentTransaction->commit(); currentTransaction = nullptr; } if (!remoteCollections.isEmpty()) { q->setError(Unknown); q->setErrorText(i18n("Found unresolved orphan collections")); } emitResult(); } void emitResult() { //Prevent double result emission Q_ASSERT(!resultEmitted); if (!resultEmitted) { if (q->hasSubjobs()) { // If there are subjobs, pick one, wait for it to finish, then // try again. This way we make sure we don't emit result() signal // while there is still a Transaction job running KJob *subjob = q->subjobs().at(0); connect(subjob, &KJob::result, q, [this](KJob *) { emitResult(); }, Qt::QueuedConnection); } else { resultEmitted = true; q->emitResult(); } } } void createTransaction() { currentTransaction = new TransactionSequence(q); currentTransaction->setAutomaticCommittingEnabled(false); - q->connect(currentTransaction, SIGNAL(finished(KJob*)), - q, SLOT(transactionSequenceResult(KJob*))); + q->connect(currentTransaction, &TransactionSequence::finished, q, [this](KJob *job) { transactionSequenceResult(job); }); } /** After the transaction has finished report we're done as well. */ void transactionSequenceResult(KJob *job) { if (job->error()) { return; // handled by the base class } // If this was the last transaction, then finish, otherwise there's // a new transaction in the queue already if (job == currentTransaction) { currentTransaction = nullptr; } } /** Process what's currently available. */ void execute() { qCDebug(AKONADICORE_LOG) << "localListDone: " << localListDone << " deliveryDone: " << deliveryDone; if (!localListDone && !deliveryDone) { return; } if (!localListDone && deliveryDone) { Job *parent = (currentTransaction ? static_cast(currentTransaction) : static_cast(q)); CollectionFetchJob *job = new CollectionFetchJob(akonadiRootCollection, CollectionFetchJob::Recursive, parent); job->fetchScope().setResource(resourceId); job->fetchScope().setListFilter(CollectionFetchScope::NoFilter); job->fetchScope().setAncestorRetrieval(CollectionFetchScope::All); - q->connect(job, SIGNAL(collectionsReceived(Akonadi::Collection::List)), - q, SLOT(localCollectionsReceived(Akonadi::Collection::List))); - q->connect(job, SIGNAL(result(KJob*)), - q, SLOT(localCollectionFetchResult(KJob*))); + q->connect(job, &CollectionFetchJob::collectionsReceived, q, [this](const auto &cols) { localCollectionsReceived(cols); }); + q->connect(job, &KJob::result, q, [this](KJob *job) { localCollectionFetchResult(job); }); return; } // If a transaction is not started yet, it means we just finished local listing if (!currentTransaction) { // There's nothing to do after local listing -> we are done! if (remoteCollectionsToCreate.isEmpty() && remoteCollectionsToUpdate.isEmpty() && localCollectionsToRemove.isEmpty()) { qCDebug(AKONADICORE_LOG) << "Nothing to do"; emitResult(); return; } // Ok, there's some work to do, so create a transaction we can use createTransaction(); } createLocalCollections(); } CollectionSync *q; QString resourceId; int pendingJobs; int progress; TransactionSequence *currentTransaction; bool incremental; bool streaming; bool hierarchicalRIDs; bool localListDone; bool deliveryDone; // List of parts where local changes should not be overwritten QSet keepLocalChanges; QHash removedRemoteCollections; QHash remoteCollections; QHash localCollections; Collection::List localCollectionsToRemove; Collection::List remoteCollectionsToCreate; QList > remoteCollectionsToUpdate; QHash uidRidMap; // HACK: To workaround Collection copy constructor being very expensive, we // store the Collection::root() collection in a variable here for faster // access Collection akonadiRootCollection; bool resultEmitted; }; CollectionSync::CollectionSync(const QString &resourceId, QObject *parent) : Job(parent) , d(new Private(this)) { d->resourceId = resourceId; setTotalAmount(KJob::Bytes, 0); } CollectionSync::~CollectionSync() { delete d; } void CollectionSync::setRemoteCollections(const Collection::List &remoteCollections) { setTotalAmount(KJob::Bytes, totalAmount(KJob::Bytes) + remoteCollections.count()); for (const Collection &c : remoteCollections) { d->addRemoteColection(c); } if (!d->streaming) { d->deliveryDone = true; } d->execute(); } void CollectionSync::setRemoteCollections(const Collection::List &changedCollections, const Collection::List &removedCollections) { setTotalAmount(KJob::Bytes, totalAmount(KJob::Bytes) + changedCollections.count()); d->incremental = true; for (const Collection &c : changedCollections) { d->addRemoteColection(c); } for (const Collection &c : removedCollections) { d->addRemoteColection(c, true); } if (!d->streaming) { d->deliveryDone = true; } d->execute(); } void CollectionSync::doStart() { } void CollectionSync::setStreamingEnabled(bool streaming) { d->streaming = streaming; } void CollectionSync::retrievalDone() { d->deliveryDone = true; d->execute(); } void CollectionSync::setHierarchicalRemoteIds(bool hierarchical) { d->hierarchicalRIDs = hierarchical; } void CollectionSync::rollback() { if (d->currentTransaction) { d->currentTransaction->rollback(); } } void CollectionSync::setKeepLocalChanges(const QSet &parts) { d->keepLocalChanges = parts; } #include "moc_collectionsync_p.cpp" diff --git a/src/core/collectionsync_p.h b/src/core/collectionsync_p.h index 0c186d055..0bdb8215c 100644 --- a/src/core/collectionsync_p.h +++ b/src/core/collectionsync_p.h @@ -1,144 +1,137 @@ /* Copyright (c) 2007, 2009 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. */ #ifndef AKONADI_COLLECTIONSYNC_P_H #define AKONADI_COLLECTIONSYNC_P_H #include "akonadicore_export.h" #include "collection.h" #include "transactionsequence.h" namespace Akonadi { /** @internal Syncs remote and local collections. Basic terminology: - "local": The current state in the Akonadi server - "remote": The state in the backend, which is also the state the Akonadi server is supposed to have afterwards. There are three options to influence the way syncing is done: - Streaming vs. complete delivery: If streaming is enabled remote collections do not need to be delivered in a single batch but can be delivered in multiple chunks. This improves performance but requires an explicit notification when delivery has been completed. - Incremental vs. non-incremental: In the incremental case only remote changes since the last sync have to be delivered, in the non-incremental mode the full remote state has to be provided. The first is obviously the preferred way, but requires support by the backend. - Hierarchical vs. global RIDs: The first requires RIDs to be unique per parent collection, the second one requires globally unique RIDs (per resource). Those have different advantages and disadvantages, esp. regarding moving. Which one to chose mostly depends on what the backend provides in this regard. */ class AKONADICORE_EXPORT CollectionSync : public Job { Q_OBJECT public: /** Creates a new collection synchronzier. @param resourceId The identifier of the resource we are syncing. @param parent The parent object. */ explicit CollectionSync(const QString &resourceId, QObject *parent = nullptr); /** Destroys this job. */ ~CollectionSync() override; /** Sets the result of a full remote collection listing. @param remoteCollections A list of collections. Important: All of these need a unique remote identifier and parent remote identifier. */ void setRemoteCollections(const Collection::List &remoteCollections); /** Sets the result of an incremental remote collection listing. @param changedCollections A list of remotely added or changed collections. @param removedCollections A list of remotely deleted collections. */ void setRemoteCollections(const Collection::List &changedCollections, const Collection::List &removedCollections); /** Enables streaming, that is not all collections are delivered at once. Use setRemoteCollections() multiple times when streaming is enabled and call retrievalDone() when all collections have been retrieved. Must be called before the first call to setRemoteCollections(). @param streaming enables streaming if set as @c true */ void setStreamingEnabled(bool streaming); /** Indicate that all collections have been retrieved in streaming mode. */ void retrievalDone(); /** Indicate whether the resource supplies collections with hierarchical or global remote identifiers. @c false by default. Must be called before the first call to setRemoteCollections(). @param hierarchical @c true if collection remote IDs are relative to their parents' remote IDs */ void setHierarchicalRemoteIds(bool hierarchical); /** Do a rollback operation if needed. In read only cases this is a noop. */ void rollback(); /** * Allows to specify parts of the collection that should not be changed if locally available. * * This is useful for resources to provide default values during the collection sync, while * preserving more up-to date values if available. * * Use CONTENTMIMETYPES as identifier to not overwrite the content mimetypes. * * @since 4.14 */ void setKeepLocalChanges(const QSet &parts); protected: void doStart() override; private: class Private; Private *const d; - - Q_PRIVATE_SLOT(d, void localCollectionsReceived(const Akonadi::Collection::List &localCols)) - Q_PRIVATE_SLOT(d, void localCollectionFetchResult(KJob *job)) - Q_PRIVATE_SLOT(d, void updateLocalCollectionResult(KJob *job)) - Q_PRIVATE_SLOT(d, void createLocalCollectionResult(KJob *job)) - Q_PRIVATE_SLOT(d, void deleteLocalCollectionsResult(KJob *job)) - Q_PRIVATE_SLOT(d, void transactionSequenceResult(KJob *job)) }; } #endif diff --git a/src/core/commandbuffer_p.h b/src/core/commandbuffer_p.h index 412d2c262..bc5ac1fc4 100644 --- a/src/core/commandbuffer_p.h +++ b/src/core/commandbuffer_p.h @@ -1,146 +1,152 @@ /* 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 AKONADICORE_COMMANDBUFFER_P_H_ #define AKONADICORE_COMMANDBUFFER_P_H_ class QObject; #include #include #include #include "akonadicore_debug.h" namespace Akonadi { class CommandBufferLocker; class CommandBufferNotifyBlocker; class CommandBuffer { friend class CommandBufferLocker; friend class CommandBufferNotifyBlocker; public: struct Command { qint64 tag; Protocol::CommandPtr command; }; CommandBuffer(QObject *parent, const char *notifySlot) : mParent(parent) , mNotifySlot(notifySlot) { } void enqueue(qint64 tag, const Protocol::CommandPtr &command) { mCommands.enqueue({ tag, command }); if (mNotify) { const bool ok = QMetaObject::invokeMethod(mParent, mNotifySlot.constData(), Qt::QueuedConnection); Q_ASSERT(ok); Q_UNUSED(ok); } } inline Command dequeue() { return mCommands.dequeue(); } inline bool isEmpty() const { return mCommands.isEmpty(); } inline int size() const { return mCommands.size(); } private: + Q_DISABLE_COPY_MOVE(CommandBuffer) + QObject *mParent = nullptr; QByteArray mNotifySlot; QQueue mCommands; QMutex mLock; bool mNotify = true; }; class CommandBufferLocker { public: explicit CommandBufferLocker(CommandBuffer *buffer) : mBuffer(buffer) { relock(); } ~CommandBufferLocker() { unlock(); } inline void unlock() { if (mLocked) { mBuffer->mLock.unlock(); mLocked = false; } } inline void relock() { if (!mLocked) { mBuffer->mLock.lock(); mLocked = true; } } private: + Q_DISABLE_COPY_MOVE(CommandBufferLocker) + CommandBuffer *mBuffer = nullptr; bool mLocked = false; }; class CommandBufferNotifyBlocker { public: explicit CommandBufferNotifyBlocker(CommandBuffer *buffer) : mBuffer(buffer) { mBuffer->mNotify = false; } ~CommandBufferNotifyBlocker() { unblock(); } void unblock() { mBuffer->mNotify = true; } private: + Q_DISABLE_COPY_MOVE(CommandBufferNotifyBlocker) + CommandBuffer *mBuffer; }; } // namespace #endif diff --git a/src/core/connection.cpp b/src/core/connection.cpp index fc9d4bc87..7299c3dca 100644 --- a/src/core/connection.cpp +++ b/src/core/connection.cpp @@ -1,345 +1,345 @@ /* * Copyright 2015 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 "connection_p.h" #include "session_p.h" #include "servermanager_p.h" #include "akonadicore_debug.h" #include "commandbuffer_p.h" #include #include #include #include #include #include #include #include #include #include #include using namespace Akonadi; Connection::Connection(ConnectionType connType, const QByteArray &sessionId, CommandBuffer *commandBuffer, QObject *parent) : QObject(parent) , mConnectionType(connType) , mSessionId(sessionId) , mCommandBuffer(commandBuffer) { qRegisterMetaType(); qRegisterMetaType(); const QByteArray sessionLogFile = qgetenv("AKONADI_SESSION_LOGFILE"); if (!sessionLogFile.isEmpty()) { mLogFile = new QFile(QStringLiteral("%1.%2.%3.%4-%5").arg(QString::fromLatin1(sessionLogFile)) .arg(QApplication::applicationPid()) .arg(QString::number(reinterpret_cast(this), 16), QString::fromLatin1(mSessionId.replace('/', '_')), connType == CommandConnection ? QStringLiteral("Cmd") : QStringLiteral("Ntf"))); if (!mLogFile->open(QIODevice::WriteOnly | QIODevice::Truncate)) { qCWarning(AKONADICORE_LOG) << "Failed to open Akonadi Session log file" << mLogFile->fileName(); delete mLogFile; mLogFile = nullptr; } } } Connection::~Connection() { delete mLogFile; if (mSocket) { mSocket->disconnect(); mSocket->disconnectFromServer(); mSocket->close(); mSocket.reset(); } } void Connection::reconnect() { const bool ok = QMetaObject::invokeMethod(this, &Connection::doReconnect, Qt::QueuedConnection); Q_ASSERT(ok); Q_UNUSED(ok) } QString Connection::defaultAddressForTypeAndMethod(ConnectionType type, const QString &method) { if (method == QLatin1String("UnixPath")) { const QString defaultSocketDir = StandardDirs::saveDir("data"); if (type == CommandConnection) { return defaultSocketDir % QStringLiteral("akonadiserver-cmd.socket"); } else if (type == NotificationConnection) { return defaultSocketDir % QStringLiteral("akonadiserver-ntf.socket"); } } else if (method == QLatin1String("NamedPipe")) { QString suffix; if (Instance::hasIdentifier()) { suffix += QStringLiteral("%1-").arg(Instance::identifier()); } suffix += QString::fromUtf8(QUrl::toPercentEncoding(qApp->applicationDirPath())); if (type == CommandConnection) { return QStringLiteral("Akonadi-Cmd-") % suffix; } else if (type == NotificationConnection) { return QStringLiteral("Akonadi-Ntf-") % suffix; } } Q_UNREACHABLE(); } void Connection::doReconnect() { Q_ASSERT(QThread::currentThread() == thread()); if (mSocket && (mSocket->state() == QLocalSocket::ConnectedState || mSocket->state() == QLocalSocket::ConnectingState)) { // nothing to do, we are still/already connected return; } if (ServerManager::self()->state() != ServerManager::Running) { return; } // try to figure out where to connect to QString serverAddress; // env var has precedence const QByteArray serverAddressEnvVar = qgetenv("AKONADI_SERVER_ADDRESS"); if (!serverAddressEnvVar.isEmpty()) { const int pos = serverAddressEnvVar.indexOf(':'); const QByteArray protocol = serverAddressEnvVar.left(pos); QMap options; const QStringList lst = QString::fromLatin1(serverAddressEnvVar.mid(pos + 1)).split(QLatin1Char(',')); for (const QString &entry : lst) { const QStringList pair = entry.split(QLatin1Char('=')); if (pair.size() != 2) { continue; } options.insert(pair.first(), pair.last()); } if (protocol == "unix") { serverAddress = options.value(QStringLiteral("path")); } else if (protocol == "pipe") { serverAddress = options.value(QStringLiteral("name")); } } // try config file next, fall back to defaults if that fails as well if (serverAddress.isEmpty()) { const QString connectionConfigFile = StandardDirs::connectionConfigFile(); const QFileInfo fileInfo(connectionConfigFile); if (!fileInfo.exists()) { qCWarning(AKONADICORE_LOG) << "Akonadi Client Session: connection config file '" "akonadi/akonadiconnectionrc' can not be found!"; } QSettings connectionSettings(connectionConfigFile, QSettings::IniFormat); QString connectionType; if (mConnectionType == CommandConnection) { connectionType = QStringLiteral("Data"); } else if (mConnectionType == NotificationConnection) { connectionType = QStringLiteral("Notifications"); } connectionSettings.beginGroup(connectionType); const auto method = connectionSettings.value(QStringLiteral("Method"), QStringLiteral("UnixPath")).toString(); serverAddress = connectionSettings.value(method, defaultAddressForTypeAndMethod(mConnectionType, method)).toString(); } mSocket.reset(new QLocalSocket(this)); #if (QT_VERSION < QT_VERSION_CHECK(5, 15, 0)) connect(mSocket.data(), static_cast(&QLocalSocket::error), this, #else connect(mSocket.data(), &QLocalSocket::errorOccurred, this, #endif [this](QLocalSocket::LocalSocketError) { qCWarning(AKONADICORE_LOG) << mSocket->errorString() << mSocket->serverName(); Q_EMIT socketError(mSocket->errorString()); Q_EMIT socketDisconnected(); }); connect(mSocket.data(), &QLocalSocket::disconnected, this, &Connection::socketDisconnected); // note: we temporarily disconnect from readyRead-signal inside handleIncomingData() connect(mSocket.data(), &QLocalSocket::readyRead, this, &Connection::handleIncomingData); // actually do connect qCDebug(AKONADICORE_LOG) << "connectToServer" << serverAddress; mSocket->connectToServer(serverAddress); if (!mSocket->waitForConnected()) { qCWarning(AKONADICORE_LOG) << "Failed to connect to server!"; Q_EMIT socketError(tr("Failed to connect to server!")); mSocket.reset(); return; } QTimer::singleShot(0, this, &Connection::handleIncomingData); Q_EMIT reconnected(); } void Connection::forceReconnect() { const bool ok = QMetaObject::invokeMethod(this, &Connection::doForceReconnect, Qt::QueuedConnection); Q_ASSERT(ok); Q_UNUSED(ok) } void Connection::doForceReconnect() { Q_ASSERT(QThread::currentThread() == thread()); if (mSocket) { - mSocket->disconnect(this, SIGNAL(socketDisconnected())); + disconnect(mSocket.get(), &QLocalSocket::disconnected, this, &Connection::socketDisconnected); mSocket->disconnectFromServer(); mSocket.reset(); } } void Connection::closeConnection() { const bool ok = QMetaObject::invokeMethod(this, &Connection::doCloseConnection, Qt::QueuedConnection); Q_ASSERT(ok); Q_UNUSED(ok) } void Connection::doCloseConnection() { Q_ASSERT(QThread::currentThread() == thread()); if (mSocket) { mSocket->close(); mSocket.reset(); } } QLocalSocket *Connection::socket() const { return mSocket.data(); } void Connection::handleIncomingData() { Q_ASSERT(QThread::currentThread() == thread()); if (!mSocket) { // not connected yet return; } while (mSocket->bytesAvailable() >= int(sizeof(qint64))) { Protocol::DataStream stream(mSocket.data()); qint64 tag; stream >> tag; // temporarily disconnect from readyRead-signal to avoid re-entering this function when we // call waitForData() deep inside Protocol::deserialize disconnect(mSocket.data(), &QLocalSocket::readyRead, this, &Connection::handleIncomingData); Protocol::CommandPtr cmd; try { cmd = Protocol::deserialize(mSocket.data()); } catch (const Akonadi::ProtocolException &e) { qCWarning(AKONADICORE_LOG) << "Protocol exception:" << e.what(); // cmd's type will be Invalid by default, so fall-through } // reconnect to the signal again connect(mSocket.data(), &QLocalSocket::readyRead, this, &Connection::handleIncomingData); if (!cmd || (cmd->type() == Protocol::Command::Invalid)) { qCWarning(AKONADICORE_LOG) << "Invalid command, the world is going to end!"; mSocket->close(); reconnect(); return; } if (mLogFile) { mLogFile->write("S: "); mLogFile->write(QDateTime::currentDateTime().toString(QStringLiteral("yyyy-MM-dd hh:mm:ss.zzz ")).toUtf8()); mLogFile->write(QByteArray::number(tag)); mLogFile->write(" "); mLogFile->write(Protocol::debugString(cmd).toUtf8()); mLogFile->write("\n\n"); mLogFile->flush(); } if (cmd->type() == Protocol::Command::Hello) { Q_ASSERT(cmd->isResponse()); } { CommandBufferLocker locker(mCommandBuffer); mCommandBuffer->enqueue(tag, cmd); } } } void Connection::sendCommand(qint64 tag, const Protocol::CommandPtr &cmd) { const bool ok = QMetaObject::invokeMethod(this, "doSendCommand", Qt::QueuedConnection, Q_ARG(qint64, tag), Q_ARG(Akonadi::Protocol::CommandPtr, cmd)); Q_ASSERT(ok); Q_UNUSED(ok) } void Connection::doSendCommand(qint64 tag, const Protocol::CommandPtr &cmd) { Q_ASSERT(QThread::currentThread() == thread()); if (mLogFile) { mLogFile->write("C: "); mLogFile->write(QDateTime::currentDateTime().toString(QStringLiteral("yyyy-MM-dd hh:mm:ss.zzz ")).toUtf8()); mLogFile->write(QByteArray::number(tag)); mLogFile->write(" "); mLogFile->write(Protocol::debugString(cmd).toUtf8()); mLogFile->write("\n\n"); mLogFile->flush(); } if (mSocket && mSocket->isOpen()) { Protocol::DataStream stream(mSocket.data()); try { stream << tag; Protocol::serialize(stream, cmd); stream.flush(); } catch (const Akonadi::ProtocolException &e) { qCWarning(AKONADICORE_LOG) << "Protocol Exception:" << QString::fromUtf8(e.what()); mSocket->close(); reconnect(); return; } if (!mSocket->waitForBytesWritten()) { qCWarning(AKONADICORE_LOG) << "Socket write timeout"; mSocket->close(); reconnect(); return; } } else { // TODO: Queue the commands and resend on reconnect? } } diff --git a/src/core/control.cpp b/src/core/control.cpp index 2fe56e186..762c114bf 100644 --- a/src/core/control.cpp +++ b/src/core/control.cpp @@ -1,175 +1,167 @@ /* Copyright (c) 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 "control.h" #include "servermanager.h" #include "akonadicore_debug.h" #include #include #include using namespace Akonadi; namespace Akonadi { namespace Internal { class StaticControl : public Control { -public: - StaticControl() - : Control() - { - } + Q_OBJECT }; } Q_GLOBAL_STATIC(Internal::StaticControl, s_instance) /** * @internal */ class Q_DECL_HIDDEN Control::Private { public: Private(Control *parent) : mParent(parent) , mEventLoop(nullptr) { } - ~Private() - { - } - void cleanup() { } bool exec(); void serverStateChanged(ServerManager::State state); QPointer mParent; QEventLoop *mEventLoop = nullptr; bool mSuccess = false; bool mStarting = false; bool mStopping = false; }; bool Control::Private::exec() { qCDebug(AKONADICORE_LOG) << "Starting/Stopping Akonadi (using an event loop)."; mEventLoop = new QEventLoop(mParent); mEventLoop->exec(); mEventLoop->deleteLater(); mEventLoop = nullptr; if (!mSuccess) { qCWarning(AKONADICORE_LOG) << "Could not start/stop Akonadi!"; } mStarting = false; mStopping = false; const bool rv = mSuccess; mSuccess = false; return rv; } void Control::Private::serverStateChanged(ServerManager::State state) { qCDebug(AKONADICORE_LOG) << "Server state changed to" << state; if (mEventLoop && mEventLoop->isRunning()) { // ignore transient states going into the right direction if ((mStarting && (state == ServerManager::Starting || state == ServerManager::Upgrading)) || (mStopping && state == ServerManager::Stopping)) { return; } mEventLoop->quit(); mSuccess = (mStarting && state == ServerManager::Running) || (mStopping && state == ServerManager::NotRunning); } } Control::Control() : d(new Private(this)) { connect(ServerManager::self(), &ServerManager::stateChanged, this, [this](Akonadi::ServerManager::State state) { d->serverStateChanged(state); }); // mProgressIndicator is a widget, so it better be deleted before the QApplication is deleted // Otherwise we get a crash in QCursor code with Qt-4.5 if (QCoreApplication::instance()) { connect(QCoreApplication::instance(), &QCoreApplication::aboutToQuit, this, [this]() {d->cleanup();}); } } Control::~Control() { delete d; } bool Control::start() { if (ServerManager::state() == ServerManager::Stopping) { qCDebug(AKONADICORE_LOG) << "Server is currently being stopped, wont try to start it now"; return false; } if (ServerManager::isRunning() || s_instance->d->mEventLoop) { qCDebug(AKONADICORE_LOG) << "Server is already running"; return true; } s_instance->d->mStarting = true; if (!ServerManager::start()) { qCDebug(AKONADICORE_LOG) << "ServerManager::start failed -> return false"; return false; } return s_instance->d->exec(); } bool Control::stop() { if (ServerManager::state() == ServerManager::Starting) { return false; } if (!ServerManager::isRunning() || s_instance->d->mEventLoop) { return true; } s_instance->d->mStopping = true; if (!ServerManager::stop()) { return false; } return s_instance->d->exec(); } bool Control::restart() { if (ServerManager::isRunning()) { if (!stop()) { return false; } } return start(); } } -#include "moc_control.cpp" +#include "control.moc" diff --git a/src/core/exception.cpp b/src/core/exception.cpp index a3e636b07..c3f0ace22 100644 --- a/src/core/exception.cpp +++ b/src/core/exception.cpp @@ -1,116 +1,103 @@ /* Copyright (c) 2009 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 "exceptionbase.h" #include #include using namespace Akonadi; class Exception::Private { public: explicit Private(const QByteArray &what): what(what) {} QByteArray what; QByteArray assembledWhat; }; Exception::Exception(const char *what) { try { d = std::make_unique(what); } catch (...) { } } Exception::Exception(const QByteArray &what) { try { d = std::make_unique(what); } catch (...) { } } Exception::Exception(const QString &what) { try { d = std::make_unique(what.toUtf8()); } catch (...) { } } -Exception::Exception(const Akonadi::Exception &other) - : std::exception(other) -{ - if (!other.d) { - return; - } - try { - d = std::make_unique(*other.d); - } catch (...) { - } -} - -Exception::Exception(Exception &&other) = default; Exception::~Exception() = default; QByteArray Exception::type() const { static constexpr char mytype[] = "Akonadi::Exception"; try { return QByteArray::fromRawData("Akonadi::Exception", sizeof(mytype) - 1); } catch (...) { return QByteArray(); } } const char *Exception::what() const noexcept { static constexpr char fallback[] = ""; if (!d) { return fallback; } if (d->assembledWhat.isEmpty()) { try { d->assembledWhat = QByteArray(type() + ": " + d->what); } catch (...) { return "caught some exception while assembling Akonadi::Exception::what() return value"; } } return d->assembledWhat.constData(); } #define AKONADI_EXCEPTION_IMPLEMENT_TRIVIAL_INSTANCE(classname) \ Akonadi::classname::~classname() = default; \ QByteArray Akonadi::classname::type() const { \ static constexpr char mytype[] = "Akonadi::" #classname ; \ try { \ return QByteArray::fromRawData( mytype, sizeof (mytype)-1 ); \ } catch ( ... ) { \ return QByteArray(); \ } \ } AKONADI_EXCEPTION_IMPLEMENT_TRIVIAL_INSTANCE(PayloadException) #undef AKONADI_EXCEPTION_IMPLEMENT_TRIVIAL_INSTANCE diff --git a/src/core/exceptionbase.h b/src/core/exceptionbase.h index 76f4024b6..502ed44e9 100644 --- a/src/core/exceptionbase.h +++ b/src/core/exceptionbase.h @@ -1,108 +1,103 @@ /* Copyright (c) 2009 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. */ #ifndef AKONADI_EXCEPTIONBASE_H #define AKONADI_EXCEPTIONBASE_H #include "akonadicore_export.h" #include #include #include class QString; namespace Akonadi { #ifdef _MSC_VER #pragma warning(push) #pragma warning(disable: 4275) // we are exporting a subclass of an unexported class, MSVC complains #endif /** Base class for exceptions used by the Akonadi library. */ -class AKONADICORE_EXPORT Exception : public std::exception //krazy:exclude=dpointer +class AKONADICORE_EXPORT Exception : public std::exception { public: /** Creates a new exception with the error message @p what. */ explicit Exception(const char *what); /** Creates a new exception with the error message @p what. */ explicit Exception(const QByteArray &what); /** Creates a new exception with the error message @p what. */ explicit Exception(const QString &what); - /** - Copy constructor. - */ - Exception(const Exception &other); - - Exception(Exception &&other); - /** Destructor. */ ~Exception() override; /** Returns the error message associated with this exception. */ const char *what() const noexcept override; /** Returns the type of this exception. */ virtual QByteArray type() const; // ### Akonadi 2: return const char * private: + Q_DISABLE_COPY_MOVE(Exception) + class Private; std::unique_ptr d; }; #ifdef _MSC_VER #pragma warning(pop) #endif #define AKONADI_EXCEPTION_MAKE_TRIVIAL_INSTANCE(classname) \ class AKONADICORE_EXPORT classname : public Akonadi::Exception \ { \ public: \ explicit classname(const char *what): Akonadi::Exception(what) {} \ explicit classname(const QByteArray &what): Akonadi::Exception(what) {} \ explicit classname(const QString &what): Akonadi::Exception(what) {} \ ~classname() override; \ QByteArray type() const override; \ } AKONADI_EXCEPTION_MAKE_TRIVIAL_INSTANCE(PayloadException); #undef AKONADI_EXCEPTION_MAKE_TRIVIAL_INSTANCE } #endif diff --git a/src/core/gidextractorinterface.h b/src/core/gidextractorinterface.h index efd6f4604..92c556169 100644 --- a/src/core/gidextractorinterface.h +++ b/src/core/gidextractorinterface.h @@ -1,57 +1,63 @@ /* Author: (2013) Christian Mollekopf This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef GIDEXTRACTORINTERFACE_H #define GIDEXTRACTORINTERFACE_H #include namespace Akonadi { class Item; /** * @short An interface to extract the GID of an object contained in an akonadi item. * * @author Christian Mollekopf * @since 4.11 */ class GidExtractorInterface { public: /** * Destructor. */ virtual ~GidExtractorInterface() { } /** * Extracts the globally unique id of @p item * * If you want to clear the gid from the database return QString(""). */ virtual QString extractGid(const Item &item) const = 0; + +protected: + explicit GidExtractorInterface() = default; + +private: + Q_DISABLE_COPY_MOVE(GidExtractorInterface) }; } Q_DECLARE_INTERFACE(Akonadi::GidExtractorInterface, "org.freedesktop.Akonadi.GidExtractorInterface/1.0") #endif diff --git a/src/core/itempayloadinternals_p.h b/src/core/itempayloadinternals_p.h index 714458d96..dc8fc391f 100644 --- a/src/core/itempayloadinternals_p.h +++ b/src/core/itempayloadinternals_p.h @@ -1,493 +1,492 @@ /* Copyright (c) 2007 Till Adam 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 ITEMPAYLOADINTERNALS_P_H #define ITEMPAYLOADINTERNALS_P_H #include "supertrait.h" #include #include #include #include #include #include "exceptionbase.h" //@cond PRIVATE Doxygen 1.7.1 hangs processing this file. so skip it. //for more info, see https://bugzilla.gnome.org/show_bug.cgi?id=531637 /* WARNING * The below is an implementation detail of the Item class. It is not to be * considered public API, and subject to change without notice */ // Forward-declare boost::shared_ptr so that we don't have to explicitly include // it. Caller that tries to use it will already have it included anyway namespace boost { template class shared_ptr; template shared_ptr dynamic_pointer_cast(shared_ptr const &ptr) noexcept; } namespace Akonadi { namespace Internal { template struct has_clone_method { private: template struct sfinae { }; struct No { }; struct Yes { No no[2]; }; template static No test(...); template static Yes test(sfinae *); public: static const bool value = sizeof(test(nullptr)) == sizeof(Yes); }; template struct clone_traits_helper { // runtime error (commented in) or compiletime error (commented out)? // ### runtime error, until we check has_clone_method in the // ### Item::payload impl directly... template static T *clone(U) { return nullptr; } }; template struct clone_traits_helper { static T *clone(T *t) { return t ? t->clone() : nullptr; } }; template struct clone_traits : clone_traits_helper::value> { }; template struct shared_pointer_traits { static const bool defined = false; }; template struct shared_pointer_traits> { static const bool defined = true; typedef T element_type; template struct make { typedef boost::shared_ptr type; }; typedef QSharedPointer next_shared_ptr; }; template struct shared_pointer_traits> { static const bool defined = true; typedef T element_type; template struct make { typedef QSharedPointer type; }; typedef std::shared_ptr next_shared_ptr; }; template struct shared_pointer_traits> { static const bool defined = true; typedef T element_type; template struct make { typedef std::shared_ptr type; }; typedef boost::shared_ptr next_shared_ptr; }; template struct is_shared_pointer { static const bool value = shared_pointer_traits::defined; }; template struct identity { typedef T type; }; template struct get_hierarchy_root; template struct get_hierarchy_root_recurse : get_hierarchy_root { }; template struct get_hierarchy_root_recurse : identity { }; template struct get_hierarchy_root : get_hierarchy_root_recurse::Type> { }; template struct get_hierarchy_root> { typedef boost::shared_ptr::type> type; }; template struct get_hierarchy_root> { typedef QSharedPointer::type> type; }; template struct get_hierarchy_root> { typedef std::shared_ptr::type> type; }; /** @internal Payload type traits. Implements specialized handling for polymorphic types and smart pointers. The default one is never used (as isPolymorphic is always false) and only contains safe dummy implementations to make the compiler happy (in practice it will always optimized away anyway). */ template struct PayloadTrait { /// type of the payload object contained inside a shared pointer typedef T ElementType; // the metatype id for the element type, or for pointer-to-element // type, if in a shared pointer static int elementMetaTypeId() { return qMetaTypeId(); } /// type of the base class of the payload object inside a shared pointer, /// same as ElementType if there is no super class typedef typename Akonadi::SuperClass::Type SuperElementType; /// type of this payload object typedef T Type; /// type of the payload to store a base class of this payload /// (eg. a shared pointer containing a pointer to SuperElementType) /// same as Type if there is not super class typedef typename Akonadi::SuperClass::Type SuperType; /// indicates if this payload is polymorphic, that it is a shared pointer /// and has a known super class static const bool isPolymorphic = false; /// checks an object of this payload type for being @c null static inline bool isNull(const Type &p) { Q_UNUSED(p); return true; } /// casts to Type from @c U /// throws a PayloadException if casting failed template static inline Type castFrom(const U &) { throw PayloadException("you should never get here"); } /// tests if casting from @c U to Type is possible template static inline bool canCastFrom(const U &) { return false; } /// cast to @c U from Type template static inline U castTo(const Type &) { throw PayloadException("you should never get here"); } template static T clone(const U &) { throw PayloadException("clone: you should never get here"); } /// defines the type of shared pointer used (0: none, > 0: boost::shared_ptr, QSharedPointer, ...) static const unsigned int sharedPointerId = 0; }; /** @internal Payload type trait specialization for boost::shared_ptr for documentation of the various members, see above */ template struct PayloadTrait> { typedef T ElementType; static int elementMetaTypeId() { return qMetaTypeId(); } typedef typename Akonadi::SuperClass::Type SuperElementType; typedef boost::shared_ptr Type; typedef boost::shared_ptr SuperType; static const bool isPolymorphic = !std::is_same::value; static inline bool isNull(const Type &p) { return p.get() == nullptr; } template static inline Type castFrom(const boost::shared_ptr &p) { const Type sp = boost::dynamic_pointer_cast(p); if (sp.get() != nullptr || p.get() == nullptr) { return sp; } throw PayloadException("boost::dynamic_pointer_cast failed"); } template static inline bool canCastFrom(const boost::shared_ptr &p) { const Type sp = boost::dynamic_pointer_cast(p); return sp.get() != nullptr || p.get() == nullptr; } template static inline boost::shared_ptr castTo(const Type &p) { const boost::shared_ptr sp = boost::dynamic_pointer_cast(p); return sp; } static boost::shared_ptr clone(const QSharedPointer &t) { if (T *nt = clone_traits::clone(t.data())) { return boost::shared_ptr(nt); } else { return boost::shared_ptr(); } } static boost::shared_ptr clone(const std::shared_ptr &t) { if (T *nt = clone_traits::clone(t.get())) { return boost::shared_ptr(nt); } else { return boost::shared_ptr(); } } static const unsigned int sharedPointerId = 1; }; /** @internal Payload type trait specialization for QSharedPointer for documentation of the various members, see above */ template struct PayloadTrait> { typedef T ElementType; static int elementMetaTypeId() { return qMetaTypeId(); } typedef typename Akonadi::SuperClass::Type SuperElementType; typedef QSharedPointer Type; typedef QSharedPointer SuperType; static const bool isPolymorphic = !std::is_same::value; static inline bool isNull(const Type &p) { return p.isNull(); } template static inline Type castFrom(const QSharedPointer &p) { const Type sp = qSharedPointerDynamicCast(p); if (!sp.isNull() || p.isNull()) { return sp; } throw PayloadException("qSharedPointerDynamicCast failed"); } template static inline bool canCastFrom(const QSharedPointer &p) { const Type sp = qSharedPointerDynamicCast(p); return !sp.isNull() || p.isNull(); } template static inline QSharedPointer castTo(const Type &p) { const QSharedPointer sp = qSharedPointerDynamicCast(p); return sp; } static QSharedPointer clone(const boost::shared_ptr &t) { if (T *nt = clone_traits::clone(t.get())) { return QSharedPointer(nt); } else { return QSharedPointer(); } } static QSharedPointer clone(const std::shared_ptr &t) { if (T *nt = clone_traits::clone(t.get())) { return QSharedPointer(nt); } else { return QSharedPointer(); } } static const unsigned int sharedPointerId = 2; }; /** @internal Payload type trait specialization for std::shared_ptr for documentation of the various members, see above */ template struct PayloadTrait> { typedef T ElementType; static int elementMetaTypeId() { return qMetaTypeId(); } typedef typename Akonadi::SuperClass::Type SuperElementType; typedef std::shared_ptr Type; typedef std::shared_ptr SuperType; static const bool isPolymorphic = !std::is_same::value; static inline bool isNull(const Type &p) { return p.get() == nullptr; } template static inline Type castFrom(const std::shared_ptr &p) { const Type sp = std::dynamic_pointer_cast(p); if (sp.get() != nullptr || p.get() == nullptr) { return sp; } throw PayloadException("std::dynamic_pointer_cast failed"); } template static inline bool canCastFrom(const std::shared_ptr &p) { const Type sp = std::dynamic_pointer_cast(p); return sp.get() != nullptr || p.get() == nullptr; } template static inline std::shared_ptr castTo(const Type &p) { const std::shared_ptr sp = std::dynamic_pointer_cast(p); return sp; } static std::shared_ptr clone(const boost::shared_ptr &t) { if (T *nt = clone_traits::clone(t.get())) { return std::shared_ptr(nt); } else { return std::shared_ptr(); } } static std::shared_ptr clone(const QSharedPointer &t) { if (T *nt = clone_traits::clone(t.data())) { return std::shared_ptr(nt); } else { return std::shared_ptr(); } } static const unsigned int sharedPointerId = 3; }; /** * @internal * Non-template base class for the payload container. */ struct PayloadBase { - virtual ~PayloadBase() - { - } + virtual ~PayloadBase() = default; virtual PayloadBase *clone() const = 0; virtual const char *typeName() const = 0; + +protected: + PayloadBase() = default; +private: + Q_DISABLE_COPY_MOVE(PayloadBase) }; /** * @internal * Container for the actual payload object. */ template struct Payload : public PayloadBase { - Payload() - { - } Payload(const T &p) : payload(p) - { - } + {} PayloadBase *clone() const override { return new Payload(const_cast *>(this)->payload); } const char *typeName() const override { return typeid(const_cast *>(this)).name(); } T payload; }; /** * @internal * abstract, will therefore always fail to compile for pointer payloads */ template struct Payload : public PayloadBase { }; /** @internal Basically a dynamic_cast that also works across DSO boundaries. */ template inline Payload *payload_cast(PayloadBase *payloadBase) { Payload *p = dynamic_cast *>(payloadBase); // try harder to cast, workaround for some gcc issue with template instances in multiple DSO's if (!p && payloadBase && strcmp(payloadBase->typeName(), typeid(p).name()) == 0) { p = static_cast*>(payloadBase); } return p; } } // namespace Internal } // namespace Akonadi //@endcond #endif diff --git a/src/core/itemserializerplugin.h b/src/core/itemserializerplugin.h index 5aa259ef0..3c95c6049 100644 --- a/src/core/itemserializerplugin.h +++ b/src/core/itemserializerplugin.h @@ -1,229 +1,235 @@ /* Copyright (c) 2007 Till Adam Copyright (c) 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. */ #ifndef AKONADI_ITEMSERIALIZERPLUGIN_H #define AKONADI_ITEMSERIALIZERPLUGIN_H #include #include #include "item.h" #include "akonadicore_export.h" class QIODevice; namespace Akonadi { /** * @short The base class for item type serializer plugins. * * Serializer plugins convert between the payload of Akonadi::Item objects and * a textual or binary representation of the actual content data. * This allows to easily add support for new types to Akonadi. * * The following example shows how to implement a serializer plugin for * a new data type PimNote. * * The PimNote data structure: * @code * typedef struct { * QString author; * QDateTime dateTime; * QString text; * } PimNote; * @endcode * * The serializer plugin code: * @code * #include * * class SerializerPluginPimNote : public QObject, public Akonadi::ItemSerializerPlugin * { * Q_OBJECT * Q_INTERFACES( Akonadi::ItemSerializerPlugin ) * * public: * bool deserialize( Akonadi::Item& item, const QByteArray& label, QIODevice& data, int version ) * { * // we don't handle versions in this example * Q_UNUSED( version ); * * // we work only on full payload * if ( label != Akonadi::Item::FullPayload ) * return false; * * QDataStream stream( &data ); * * PimNote note; * stream >> note.author; * stream >> note.dateTime; * stream >> note.text; * * item.setPayload( note ); * * return true; * } * * void serialize( const Akonadi::Item& item, const QByteArray& label, QIODevice& data, int &version ) * { * // we don't handle versions in this example * Q_UNUSED( version ); * * if ( label != Akonadi::Item::FullPayload || !item.hasPayload() ) * return; * * QDataStream stream( &data ); * * PimNote note = item.payload(); * * stream << note.author; * stream << note.dateTime; * stream << note.text; * } * }; * * Q_EXPORT_PLUGIN2( akonadi_serializer_pimnote, SerializerPluginPimNote ) * * @endcode * * The desktop file: * @code * [Misc] * Name=Pim Note Serializer * Comment=An Akonadi serializer plugin for note objects * * [Plugin] * Type=application/x-pimnote * X-KDE-Library=akonadi_serializer_pimnote * @endcode * * @author Till Adam , Volker Krause */ class AKONADICORE_EXPORT ItemSerializerPlugin { public: /** * Destroys the item serializer plugin. */ virtual ~ItemSerializerPlugin(); /** * Converts serialized item data provided in @p data into payload for @p item. * * @param item The item to which the payload should be added. * It is guaranteed to have a mime type matching one of the supported * mime types of this plugin. * However it might contain a unsuited payload added manually * by the application developer. * Verifying the payload type in case a payload is already available * is recommended therefore. * @param label The part identifier of the part to deserialize. * @p label might be an unsupported item part, return @c false if this is the case. * @param data A QIODevice providing access to the serialized data. * The QIODevice is opened in read-only mode and positioned at the beginning. * The QIODevice is guaranteed to be valid. * @param version The version of the data format as set by the user in serialize() or @c 0 (default). * @return @c false if the specified part is not supported by this plugin, @c true if the part * could be de-serialized successfully. */ virtual bool deserialize(Item &item, const QByteArray &label, QIODevice &data, int version) = 0; /** * Convert the payload object provided in @p item into its serialzed form into @p data. * * @param item The item which contains the payload. * It is guaranteed to have a mimetype matching one of the supported * mimetypes of this plugin as well as the existence of a payload object. * However it might contain an unsupported payload added manually by * the application developer. * Verifying the payload type is recommended therefore. * @param label The part identifier of the part to serialize. * @p label will be one of the item parts returned by parts(). * @param data The QIODevice where the serialized data should be written to. * The QIODevice is opened in write-only mode and positioned at the beginning. * The QIODevice is guaranteed to be valid. * @param version The version of the data format. Can be set by the user to handle different * versions. */ virtual void serialize(const Item &item, const QByteArray &label, QIODevice &data, int &version) = 0; /** * Returns a list of available parts for the given item payload. * The default implementation returns Item::FullPayload if a payload is set. * * @param item The item. */ virtual QSet parts(const Item &item) const; /** * Override the plugin-lookup with @p plugin. * * After calling this each lookup will always return @p plugin. * This is useful to inject a special plugin for testing purposes. * To reset the plugin, set to 0. * * @since 4.12 */ static void overridePluginLookup(QObject *plugin); /** * Merges the payload parts in @p other into @p item. * * The default implementation is slow as it requires serializing @p other, and deserializing @p item multiple times. * Reimplementing this is recommended if your type uses payload parts. * @param item receives merged parts from @p other * @param other the paylod parts to merge into @p item * @since 4.4 */ virtual void apply(Item &item, const Item &other); /** * Returns the parts available in the item @p item. * * This should be reimplemented to return available parts. * * The default implementation returns an empty set if the item has a payload, * and a set containing Item::FullPayload if the item has no payload. * @param item the item for which to list payload parts * @since 4.4 */ virtual QSet availableParts(const Item &item) const; /** * Returns the parts available in the item @p item that can be stored using * foreign payload mechanism. Is only called for items whose payload has been * set via Item::setPayloadPath(). * * By default returns "RFC822", which can always be stored as foreign payload. * Some implementations can also allow "HEAD" to be stored as foreign payload, * if HEAD is only a subset of RFC822 part. * * @since 5.7 */ virtual QSet allowedForeignParts(const Item &item) const; + +protected: + explicit ItemSerializerPlugin() = default; + +private: + Q_DISABLE_COPY_MOVE(ItemSerializerPlugin) }; } Q_DECLARE_INTERFACE(Akonadi::ItemSerializerPlugin, "org.freedesktop.Akonadi.ItemSerializerPlugin/2.0") #endif diff --git a/src/core/jobs/agentinstancecreatejob.cpp b/src/core/jobs/agentinstancecreatejob.cpp index 7f7cdcc0d..e606bf12b 100644 --- a/src/core/jobs/agentinstancecreatejob.cpp +++ b/src/core/jobs/agentinstancecreatejob.cpp @@ -1,206 +1,206 @@ /* Copyright (c) 2008 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 "agentinstancecreatejob.h" #include "agentinstance.h" #include "agentmanager.h" #include "agentmanager_p.h" #include "controlinterface.h" #include #include "kjobprivatebase_p.h" #include "servermanager.h" #include #include #ifdef Q_OS_UNIX #include #include #endif using namespace Akonadi; static const int safetyTimeout = 10000; // ms namespace Akonadi { /** * @internal */ class AgentInstanceCreateJobPrivate : public KJobPrivateBase { + Q_OBJECT public: AgentInstanceCreateJobPrivate(AgentInstanceCreateJob *parent) : q(parent) , parentWidget(nullptr) , safetyTimer(new QTimer(parent)) , doConfig(false) , tooLate(false) { - QObject::connect(AgentManager::self(), SIGNAL(instanceAdded(Akonadi::AgentInstance)), - q, SLOT(agentInstanceAdded(Akonadi::AgentInstance))); - QObject::connect(safetyTimer, &QTimer::timeout, q, [this]() {timeout(); }); + connect(AgentManager::self(), &AgentManager::instanceAdded, this, &AgentInstanceCreateJobPrivate::agentInstanceAdded); + connect(safetyTimer, &QTimer::timeout, this, &AgentInstanceCreateJobPrivate::timeout); } void agentInstanceAdded(const AgentInstance &instance) { if (agentInstance == instance && !tooLate) { safetyTimer->stop(); if (doConfig) { // return from dbus call first before doing the next one - QTimer::singleShot(0, q, [this]() { doConfigure(); }); + QTimer::singleShot(0, this, &AgentInstanceCreateJobPrivate::doConfigure); } else { q->emitResult(); } } } void doConfigure() { org::freedesktop::Akonadi::Agent::Control *agentControlIface = new org::freedesktop::Akonadi::Agent::Control(ServerManager::agentServiceName(ServerManager::Agent, agentInstance.identifier()), QStringLiteral("/"), QDBusConnection::sessionBus(), q); if (!agentControlIface || !agentControlIface->isValid()) { delete agentControlIface; q->setError(KJob::UserDefinedError); q->setErrorText(i18n("Unable to access D-Bus interface of created agent.")); q->emitResult(); return; } - q->connect(agentControlIface, &org::freedesktop::Akonadi::Agent::Control::configurationDialogAccepted, - q, [agentControlIface, this]() { - agentControlIface->deleteLater(); - q->emitResult(); - }); - q->connect(agentControlIface, &org::freedesktop::Akonadi::Agent::Control::configurationDialogRejected, - q, [agentControlIface, this]() { - agentControlIface->deleteLater(); - AgentManager::self()->removeInstance(agentInstance); - q->emitResult(); - }); + connect(agentControlIface, &org::freedesktop::Akonadi::Agent::Control::configurationDialogAccepted, + this, [agentControlIface, this]() { + agentControlIface->deleteLater(); + q->emitResult(); + }); + connect(agentControlIface, &org::freedesktop::Akonadi::Agent::Control::configurationDialogRejected, + this, [agentControlIface, this]() { + agentControlIface->deleteLater(); + AgentManager::self()->removeInstance(agentInstance); + q->emitResult(); + }); agentInstance.configure(parentWidget); } void timeout() { tooLate = true; q->setError(KJob::UserDefinedError); q->setErrorText(i18n("Agent instance creation timed out.")); q->emitResult(); } void doStart() override; AgentInstanceCreateJob * const q; AgentType agentType; QString agentTypeId; AgentInstance agentInstance; QWidget *parentWidget = nullptr; QTimer *safetyTimer = nullptr; bool doConfig; bool tooLate; }; } AgentInstanceCreateJob::AgentInstanceCreateJob(const AgentType &agentType, QObject *parent) : KJob(parent) , d(new AgentInstanceCreateJobPrivate(this)) { d->agentType = agentType; } AgentInstanceCreateJob::AgentInstanceCreateJob(const QString &typeId, QObject *parent) : KJob(parent) , d(new AgentInstanceCreateJobPrivate(this)) { d->agentTypeId = typeId; } AgentInstanceCreateJob::~AgentInstanceCreateJob() { delete d; } void AgentInstanceCreateJob::configure(QWidget *parent) { d->parentWidget = parent; d->doConfig = true; } AgentInstance AgentInstanceCreateJob::instance() const { return d->agentInstance; } void AgentInstanceCreateJob::start() { d->start(); } void AgentInstanceCreateJobPrivate::doStart() { if (!agentType.isValid() && !agentTypeId.isEmpty()) { agentType = AgentManager::self()->type(agentTypeId); } if (!agentType.isValid()) { q->setError(KJob::UserDefinedError); q->setErrorText(i18n("Unable to obtain agent type '%1'.", agentTypeId)); QTimer::singleShot(0, q, &AgentInstanceCreateJob::emitResult); return; } agentInstance = AgentManager::self()->d->createInstance(agentType); if (!agentInstance.isValid()) { q->setError(KJob::UserDefinedError); q->setErrorText(i18n("Unable to create agent instance.")); QTimer::singleShot(0, q, &AgentInstanceCreateJob::emitResult); } else { int timeout = safetyTimeout; #ifdef Q_OS_UNIX // Increate the timeout when valgrinding the agent, because that slows down things a log. QString agentValgrind = QString::fromLocal8Bit(qgetenv("AKONADI_VALGRIND")); if (!agentValgrind.isEmpty() && agentType.identifier().contains(agentValgrind)) { timeout *= 15; } #endif // change the timeout when debugging the agent, because we need time to start the debugger const QString agentDebugging = QString::fromLocal8Bit(qgetenv("AKONADI_DEBUG_WAIT")); if (!agentDebugging.isEmpty()) { // we are debugging const QString agentDebuggingTimeout = QString::fromLocal8Bit(qgetenv("AKONADI_DEBUG_TIMEOUT")); if (agentDebuggingTimeout.isEmpty()) { // use default value of 150 seconds (the same as "valgrinding", this has to be checked) timeout = 15 * safetyTimeout; } else { // use own value timeout = agentDebuggingTimeout.toInt(); } } safetyTimer->start(timeout); } } -#include "moc_agentinstancecreatejob.cpp" +#include "agentinstancecreatejob.moc" diff --git a/src/core/jobs/agentinstancecreatejob.h b/src/core/jobs/agentinstancecreatejob.h index 573814450..27a8b4402 100644 --- a/src/core/jobs/agentinstancecreatejob.h +++ b/src/core/jobs/agentinstancecreatejob.h @@ -1,128 +1,124 @@ /* Copyright (c) 2008 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. */ #ifndef AKONADI_AGENTINSTANCECREATEJOB_H #define AKONADI_AGENTINSTANCECREATEJOB_H #include "akonadicore_export.h" #include "agenttype.h" #include namespace Akonadi { class AgentInstance; class AgentInstanceCreateJobPrivate; /** * @short Job for creating new agent instances. * * This class encapsulates the procedure of creating a new agent instance * and optionally configuring it immediately. * * @code * * MyClass::MyClass( QWidget *parent ) * : QWidget( parent ) * { * // Get agent type object * Akonadi::AgentType type = Akonadi::AgentManager::self()->type( "akonadi_vcard_resource" ); * * Akonadi::AgentInstanceCreateJob *job = new Akonadi::AgentInstanceCreateJob( type ); * connect( job, SIGNAL(result(KJob*)), * this, SLOT(slotCreated(KJob*)) ); * * // use this widget as parent for the config dialog * job->configure( this ); * * job->start(); * } * * ... * * void MyClass::slotCreated( KJob *job ) * { * Akonadi::AgentInstanceCreateJob *createJob = static_cast( job ); * * qDebug() << "Created agent instance:" << createJob->instance().identifier(); * } * * @endcode * * @author Volker Krause */ class AKONADICORE_EXPORT AgentInstanceCreateJob : public KJob { Q_OBJECT public: /** * Creates a new agent instance create job. * * @param type The type of the agent to create. * @param parent The parent object. */ explicit AgentInstanceCreateJob(const AgentType &type, QObject *parent = nullptr); /** * Creates a new agent instance create job. * * @param typeId The identifier of type of the agent to create. * @param parent The parent object. * @since 4.5 */ explicit AgentInstanceCreateJob(const QString &typeId, QObject *parent = nullptr); /** * Destroys the agent instance create job. */ ~AgentInstanceCreateJob() override; /** * Setup the job to show agent configuration dialog once the agent instance * has been successfully started. * @param parent The parent window for the configuration dialog. */ void configure(QWidget *parent = nullptr); /** * Returns the AgentInstance object of the newly created agent instance. */ Q_REQUIRED_RESULT AgentInstance instance() const; /** * Starts the instance creation. */ void start() override; private: //@cond PRIVATE friend class Akonadi::AgentInstanceCreateJobPrivate; AgentInstanceCreateJobPrivate *const d; - - Q_PRIVATE_SLOT(d, void agentInstanceAdded(const Akonadi::AgentInstance &)) - Q_PRIVATE_SLOT(d, void doConfigure()) - Q_PRIVATE_SLOT(d, void timeout()) //@endcond }; } #endif diff --git a/src/core/jobs/collectionattributessynchronizationjob.cpp b/src/core/jobs/collectionattributessynchronizationjob.cpp index 5521ee633..713c5ca04 100644 --- a/src/core/jobs/collectionattributessynchronizationjob.cpp +++ b/src/core/jobs/collectionattributessynchronizationjob.cpp @@ -1,151 +1,153 @@ /* * Copyright (c) 2009 Volker Krause * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library. If not, see . */ #include "collectionattributessynchronizationjob.h" #include #include "kjobprivatebase_p.h" #include "servermanager.h" #include "akonadicore_debug.h" #include "agentinstance.h" #include "agentmanager.h" #include "collection.h" #include #include #include namespace Akonadi { class CollectionAttributesSynchronizationJobPrivate : public KJobPrivateBase { + Q_OBJECT + public: CollectionAttributesSynchronizationJobPrivate(CollectionAttributesSynchronizationJob *parent) : q(parent) { + connect(&safetyTimer, &QTimer::timeout, this, &CollectionAttributesSynchronizationJobPrivate::slotTimeout); + safetyTimer.setInterval(std::chrono::seconds{5}); + safetyTimer.setSingleShot(false); } void doStart() override; CollectionAttributesSynchronizationJob *q; AgentInstance instance; Collection collection; QDBusInterface *interface = nullptr; - QTimer *safetyTimer = nullptr; + QTimer safetyTimer; int timeoutCount = 0; static const int timeoutCountLimit; +private Q_SLOTS: void slotSynchronized(qlonglong); void slotTimeout(); }; const int CollectionAttributesSynchronizationJobPrivate::timeoutCountLimit = 2; CollectionAttributesSynchronizationJob::CollectionAttributesSynchronizationJob(const Collection &collection, QObject *parent) : KJob(parent) , d(new CollectionAttributesSynchronizationJobPrivate(this)) { d->instance = AgentManager::self()->instance(collection.resource()); d->collection = collection; - d->safetyTimer = new QTimer(this); - connect(d->safetyTimer, &QTimer::timeout, this, [this] { d->slotTimeout(); }); - d->safetyTimer->setInterval(5 * 1000); - d->safetyTimer->setSingleShot(false); } CollectionAttributesSynchronizationJob::~CollectionAttributesSynchronizationJob() { delete d; } void CollectionAttributesSynchronizationJob::start() { d->start(); } void CollectionAttributesSynchronizationJobPrivate::doStart() { if (!collection.isValid()) { q->setError(KJob::UserDefinedError); q->setErrorText(i18n("Invalid collection instance.")); q->emitResult(); return; } if (!instance.isValid()) { q->setError(KJob::UserDefinedError); q->setErrorText(i18n("Invalid resource instance.")); q->emitResult(); return; } interface = new QDBusInterface(ServerManager::agentServiceName(ServerManager::Resource, instance.identifier()), QStringLiteral("/"), QStringLiteral("org.freedesktop.Akonadi.Resource"), QDBusConnection::sessionBus(), this); - connect(interface, SIGNAL(attributesSynchronized(qlonglong)), q, SLOT(slotSynchronized(qlonglong))); + connect(interface, SIGNAL(attributesSynchronized(qlonglong)), this, SLOT(slotSynchronized(qlonglong))); // clazy:exclude=old-style-connect if (interface->isValid()) { const QDBusMessage reply = interface->call(QStringLiteral("synchronizeCollectionAttributes"), collection.id()); if (reply.type() == QDBusMessage::ErrorMessage) { // This means that the resource doesn't provide a synchronizeCollectionAttributes method, so we just finish the job q->emitResult(); return; } - safetyTimer->start(); + safetyTimer.start(); } else { q->setError(KJob::UserDefinedError); q->setErrorText(i18n("Unable to obtain D-Bus interface for resource '%1'", instance.identifier())); q->emitResult(); return; } } void CollectionAttributesSynchronizationJobPrivate::slotSynchronized(qlonglong id) { if (id == collection.id()) { - q->disconnect(interface, SIGNAL(attributesSynchronized(qlonglong)), q, SLOT(slotSynchronized(qlonglong))); - safetyTimer->stop(); + disconnect(interface, SIGNAL(attributesSynchronized(qlonglong)), this, SLOT(slotSynchronized(qlonglong))); // clazy:exclude=old-style-connect + safetyTimer.stop(); q->emitResult(); } } void CollectionAttributesSynchronizationJobPrivate::slotTimeout() { instance = AgentManager::self()->instance(instance.identifier()); timeoutCount++; if (timeoutCount > timeoutCountLimit) { - safetyTimer->stop(); + safetyTimer.stop(); q->setError(KJob::UserDefinedError); q->setErrorText(i18n("Collection attributes synchronization timed out.")); q->emitResult(); return; } if (instance.status() == AgentInstance::Idle) { // try again, we might have lost the synchronized() signal qCDebug(AKONADICORE_LOG) << "collection attributes" << collection.id() << instance.identifier(); interface->call(QStringLiteral("synchronizeCollectionAttributes"), collection.id()); } } } -#include "moc_collectionattributessynchronizationjob.cpp" +#include "collectionattributessynchronizationjob.moc" diff --git a/src/core/jobs/collectionattributessynchronizationjob.h b/src/core/jobs/collectionattributessynchronizationjob.h index b01e45aae..cb0f8be5e 100644 --- a/src/core/jobs/collectionattributessynchronizationjob.h +++ b/src/core/jobs/collectionattributessynchronizationjob.h @@ -1,87 +1,84 @@ /* * Copyright (c) 2009 Volker Krause * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library. If not, see . */ #ifndef AKONADI_COLLECTIONATTRIBUTESSYNCHRONIZATIONJOB_H #define AKONADI_COLLECTIONATTRIBUTESSYNCHRONIZATIONJOB_H #include "akonadicore_export.h" #include namespace Akonadi { class Collection; class CollectionAttributesSynchronizationJobPrivate; /** * @short Job that synchronizes the attributes of a collection. * * This job will trigger a resource to synchronize the attributes of * a collection based on what the backend is reporting to store them in the * Akonadi storage. * * Example: * * @code * using namespace Akonadi; * * const Collection collection = ...; * * CollectionAttributesSynchronizationJob *job = new CollectionAttributesSynchronizationJob( collection ); * connect( job, SIGNAL(result(KJob*)), SLOT(synchronizationFinished(KJob*)) ); * * @endcode * * @note This is a KJob not an Akonadi::Job, so it wont auto-start! * * @author Volker Krause * @since 4.6 */ class AKONADICORE_EXPORT CollectionAttributesSynchronizationJob : public KJob { Q_OBJECT public: /** * Creates a new synchronization job for the given collection. * * @param collection The collection to synchronize. */ explicit CollectionAttributesSynchronizationJob(const Collection &collection, QObject *parent = nullptr); /** * Destroys the synchronization job. */ ~CollectionAttributesSynchronizationJob() override; /* reimpl */ void start() override; private: //@cond PRIVATE CollectionAttributesSynchronizationJobPrivate *const d; friend class CollectionAttributesSynchronizationJobPrivate; - - Q_PRIVATE_SLOT(d, void slotSynchronized(qlonglong)) - Q_PRIVATE_SLOT(d, void slotTimeout()) //@endcond }; } #endif diff --git a/src/core/jobs/collectionfetchjob.cpp b/src/core/jobs/collectionfetchjob.cpp index 14c502a62..eec487264 100644 --- a/src/core/jobs/collectionfetchjob.cpp +++ b/src/core/jobs/collectionfetchjob.cpp @@ -1,426 +1,416 @@ /* 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 "collectionfetchjob.h" #include "job_p.h" #include "protocolhelper_p.h" #include "collection_p.h" #include "collectionfetchscope.h" #include "collectionutils.h" #include "private/protocol_p.h" #include "akonadicore_debug.h" #include #include #include #include using namespace Akonadi; class Akonadi::CollectionFetchJobPrivate : public JobPrivate { public: CollectionFetchJobPrivate(CollectionFetchJob *parent) : JobPrivate(parent) , mType(CollectionFetchJob::Base) { - - } - - void init() - { - mEmitTimer = new QTimer(q_ptr); - mEmitTimer->setSingleShot(true); - mEmitTimer->setInterval(100); - q_ptr->connect(mEmitTimer, SIGNAL(timeout()), q_ptr, SLOT(timeout())); + mEmitTimer.setSingleShot(true); + mEmitTimer.setInterval(std::chrono::milliseconds{100}); + q_ptr->connect(&mEmitTimer, &QTimer::timeout, q_ptr, [this]() { timeout(); }); } Q_DECLARE_PUBLIC(CollectionFetchJob) CollectionFetchJob::Type mType; Collection mBase; Collection::List mBaseList; Collection::List mCollections; CollectionFetchScope mScope; Collection::List mPendingCollections; - QTimer *mEmitTimer = nullptr; + QTimer mEmitTimer; bool mBasePrefetch = false; Collection::List mPrefetchList; void aboutToFinish() override { timeout(); } void timeout() { Q_Q(CollectionFetchJob); - mEmitTimer->stop(); // in case we are called by result() + mEmitTimer.stop(); // in case we are called by result() if (!mPendingCollections.isEmpty()) { if (!q->error() || mScope.ignoreRetrievalErrors()) { Q_EMIT q->collectionsReceived(mPendingCollections); } mPendingCollections.clear(); } } void subJobCollectionReceived(const Akonadi::Collection::List &collections) { mPendingCollections += collections; - if (!mEmitTimer->isActive()) { - mEmitTimer->start(); + if (!mEmitTimer.isActive()) { + mEmitTimer.start(); } } QString jobDebuggingString() const override { if (mBase.isValid()) { return QStringLiteral("Collection Id %1").arg(mBase.id()); } else if (CollectionUtils::hasValidHierarchicalRID(mBase)) { //return QLatin1String("(") + ProtocolHelper::hierarchicalRidToScope(mBase).hridChain().join(QLatin1String(", ")) + QLatin1Char(')'); return QStringLiteral("HRID chain"); } else { return QStringLiteral("Collection RemoteId %1").arg(mBase.remoteId()); } } bool jobFailed(KJob *job) { Q_Q(CollectionFetchJob); if (mScope.ignoreRetrievalErrors()) { int error = job->error(); if (error && !q->error()) { q->setError(error); q->setErrorText(job->errorText()); } if (error == Job::ConnectionFailed || error == Job::ProtocolVersionMismatch || error == Job::UserCanceled) { return true; } return false; } else { return job->error(); } } }; CollectionFetchJob::CollectionFetchJob(const Collection &collection, Type type, QObject *parent) : Job(new CollectionFetchJobPrivate(this), parent) { Q_D(CollectionFetchJob); - d->init(); d->mBase = collection; d->mType = type; } CollectionFetchJob::CollectionFetchJob(const Collection::List &cols, QObject *parent) : Job(new CollectionFetchJobPrivate(this), parent) { Q_D(CollectionFetchJob); - d->init(); Q_ASSERT(!cols.isEmpty()); if (cols.size() == 1) { d->mBase = cols.first(); } else { d->mBaseList = cols; } d->mType = CollectionFetchJob::Base; } CollectionFetchJob::CollectionFetchJob(const Collection::List &cols, Type type, QObject *parent) : Job(new CollectionFetchJobPrivate(this), parent) { Q_D(CollectionFetchJob); - d->init(); Q_ASSERT(!cols.isEmpty()); if (cols.size() == 1) { d->mBase = cols.first(); } else { d->mBaseList = cols; } d->mType = type; } CollectionFetchJob::CollectionFetchJob(const QList &cols, Type type, QObject *parent) : Job(new CollectionFetchJobPrivate(this), parent) { Q_D(CollectionFetchJob); - d->init(); Q_ASSERT(!cols.isEmpty()); if (cols.size() == 1) { d->mBase = Collection(cols.first()); } else { for (Collection::Id id : cols) { d->mBaseList.append(Collection(id)); } } d->mType = type; } CollectionFetchJob::~CollectionFetchJob() { } Akonadi::Collection::List CollectionFetchJob::collections() const { Q_D(const CollectionFetchJob); return d->mCollections; } void CollectionFetchJob::doStart() { Q_D(CollectionFetchJob); if (!d->mBaseList.isEmpty()) { if (d->mType == Recursive) { // Because doStart starts several subjobs and @p cols could contain descendants of // other elements in the list, if type is Recursive, we could end up with duplicates in the result. // To fix this we require an initial fetch of @p cols with Base and RetrieveAncestors, // Iterate over that result removing intersections and then perform the Recursive fetch on // the remainder. d->mBasePrefetch = true; // No need to connect to the collectionsReceived signal here. This job is internal. The // result needs to be filtered through filterDescendants before it is useful. new CollectionFetchJob(d->mBaseList, NonOverlappingRoots, this); } else if (d->mType == NonOverlappingRoots) { for (const Collection &col : qAsConst(d->mBaseList)) { // No need to connect to the collectionsReceived signal here. This job is internal. The (aggregated) // result needs to be filtered through filterDescendants before it is useful. CollectionFetchJob *subJob = new CollectionFetchJob(col, Base, this); subJob->fetchScope().setAncestorRetrieval(Akonadi::CollectionFetchScope::All); } } else { for (const Collection &col : qAsConst(d->mBaseList)) { CollectionFetchJob *subJob = new CollectionFetchJob(col, d->mType, this); - connect(subJob, SIGNAL(collectionsReceived(Akonadi::Collection::List)), SLOT(subJobCollectionReceived(Akonadi::Collection::List))); + connect(subJob, &CollectionFetchJob::collectionsReceived, this, [d](const auto &cols) { d->subJobCollectionReceived(cols); }); subJob->setFetchScope(fetchScope()); } } return; } if (!d->mBase.isValid() && d->mBase.remoteId().isEmpty()) { setError(Unknown); setErrorText(i18n("Invalid collection given.")); emitResult(); return; } const auto cmd = Protocol::FetchCollectionsCommandPtr::create(ProtocolHelper::entityToScope(d->mBase)); switch (d->mType) { case Base: cmd->setDepth(Protocol::FetchCollectionsCommand::BaseCollection); break; case Akonadi::CollectionFetchJob::FirstLevel: cmd->setDepth(Protocol::FetchCollectionsCommand::ParentCollection); break; case Akonadi::CollectionFetchJob::Recursive: cmd->setDepth(Protocol::FetchCollectionsCommand::AllCollections); break; default: Q_ASSERT(false); } cmd->setResource(d->mScope.resource()); cmd->setMimeTypes(d->mScope.contentMimeTypes()); switch (d->mScope.listFilter()) { case CollectionFetchScope::Display: cmd->setDisplayPref(true); break; case CollectionFetchScope::Sync: cmd->setSyncPref(true); break; case CollectionFetchScope::Index: cmd->setIndexPref(true); break; case CollectionFetchScope::Enabled: cmd->setEnabled(true); break; case CollectionFetchScope::NoFilter: break; default: Q_ASSERT(false); } cmd->setFetchStats(d->mScope.includeStatistics()); switch (d->mScope.ancestorRetrieval()) { case CollectionFetchScope::None: cmd->setAncestorsDepth(Protocol::Ancestor::NoAncestor); break; case CollectionFetchScope::Parent: cmd->setAncestorsDepth(Protocol::Ancestor::ParentAncestor); break; case CollectionFetchScope::All: cmd->setAncestorsDepth(Protocol::Ancestor::AllAncestors); break; } if (d->mScope.ancestorRetrieval() != CollectionFetchScope::None) { cmd->setAncestorsAttributes(d->mScope.ancestorFetchScope().attributes()); } d->sendCommand(cmd); } bool CollectionFetchJob::doHandleResponse(qint64 tag, const Protocol::CommandPtr &response) { Q_D(CollectionFetchJob); if (d->mBasePrefetch || d->mType == NonOverlappingRoots) { return false; } if (!response->isResponse() || response->type() != Protocol::Command::FetchCollections) { return Job::doHandleResponse(tag, response); } const auto &resp = Protocol::cmdCast(response); // Invalid response (no ID) means this was the last response if (resp.id() == -1) { return true; } Collection collection = ProtocolHelper::parseCollection(resp, true); if (!collection.isValid()) { return false; } collection.d_ptr->resetChangeLog(); d->mCollections.append(collection); d->mPendingCollections.append(collection); - if (!d->mEmitTimer->isActive()) { - d->mEmitTimer->start(); + if (!d->mEmitTimer.isActive()) { + d->mEmitTimer.start(); } return false; } static Collection::List filterDescendants(const Collection::List &list) { Collection::List result; QVector > ids; ids.reserve(list.count()); for (const Collection &collection : list) { QList ancestors; Collection parent = collection.parentCollection(); ancestors << parent.id(); if (parent != Collection::root()) { while (parent.parentCollection() != Collection::root()) { parent = parent.parentCollection(); QList::iterator i = std::lower_bound(ancestors.begin(), ancestors.end(), parent.id()); ancestors.insert(i, parent.id()); } } ids << ancestors; } QSet excludeList; for (const Collection &collection : list) { int i = 0; for (const QList &ancestors : qAsConst(ids)) { if (std::binary_search(ancestors.cbegin(), ancestors.cend(), collection.id())) { excludeList.insert(list.at(i).id()); } ++i; } } for (const Collection &collection : list) { if (!excludeList.contains(collection.id())) { result.append(collection); } } return result; } void CollectionFetchJob::slotResult(KJob *job) { Q_D(CollectionFetchJob); CollectionFetchJob *list = qobject_cast(job); Q_ASSERT(job); if (d->mType == NonOverlappingRoots) { d->mPrefetchList += list->collections(); } else if (!d->mBasePrefetch) { d->mCollections += list->collections(); } if (d_ptr->mCurrentSubJob == job && !d->jobFailed(job)) { if (job->error()) { qCWarning(AKONADICORE_LOG) << "Error during CollectionFetchJob: " << job->errorString(); } d_ptr->mCurrentSubJob = nullptr; removeSubjob(job); QTimer::singleShot(0, this, [d]() { d->startNext(); }); } else { Job::slotResult(job); } if (d->mBasePrefetch) { d->mBasePrefetch = false; const Collection::List roots = list->collections(); Q_ASSERT(!hasSubjobs()); if (!job->error()) { for (const Collection &col : roots) { CollectionFetchJob *subJob = new CollectionFetchJob(col, d->mType, this); - connect(subJob, SIGNAL(collectionsReceived(Akonadi::Collection::List)), SLOT(subJobCollectionReceived(Akonadi::Collection::List))); + connect(subJob, &CollectionFetchJob::collectionsReceived, this, [d](const auto &cols) { d->subJobCollectionReceived(cols); }); subJob->setFetchScope(fetchScope()); } } // No result yet. } else if (d->mType == NonOverlappingRoots) { if (!d->jobFailed(job) && !hasSubjobs()) { const Collection::List result = filterDescendants(d->mPrefetchList); d->mPendingCollections += result; d->mCollections = result; d->delayedEmitResult(); } } else { if (!d->jobFailed(job) && !hasSubjobs()) { d->delayedEmitResult(); } } } void CollectionFetchJob::setFetchScope(const CollectionFetchScope &scope) { Q_D(CollectionFetchJob); d->mScope = scope; } CollectionFetchScope &CollectionFetchJob::fetchScope() { Q_D(CollectionFetchJob); return d->mScope; } #include "moc_collectionfetchjob.cpp" diff --git a/src/core/jobs/collectionfetchjob.h b/src/core/jobs/collectionfetchjob.h index abbc9f4a0..61ea4fcb3 100644 --- a/src/core/jobs/collectionfetchjob.h +++ b/src/core/jobs/collectionfetchjob.h @@ -1,200 +1,195 @@ /* 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. */ #ifndef AKONADI_COLLECTIONFETCHJOB_H #define AKONADI_COLLECTIONFETCHJOB_H #include "akonadicore_export.h" #include "collection.h" #include "job.h" namespace Akonadi { class CollectionFetchScope; class CollectionFetchJobPrivate; /** * @short Job that fetches collections from the Akonadi storage. * * This class can be used to retrieve the complete or partial collection tree * from the Akonadi storage. This fetches collection data, not item data. * * @code * * using namespace Akonadi; * * // fetching all collections containing emails recursively, starting at the root collection * CollectionFetchJob *job = new CollectionFetchJob(Collection::root(), CollectionFetchJob::Recursive, this); * job->fetchScope().setContentMimeTypes(QStringList() << "message/rfc822"); * connect(job, SIGNAL(collectionsReceived(Akonadi::Collection::List)), * this, SLOT(myCollectionsReceived(Akonadi::Collection::List))); * connect(job, SIGNAL(result(KJob*)), this, SLOT(collectionFetchResult(KJob*))); * * @endcode * * @author Volker Krause */ class AKONADICORE_EXPORT CollectionFetchJob : public Job { Q_OBJECT public: /** * Describes the type of fetch depth. */ enum Type { Base, ///< Only fetch the base collection. FirstLevel, ///< Only list direct sub-collections of the base collection. Recursive, ///< List all sub-collections. NonOverlappingRoots ///< List the roots of a list of fetched collections. @since 4.7 }; /** * Creates a new collection fetch job. If the given base collection * has a unique identifier, this is used to identify the collection in the * Akonadi server. If only a remote identifier is available the collection * is identified using that, provided that a resource search context has * been specified by calling setResource(). * * @internal * For internal use only, if a remote identifier is set, the resource * search context can be set globally using ResourceSelectJob. * @endinternal * * @param collection The base collection for the listing. * @param type The type of fetch depth. * @param parent The parent object. */ explicit CollectionFetchJob(const Collection &collection, Type type = FirstLevel, QObject *parent = nullptr); /** * Creates a new collection fetch job to retrieve a list of collections. * If a given collection has a unique identifier, this is used to identify * the collection in the Akonadi server. If only a remote identifier is * available the collection is identified using that, provided that a * resource search context has been specified by calling setResource(). * * @internal * For internal use only, if a remote identifier is set, the resource * search context can be set globally using ResourceSelectJob. * @endinternal * * @param collections A list of collections to fetch. Must not be empty. * @param parent The parent object. */ explicit CollectionFetchJob(const Collection::List &collections, QObject *parent = nullptr); /** * Creates a new collection fetch job to retrieve a list of collections. * If a given collection has a unique identifier, this is used to identify * the collection in the Akonadi server. If only a remote identifier is * available the collection is identified using that, provided that a * resource search context has been specified by calling setResource(). * * @internal * For internal use only, if a remote identifier is set, the resource * search context can be set globally using ResourceSelectJob. * @endinternal * * @param collections A list of collections to fetch. Must not be empty. * @param type The type of fetch depth. * @param parent The parent object. * @todo KDE5 merge with ctor above. * @since 4.7 */ CollectionFetchJob(const Collection::List &collections, Type type, QObject *parent = nullptr); /** * Convenience ctor equivalent to CollectionFetchJob(const Collection::List &collections, Type type, QObject *parent = nullptr) * @since 4.8 * @param collections list of collection ids * @param type fetch job type * @param parent parent object */ explicit CollectionFetchJob(const QList &collections, Type type = Base, QObject *parent = nullptr); /** * Destroys the collection fetch job. */ ~CollectionFetchJob() override; /** * Returns the list of fetched collection. */ Q_REQUIRED_RESULT Collection::List collections() const; /** * Sets the collection fetch scope. * * The CollectionFetchScope controls how much of a collection's data is * fetched from the server as well as a filter to select which collections * to fetch. * * @param fetchScope The new scope for collection fetch operations. * * @see fetchScope() * @since 4.4 */ void setFetchScope(const CollectionFetchScope &fetchScope); /** * Returns the collection fetch scope. * * Since this returns a reference it can be used to conveniently modify the * current scope in-place, i.e. by calling a method on the returned reference * without storing it in a local variable. See the CollectionFetchScope documentation * for an example. * * @return a reference to the current collection fetch scope * * @see setFetchScope() for replacing the current collection fetch scope * @since 4.4 */ Q_REQUIRED_RESULT CollectionFetchScope &fetchScope(); Q_SIGNALS: /** * This signal is emitted whenever the job has received collections. * * @param collections The received collections. */ void collectionsReceived(const Akonadi::Collection::List &collections); protected: void doStart() override; bool doHandleResponse(qint64 tag, const Protocol::CommandPtr &response) override; protected Q_SLOTS: //@cond PRIVATE void slotResult(KJob *job) override; //@endcond private: Q_DECLARE_PRIVATE(CollectionFetchJob) - - //@cond PRIVATE - Q_PRIVATE_SLOT(d_func(), void timeout()) - Q_PRIVATE_SLOT(d_func(), void subJobCollectionReceived(const Akonadi::Collection::List &)) - //@endcond }; } #endif diff --git a/src/core/jobs/invalidatecachejob.cpp b/src/core/jobs/invalidatecachejob.cpp index 8372021a3..017c0f6ff 100644 --- a/src/core/jobs/invalidatecachejob.cpp +++ b/src/core/jobs/invalidatecachejob.cpp @@ -1,127 +1,127 @@ /* Copyright (c) 2011 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 "invalidatecachejob_p.h" #include "job_p.h" #include "collectionfetchjob.h" #include "itemfetchjob.h" #include "itemmodifyjob.h" #include using namespace Akonadi; namespace Akonadi { class InvalidateCacheJobPrivate : JobPrivate { public: InvalidateCacheJobPrivate(InvalidateCacheJob *qq) : JobPrivate(qq) { } Collection collection; QString jobDebuggingString() const override; void collectionFetchResult(KJob *job); void itemFetchResult(KJob *job); void itemStoreResult(KJob *job); Q_DECLARE_PUBLIC(InvalidateCacheJob) }; QString InvalidateCacheJobPrivate::jobDebuggingString() const { return QStringLiteral("Invalidate Cache from collection id: %1").arg(collection.id()); } } void InvalidateCacheJobPrivate::collectionFetchResult(KJob *job) { Q_Q(InvalidateCacheJob); if (job->error()) { return; // handled by KCompositeJob } CollectionFetchJob *fetchJob = qobject_cast(job); Q_ASSERT(fetchJob); if (fetchJob->collections().size() == 1) { collection = fetchJob->collections().at(0); } if (!collection.isValid()) { q->setError(Job::Unknown); q->setErrorText(i18n("Invalid collection.")); q->emitResult(); return; } ItemFetchJob *itemFetch = new ItemFetchJob(collection, q); QObject::connect(itemFetch, &ItemFetchJob::result, q, [this](KJob* job) { itemFetchResult(job);} ); } void InvalidateCacheJobPrivate::itemFetchResult(KJob *job) { Q_Q(InvalidateCacheJob); if (job->error()) { return; } ItemFetchJob *fetchJob = qobject_cast(job); Q_ASSERT(fetchJob); if (fetchJob->items().isEmpty()) { q->emitResult(); return; } ItemModifyJob *modJob = nullptr; const Akonadi::Item::List itemsLst = fetchJob->items(); for (Item item : itemsLst) { item.clearPayload(); modJob = new ItemModifyJob(item, q); } - QObject::connect(modJob, SIGNAL(result(KJob*)), q, SLOT(itemStoreResult(KJob*))); + QObject::connect(modJob, &KJob::result, q, [this](KJob *job) { itemStoreResult(job); }); } void InvalidateCacheJobPrivate::itemStoreResult(KJob *job) { Q_Q(InvalidateCacheJob); if (job->error()) { return; } q->emitResult(); } InvalidateCacheJob::InvalidateCacheJob(const Collection &collection, QObject *parent) : Job(new InvalidateCacheJobPrivate(this), parent) { Q_D(InvalidateCacheJob); d->collection = collection; } void InvalidateCacheJob::doStart() { Q_D(InvalidateCacheJob); // resolve RID-only collections CollectionFetchJob *job = new CollectionFetchJob(d->collection, Akonadi::CollectionFetchJob::Base, this); - connect(job, SIGNAL(result(KJob*)), SLOT(collectionFetchResult(KJob*))); + connect(job, &KJob::result, this, [d](KJob *job) { d->collectionFetchResult(job); }); } #include "moc_invalidatecachejob_p.cpp" diff --git a/src/core/jobs/invalidatecachejob_p.h b/src/core/jobs/invalidatecachejob_p.h index e53f6d25b..bfb0b1557 100644 --- a/src/core/jobs/invalidatecachejob_p.h +++ b/src/core/jobs/invalidatecachejob_p.h @@ -1,56 +1,54 @@ /* Copyright (c) 2011 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. */ #ifndef AKONADI_INVALIDATECACHEJOB_P_H #define AKONADI_INVALIDATECACHEJOB_P_H #include "akonadicore_export.h" #include "job.h" namespace Akonadi { class Collection; class InvalidateCacheJobPrivate; /** * Helper job to invalidate item cache for an entire collection. * @since 4.8 */ class AKONADICORE_EXPORT InvalidateCacheJob : public Akonadi::Job { Q_OBJECT public: /** * Create a job to invalidate all cached content in @p collection. */ explicit InvalidateCacheJob(const Collection &collection, QObject *parent); protected: void doStart() override; private: Q_DECLARE_PRIVATE(InvalidateCacheJob) - Q_PRIVATE_SLOT(d_func(), void collectionFetchResult(KJob *job)) - Q_PRIVATE_SLOT(d_func(), void itemStoreResult(KJob *job)) }; } #endif // AKONADI_INVALIDATECACHEJOB_P_H diff --git a/src/core/jobs/itemfetchjob.cpp b/src/core/jobs/itemfetchjob.cpp index ee36877a3..a5109e0a1 100644 --- a/src/core/jobs/itemfetchjob.cpp +++ b/src/core/jobs/itemfetchjob.cpp @@ -1,300 +1,288 @@ /* 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 "itemfetchjob.h" #include "attributefactory.h" #include "collection.h" #include "itemfetchscope.h" #include "job_p.h" #include "protocolhelper_p.h" #include "session_p.h" #include "tagfetchscope.h" #include "private/protocol_p.h" #include using namespace Akonadi; class Akonadi::ItemFetchJobPrivate : public JobPrivate { public: ItemFetchJobPrivate(ItemFetchJob *parent) : JobPrivate(parent) { mCollection = Collection::root(); + mEmitTimer.setSingleShot(true); + mEmitTimer.setInterval(std::chrono::milliseconds{100}); + QObject::connect(&mEmitTimer, &QTimer::timeout, q_ptr, [this]() { timeout(); }); } ~ItemFetchJobPrivate() override { delete mValuePool; } - void init() - { - Q_Q(ItemFetchJob); - mEmitTimer = new QTimer(q); - mEmitTimer->setSingleShot(true); - mEmitTimer->setInterval(100); - q->connect(mEmitTimer, SIGNAL(timeout()), q, SLOT(timeout())); - } - void aboutToFinish() override { timeout(); } void timeout() { Q_Q(ItemFetchJob); - mEmitTimer->stop(); // in case we are called by result() + mEmitTimer.stop(); // in case we are called by result() if (!mPendingItems.isEmpty()) { if (!q->error()) { Q_EMIT q->itemsReceived(mPendingItems); } mPendingItems.clear(); } } QString jobDebuggingString() const override { if (mRequestedItems.isEmpty()) { QString str = QStringLiteral("All items from collection %1").arg(mCollection.id()); if (mFetchScope.fetchChangedSince().isValid()) { str += QStringLiteral(" changed since %1").arg(mFetchScope.fetchChangedSince().toString()); } return str; } else { try { QString itemStr = QStringLiteral("items id: "); bool firstItem = true; for (const Akonadi::Item &item : qAsConst(mRequestedItems)) { if (firstItem) { firstItem = false; } else { itemStr += QStringLiteral(", "); } itemStr += QString::number(item.id()); const Akonadi::Collection parentCollection = item.parentCollection(); if (parentCollection.isValid()) { itemStr += QStringLiteral(" from collection %1").arg(parentCollection.id()); } } return itemStr; //return QString(); //QString::fromLatin1(ProtocolHelper::entitySetToScope(mRequestedItems)); } catch (const Exception &e) { return QString::fromUtf8(e.what()); } } } Q_DECLARE_PUBLIC(ItemFetchJob) Collection mCollection; Tag mCurrentTag; Item::List mRequestedItems; Item::List mResultItems; ItemFetchScope mFetchScope; Item::List mPendingItems; // items pending for emitting itemsReceived() - QTimer *mEmitTimer = nullptr; + QTimer mEmitTimer; ProtocolHelperValuePool *mValuePool = nullptr; ItemFetchJob::DeliveryOptions mDeliveryOptions = ItemFetchJob::Default; int mCount = 0; }; ItemFetchJob::ItemFetchJob(const Collection &collection, QObject *parent) : Job(new ItemFetchJobPrivate(this), parent) { Q_D(ItemFetchJob); - d->init(); d->mCollection = collection; d->mValuePool = new ProtocolHelperValuePool; // only worth it for lots of results } ItemFetchJob::ItemFetchJob(const Item &item, QObject *parent) : Job(new ItemFetchJobPrivate(this), parent) { Q_D(ItemFetchJob); - d->init(); d->mRequestedItems.append(item); } ItemFetchJob::ItemFetchJob(const Item::List &items, QObject *parent) : Job(new ItemFetchJobPrivate(this), parent) { Q_D(ItemFetchJob); - d->init(); d->mRequestedItems = items; } ItemFetchJob::ItemFetchJob(const QList &items, QObject *parent) : Job(new ItemFetchJobPrivate(this), parent) { Q_D(ItemFetchJob); - d->init(); d->mRequestedItems.reserve(items.size()); for (auto id : items) { d->mRequestedItems.append(Item(id)); } } ItemFetchJob::ItemFetchJob(const QVector &items, QObject *parent) : Job(new ItemFetchJobPrivate(this), parent) { Q_D(ItemFetchJob); - d->init(); d->mRequestedItems.reserve(items.size()); for (auto id : items) { d->mRequestedItems.append(Item(id)); } } ItemFetchJob::ItemFetchJob(const Tag &tag, QObject *parent) : Job(new ItemFetchJobPrivate(this), parent) { Q_D(ItemFetchJob); - d->init(); d->mCurrentTag = tag; d->mValuePool = new ProtocolHelperValuePool; } ItemFetchJob::~ItemFetchJob() { } void ItemFetchJob::doStart() { Q_D(ItemFetchJob); try { d->sendCommand(Protocol::FetchItemsCommandPtr::create( d->mRequestedItems.isEmpty() ? Scope() : ProtocolHelper::entitySetToScope(d->mRequestedItems), ProtocolHelper::commandContextToProtocol(d->mCollection, d->mCurrentTag, d->mRequestedItems), ProtocolHelper::itemFetchScopeToProtocol(d->mFetchScope), ProtocolHelper::tagFetchScopeToProtocol(d->mFetchScope.tagFetchScope()))); } catch (const Akonadi::Exception &e) { setError(Job::Unknown); setErrorText(QString::fromUtf8(e.what())); emitResult(); return; } } bool ItemFetchJob::doHandleResponse(qint64 tag, const Protocol::CommandPtr &response) { Q_D(ItemFetchJob); if (!response->isResponse() || response->type() != Protocol::Command::FetchItems) { return Job::doHandleResponse(tag, response); } const auto resp = Protocol::cmdCast(response); // Invalid ID marks the last part of the response if (resp.id() < 0) { return true; } const Item item = ProtocolHelper::parseItemFetchResult(resp, nullptr, d->mValuePool); if (!item.isValid()) { return false; } d->mCount++; if (d->mDeliveryOptions & ItemGetter) { d->mResultItems.append(item); } if (d->mDeliveryOptions & EmitItemsInBatches) { d->mPendingItems.append(item); - if (!d->mEmitTimer->isActive()) { - d->mEmitTimer->start(); + if (!d->mEmitTimer.isActive()) { + d->mEmitTimer.start(); } } else if (d->mDeliveryOptions & EmitItemsIndividually) { Q_EMIT itemsReceived(Item::List() << item); } return false; } Item::List ItemFetchJob::items() const { Q_D(const ItemFetchJob); return d->mResultItems; } void ItemFetchJob::clearItems() { Q_D(ItemFetchJob); d->mResultItems.clear(); } void ItemFetchJob::setFetchScope(const ItemFetchScope &fetchScope) { Q_D(ItemFetchJob); d->mFetchScope = fetchScope; } ItemFetchScope &ItemFetchJob::fetchScope() { Q_D(ItemFetchJob); return d->mFetchScope; } void ItemFetchJob::setCollection(const Akonadi::Collection &collection) { Q_D(ItemFetchJob); d->mCollection = collection; } void ItemFetchJob::setDeliveryOption(DeliveryOptions options) { Q_D(ItemFetchJob); d->mDeliveryOptions = options; } ItemFetchJob::DeliveryOptions ItemFetchJob::deliveryOptions() const { Q_D(const ItemFetchJob); return d->mDeliveryOptions; } int ItemFetchJob::count() const { Q_D(const ItemFetchJob); return d->mCount; } #include "moc_itemfetchjob.cpp" diff --git a/src/core/jobs/itemfetchjob.h b/src/core/jobs/itemfetchjob.h index af3ff948e..39c0006ec 100644 --- a/src/core/jobs/itemfetchjob.h +++ b/src/core/jobs/itemfetchjob.h @@ -1,271 +1,267 @@ /* 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. */ #ifndef AKONADI_ITEMFETCHJOB_H #define AKONADI_ITEMFETCHJOB_H #include "akonadicore_export.h" #include "item.h" #include "job.h" namespace Akonadi { class Collection; class ItemFetchJobPrivate; class ItemFetchScope; /** * @short Job that fetches items from the Akonadi storage. * * This class is used to fetch items from the Akonadi storage. * Which parts of the items (e.g. headers only, attachments or all) * can be specified by the ItemFetchScope. * * Note that ItemFetchJob does not refresh the Akonadi storage from the * backend; this is unnecessary due to the fact that backend updates * automatically trigger an update to the Akonadi database whenever they occur * (unless the resource is offline). * * Note that items can not be created in the root collection (Collection::root()) * and therefore can not be fetched from there either. That is - an item fetch in * the root collection will yield an empty list. * * * Example: * * @code * * // Fetch all items with full payload from a collection * * const Collection collection = getCollection(); * * Akonadi::ItemFetchJob *job = new Akonadi::ItemFetchJob(collection); * connect(job, SIGNAL(result(KJob*)), SLOT(jobFinished(KJob*))); * job->fetchScope().fetchFullPayload(); * * ... * * MyClass::jobFinished(KJob *job) * { * if (job->error()) { * qDebug() << "Error occurred"; * return; * } * * Akonadi::ItemFetchJob *fetchJob = qobject_cast(job); * * const Akonadi::Item::List items = fetchJob->items(); * for (const Akonadi::Item &item : items) { * qDebug() << "Item ID:" << item.id(); * } * } * * @endcode * * @author Volker Krause */ class AKONADICORE_EXPORT ItemFetchJob : public Job { Q_OBJECT Q_FLAGS(DeliveryOptions) public: /** * Creates a new item fetch job that retrieves all items inside the given collection. * * @param collection The parent collection to fetch all items from. * @param parent The parent object. */ explicit ItemFetchJob(const Collection &collection, QObject *parent = nullptr); /** * Creates a new item fetch job that retrieves the specified item. * If the item has a uid set, this is used to identify the item on the Akonadi * server. If only a remote identifier is available, that is used. * However, as remote identifiers are not necessarily globally unique, you * need to specify the collection to search in in that case, using * setCollection(). * * @internal * For internal use only when using remote identifiers, the resource search * context can be set globally by ResourceSelectJob. * @endinternal * * @param item The item to fetch. * @param parent The parent object. */ explicit ItemFetchJob(const Item &item, QObject *parent = nullptr); /** * Creates a new item fetch job that retrieves the specified items. * If the items have a uid set, this is used to identify the item on the Akonadi * server. If only a remote identifier is available, that is used. * However, as remote identifiers are not necessarily globally unique, you * need to specify the collection to search in in that case, using * setCollection(). * * @internal * For internal use only when using remote identifiers, the resource search * context can be set globally by ResourceSelectJob. * @endinternal * * @param items The items to fetch. * @param parent The parent object. * @since 4.4 */ explicit ItemFetchJob(const Item::List &items, QObject *parent = nullptr); /** * Convenience ctor equivalent to ItemFetchJob(const Item::List &items, QObject *parent = nullptr) * @since 4.8 */ explicit ItemFetchJob(const QList &items, QObject *parent = nullptr); /** * Convenience ctor equivalent to ItemFetchJob(const Item::List &items, QObject *parent = nullptr) * @since 5.4 */ explicit ItemFetchJob(const QVector &items, QObject *parent = nullptr); /** * Creates a new item fetch job that retrieves all items tagged with specified @p tag. * * @param tag The tag to fetch all items from. * @param parent The parent object. * * @since 4.14 */ explicit ItemFetchJob(const Tag &tag, QObject *parent = nullptr); /** * Destroys the item fetch job. */ ~ItemFetchJob() override; /** * Returns the fetched items. * * This returns an empty list when not using the ItemGetter DeliveryOption. * * @note The items are invalid before the result(KJob*) * signal has been emitted or if an error occurred. */ Q_REQUIRED_RESULT Item::List items() const; /** * Save memory by clearing the fetched items. * @since 4.12 */ void clearItems(); /** * Sets the item fetch scope. * * The ItemFetchScope controls how much of an item's data is fetched * from the server, e.g. whether to fetch the full item payload or * only meta data. * * @param fetchScope The new scope for item fetch operations. * * @see fetchScope() * @since 4.4 */ void setFetchScope(const ItemFetchScope &fetchScope); /** * Returns the item fetch scope. * * Since this returns a reference it can be used to conveniently modify the * current scope in-place, i.e. by calling a method on the returned reference * without storing it in a local variable. See the ItemFetchScope documentation * for an example. * * @return a reference to the current item fetch scope * * @see setFetchScope() for replacing the current item fetch scope */ ItemFetchScope &fetchScope(); /** * Specifies the collection the item is in. * This is only required when retrieving an item based on its remote id * which might not be unique globally. * * @internal * @see ResourceSelectJob (for internal use only) * @endinternal */ void setCollection(const Collection &collection); enum DeliveryOption { ItemGetter = 0x1, ///< items available through items() EmitItemsIndividually = 0x2, ///< emitted via signal upon reception EmitItemsInBatches = 0x4, ///< emitted via signal in bulk (collected and emitted delayed via timer) Default = ItemGetter | EmitItemsInBatches }; Q_DECLARE_FLAGS(DeliveryOptions, DeliveryOption) /** * Sets the mechanisms by which the items should be fetched * @since 4.13 */ void setDeliveryOption(DeliveryOptions options); /** * Returns the delivery options * @since 4.13 */ DeliveryOptions deliveryOptions() const; /** * Returns the total number of retrieved items. * This works also without the ItemGetter DeliveryOption. * @since 4.14 */ int count() const; Q_SIGNALS: /** * This signal is emitted whenever new items have been fetched completely. * * @note This is an optimization; instead of waiting for the end of the job * and calling items(), you can connect to this signal and get the items * incrementally. * * @param items The fetched items. */ void itemsReceived(const Akonadi::Item::List &items); protected: void doStart() override; bool doHandleResponse(qint64 tag, const Protocol::CommandPtr &response) override; private: Q_DECLARE_PRIVATE(ItemFetchJob) - - //@cond PRIVATE - Q_PRIVATE_SLOT(d_func(), void timeout()) - //@endcond }; } Q_DECLARE_OPERATORS_FOR_FLAGS(Akonadi::ItemFetchJob::DeliveryOptions) #endif diff --git a/src/core/jobs/itemsearchjob.cpp b/src/core/jobs/itemsearchjob.cpp index ed57004ea..8930e974c 100644 --- a/src/core/jobs/itemsearchjob.cpp +++ b/src/core/jobs/itemsearchjob.cpp @@ -1,284 +1,272 @@ /* Copyright (c) 2009 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. */ #include "itemsearchjob.h" #include "itemfetchscope.h" #include "tagfetchscope.h" #include "job_p.h" #include "protocolhelper_p.h" #include "searchquery.h" #include "private/protocol_p.h" #include #include #include using namespace Akonadi; class Akonadi::ItemSearchJobPrivate : public JobPrivate { public: ItemSearchJobPrivate(ItemSearchJob *parent, const SearchQuery &query) : JobPrivate(parent) , mQuery(query) { + mEmitTimer.setSingleShot(true); + mEmitTimer.setInterval(std::chrono::milliseconds{100}); + QObject::connect(&mEmitTimer, &QTimer::timeout, q_ptr, [this]() { timeout(); }); } - void init() + void aboutToFinish() override { - Q_Q(ItemSearchJob); - mEmitTimer = new QTimer(q); - mEmitTimer->setSingleShot(true); - mEmitTimer->setInterval(100); - q->connect(mEmitTimer, SIGNAL(timeout()), q, SLOT(timeout())); - q->connect(q, SIGNAL(result(KJob*)), q, SLOT(timeout())); + timeout(); } void timeout() { Q_Q(Akonadi::ItemSearchJob); - mEmitTimer->stop(); // in case we are called by result() + mEmitTimer.stop(); // in case we are called by result() if (!mPendingItems.isEmpty()) { if (!q->error()) { Q_EMIT q->itemsReceived(mPendingItems); } mPendingItems.clear(); } } QString jobDebuggingString() const override { QStringList flags; if (mRecursive) { flags.append(QStringLiteral("recursive")); } if (mRemote) { flags.append(QStringLiteral("remote")); } if (mCollections.isEmpty()) { flags.append(QStringLiteral("all collections")); } else { flags.append(QStringLiteral("%1 collections").arg(mCollections.count())); } return QStringLiteral("%1,json=%2").arg(flags.join(QLatin1Char(',')), QString::fromUtf8(mQuery.toJSON())); } Q_DECLARE_PUBLIC(ItemSearchJob) SearchQuery mQuery; Collection::List mCollections; QStringList mMimeTypes; bool mRecursive = false; bool mRemote = false; ItemFetchScope mItemFetchScope; TagFetchScope mTagFetchScope; Item::List mItems; Item::List mPendingItems; // items pending for emitting itemsReceived() - QTimer *mEmitTimer = nullptr; + QTimer mEmitTimer; }; QThreadStorage instances; static void cleanupDefaultSearchSession() { instances.setLocalData(nullptr); } static Session *defaultSearchSession() { if (!instances.hasLocalData()) { const QByteArray sessionName = Session::defaultSession()->sessionId() + "-SearchSession"; instances.setLocalData(new Session(sessionName)); qAddPostRoutine(cleanupDefaultSearchSession); } return instances.localData(); } static QObject *sessionForJob(QObject *parent) { if (qobject_cast(parent) || qobject_cast(parent)) { return parent; } return defaultSearchSession(); } ItemSearchJob::ItemSearchJob(QObject *parent) : Job(new ItemSearchJobPrivate(this, SearchQuery()), sessionForJob(parent)) -{ - Q_D(ItemSearchJob); - - d->init(); -} +{} ItemSearchJob::ItemSearchJob(const SearchQuery &query, QObject *parent) : Job(new ItemSearchJobPrivate(this, query), sessionForJob(parent)) -{ - Q_D(ItemSearchJob); - - d->init(); -} +{} -ItemSearchJob::~ItemSearchJob() -{ -} +ItemSearchJob::~ItemSearchJob() = default; void ItemSearchJob::setQuery(const SearchQuery &query) { Q_D(ItemSearchJob); d->mQuery = query; } void ItemSearchJob::setFetchScope(const ItemFetchScope &fetchScope) { Q_D(ItemSearchJob); d->mItemFetchScope = fetchScope; } ItemFetchScope &ItemSearchJob::fetchScope() { Q_D(ItemSearchJob); return d->mItemFetchScope; } void ItemSearchJob::setTagFetchScope(const TagFetchScope &fetchScope) { Q_D(ItemSearchJob); d->mTagFetchScope = fetchScope; } TagFetchScope &ItemSearchJob::tagFetchScope() { Q_D(ItemSearchJob); return d->mTagFetchScope; } void ItemSearchJob::setSearchCollections(const Collection::List &collections) { Q_D(ItemSearchJob); d->mCollections = collections; } Collection::List ItemSearchJob::searchCollections() const { return d_func()->mCollections; } void ItemSearchJob::setMimeTypes(const QStringList &mimeTypes) { Q_D(ItemSearchJob); d->mMimeTypes = mimeTypes; } QStringList ItemSearchJob::mimeTypes() const { return d_func()->mMimeTypes; } void ItemSearchJob::setRecursive(bool recursive) { Q_D(ItemSearchJob); d->mRecursive = recursive; } bool ItemSearchJob::isRecursive() const { return d_func()->mRecursive; } void ItemSearchJob::setRemoteSearchEnabled(bool enabled) { Q_D(ItemSearchJob); d->mRemote = enabled; } bool ItemSearchJob::isRemoteSearchEnabled() const { return d_func()->mRemote; } void ItemSearchJob::doStart() { Q_D(ItemSearchJob); auto cmd = Protocol::SearchCommandPtr::create(); cmd->setMimeTypes(d->mMimeTypes); if (!d->mCollections.isEmpty()) { QVector ids; ids.reserve(d->mCollections.size()); for (const Collection &col : qAsConst(d->mCollections)) { ids << col.id(); } cmd->setCollections(ids); } cmd->setRecursive(d->mRecursive); cmd->setRemote(d->mRemote); cmd->setQuery(QString::fromUtf8(d->mQuery.toJSON())); cmd->setItemFetchScope(ProtocolHelper::itemFetchScopeToProtocol(d->mItemFetchScope)); cmd->setTagFetchScope(ProtocolHelper::tagFetchScopeToProtocol(d->mTagFetchScope)); d->sendCommand(cmd); } bool ItemSearchJob::doHandleResponse(qint64 tag, const Protocol::CommandPtr &response) { Q_D(ItemSearchJob); if (response->isResponse() && response->type() == Protocol::Command::FetchItems) { const Item item = ProtocolHelper::parseItemFetchResult( Protocol::cmdCast(response)); if (!item.isValid()) { return false; } d->mItems.append(item); d->mPendingItems.append(item); - if (!d->mEmitTimer->isActive()) { - d->mEmitTimer->start(); + if (!d->mEmitTimer.isActive()) { + d->mEmitTimer.start(); } return false; } if (response->isResponse() && response->type() == Protocol::Command::Search) { return true; } return Job::doHandleResponse(tag, response); } Item::List ItemSearchJob::items() const { Q_D(const ItemSearchJob); return d->mItems; } #include "moc_itemsearchjob.cpp" diff --git a/src/core/jobs/job.cpp b/src/core/jobs/job.cpp index 4e1e9c7cd..6a414f0a5 100644 --- a/src/core/jobs/job.cpp +++ b/src/core/jobs/job.cpp @@ -1,417 +1,417 @@ /* Copyright (c) 2006 Tobias Koenig 2006 Marc Mutz 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 "job.h" #include "job_p.h" #include "akonadicore_debug.h" #include #include #include "private/protocol_p.h" #include "private/instance_p.h" #include "session.h" #include "session_p.h" #include #include #include #include #include using namespace Akonadi; static QDBusAbstractInterface *s_jobtracker = nullptr; //@cond PRIVATE void JobPrivate::handleResponse(qint64 tag, const Protocol::CommandPtr &response) { Q_Q(Job); if (mCurrentSubJob) { mCurrentSubJob->d_ptr->handleResponse(tag, response); return; } if (tag == mTag) { if (response->isResponse()) { const auto &resp = Protocol::cmdCast(response); if (resp.isError()) { q->setError(Job::Unknown); q->setErrorText(resp.errorMessage()); q->emitResult(); return; } } } if (mTag != tag) { qCWarning(AKONADICORE_LOG) << "Received response with a different tag!"; qCDebug(AKONADICORE_LOG) << "Response tag:" << tag << ", response type:" << response->type(); qCDebug(AKONADICORE_LOG) << "Job tag:" << mTag << ", job:" << q; return; } if (mStarted) { if (mReadingFinished) { qCWarning(AKONADICORE_LOG) << "Received response for a job that does not expect any more data, ignoring"; qCDebug(AKONADICORE_LOG) << "Response tag:" << tag << ", response type:" << response->type(); qCDebug(AKONADICORE_LOG) << "Job tag:" << mTag << ", job:" << q; Q_ASSERT(!mReadingFinished); return; } if (q->doHandleResponse(tag, response)) { mReadingFinished = true; QTimer::singleShot(0, q, [this]() {delayedEmitResult(); }); } } } void JobPrivate::init(QObject *parent) { Q_Q(Job); mParentJob = qobject_cast(parent); mSession = qobject_cast(parent); if (!mSession) { if (!mParentJob) { mSession = Session::defaultSession(); } else { mSession = mParentJob->d_ptr->mSession; } } if (!mParentJob) { mSession->d->addJob(q); } else { mParentJob->addSubjob(q); } publishJob(); } void JobPrivate::publishJob() { Q_Q(Job); // if there's a job tracker running, tell it about the new job if (!s_jobtracker) { // Let's only check for the debugging console every 3 seconds, otherwise every single job // makes a dbus call to the dbus daemon, doesn't help performance. static QElapsedTimer s_lastTime; if (!s_lastTime.isValid() || s_lastTime.elapsed() > 3000) { if (!s_lastTime.isValid()) { s_lastTime.start(); } const QString suffix = Akonadi::Instance::identifier().isEmpty() ? QString() : QLatin1Char('-') + Akonadi::Instance::identifier(); if (QDBusConnection::sessionBus().interface()->isServiceRegistered(QStringLiteral("org.kde.akonadiconsole") + suffix)) { s_jobtracker = new QDBusInterface(QLatin1String("org.kde.akonadiconsole") + suffix, QStringLiteral("/jobtracker"), QStringLiteral("org.freedesktop.Akonadi.JobTracker"), QDBusConnection::sessionBus(), nullptr); mSession->d->publishOtherJobs(q); } else { s_lastTime.restart(); } } // Note: we never reset s_jobtracker to 0 when a call fails; but if we did // then we should restart s_lastTime. } QMetaObject::invokeMethod(q, "signalCreationToJobTracker", Qt::QueuedConnection); } void JobPrivate::signalCreationToJobTracker() { Q_Q(Job); if (s_jobtracker) { // We do these dbus calls manually, so as to avoid having to install (or copy) the console's // xml interface document. Since this is purely a debugging aid, that seems preferable to // publishing something not intended for public consumption. // WARNING: for any signature change here, apply it to resourcescheduler.cpp too const QList argumentList = QList() << QLatin1String(mSession->sessionId()) << QString::number(reinterpret_cast(q), 16) << (mParentJob ? QString::number(reinterpret_cast(mParentJob), 16) : QString()) << QString::fromLatin1(q->metaObject()->className()) << jobDebuggingString(); QDBusPendingCall call = s_jobtracker->asyncCallWithArgumentList(QStringLiteral("jobCreated"), argumentList); QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(call, s_jobtracker); QObject::connect(watcher, &QDBusPendingCallWatcher::finished, s_jobtracker, [](QDBusPendingCallWatcher *w) { QDBusPendingReply reply = *w; if (reply.isError() && s_jobtracker) { qDebug() << reply.error().name() << reply.error().message(); s_jobtracker->deleteLater(); s_jobtracker = nullptr; } w->deleteLater(); }); } } void JobPrivate::signalStartedToJobTracker() { Q_Q(Job); if (s_jobtracker) { // if there's a job tracker running, tell it a job started const QList argumentList = { QString::number(reinterpret_cast(q), 16) }; s_jobtracker->callWithArgumentList(QDBus::NoBlock, QStringLiteral("jobStarted"), argumentList); } } void JobPrivate::aboutToFinish() { // Dummy } void JobPrivate::delayedEmitResult() { Q_Q(Job); if (q->hasSubjobs()) { // We still have subjobs, wait for them to finish mFinishPending = true; } else { aboutToFinish(); q->emitResult(); } } void JobPrivate::startQueued() { Q_Q(Job); mStarted = true; Q_EMIT q->aboutToStart(q); q->doStart(); QTimer::singleShot(0, q, [this]() { startNext(); }); QMetaObject::invokeMethod(q, "signalStartedToJobTracker", Qt::QueuedConnection); } void JobPrivate::lostConnection() { Q_Q(Job); if (mCurrentSubJob) { mCurrentSubJob->d_ptr->lostConnection(); } else { q->setError(Job::ConnectionFailed); q->emitResult(); } } void JobPrivate::slotSubJobAboutToStart(Job *job) { Q_ASSERT(mCurrentSubJob == nullptr); mCurrentSubJob = job; } void JobPrivate::startNext() { Q_Q(Job); if (mStarted && !mCurrentSubJob && q->hasSubjobs()) { Job *job = qobject_cast(q->subjobs().at(0)); Q_ASSERT(job); job->d_ptr->startQueued(); } else if (mFinishPending && !q->hasSubjobs()) { // The last subjob we've been waiting for has finished, emitResult() finally QTimer::singleShot(0, q, [this]() {delayedEmitResult(); }); } } qint64 JobPrivate::newTag() { if (mParentJob) { mTag = mParentJob->d_ptr->newTag(); } else { mTag = mSession->d->nextTag(); } return mTag; } qint64 JobPrivate::tag() const { return mTag; } void JobPrivate::sendCommand(qint64 tag, const Protocol::CommandPtr &cmd) { if (mParentJob) { mParentJob->d_ptr->sendCommand(tag, cmd); } else { mSession->d->sendCommand(tag, cmd); } } void JobPrivate::sendCommand(const Protocol::CommandPtr &cmd) { sendCommand(newTag(), cmd); } void JobPrivate::itemRevisionChanged(Akonadi::Item::Id itemId, int oldRevision, int newRevision) { mSession->d->itemRevisionChanged(itemId, oldRevision, newRevision); } void JobPrivate::updateItemRevision(Akonadi::Item::Id itemId, int oldRevision, int newRevision) { Q_Q(Job); const auto subjobs = q->subjobs(); for (KJob *j : subjobs) { Akonadi::Job *job = qobject_cast(j); if (job) { job->d_ptr->updateItemRevision(itemId, oldRevision, newRevision); } } doUpdateItemRevision(itemId, oldRevision, newRevision); } void JobPrivate::doUpdateItemRevision(Akonadi::Item::Id itemId, int oldRevision, int newRevision) { Q_UNUSED(itemId); Q_UNUSED(oldRevision); Q_UNUSED(newRevision); } int JobPrivate::protocolVersion() const { return mSession->d->protocolVersion; } //@endcond Job::Job(QObject *parent) : KCompositeJob(parent) , d_ptr(new JobPrivate(this)) { d_ptr->init(parent); } Job::Job(JobPrivate *dd, QObject *parent) : KCompositeJob(parent) , d_ptr(dd) { d_ptr->init(parent); } Job::~Job() { delete d_ptr; // if there is a job tracer listening, tell it the job is done now if (s_jobtracker) { const QList argumentList = {QString::number(reinterpret_cast(this), 16), errorString()}; s_jobtracker->callWithArgumentList(QDBus::NoBlock, QStringLiteral("jobEnded"), argumentList); } } void Job::start() { } bool Job::doKill() { Q_D(Job); if (d->mStarted) { // the only way to cancel an already started job is reconnecting to the server d->mSession->d->forceReconnect(); } d->mStarted = false; return true; } QString Job::errorString() const { QString str; switch (error()) { case NoError: break; case ConnectionFailed: str = i18n("Cannot connect to the Akonadi service."); break; case ProtocolVersionMismatch: str = i18n("The protocol version of the Akonadi server is incompatible. Make sure you have a compatible version installed."); break; case UserCanceled: str = i18n("User canceled operation."); break; case Unknown: return errorText(); case UserError: str = i18n("Unknown error."); break; } if (!errorText().isEmpty()) { str += QStringLiteral(" (%1)").arg(errorText()); } return str; } bool Job::addSubjob(KJob *job) { bool rv = KCompositeJob::addSubjob(job); if (rv) { - connect(job, SIGNAL(aboutToStart(Akonadi::Job*)), SLOT(slotSubJobAboutToStart(Akonadi::Job*))); + connect(qobject_cast(job), &Job::aboutToStart, this, [this](Job *job) { d_ptr->slotSubJobAboutToStart(job); }); QTimer::singleShot(0, this, [this]() { d_ptr->startNext(); }); } return rv; } bool Job::removeSubjob(KJob *job) { bool rv = KCompositeJob::removeSubjob(job); if (job == d_ptr->mCurrentSubJob) { d_ptr->mCurrentSubJob = nullptr; QTimer::singleShot(0, this, [this]() { d_ptr->startNext(); }); } return rv; } bool Akonadi::Job::doHandleResponse(qint64 tag, const Akonadi::Protocol::CommandPtr &response) { qCDebug(AKONADICORE_LOG) << this << "Unhandled response: " << tag << Protocol::debugString(response); setError(Unknown); setErrorText(i18n("Unexpected response")); emitResult(); return true; } void Job::slotResult(KJob *job) { if (d_ptr->mCurrentSubJob == job) { // current job finished, start the next one d_ptr->mCurrentSubJob = nullptr; KCompositeJob::slotResult(job); if (!job->error()) { QTimer::singleShot(0, this, [this]() { d_ptr->startNext(); }); } } else { // job that was still waiting for execution finished, probably canceled, // so just remove it from the queue and move on without caring about // its error code KCompositeJob::removeSubjob(job); } } void Job::emitWriteFinished() { d_ptr->mWriteFinished = true; Q_EMIT writeFinished(this); } #include "moc_job.cpp" diff --git a/src/core/jobs/job.h b/src/core/jobs/job.h index 35b6f5cdf..8dd25ad37 100644 --- a/src/core/jobs/job.h +++ b/src/core/jobs/job.h @@ -1,237 +1,236 @@ /* Copyright (c) 2006 Tobias Koenig 2006 Marc Mutz 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. */ #ifndef AKONADI_JOB_H #define AKONADI_JOB_H #include "akonadicore_export.h" #include class QString; namespace Akonadi { namespace Protocol { class Command; using CommandPtr = QSharedPointer; } class JobPrivate; class Session; class SessionPrivate; /** * @short Base class for all actions in the Akonadi storage. * * This class encapsulates a request to the pim storage service, * the code looks like * * @code * * Akonadi::Job *job = new Akonadi::SomeJob( some parameter ); * connect( job, SIGNAL(result(KJob*)), * this, SLOT(slotResult(KJob*)) ); * * @endcode * * The job is queued for execution as soon as the event loop is entered * again. * * And the slotResult is usually at least: * * @code * * if ( job->error() ) { * // handle error... * } * * @endcode * * With the synchronous interface the code looks like * * @code * Akonadi::SomeJob *job = new Akonadi::SomeJob( some parameter ); * if ( !job->exec() ) { * qDebug() << "Error:" << job->errorString(); * } else { * // do something * } * @endcode * * @warning Using the synchronous method is error prone, use this only * if the asynchronous access is not possible. See the documentation of * KJob::exec() for more details. * * Subclasses must reimplement doStart(). * * @note KJob-derived objects delete itself, it is thus not possible * to create job objects on the stack! * * @author Volker Krause , Tobias Koenig , Marc Mutz */ class AKONADICORE_EXPORT Job : public KCompositeJob { Q_OBJECT friend class Session; friend class SessionPrivate; public: /** * Describes a list of jobs. */ typedef QList List; /** * Describes the error codes that can be emitted by this class. * Subclasses can provide additional codes, starting from UserError * onwards */ enum Error { ConnectionFailed = UserDefinedError, ///< The connection to the Akonadi server failed. ProtocolVersionMismatch, ///< The server protocol version is too old or too new. UserCanceled, ///< The user canceled this job. Unknown, ///< Unknown error. UserError = UserDefinedError + 42 ///< Starting point for error codes defined by sub-classes. }; /** * Creates a new job. * * If the parent object is a Job object, the new job will be a subjob of @p parent. * If the parent object is a Session object, it will be used for server communication * instead of the default session. * * @param parent The parent object, job or session. */ explicit Job(QObject *parent = nullptr); /** * Destroys the job. */ ~Job() override; /** * Jobs are started automatically once entering the event loop again, no need * to explicitly call this. */ void start() override; /** * Returns the error string, if there has been an error, an empty * string otherwise. */ Q_REQUIRED_RESULT QString errorString() const override; Q_SIGNALS: /** * This signal is emitted directly before the job will be started. * * @param job The started job. */ void aboutToStart(Akonadi::Job *job); /** * This signal is emitted if the job has finished all write operations, ie. * if this signal is emitted, the job guarantees to not call writeData() again. * Do not emit this signal directly, call emitWriteFinished() instead. * * @param job This job. * @see emitWriteFinished() */ void writeFinished(Akonadi::Job *job); protected: /** * This method must be reimplemented in the concrete jobs. It will be called * after the job has been started and a connection to the Akonadi backend has * been established. */ virtual void doStart() = 0; /** * This method should be reimplemented in the concrete jobs in case you want * to handle incoming data. It will be called on received data from the backend. * The default implementation does nothing. * * @param tag The tag of the corresponding command, empty if this is an untagged response. * @param response The received response * * @return Implementations should return true if the last response was processed and * the job can emit result. Return false if more responses from server are expected. */ virtual bool doHandleResponse(qint64 tag, const Protocol::CommandPtr &response); /** * Adds the given job as a subjob to this job. This method is automatically called * if you construct a job using another job as parent object. * The base implementation does the necessary setup to share the network connection * with the backend. * * @param job The new subjob. */ bool addSubjob(KJob *job) override; /** * Removes the given subjob of this job. * * @param job The subjob to remove. */ bool removeSubjob(KJob *job) override; /** * Kills the execution of the job. */ bool doKill() override; /** * Call this method to indicate that this job will not call writeData() again. * @see writeFinished() */ void emitWriteFinished(); protected Q_SLOTS: void slotResult(KJob *job) override; protected: //@cond PRIVATE Job(JobPrivate *dd, QObject *parent); JobPrivate *const d_ptr; //@endcond private: Q_DECLARE_PRIVATE(Job) //@cond PRIVATE - Q_PRIVATE_SLOT(d_func(), void slotSubJobAboutToStart(Akonadi::Job *)) Q_PRIVATE_SLOT(d_func(), void startNext()) Q_PRIVATE_SLOT(d_func(), void signalCreationToJobTracker()) Q_PRIVATE_SLOT(d_func(), void signalStartedToJobTracker()) Q_PRIVATE_SLOT(d_func(), void delayedEmitResult()) //@endcond }; } #endif diff --git a/src/core/jobs/job_p.h b/src/core/jobs/job_p.h index 4e7231ada..893bc806c 100644 --- a/src/core/jobs/job_p.h +++ b/src/core/jobs/job_p.h @@ -1,133 +1,134 @@ /* 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. */ #ifndef AKONADI_JOB_P_H #define AKONADI_JOB_P_H #include "session.h" #include "item.h" namespace Akonadi { namespace Protocol { class Command; } /** * @internal */ class JobPrivate { public: explicit JobPrivate(Job *parent) : q_ptr(parent) { } - virtual ~JobPrivate() - { - } + virtual ~JobPrivate() = default; void init(QObject *parent); void handleResponse(qint64 tag, const Protocol::CommandPtr &response); void startQueued(); void lostConnection(); void slotSubJobAboutToStart(Akonadi::Job *job); void startNext(); void signalCreationToJobTracker(); void signalStartedToJobTracker(); void delayedEmitResult(); void publishJob(); /* Returns a string to display in akonadi console's job tracker. E.g. item ID. */ virtual QString jobDebuggingString() const { return QString(); } /** Returns a new unique command tag for communication with the backend. */ Q_REQUIRED_RESULT qint64 newTag(); /** Return the tag used for the request. */ Q_REQUIRED_RESULT qint64 tag() const; /** Sends the @p command to the backend */ void sendCommand(qint64 tag, const Protocol::CommandPtr &command); /** * Same as calling JobPrivate::sendCommand(newTag(), command) */ void sendCommand(const Protocol::CommandPtr &command); /** * Notify following jobs about item revision changes. * This is used to avoid phantom conflicts between pipelined modify jobs on the same item. * @param itemId the id of the item which has changed * @param oldRevision the old item revision * @param newRevision the new item revision */ void itemRevisionChanged(Akonadi::Item::Id itemId, int oldRevision, int newRevision); /** * Propagate item revision changes to this job and its sub-jobs. */ void updateItemRevision(Akonadi::Item::Id itemId, int oldRevision, int newRevision); /** * Overwrite this if your job does operations with conflict detection and update * the item revisions if your items are affected. The default implementation does nothing. */ virtual void doUpdateItemRevision(Akonadi::Item::Id, int oldRevision, int newRevision); /** * This method is called right before result() and finished() signals are emitted. * Overwrite this method in your job if you need to emit some signals or process * some data before the job finishes. * * Default implementation does nothing. */ virtual void aboutToFinish(); Q_REQUIRED_RESULT int protocolVersion() const; Job *q_ptr; Q_DECLARE_PUBLIC(Job) Job *mParentJob = nullptr; Job *mCurrentSubJob = nullptr; qint64 mTag = -1; Session *mSession = nullptr; bool mWriteFinished = false; bool mReadingFinished = false; bool mStarted = false; bool mFinishPending = false; + +private: + Q_DISABLE_COPY_MOVE(JobPrivate) }; } #endif diff --git a/src/core/jobs/recursiveitemfetchjob.cpp b/src/core/jobs/recursiveitemfetchjob.cpp index b5ceb5052..6123aafdf 100644 --- a/src/core/jobs/recursiveitemfetchjob.cpp +++ b/src/core/jobs/recursiveitemfetchjob.cpp @@ -1,134 +1,133 @@ /* Copyright (c) 2009 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. */ #include "recursiveitemfetchjob.h" #include "collectionfetchjob.h" #include "collectionfetchscope.h" #include "itemfetchjob.h" #include "itemfetchscope.h" #include using namespace Akonadi; class Q_DECL_HIDDEN RecursiveItemFetchJob::Private { public: Private(const Collection &collection, const QStringList &mimeTypes, RecursiveItemFetchJob *parent) : mParent(parent) , mCollection(collection) , mMimeTypes(mimeTypes) , mFetchCount(0) { } void collectionFetchResult(KJob *job) { if (job->error()) { mParent->emitResult(); return; } const CollectionFetchJob *fetchJob = qobject_cast(job); Collection::List collections = fetchJob->collections(); collections.prepend(mCollection); for (const Collection &collection : qAsConst(collections)) { ItemFetchJob *itemFetchJob = new ItemFetchJob(collection, mParent); itemFetchJob->setFetchScope(mFetchScope); - mParent->connect(itemFetchJob, SIGNAL(result(KJob*)), - mParent, SLOT(itemFetchResult(KJob*))); + mParent->connect(itemFetchJob, &KJob::result, mParent, [this](KJob *job) { itemFetchResult(job) ;}); mFetchCount++; } } void itemFetchResult(KJob *job) { if (!job->error()) { const ItemFetchJob *fetchJob = qobject_cast(job); if (!mMimeTypes.isEmpty()) { const Akonadi::Item::List lstItems = fetchJob->items(); for (const Item &item : lstItems) { if (mMimeTypes.contains(item.mimeType())) { mItems << item; } } } else { mItems << fetchJob->items(); } } mFetchCount--; if (mFetchCount == 0) { mParent->emitResult(); } } RecursiveItemFetchJob *mParent = nullptr; Collection mCollection; Item::List mItems; ItemFetchScope mFetchScope; QStringList mMimeTypes; int mFetchCount; }; RecursiveItemFetchJob::RecursiveItemFetchJob(const Collection &collection, const QStringList &mimeTypes, QObject *parent) : KJob(parent) , d(new Private(collection, mimeTypes, this)) { } RecursiveItemFetchJob::~RecursiveItemFetchJob() { delete d; } void RecursiveItemFetchJob::setFetchScope(const ItemFetchScope &fetchScope) { d->mFetchScope = fetchScope; } ItemFetchScope &RecursiveItemFetchJob::fetchScope() { return d->mFetchScope; } void RecursiveItemFetchJob::start() { CollectionFetchJob *job = new CollectionFetchJob(d->mCollection, CollectionFetchJob::Recursive, this); if (!d->mMimeTypes.isEmpty()) { job->fetchScope().setContentMimeTypes(d->mMimeTypes); } connect(job, &CollectionFetchJob::result, this, [this](KJob *job) { d->collectionFetchResult(job); }); } Akonadi::Item::List RecursiveItemFetchJob::items() const { return d->mItems; } #include "moc_recursiveitemfetchjob.cpp" diff --git a/src/core/jobs/recursiveitemfetchjob.h b/src/core/jobs/recursiveitemfetchjob.h index 8f3293dfa..48a741633 100644 --- a/src/core/jobs/recursiveitemfetchjob.h +++ b/src/core/jobs/recursiveitemfetchjob.h @@ -1,154 +1,152 @@ /* Copyright (c) 2009 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 AKONADI_RECURSIVEITEMFETCHJOB_H #define AKONADI_RECURSIVEITEMFETCHJOB_H #include "akonadicore_export.h" #include "item.h" #include namespace Akonadi { class Collection; class ItemFetchScope; /** * @short Job that fetches all items of a collection recursive. * * This job takes a collection as argument and returns a list of * all items that are part of the passed collection and its child * collections. The items to fetch can be filtered by mime types and * the parts of the items that shall be fetched can * be specified via an ItemFetchScope. * * Example: * * @code * * // Assume the following Akonadi storage tree structure: * // * // Root Collection * // | * // +- Contacts * // | | * // | +- Private * // | | * // | `- Business * // | * // `- Events * // * // Collection 'Contacts' has the ID 15, then the following code * // returns all contact items from 'Contacts', 'Private' and 'Business'. * * const Akonadi::Collection contactsCollection( 15 ); * const QStringList mimeTypes = QStringList() << KContacts::Addressee::mimeType(); * * Akonadi::RecursiveItemFetchJob *job = new Akonadi::RecursiveItemFetchJob( contactsCollection, mimeTypes ); * job->fetchScope().fetchFullPayload(); * connect( job, SIGNAL(result(KJob*)), this, SLOT(fetchResult(KJob*)) ); * * job->start(); * * ... * * MyClass::fetchResult( KJob *job ) * { * Akonadi::RecursiveItemFetchJob *fetchJob = qobject_cast( job ); * const Akonadi::Item::List items = fetchJob->items(); * // do something with the items * } * * @endcode * * @author Tobias Koenig * @since 4.6 */ class AKONADICORE_EXPORT RecursiveItemFetchJob : public KJob { Q_OBJECT public: /** * Creates a new recursive item fetch job. * * @param collection The collection that shall be fetched recursive. * @param mimeTypes The list of mime types that will be used for filtering. * @param parent The parent object. */ explicit RecursiveItemFetchJob(const Akonadi::Collection &collection, const QStringList &mimeTypes, QObject *parent = nullptr); /** * Destroys the recursive item fetch job. */ ~RecursiveItemFetchJob() override; /** * Sets the item fetch scope. * * The ItemFetchScope controls how much of an item's data is fetched * from the server, e.g. whether to fetch the full item payload or * only meta data. * * @param fetchScope The new scope for item fetch operations. * * @see fetchScope() */ void setFetchScope(const Akonadi::ItemFetchScope &fetchScope); /** * Returns the item fetch scope. * * Since this returns a reference it can be used to conveniently modify the * current scope in-place, i.e. by calling a method on the returned reference * without storing it in a local variable. See the ItemFetchScope documentation * for an example. * * @return a reference to the current item fetch scope * * @see setFetchScope() for replacing the current item fetch scope */ Akonadi::ItemFetchScope &fetchScope(); /** * Returns the list of fetched items. */ Q_REQUIRED_RESULT Akonadi::Item::List items() const; /** * Starts the recursive item fetch job. */ void start() override; private: //@cond PRIVATE class Private; Private *const d; - - Q_PRIVATE_SLOT(d, void itemFetchResult(KJob *)) //@endcond }; } #endif diff --git a/src/core/jobs/relationfetchjob.cpp b/src/core/jobs/relationfetchjob.cpp index c6bacb634..9116a7ec5 100644 --- a/src/core/jobs/relationfetchjob.cpp +++ b/src/core/jobs/relationfetchjob.cpp @@ -1,135 +1,127 @@ /* Copyright (c) 2014 Christian Mollekopf This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "relationfetchjob.h" #include "job_p.h" #include "relation.h" #include "protocolhelper_p.h" #include "private/protocol_p.h" #include using namespace Akonadi; class Akonadi::RelationFetchJobPrivate : public JobPrivate { public: RelationFetchJobPrivate(RelationFetchJob *parent) : JobPrivate(parent) , mEmitTimer(nullptr) { - } - - void init() - { - Q_Q(RelationFetchJob); - mEmitTimer = new QTimer(q); - mEmitTimer->setSingleShot(true); - mEmitTimer->setInterval(100); - q->connect(mEmitTimer, SIGNAL(timeout()), q, SLOT(timeout())); + mEmitTimer.setSingleShot(true); + mEmitTimer.setInterval(std::chrono::milliseconds{100}); + QObject::connect(&mEmitTimer, &QTimer::timeout, q_ptr, [this]() { timeout(); }); } void aboutToFinish() override { timeout(); } void timeout() { Q_Q(RelationFetchJob); - mEmitTimer->stop(); // in case we are called by result() + mEmitTimer.stop(); // in case we are called by result() if (!mPendingRelations.isEmpty()) { if (!q->error()) { Q_EMIT q->relationsReceived(mPendingRelations); } mPendingRelations.clear(); } } Q_DECLARE_PUBLIC(RelationFetchJob) Relation::List mResultRelations; Relation::List mPendingRelations; // relation pending for emitting itemsReceived() - QTimer *mEmitTimer = nullptr; + QTimer mEmitTimer; QVector mTypes; QString mResource; Relation mRequestedRelation; }; RelationFetchJob::RelationFetchJob(const Relation &relation, QObject *parent) : Job(new RelationFetchJobPrivate(this), parent) { Q_D(RelationFetchJob); - d->init(); d->mRequestedRelation = relation; } RelationFetchJob::RelationFetchJob(const QVector &types, QObject *parent) : Job(new RelationFetchJobPrivate(this), parent) { Q_D(RelationFetchJob); - d->init(); d->mTypes = types; } void RelationFetchJob::doStart() { Q_D(RelationFetchJob); d->sendCommand(Protocol::FetchRelationsCommandPtr::create( d->mRequestedRelation.left().id(), d->mRequestedRelation.right().id(), (d->mTypes.isEmpty() && !d->mRequestedRelation.type().isEmpty()) ? QVector() << d->mRequestedRelation.type() : d->mTypes, d->mResource)); } bool RelationFetchJob::doHandleResponse(qint64 tag, const Protocol::CommandPtr &response) { Q_D(RelationFetchJob); if (!response->isResponse() || response->type() != Protocol::Command::FetchRelations) { return Job::doHandleResponse(tag, response); } const Relation rel = ProtocolHelper::parseRelationFetchResult( Protocol::cmdCast(response)); // Invalid response means there will be no more responses if (!rel.isValid()) { return true; } d->mResultRelations.append(rel); d->mPendingRelations.append(rel); - if (!d->mEmitTimer->isActive()) { - d->mEmitTimer->start(); + if (!d->mEmitTimer.isActive()) { + d->mEmitTimer.start(); } return false; } Relation::List RelationFetchJob::relations() const { Q_D(const RelationFetchJob); return d->mResultRelations; } void RelationFetchJob::setResource(const QString &identifier) { Q_D(RelationFetchJob); d->mResource = identifier; } #include "moc_relationfetchjob.cpp" diff --git a/src/core/jobs/relationfetchjob.h b/src/core/jobs/relationfetchjob.h index 2ed4f72bd..3b9bcb618 100644 --- a/src/core/jobs/relationfetchjob.h +++ b/src/core/jobs/relationfetchjob.h @@ -1,80 +1,76 @@ /* Copyright (c) 2014 Christian Mollekopf This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef AKONADI_RELATIONFETCHJOB_H #define AKONADI_RELATIONFETCHJOB_H #include "job.h" #include "relation.h" namespace Akonadi { class Relation; class RelationFetchJobPrivate; /** * @short Job that to fetch relations from Akonadi storage. * @since 4.15 */ class AKONADICORE_EXPORT RelationFetchJob : public Job { Q_OBJECT public: /** * Creates a new relation fetch job. * * @param relation The relation to fetch. * @param parent The parent object. */ explicit RelationFetchJob(const Relation &relation, QObject *parent = nullptr); explicit RelationFetchJob(const QVector &types, QObject *parent = nullptr); void setResource(const QString &identifier); /** * Returns the relations. */ Q_REQUIRED_RESULT Relation::List relations() const; Q_SIGNALS: /** * This signal is emitted whenever new relations have been fetched completely. * * @param relations The fetched relations. */ void relationsReceived(const Akonadi::Relation::List &relations); protected: void doStart() override; bool doHandleResponse(qint64 tag, const Protocol::CommandPtr &response) override; private: Q_DECLARE_PRIVATE(RelationFetchJob) - - //@cond PRIVATE - Q_PRIVATE_SLOT(d_func(), void timeout()) - //@endcond }; } #endif diff --git a/src/core/jobs/resourcesynchronizationjob.cpp b/src/core/jobs/resourcesynchronizationjob.cpp index e08b1a7f9..bc289bba1 100644 --- a/src/core/jobs/resourcesynchronizationjob.cpp +++ b/src/core/jobs/resourcesynchronizationjob.cpp @@ -1,174 +1,178 @@ /* * Copyright (c) 2009 Volker Krause * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library. If not, see . */ #include "resourcesynchronizationjob.h" #include #include "kjobprivatebase_p.h" #include "servermanager.h" #include "agentinstance.h" #include "agentmanager.h" #include "akonadicore_debug.h" +#include "resourceinterface.h" #include #include #include namespace Akonadi { class ResourceSynchronizationJobPrivate : public KJobPrivateBase { + Q_OBJECT + public: ResourceSynchronizationJobPrivate(ResourceSynchronizationJob *parent) : q(parent) { + connect(&safetyTimer, &QTimer::timeout, this, &ResourceSynchronizationJobPrivate::slotTimeout); + safetyTimer.setInterval(std::chrono::seconds{30}); + safetyTimer.setSingleShot(true); } void doStart() override; ResourceSynchronizationJob *q; AgentInstance instance; - QDBusInterface *interface = nullptr; - QTimer *safetyTimer = nullptr; + std::unique_ptr interface; + QTimer safetyTimer; int timeoutCount = 60; bool collectionTreeOnly = false; int timeoutCountLimit = 0; void slotSynchronized(); void slotTimeout(); }; ResourceSynchronizationJob::ResourceSynchronizationJob(const AgentInstance &instance, QObject *parent) : KJob(parent) , d(new ResourceSynchronizationJobPrivate(this)) { d->instance = instance; - d->safetyTimer = new QTimer(this); - connect(d->safetyTimer, &QTimer::timeout, this, [this]() { d->slotTimeout(); }); - d->safetyTimer->setInterval(30 * 1000); - d->safetyTimer->setSingleShot(false); } ResourceSynchronizationJob::~ResourceSynchronizationJob() { delete d; } void ResourceSynchronizationJob::start() { d->start(); } void ResourceSynchronizationJob::setTimeoutCountLimit(int count) { d->timeoutCountLimit = count; } int ResourceSynchronizationJob::timeoutCountLimit() const { return d->timeoutCountLimit; } bool ResourceSynchronizationJob::collectionTreeOnly() const { return d->collectionTreeOnly; } void ResourceSynchronizationJob::setCollectionTreeOnly(bool b) { d->collectionTreeOnly = b; } void ResourceSynchronizationJobPrivate::doStart() { if (!instance.isValid()) { q->setError(KJob::UserDefinedError); q->setErrorText(i18n("Invalid resource instance.")); q->emitResult(); return; } - interface = new QDBusInterface(ServerManager::agentServiceName(ServerManager::Resource, instance.identifier()), - QStringLiteral("/"), - QStringLiteral("org.freedesktop.Akonadi.Resource"), - QDBusConnection::sessionBus(), this); + using ResourceIface = org::freedesktop::Akonadi::Resource; + interface = std::make_unique( + ServerManager::agentServiceName(ServerManager::Resource, instance.identifier()), + QStringLiteral("/"), + QDBusConnection::sessionBus()); if (collectionTreeOnly) { - connect(interface, SIGNAL(collectionTreeSynchronized()), q, SLOT(slotSynchronized())); + connect(interface.get(), &ResourceIface::collectionTreeSynchronized, this, &ResourceSynchronizationJobPrivate::slotSynchronized); } else { - connect(interface, SIGNAL(synchronized()), q, SLOT(slotSynchronized())); + connect(interface.get(), &ResourceIface::synchronized, this, &ResourceSynchronizationJobPrivate::slotSynchronized); } if (interface->isValid()) { if (collectionTreeOnly) { instance.synchronizeCollectionTree(); } else { instance.synchronize(); } - safetyTimer->start(); + safetyTimer.start(); } else { q->setError(KJob::UserDefinedError); q->setErrorText(i18n("Unable to obtain D-Bus interface for resource '%1'", instance.identifier())); q->emitResult(); return; } } void ResourceSynchronizationJobPrivate::slotSynchronized() { + using ResourceIface = org::freedesktop::Akonadi::Resource; if (collectionTreeOnly) { - q->disconnect(interface, SIGNAL(collectionTreeSynchronized()), q, SLOT(slotSynchronized())); + disconnect(interface.get(), &ResourceIface::collectionTreeSynchronized, this, &ResourceSynchronizationJobPrivate::slotSynchronized); } else { - q->disconnect(interface, SIGNAL(synchronized()), q, SLOT(slotSynchronized())); + disconnect(interface.get(), &ResourceIface::synchronized, this, &ResourceSynchronizationJobPrivate::slotSynchronized); } - safetyTimer->stop(); + safetyTimer.stop(); q->emitResult(); } void ResourceSynchronizationJobPrivate::slotTimeout() { instance = AgentManager::self()->instance(instance.identifier()); timeoutCount++; if (timeoutCount > timeoutCountLimit) { - safetyTimer->stop(); + safetyTimer.stop(); q->setError(KJob::UserDefinedError); q->setErrorText(i18n("Resource synchronization timed out.")); q->emitResult(); return; } if (instance.status() == AgentInstance::Idle) { // try again, we might have lost the synchronized()/synchronizedCollectionTree() signal qCDebug(AKONADICORE_LOG) << "trying again to sync resource" << instance.identifier(); if (collectionTreeOnly) { instance.synchronizeCollectionTree(); } else { instance.synchronize(); } } } AgentInstance ResourceSynchronizationJob::resource() const { return d->instance; } } -#include "moc_resourcesynchronizationjob.cpp" +#include "resourcesynchronizationjob.moc" diff --git a/src/core/jobs/resourcesynchronizationjob.h b/src/core/jobs/resourcesynchronizationjob.h index 4f19accc0..6e107c685 100644 --- a/src/core/jobs/resourcesynchronizationjob.h +++ b/src/core/jobs/resourcesynchronizationjob.h @@ -1,119 +1,116 @@ /* * Copyright (c) 2009 Volker Krause * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library. If not, see . */ #ifndef AKONADI_RESOURCESYNCHRONIZATIONJOB_H #define AKONADI_RESOURCESYNCHRONIZATIONJOB_H #include "akonadicore_export.h" #include namespace Akonadi { class AgentInstance; class ResourceSynchronizationJobPrivate; /** * @short Job that synchronizes a resource. * * This job will trigger a resource to synchronize the backend it is * responsible for (e.g. a local file or a groupware server) with the * Akonadi storage. * * If you only want to trigger the synchronization without being * interested in the result, using Akonadi::AgentInstance::synchronize() is enough. * If you want to wait until it's finished, use this class. * * Example: * * @code * using namespace Akonadi; * * const AgentInstance resource = AgentManager::self()->instance( "myresourceidentifier" ); * * ResourceSynchronizationJob *job = new ResourceSynchronizationJob( resource ); * connect( job, SIGNAL(result(KJob*)), SLOT(synchronizationFinished(KJob*)) ); * job->start(); * * @endcode * * @note This is a KJob, not an Akonadi::Job, so it won't auto-start! * * @author Volker Krause * @since 4.4 */ class AKONADICORE_EXPORT ResourceSynchronizationJob : public KJob { Q_OBJECT public: /** * Creates a new synchronization job for the given resource. * * @param instance The resource instance to synchronize. */ explicit ResourceSynchronizationJob(const AgentInstance &instance, QObject *parent = nullptr); /** * Destroys the synchronization job. */ ~ResourceSynchronizationJob() override; /** * Returns whether a full synchronization will be done, or just the collection tree (without items). * The default is @c false, i.e. a full sync will be requested. * * @since 4.8 */ Q_REQUIRED_RESULT bool collectionTreeOnly() const; /** * Sets the collectionTreeOnly property. * * @param collectionTreeOnly If set, only the collection tree will be synchronized. * @since 4.8 */ void setCollectionTreeOnly(bool collectionTreeOnly); /** * Returns the resource that has been synchronized. */ Q_REQUIRED_RESULT AgentInstance resource() const; /* reimpl */ void start() override; /* * @since 5.1 */ void setTimeoutCountLimit(int count); Q_REQUIRED_RESULT int timeoutCountLimit() const; private: //@cond PRIVATE ResourceSynchronizationJobPrivate *const d; friend class ResourceSynchronizationJobPrivate; - - Q_PRIVATE_SLOT(d, void slotSynchronized()) - Q_PRIVATE_SLOT(d, void slotTimeout()) //@endcond }; } #endif diff --git a/src/core/jobs/specialcollectionsrequestjob.cpp b/src/core/jobs/specialcollectionsrequestjob.cpp index 6683429ef..435077274 100644 --- a/src/core/jobs/specialcollectionsrequestjob.cpp +++ b/src/core/jobs/specialcollectionsrequestjob.cpp @@ -1,366 +1,358 @@ /* Copyright (c) 2009 Constantin Berzan 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 "specialcollectionsrequestjob.h" #include "specialcollectionattribute.h" #include "specialcollections.h" #include "specialcollections_p.h" #include "specialcollectionshelperjobs_p.h" #include "agentmanager.h" #include "collectioncreatejob.h" #include "entitydisplayattribute.h" #include "akonadicore_debug.h" #include using namespace Akonadi; /** @internal */ class Akonadi::SpecialCollectionsRequestJobPrivate { public: SpecialCollectionsRequestJobPrivate(SpecialCollections *collections, SpecialCollectionsRequestJob *qq); bool isEverythingReady(); void lockResult(KJob *job); // slot void releaseLock(); // slot void nextResource(); void resourceScanResult(KJob *job); // slot void createRequestedFolders(ResourceScanJob *job, QHash &requestedFolders); void collectionCreateResult(KJob *job); // slot SpecialCollectionsRequestJob *q; SpecialCollections *mSpecialCollections = nullptr; int mPendingCreateJobs; QByteArray mRequestedType; AgentInstance mRequestedResource; // Input: QHash mDefaultFolders; bool mRequestingDefaultFolders; QHash< QString, QHash > mFoldersForResource; QString mDefaultResourceType; QVariantMap mDefaultResourceOptions; QList mKnownTypes; QMap mNameForTypeMap; QMap mIconForTypeMap; // Output: QStringList mToForget; QVector< QPair > mToRegister; }; SpecialCollectionsRequestJobPrivate::SpecialCollectionsRequestJobPrivate(SpecialCollections *collections, SpecialCollectionsRequestJob *qq) : q(qq) , mSpecialCollections(collections) , mPendingCreateJobs(0) , mRequestingDefaultFolders(false) { } bool SpecialCollectionsRequestJobPrivate::isEverythingReady() { // check if all requested folders are known already if (mRequestingDefaultFolders) { - QHash::const_iterator it = mDefaultFolders.constBegin(); - const QHash::const_iterator end = mDefaultFolders.constEnd(); - while (it != end) { + for (auto it = mDefaultFolders.cbegin(), end = mDefaultFolders.cend(); it != end; ++it) { if (it.value() && !mSpecialCollections->hasDefaultCollection(it.key())) { return false; } - ++it; } } - QHashIterator > resourceIt(mFoldersForResource); - while (resourceIt.hasNext()) { - resourceIt.next(); - + for (auto resourceIt = mFoldersForResource.cbegin(), end = mFoldersForResource.cend(); resourceIt != end; ++resourceIt) { const QHash &requested = resourceIt.value(); - QHash::const_iterator it = requested.cbegin(); - const QHash::const_iterator itEnd = requested.cend(); - for (;it != itEnd; ++it) { + for (auto it = requested.cbegin(), end = requested.cend(); it != end; ++it) { if (it.value() && !mSpecialCollections->hasCollection(it.key(), AgentManager::self()->instance(resourceIt.key()))) { return false; } } } return true; } void SpecialCollectionsRequestJobPrivate::lockResult(KJob *job) { if (job->error()) { qCWarning(AKONADICORE_LOG) << "Failed to get lock:" << job->errorString(); q->setError(job->error()); q->setErrorText(job->errorString()); q->emitResult(); return; } if (mRequestingDefaultFolders) { // If default folders are requested, deal with that first. DefaultResourceJob *resjob = new DefaultResourceJob(mSpecialCollections->d->mSettings, q); resjob->setDefaultResourceType(mDefaultResourceType); resjob->setDefaultResourceOptions(mDefaultResourceOptions); resjob->setTypes(mKnownTypes); resjob->setNameForTypeMap(mNameForTypeMap); resjob->setIconForTypeMap(mIconForTypeMap); - QObject::connect(resjob, SIGNAL(result(KJob*)), q, SLOT(resourceScanResult(KJob*))); + QObject::connect(resjob, &KJob::result, q, [this](KJob *job) { resourceScanResult(job); }); } else { // If no default folders are requested, go straight to the next step. nextResource(); } } void SpecialCollectionsRequestJobPrivate::releaseLock() { const bool ok = Akonadi::releaseLock(); if (!ok) { qCWarning(AKONADICORE_LOG) << "WTF, can't release lock."; } } void SpecialCollectionsRequestJobPrivate::nextResource() { if (mFoldersForResource.isEmpty()) { qCDebug(AKONADICORE_LOG) << "All done! Committing."; mSpecialCollections->d->beginBatchRegister(); // Forget everything we knew before about these resources. for (const QString &resourceId : qAsConst(mToForget)) { mSpecialCollections->d->forgetFoldersForResource(resourceId); } // Register all the collections that we fetched / created. typedef QPair RegisterPair; for (const RegisterPair &pair : qAsConst(mToRegister)) { const bool ok = mSpecialCollections->registerCollection(pair.second, pair.first); Q_ASSERT(ok); Q_UNUSED(ok); } mSpecialCollections->d->endBatchRegister(); // Release the lock once the transaction has been committed. - QObject::connect(q, SIGNAL(result(KJob*)), q, SLOT(releaseLock())); + QObject::connect(q, &KJob::result, q, [this]() { releaseLock(); }); // We are done! q->commit(); } else { const QString resourceId = mFoldersForResource.cbegin().key(); qCDebug(AKONADICORE_LOG) << "A resource is done," << mFoldersForResource.count() << "more to do. Now doing resource" << resourceId; ResourceScanJob *resjob = new ResourceScanJob(resourceId, mSpecialCollections->d->mSettings, q); - QObject::connect(resjob, SIGNAL(result(KJob*)), q, SLOT(resourceScanResult(KJob*))); + QObject::connect(resjob, &KJob::result, q, [this](KJob *job) { resourceScanResult(job); }); } } void SpecialCollectionsRequestJobPrivate::resourceScanResult(KJob *job) { ResourceScanJob *resjob = qobject_cast(job); Q_ASSERT(resjob); const QString resourceId = resjob->resourceId(); qCDebug(AKONADICORE_LOG) << "resourceId" << resourceId; if (job->error()) { qCWarning(AKONADICORE_LOG) << "Failed to request resource" << resourceId << ":" << job->errorString(); return; } if (qobject_cast(job)) { // This is the default resource. if (resourceId != mSpecialCollections->d->defaultResourceId()) { qCWarning(AKONADICORE_LOG) << "Resource id's don't match: " << resourceId << mSpecialCollections->d->defaultResourceId(); Q_ASSERT(false); } //mToForget.append( mSpecialCollections->defaultResourceId() ); createRequestedFolders(resjob, mDefaultFolders); } else { // This is not the default resource. QHash requestedFolders = mFoldersForResource[resourceId]; mFoldersForResource.remove(resourceId); createRequestedFolders(resjob, requestedFolders); } } void SpecialCollectionsRequestJobPrivate::createRequestedFolders(ResourceScanJob *scanJob, QHash &requestedFolders) { // Remove from the request list the folders which already exist. const Akonadi::Collection::List lstSpecialCols = scanJob->specialCollections(); for (const Collection &collection : lstSpecialCols) { Q_ASSERT(collection.hasAttribute()); const SpecialCollectionAttribute *attr = collection.attribute(); const QByteArray type = attr->collectionType(); if (!type.isEmpty()) { mToRegister.append(qMakePair(collection, type)); requestedFolders.insert(type, false); } } mToForget.append(scanJob->resourceId()); // Folders left in the request list must be created. Q_ASSERT(mPendingCreateJobs == 0); Q_ASSERT(scanJob->rootResourceCollection().isValid()); QHashIterator it(requestedFolders); while (it.hasNext()) { it.next(); if (it.value()) { Collection collection; collection.setParentCollection(scanJob->rootResourceCollection()); collection.setName(mNameForTypeMap.value(it.key())); setCollectionAttributes(collection, it.key(), mNameForTypeMap, mIconForTypeMap); CollectionCreateJob *createJob = new CollectionCreateJob(collection, q); createJob->setProperty("type", it.key()); - QObject::connect(createJob, SIGNAL(result(KJob*)), q, SLOT(collectionCreateResult(KJob*))); + QObject::connect(createJob, &KJob::result, q, [this](KJob *job) { collectionCreateResult(job); }); mPendingCreateJobs++; } } if (mPendingCreateJobs == 0) { nextResource(); } } void SpecialCollectionsRequestJobPrivate::collectionCreateResult(KJob *job) { if (job->error()) { qCWarning(AKONADICORE_LOG) << "Failed CollectionCreateJob." << job->errorString(); return; } CollectionCreateJob *createJob = qobject_cast(job); Q_ASSERT(createJob); const Collection collection = createJob->collection(); mToRegister.append(qMakePair(collection, createJob->property("type").toByteArray())); Q_ASSERT(mPendingCreateJobs > 0); mPendingCreateJobs--; qCDebug(AKONADICORE_LOG) << "mPendingCreateJobs now" << mPendingCreateJobs; if (mPendingCreateJobs == 0) { nextResource(); } } // TODO KDE5: do not inherit from TransactionSequence SpecialCollectionsRequestJob::SpecialCollectionsRequestJob(SpecialCollections *collections, QObject *parent) : TransactionSequence(parent) , d(new SpecialCollectionsRequestJobPrivate(collections, this)) { setProperty("transactionsDisabled", true); } SpecialCollectionsRequestJob::~SpecialCollectionsRequestJob() { delete d; } void SpecialCollectionsRequestJob::requestDefaultCollection(const QByteArray &type) { d->mDefaultFolders[type] = true; d->mRequestingDefaultFolders = true; d->mRequestedType = type; } void SpecialCollectionsRequestJob::requestCollection(const QByteArray &type, const AgentInstance &instance) { d->mFoldersForResource[instance.identifier()][type] = true; d->mRequestedType = type; d->mRequestedResource = instance; } Akonadi::Collection SpecialCollectionsRequestJob::collection() const { if (d->mRequestedResource.isValid()) { return d->mSpecialCollections->collection(d->mRequestedType, d->mRequestedResource); } else { return d->mSpecialCollections->defaultCollection(d->mRequestedType); } } void SpecialCollectionsRequestJob::setDefaultResourceType(const QString &type) { d->mDefaultResourceType = type; } void SpecialCollectionsRequestJob::setDefaultResourceOptions(const QVariantMap &options) { d->mDefaultResourceOptions = options; } void SpecialCollectionsRequestJob::setTypes(const QList &types) { d->mKnownTypes = types; } void SpecialCollectionsRequestJob::setNameForTypeMap(const QMap &map) { d->mNameForTypeMap = map; } void SpecialCollectionsRequestJob::setIconForTypeMap(const QMap &map) { d->mIconForTypeMap = map; } void SpecialCollectionsRequestJob::doStart() { if (d->isEverythingReady()) { emitResult(); } else { GetLockJob *lockJob = new GetLockJob(this); connect(lockJob, &GetLockJob::result, this, [this](KJob*job) { d->lockResult(job);}); lockJob->start(); } } void SpecialCollectionsRequestJob::slotResult(KJob *job) { if (job->error()) { // If we failed, let others try. qCWarning(AKONADICORE_LOG) << "Failed SpecialCollectionsRequestJob::slotResult" << job->errorString(); d->releaseLock(); } TransactionSequence::slotResult(job); } #include "moc_specialcollectionsrequestjob.cpp" diff --git a/src/core/jobs/trashjob.cpp b/src/core/jobs/trashjob.cpp index e36cd0473..9abb007c6 100644 --- a/src/core/jobs/trashjob.cpp +++ b/src/core/jobs/trashjob.cpp @@ -1,373 +1,372 @@ /* Copyright (c) 2011 Christian Mollekopf This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "trashjob.h" #include "collection.h" #include "entitydeletedattribute.h" #include "item.h" #include "job_p.h" #include "trashsettings.h" #include #include "itemdeletejob.h" #include "collectiondeletejob.h" #include "itemmovejob.h" #include "collectionmovejob.h" #include "itemmodifyjob.h" #include "collectionmodifyjob.h" #include "itemfetchscope.h" #include "collectionfetchscope.h" #include "itemfetchjob.h" #include "collectionfetchjob.h" #include "akonadicore_debug.h" #include using namespace Akonadi; class TrashJob::TrashJobPrivate : public JobPrivate { public: TrashJobPrivate(TrashJob *parent) : JobPrivate(parent) , mKeepTrashInCollection(false) , mSetRestoreCollection(false) , mDeleteIfInTrash(false) { } //4. void selectResult(KJob *job); //3. //Helper functions to recursively set the attribute on deleted collections void setAttribute(const Akonadi::Collection::List &); void setAttribute(const Akonadi::Item::List &); //Set attributes after ensuring that move job was successful void setAttribute(KJob *job); //2. //called after parent of the trashed item was fetched (needed to see in which resource the item is in) void parentCollectionReceived(const Akonadi::Collection::List &); //1. //called after initial fetch of trashed items void itemsReceived(const Akonadi::Item::List &); //called after initial fetch of trashed collection void collectionsReceived(const Akonadi::Collection::List &); Q_DECLARE_PUBLIC(TrashJob) Item::List mItems; Collection mCollection; Collection mRestoreCollection; Collection mTrashCollection; bool mKeepTrashInCollection; bool mSetRestoreCollection; //only set restore collection when moved to trash collection (not in place) bool mDeleteIfInTrash; QHash mCollectionItems; //list of trashed items sorted according to parent collection QHash mParentCollections; //fetched parent collection of items (containing the resource name) }; void TrashJob::TrashJobPrivate::selectResult(KJob *job) { Q_Q(TrashJob); if (job->error()) { qCWarning(AKONADICORE_LOG) << job->objectName(); qCWarning(AKONADICORE_LOG) << job->errorString(); return; // KCompositeJob takes care of errors } if (!q->hasSubjobs() || (q->subjobs().contains(static_cast(q->sender())) && q->subjobs().size() == 1)) { q->emitResult(); } } void TrashJob::TrashJobPrivate::setAttribute(const Akonadi::Collection::List &list) { Q_Q(TrashJob); QVectorIterator i(list); while (i.hasNext()) { const Collection &col = i.next(); EntityDeletedAttribute *eda = new EntityDeletedAttribute(); if (mSetRestoreCollection) { Q_ASSERT(mRestoreCollection.isValid()); eda->setRestoreCollection(mRestoreCollection); } Collection modCol(col.id()); //really only modify attribute (forget old remote ids, etc.), otherwise we have an error because of the move modCol.addAttribute(eda); CollectionModifyJob *job = new CollectionModifyJob(modCol, q); - q->connect(job, SIGNAL(result(KJob*)), SLOT(selectResult(KJob*))); + q->connect(job, &KJob::result, q, [this](KJob *job) { selectResult(job); }); ItemFetchJob *itemFetchJob = new ItemFetchJob(col, q); //TODO not sure if it is guaranteed that itemsReceived is always before result (otherwise the result is emitted before the attributes are set) - q->connect(itemFetchJob, SIGNAL(itemsReceived(Akonadi::Item::List)), SLOT(setAttribute(Akonadi::Item::List))); - q->connect(itemFetchJob, SIGNAL(result(KJob*)), SLOT(selectResult(KJob*))); + q->connect(itemFetchJob, &ItemFetchJob::itemsReceived, q, [this](const auto &items) { setAttribute(items); }); + q->connect(itemFetchJob, &KJob::result, q, [this](KJob *job) { selectResult(job); }); } } void TrashJob::TrashJobPrivate::setAttribute(const Akonadi::Item::List &list) { Q_Q(TrashJob); Item::List items = list; QMutableVectorIterator i(items); while (i.hasNext()) { const Item &item = i.next(); EntityDeletedAttribute *eda = new EntityDeletedAttribute(); if (mSetRestoreCollection) { //When deleting a collection, we want to restore the deleted collection's items restored to the deleted collection's parent, not the items parent if (mRestoreCollection.isValid()) { eda->setRestoreCollection(mRestoreCollection); } else { Q_ASSERT(mParentCollections.contains(item.parentCollection().id())); eda->setRestoreCollection(mParentCollections.value(item.parentCollection().id())); } } Item modItem(item.id()); //really only modify attribute (forget old remote ids, etc.) modItem.addAttribute(eda); ItemModifyJob *job = new ItemModifyJob(modItem, q); job->setIgnorePayload(true); - q->connect(job, SIGNAL(result(KJob*)), SLOT(selectResult(KJob*))); + q->connect(job, &KJob::result, q, [this](KJob *job) { selectResult(job); }); } //For some reason it is not possible to apply this change to multiple items at once /*ItemModifyJob *job = new ItemModifyJob(items, q); q->connect( job, SIGNAL(result(KJob*)), SLOT(selectResult(KJob*)) );*/ } void TrashJob::TrashJobPrivate::setAttribute(KJob *job) { Q_Q(TrashJob); if (job->error()) { qCWarning(AKONADICORE_LOG) << job->objectName(); qCWarning(AKONADICORE_LOG) << job->errorString(); q->setError(Job::Unknown); q->setErrorText(i18n("Move to trash collection failed, aborting trash operation")); return; } //For Items const QVariant var = job->property("MovedItems"); if (var.isValid()) { int id = var.toInt(); Q_ASSERT(id >= 0); setAttribute(mCollectionItems.value(Collection(id))); return; } //For a collection Q_ASSERT(mCollection.isValid()); setAttribute(Collection::List() << mCollection); //Set the attribute on all subcollections and items CollectionFetchJob *colFetchJob = new CollectionFetchJob(mCollection, CollectionFetchJob::Recursive, q); - q->connect(colFetchJob, SIGNAL(collectionsReceived(Akonadi::Collection::List)), SLOT(setAttribute(Akonadi::Collection::List))); - q->connect(colFetchJob, SIGNAL(result(KJob*)), SLOT(selectResult(KJob*))); + q->connect(colFetchJob, &CollectionFetchJob::collectionsReceived, q, [this](const auto &cols) { setAttribute(cols); }); + q->connect(colFetchJob, &KJob::result, q, [this](KJob *job) { selectResult(job); }); } void TrashJob::TrashJobPrivate::parentCollectionReceived(const Akonadi::Collection::List &collections) { Q_Q(TrashJob); Q_ASSERT(collections.size() == 1); const Collection &parentCollection = collections.first(); //store attribute Q_ASSERT(!parentCollection.resource().isEmpty()); Collection trashCollection = mTrashCollection; if (!mTrashCollection.isValid()) { trashCollection = TrashSettings::getTrashCollection(parentCollection.resource()); } if (!mKeepTrashInCollection && trashCollection.isValid()) { //Only set the restore collection if the item is moved to trash mSetRestoreCollection = true; } mParentCollections.insert(parentCollection.id(), parentCollection); if (trashCollection.isValid()) { //Move the items to the correct collection if available ItemMoveJob *job = new ItemMoveJob(mCollectionItems.value(parentCollection), trashCollection, q); job->setProperty("MovedItems", parentCollection.id()); - q->connect(job, SIGNAL(result(KJob*)), SLOT(setAttribute(KJob*))); //Wait until the move finished to set the attribute - q->connect(job, SIGNAL(result(KJob*)), SLOT(selectResult(KJob*))); + q->connect(job, &KJob::result, q, [this](KJob *job) { setAttribute(job); }); //Wait until the move finished to set the attribute + q->connect(job, &KJob::result, q, [this](KJob *job) { selectResult(job); }); } else { setAttribute(mCollectionItems.value(parentCollection)); } } void TrashJob::TrashJobPrivate::itemsReceived(const Akonadi::Item::List &items) { Q_Q(TrashJob); if (items.isEmpty()) { q->setError(Job::Unknown); q->setErrorText(i18n("Invalid items passed")); q->emitResult(); return; } Item::List toDelete; QVectorIterator i(items); while (i.hasNext()) { const Item &item = i.next(); if (item.hasAttribute()) { toDelete.append(item); continue; } Q_ASSERT(item.parentCollection().isValid()); mCollectionItems[item.parentCollection()].append(item); //Sort by parent col ( = restore collection) } for (auto it = mCollectionItems.cbegin(), e = mCollectionItems.cend(); it != e; ++it) { CollectionFetchJob *job = new CollectionFetchJob(it.key(), Akonadi::CollectionFetchJob::Base, q); - q->connect(job, SIGNAL(collectionsReceived(Akonadi::Collection::List)), - SLOT(parentCollectionReceived(Akonadi::Collection::List))); + q->connect(job, &CollectionFetchJob::collectionsReceived, q, [this](const auto &cols) { parentCollectionReceived(cols); }); } if (mDeleteIfInTrash && !toDelete.isEmpty()) { ItemDeleteJob *job = new ItemDeleteJob(toDelete, q); - q->connect(job, SIGNAL(result(KJob*)), SLOT(selectResult(KJob*))); + q->connect(job, &KJob::result, q, [this](KJob *job) { selectResult(job); }); } else if (mCollectionItems.isEmpty()) { //No job started, so we abort the job qCWarning(AKONADICORE_LOG) << "Nothing to do"; q->emitResult(); } } void TrashJob::TrashJobPrivate::collectionsReceived(const Akonadi::Collection::List &collections) { Q_Q(TrashJob); if (collections.isEmpty()) { q->setError(Job::Unknown); q->setErrorText(i18n("Invalid collection passed")); q->emitResult(); return; } Q_ASSERT(collections.size() == 1); mCollection = collections.first(); if (mCollection.hasAttribute()) { //marked as deleted if (mDeleteIfInTrash) { CollectionDeleteJob *job = new CollectionDeleteJob(mCollection, q); - q->connect(job, SIGNAL(result(KJob*)), SLOT(selectResult(KJob*))); + q->connect(job, &KJob::result, q, [this](KJob *job) { selectResult(job); }); } else { qCWarning(AKONADICORE_LOG) << "Nothing to do"; q->emitResult(); } return; } Collection trashCollection = mTrashCollection; if (!mTrashCollection.isValid()) { trashCollection = TrashSettings::getTrashCollection(mCollection.resource()); } if (!mKeepTrashInCollection && trashCollection.isValid()) { //only set the restore collection if the item is moved to trash mSetRestoreCollection = true; Q_ASSERT(mCollection.parentCollection().isValid()); mRestoreCollection = mCollection.parentCollection(); mRestoreCollection.setResource(mCollection.resource()); //The parent collection doesn't contain the resource, so we have to set it manually } if (trashCollection.isValid()) { CollectionMoveJob *job = new CollectionMoveJob(mCollection, trashCollection, q); - q->connect(job, SIGNAL(result(KJob*)), SLOT(setAttribute(KJob*))); - q->connect(job, SIGNAL(result(KJob*)), SLOT(selectResult(KJob*))); + q->connect(job, &KJob::result, q, [this](KJob *job) { setAttribute(job); }); + q->connect(job, &KJob::result, q, [this](KJob *job) { selectResult(job); }); } else { setAttribute(Collection::List() << mCollection); } } TrashJob::TrashJob(const Item &item, QObject *parent) : Job(new TrashJobPrivate(this), parent) { Q_D(TrashJob); d->mItems << item; } TrashJob::TrashJob(const Item::List &items, QObject *parent) : Job(new TrashJobPrivate(this), parent) { Q_D(TrashJob); d->mItems = items; } TrashJob::TrashJob(const Collection &collection, QObject *parent) : Job(new TrashJobPrivate(this), parent) { Q_D(TrashJob); d->mCollection = collection; } TrashJob::~TrashJob() { } Item::List TrashJob::items() const { Q_D(const TrashJob); return d->mItems; } void TrashJob::setTrashCollection(const Akonadi::Collection &collection) { Q_D(TrashJob); d->mTrashCollection = collection; } void TrashJob::keepTrashInCollection(bool enable) { Q_D(TrashJob); d->mKeepTrashInCollection = enable; } void TrashJob::deleteIfInTrash(bool enable) { Q_D(TrashJob); d->mDeleteIfInTrash = enable; } void TrashJob::doStart() { Q_D(TrashJob); //Fetch items first to ensure that the EntityDeletedAttribute is available if (!d->mItems.isEmpty()) { ItemFetchJob *job = new ItemFetchJob(d->mItems, this); job->fetchScope().setAncestorRetrieval(Akonadi::ItemFetchScope::Parent); //so we have access to the resource //job->fetchScope().setCacheOnly(true); job->fetchScope().fetchAttribute(true); - connect(job, SIGNAL(itemsReceived(Akonadi::Item::List)), this, SLOT(itemsReceived(Akonadi::Item::List))); + connect(job, &ItemFetchJob::itemsReceived, this, [d](const auto &items) { d->itemsReceived(items); }); } else if (d->mCollection.isValid()) { CollectionFetchJob *job = new CollectionFetchJob(d->mCollection, CollectionFetchJob::Base, this); job->fetchScope().setAncestorRetrieval(Akonadi::CollectionFetchScope::Parent); - connect(job, SIGNAL(collectionsReceived(Akonadi::Collection::List)), this, SLOT(collectionsReceived(Akonadi::Collection::List))); + connect(job, &CollectionFetchJob::collectionsReceived, this, [d](const auto &cols) { d->collectionsReceived(cols); }); } else { qCWarning(AKONADICORE_LOG) << "No valid collection or empty itemlist"; setError(Job::Unknown); setErrorText(i18n("No valid collection or empty itemlist")); emitResult(); } } #include "moc_trashjob.cpp" diff --git a/src/core/jobs/trashjob.h b/src/core/jobs/trashjob.h index b010dd702..9b8d6f6c1 100644 --- a/src/core/jobs/trashjob.h +++ b/src/core/jobs/trashjob.h @@ -1,142 +1,135 @@ /* Copyright (c) 2011 Christian Mollekopf This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef AKONADI_TRASHJOB_H #define AKONADI_TRASHJOB_H #include "akonadicore_export.h" #include "item.h" #include "collection.h" #include "job.h" namespace Akonadi { /** * @short Job that moves items/collection to trash. * * This job marks the given entities as trash and moves them to a given trash collection, if available. * * Priorities of trash collections are the following: * 1. keepTrashInCollection() * 2. setTrashCollection() * 3. configured collection in TrashSettings * 4. keep in collection if nothing is configured * * If the item is already marked as trash, it will be deleted instead * only if deleteIfInTrash() is set. * The entity is marked as trash with the EntityDeletedAttribute. * * The restore collection in the EntityDeletedAttribute is set the following way: * -If entities are not moved to trash -> no restore collection * -If collection is deleted -> also subentities get collection.parentCollection as restore collection * -If multiple items are deleted -> all items get their parentCollection as restore collection * * Example: * * @code * * const Akonadi::Item::List items = ... * * TrashJob *job = new TrashJob( items ); * connect( job, SIGNAL(result(KJob*)), this, SLOT(deletionResult(KJob*)) ); * * @endcode * * @author Christian Mollekopf * @since 4.8 */ class AKONADICORE_EXPORT TrashJob : public Job { Q_OBJECT public: /** * Creates a new trash job that marks @p item as trash, and moves it to the configured trash collection. * * If @p keepTrashInCollection is set, the item will not be moved to the configured trash collection. * * @param item The item to mark as trash. * @param parent The parent object. */ explicit TrashJob(const Item &item, QObject *parent = nullptr); /** * Creates a new trash job that marks all items in the list * @p items as trash, and moves it to the configured trash collection. * The items can be in different collections/resources and will still be moved to the correct trash collection. * * If @p keepTrashInCollection is set, the item will not be moved to the configured trash collection. * * @param items The items to mark as trash. * @param parent The parent object. */ explicit TrashJob(const Item::List &items, QObject *parent = nullptr); /** * Creates a new trash job that marks @p collection as trash, and moves it to the configured trash collection. * The subentities of the collection are also marked as trash. * * If @p keepTrashInCollection is set, the item will not be moved to the configured trash collection. * * @param collection The collection to mark as trash. * @param parent The parent object. */ explicit TrashJob(const Collection &collection, QObject *parent = nullptr); ~TrashJob() override; /** * Ignore configured Trash collections and keep all items local */ void keepTrashInCollection(bool enable); /** * Moves all entities to the give collection */ void setTrashCollection(const Collection &trashcollection); /** * Delete Items which are already in trash, instead of ignoring them */ void deleteIfInTrash(bool enable); Q_REQUIRED_RESULT Item::List items() const; protected: void doStart() override; private: //@cond PRIVATE class TrashJobPrivate; Q_DECLARE_PRIVATE(TrashJob) - Q_PRIVATE_SLOT(d_func(), void selectResult(KJob *)) - Q_PRIVATE_SLOT(d_func(), void setAttribute(const Akonadi::Collection::List &)) - Q_PRIVATE_SLOT(d_func(), void setAttribute(const Akonadi::Item::List &)) - Q_PRIVATE_SLOT(d_func(), void setAttribute(KJob *)) - Q_PRIVATE_SLOT(d_func(), void collectionsReceived(const Akonadi::Collection::List &)) - Q_PRIVATE_SLOT(d_func(), void itemsReceived(const Akonadi::Item::List &)) - Q_PRIVATE_SLOT(d_func(), void parentCollectionReceived(const Akonadi::Collection::List &)) //@endcond }; } #endif diff --git a/src/core/jobs/trashrestorejob.cpp b/src/core/jobs/trashrestorejob.cpp index 726e60536..5f02a6880 100644 --- a/src/core/jobs/trashrestorejob.cpp +++ b/src/core/jobs/trashrestorejob.cpp @@ -1,336 +1,336 @@ /* * Copyright (c) 2011 Christian Mollekopf * * This library is free software; you can redistribute it and/or modify it * under the terms of the GNU Library General Public License as published by * the Free Software Foundation; either version 2 of the License, or (at your * option) any later version. * * This library is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public * License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to the * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * 02110-1301, USA. */ #include "trashrestorejob.h" #include "collection.h" #include "entitydeletedattribute.h" #include "item.h" #include "job_p.h" #include "trashsettings.h" #include #include "itemdeletejob.h" #include "collectiondeletejob.h" #include "itemmovejob.h" #include "collectionmovejob.h" #include "itemmodifyjob.h" #include "collectionmodifyjob.h" #include "collectionfetchjob.h" #include "itemfetchjob.h" #include "collectionfetchscope.h" #include "itemfetchscope.h" #include "akonadicore_debug.h" #include using namespace Akonadi; class TrashRestoreJob::TrashRestoreJobPrivate : public JobPrivate { public: TrashRestoreJobPrivate(TrashRestoreJob *parent) : JobPrivate(parent) { } void selectResult(KJob *job); //Called when the target collection was fetched, //will issue the move and the removal of the attributes if collection is valid void targetCollectionFetched(KJob *job); void removeAttribute(const Akonadi::Item::List &list); void removeAttribute(const Akonadi::Collection::List &list); //Called after initial fetch of items, issues fetch of target collection or removes attributes for in place restore void itemsReceived(const Akonadi::Item::List &items); void collectionsReceived(const Akonadi::Collection::List &collections); Q_DECLARE_PUBLIC(TrashRestoreJob) Item::List mItems; Collection mCollection; Collection mTargetCollection; QHash restoreCollections; //groups items to target restore collections }; void TrashRestoreJob::TrashRestoreJobPrivate::selectResult(KJob *job) { Q_Q(TrashRestoreJob); if (job->error()) { qCWarning(AKONADICORE_LOG) << job->errorString(); return; // KCompositeJob takes care of errors } if (!q->hasSubjobs() || (q->subjobs().contains(static_cast(q->sender())) && q->subjobs().size() == 1)) { //qCWarning(AKONADICORE_LOG) << "trash restore finished"; q->emitResult(); } } void TrashRestoreJob::TrashRestoreJobPrivate::targetCollectionFetched(KJob *job) { Q_Q(TrashRestoreJob); CollectionFetchJob *fetchJob = qobject_cast (job); Q_ASSERT(fetchJob); const Collection::List &list = fetchJob->collections(); if (list.isEmpty() || !list.first().isValid() || list.first().hasAttribute()) { //target collection is invalid/not existing const QString res = fetchJob->property("Resource").toString(); if (res.isEmpty()) { //There is no fallback q->setError(Job::Unknown); q->setErrorText(i18n("Could not find restore collection and restore resource is not available")); q->emitResult(); //FAIL qCWarning(AKONADICORE_LOG) << "restore collection not available"; return; } //Try again with the root collection of the resource as fallback CollectionFetchJob *resRootFetch = new CollectionFetchJob(Collection::root(), CollectionFetchJob::FirstLevel, q); resRootFetch->fetchScope().setResource(res); const QVariant &var = fetchJob->property("Items"); if (var.isValid()) { resRootFetch->setProperty("Items", var.toInt()); } - q->connect(resRootFetch, SIGNAL(result(KJob*)), SLOT(targetCollectionFetched(KJob*))); - q->connect(resRootFetch, SIGNAL(result(KJob*)), SLOT(selectResult(KJob*))); + q->connect(resRootFetch, &KJob::result, q, [this](KJob *job) { targetCollectionFetched(job); }); + q->connect(resRootFetch, &KJob::result, q, [this](KJob *job) { selectResult(job); }); return; } Q_ASSERT(list.size() == 1); //SUCCESS //We know where to move the entity, so remove the attributes and move them to the right location if (!mItems.isEmpty()) { const QVariant &var = fetchJob->property("Items"); Q_ASSERT(var.isValid()); const Item::List &items = restoreCollections[Collection(var.toInt())]; //store removed attribute if destination collection is valid or the item doesn't have a restore collection //TODO only remove the attribute if the move job was successful (although it is unlikely that it fails since we already fetched the collection) removeAttribute(items); if (items.first().parentCollection() != list.first()) { ItemMoveJob *job = new ItemMoveJob(items, list.first(), q); - q->connect(job, SIGNAL(result(KJob*)), SLOT(selectResult(KJob*))); + q->connect(job, &KJob::result, q, [this](KJob *job) { selectResult(job); }); } } else { Q_ASSERT(mCollection.isValid()); //TODO only remove the attribute if the move job was successful removeAttribute(Collection::List() << mCollection); CollectionFetchJob *collectionFetchJob = new CollectionFetchJob(mCollection, CollectionFetchJob::Recursive, q); - q->connect(collectionFetchJob, SIGNAL(result(KJob*)), SLOT(selectResult(KJob*))); - q->connect(collectionFetchJob, SIGNAL(collectionsReceived(Akonadi::Collection::List)), SLOT(removeAttribute(Akonadi::Collection::List))); + q->connect(collectionFetchJob, &KJob::result, q, [this](KJob *job) { selectResult(job); }); + q->connect(collectionFetchJob, &CollectionFetchJob::collectionsReceived, q, [this](const auto &cols) { removeAttribute(cols); }); if (mCollection.parentCollection() != list.first()) { CollectionMoveJob *job = new CollectionMoveJob(mCollection, list.first(), q); - q->connect(job, SIGNAL(result(KJob*)), SLOT(selectResult(KJob*))); + q->connect(job, &KJob::result, q, [this](KJob *job) { selectResult(job); }); } } } void TrashRestoreJob::TrashRestoreJobPrivate::itemsReceived(const Akonadi::Item::List &items) { Q_Q(TrashRestoreJob); if (items.isEmpty()) { q->setError(Job::Unknown); q->setErrorText(i18n("Invalid items passed")); q->emitResult(); return; } mItems = items; //Sort by restore collection for (const Item &item : qAsConst(mItems)) { if (!item.hasAttribute()) { continue; } //If the restore collection is invalid we restore the item in place, so we don't need to know its restore resource => we can put those cases in the same list restoreCollections[item.attribute()->restoreCollection()].append(item); } for (auto it = restoreCollections.cbegin(), e = restoreCollections.cend(); it != e; ++it) { const Item &first = it.value().first(); //Move the items to the correct collection if available Collection targetCollection = it.key(); const QString restoreResource = first.attribute()->restoreResource(); //Restore in place if no restore collection is set if (!targetCollection.isValid()) { removeAttribute(it.value()); return; } //Explicit target overrides the resource if (mTargetCollection.isValid()) { targetCollection = mTargetCollection; } //Try to fetch the target resource to see if it is available CollectionFetchJob *fetchJob = new CollectionFetchJob(targetCollection, Akonadi::CollectionFetchJob::Base, q); if (!mTargetCollection.isValid()) { //explicit targets don't have a fallback fetchJob->setProperty("Resource", restoreResource); } fetchJob->setProperty("Items", it.key().id()); //to find the items in restore collections again - q->connect(fetchJob, SIGNAL(result(KJob*)), SLOT(targetCollectionFetched(KJob*))); + q->connect(fetchJob, &KJob::result, q, [this](KJob *job) { targetCollectionFetched(job); }); } } void TrashRestoreJob::TrashRestoreJobPrivate::collectionsReceived(const Akonadi::Collection::List &collections) { Q_Q(TrashRestoreJob); if (collections.isEmpty()) { q->setError(Job::Unknown); q->setErrorText(i18n("Invalid collection passed")); q->emitResult(); return; } Q_ASSERT(collections.size() == 1); mCollection = collections.first(); if (!mCollection.hasAttribute()) { return; } const QString restoreResource = mCollection.attribute()->restoreResource(); Collection targetCollection = mCollection.attribute()->restoreCollection(); //Restore in place if no restore collection/resource is set if (!targetCollection.isValid()) { removeAttribute(Collection::List() << mCollection); CollectionFetchJob *collectionFetchJob = new CollectionFetchJob(mCollection, CollectionFetchJob::Recursive, q); - q->connect(collectionFetchJob, SIGNAL(result(KJob*)), SLOT(selectResult(KJob*))); - q->connect(collectionFetchJob, SIGNAL(collectionsReceived(Akonadi::Collection::List)), SLOT(removeAttribute(Akonadi::Collection::List))); + q->connect(collectionFetchJob, &KJob::result, q, [this](KJob *job) { selectResult(job); }); + q->connect(collectionFetchJob, &CollectionFetchJob::collectionsReceived, q, [this](const auto &cols) { removeAttribute(cols); }); return; } //Explicit target overrides the resource/configured restore collection if (mTargetCollection.isValid()) { targetCollection = mTargetCollection; } //Fetch the target collection to check if it's valid CollectionFetchJob *fetchJob = new CollectionFetchJob(targetCollection, CollectionFetchJob::Base, q); if (!mTargetCollection.isValid()) { //explicit targets don't have a fallback fetchJob->setProperty("Resource", restoreResource); } - q->connect(fetchJob, SIGNAL(result(KJob*)), SLOT(targetCollectionFetched(KJob*))); + q->connect(fetchJob, &KJob::result, q, [this](KJob *job) { targetCollectionFetched(job); }); } void TrashRestoreJob::TrashRestoreJobPrivate::removeAttribute(const Akonadi::Collection::List &list) { Q_Q(TrashRestoreJob); QVectorIterator i(list); while (i.hasNext()) { Collection col = i.next(); col.removeAttribute(); CollectionModifyJob *job = new CollectionModifyJob(col, q); - q->connect(job, SIGNAL(result(KJob*)), SLOT(selectResult(KJob*))); + q->connect(job, &KJob::result, q, [this](KJob *job) { selectResult(job); }); ItemFetchJob *itemFetchJob = new ItemFetchJob(col, q); itemFetchJob->fetchScope().fetchAttribute (true); - q->connect(itemFetchJob, SIGNAL(result(KJob*)), SLOT(selectResult(KJob*))); - q->connect(itemFetchJob, SIGNAL(itemsReceived(Akonadi::Item::List)), SLOT(removeAttribute(Akonadi::Item::List))); + q->connect(itemFetchJob, &KJob::result, q, [this](KJob *job) { selectResult(job); }); + q->connect(itemFetchJob, &ItemFetchJob::itemsReceived, q, [this](const auto &items) { removeAttribute(items); }); } } void TrashRestoreJob::TrashRestoreJobPrivate::removeAttribute(const Akonadi::Item::List &list) { Q_Q(TrashRestoreJob); Item::List items = list; QMutableVectorIterator i(items); while (i.hasNext()) { Item &item = i.next(); item.removeAttribute(); ItemModifyJob *job = new ItemModifyJob(item, q); job->setIgnorePayload(true); - q->connect(job, SIGNAL(result(KJob*)), SLOT(selectResult(KJob*))); + q->connect(job, &KJob::result, q, [this](KJob *job) { selectResult(job); }); } //For some reason it is not possible to apply this change to multiple items at once //ItemModifyJob *job = new ItemModifyJob(items, q); //q->connect( job, SIGNAL(result(KJob*)), SLOT(selectResult(KJob*)) ); } TrashRestoreJob::TrashRestoreJob(const Item &item, QObject *parent) : Job(new TrashRestoreJobPrivate(this), parent) { Q_D(TrashRestoreJob); d->mItems << item; } TrashRestoreJob::TrashRestoreJob(const Item::List &items, QObject *parent) : Job(new TrashRestoreJobPrivate(this), parent) { Q_D(TrashRestoreJob); d->mItems = items; } TrashRestoreJob::TrashRestoreJob(const Collection &collection, QObject *parent) : Job(new TrashRestoreJobPrivate(this), parent) { Q_D(TrashRestoreJob); d->mCollection = collection; } TrashRestoreJob::~TrashRestoreJob() { } void TrashRestoreJob::setTargetCollection(const Akonadi::Collection &collection) { Q_D(TrashRestoreJob); d->mTargetCollection = collection; } Item::List TrashRestoreJob::items() const { Q_D(const TrashRestoreJob); return d->mItems; } void TrashRestoreJob::doStart() { Q_D(TrashRestoreJob); //We always have to fetch the entities to ensure that the EntityDeletedAttribute is available if (!d->mItems.isEmpty()) { ItemFetchJob *job = new ItemFetchJob(d->mItems, this); job->fetchScope().setCacheOnly(true); job->fetchScope().fetchAttribute (true); - connect(job, SIGNAL(itemsReceived(Akonadi::Item::List)), this, SLOT(itemsReceived(Akonadi::Item::List))); + connect(job, &ItemFetchJob::itemsReceived, this, [d](const auto &items) { d->itemsReceived(items); }); } else if (d->mCollection.isValid()) { CollectionFetchJob *job = new CollectionFetchJob(d->mCollection, CollectionFetchJob::Base, this); - connect(job, SIGNAL(collectionsReceived(Akonadi::Collection::List)), this, SLOT(collectionsReceived(Akonadi::Collection::List))); + connect(job, &CollectionFetchJob::collectionsReceived, this, [d](const auto &cols) { d->collectionsReceived(cols); }); } else { qCWarning(AKONADICORE_LOG) << "No valid collection or empty itemlist"; setError(Job::Unknown); setErrorText(i18n("No valid collection or empty itemlist")); emitResult(); } } #include "moc_trashrestorejob.cpp" diff --git a/src/core/jobs/trashrestorejob.h b/src/core/jobs/trashrestorejob.h index 0f56f18e5..1fe8c6aeb 100644 --- a/src/core/jobs/trashrestorejob.h +++ b/src/core/jobs/trashrestorejob.h @@ -1,96 +1,90 @@ /* * Copyright (c) 2011 Christian Mollekopf * * This library is free software; you can redistribute it and/or modify it * under the terms of the GNU Library General Public License as published by * the Free Software Foundation; either version 2 of the License, or (at your * option) any later version. * * This library is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public * License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to the * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * 02110-1301, USA. */ #ifndef AKONADI_TRASHRESTOREJOB_H #define AKONADI_TRASHRESTOREJOB_H #include "akonadicore_export.h" #include "collection.h" #include "job.h" #include "item.h" namespace Akonadi { /** * @short Job that restores entities from trash * * This job restores the given entities from trash. * The EntityDeletedAttribute is removed and the item is restored to the stored restore collection. * * If the stored restore collection is not available, the root collection of the original resource is used. * If also this is not available, setTargetCollection has to be used to restore the item to a specific collection. * * Example: * * @code * * const Akonadi::Item::List items = ... * * TrashRestoreJob *job = new TrashRestoreJob( items ); * connect( job, SIGNAL(result(KJob*)), this, SLOT(restoreResult(KJob*)) ); * * @endcode * * @author Christian Mollekopf * @since 4.8 */ class AKONADICORE_EXPORT TrashRestoreJob : public Job { Q_OBJECT public: /** * All items need to be from the same resource */ explicit TrashRestoreJob(const Item &item, QObject *parent = nullptr); explicit TrashRestoreJob(const Item::List &items, QObject *parent = nullptr); explicit TrashRestoreJob(const Collection &collection, QObject *parent = nullptr); ~TrashRestoreJob() override; /** * Sets the target collection, where the item is moved to. * If not set the item will be restored in the collection saved in the EntityDeletedAttribute. * @param collection the collection to set as target */ void setTargetCollection(const Collection &collection); Q_REQUIRED_RESULT Item::List items() const; protected: void doStart() override; private: //@cond PRIVATE class TrashRestoreJobPrivate; Q_DECLARE_PRIVATE(TrashRestoreJob) - Q_PRIVATE_SLOT(d_func(), void selectResult(KJob *)) - Q_PRIVATE_SLOT(d_func(), void targetCollectionFetched(KJob *)) - Q_PRIVATE_SLOT(d_func(), void removeAttribute(const Akonadi::Collection::List &)) - Q_PRIVATE_SLOT(d_func(), void removeAttribute(const Akonadi::Item::List &)) - Q_PRIVATE_SLOT(d_func(), void collectionsReceived(const Akonadi::Collection::List &)) - Q_PRIVATE_SLOT(d_func(), void itemsReceived(const Akonadi::Item::List &)) //@endcond }; } #endif diff --git a/src/core/models/agentinstancemodel.cpp b/src/core/models/agentinstancemodel.cpp index 96938be1e..efe690246 100644 --- a/src/core/models/agentinstancemodel.cpp +++ b/src/core/models/agentinstancemodel.cpp @@ -1,250 +1,250 @@ /* Copyright (c) 2006 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. */ #include "agentinstancemodel.h" #include "agentinstance.h" #include "agentmanager.h" #include #include using namespace Akonadi; /** * @internal */ class Q_DECL_HIDDEN AgentInstanceModel::Private { public: Private(AgentInstanceModel *parent) : mParent(parent) { } AgentInstanceModel *mParent = nullptr; AgentInstance::List mInstances; void instanceAdded(const AgentInstance &); void instanceRemoved(const AgentInstance &); void instanceChanged(const AgentInstance &); }; void AgentInstanceModel::Private::instanceAdded(const AgentInstance &instance) { mParent->beginInsertRows(QModelIndex(), mInstances.count(), mInstances.count()); mInstances.append(instance); mParent->endInsertRows(); } void AgentInstanceModel::Private::instanceRemoved(const AgentInstance &instance) { const int index = mInstances.indexOf(instance); if (index == -1) { return; } mParent->beginRemoveRows(QModelIndex(), index, index); mInstances.removeAll(instance); mParent->endRemoveRows(); } void AgentInstanceModel::Private::instanceChanged(const AgentInstance &instance) { const int numberOfInstance(mInstances.count()); for (int i = 0; i < numberOfInstance; ++i) { if (mInstances[i] == instance) { //TODO why reassign it if equals ? mInstances[i] = instance; const QModelIndex idx = mParent->index(i, 0); Q_EMIT mParent->dataChanged(idx, idx); return; } } } AgentInstanceModel::AgentInstanceModel(QObject *parent) : QAbstractItemModel(parent) , d(new Private(this)) { d->mInstances = AgentManager::self()->instances(); connect(AgentManager::self(), &AgentManager::instanceAdded, this, [this](const Akonadi::AgentInstance &inst) { d->instanceAdded(inst);}); connect(AgentManager::self(), &AgentManager::instanceRemoved, this, [this](const Akonadi::AgentInstance &inst) { d->instanceRemoved(inst);}); connect(AgentManager::self(), &AgentManager::instanceStatusChanged, this, [this](const Akonadi::AgentInstance &inst) { d->instanceChanged(inst);}); connect(AgentManager::self(), &AgentManager::instanceProgressChanged, this, [this](const Akonadi::AgentInstance &inst) { d->instanceChanged(inst);}); connect(AgentManager::self(), &AgentManager::instanceNameChanged, this, [this](const Akonadi::AgentInstance &inst) { d->instanceChanged(inst);}); - connect(AgentManager::self(), SIGNAL(instanceOnline(Akonadi::AgentInstance,bool)), - this, SLOT(instanceChanged(Akonadi::AgentInstance))); + connect(AgentManager::self(), &AgentManager::instanceOnline, + this, [this](const Akonadi::AgentInstance &inst) { d->instanceChanged(inst);}); } AgentInstanceModel::~AgentInstanceModel() { delete d; } QHash AgentInstanceModel::roleNames() const { QHash roles = QAbstractItemModel::roleNames(); roles.insert(StatusRole, "status"); roles.insert(StatusMessageRole, "statusMessage"); roles.insert(ProgressRole, "progress"); roles.insert(OnlineRole, "online"); return roles; } int AgentInstanceModel::columnCount(const QModelIndex &index) const { return index.isValid() ? 0 : 1; } int AgentInstanceModel::rowCount(const QModelIndex &index) const { return index.isValid() ? 0 : d->mInstances.count(); } QVariant AgentInstanceModel::data(const QModelIndex &index, int role) const { if (!index.isValid()) { return QVariant(); } if (index.row() < 0 || index.row() >= d->mInstances.count()) { return QVariant(); } const AgentInstance &instance = d->mInstances[index.row()]; switch (role) { case Qt::DisplayRole: return instance.name(); case Qt::DecorationRole: return instance.type().icon(); case InstanceRole: { QVariant var; var.setValue(instance); return var; } case InstanceIdentifierRole: return instance.identifier(); case Qt::ToolTipRole: return QStringLiteral("

%1

%2
").arg(instance.name(), instance.type().description()); case StatusRole: return instance.status(); case StatusMessageRole: return instance.statusMessage(); case ProgressRole: return instance.progress(); case OnlineRole: return instance.isOnline(); case TypeRole: { QVariant var; var.setValue(instance.type()); return var; } case TypeIdentifierRole: return instance.type().identifier(); case DescriptionRole: return instance.type().description(); case CapabilitiesRole: return instance.type().capabilities(); case MimeTypesRole: return instance.type().mimeTypes(); } return QVariant(); } QVariant AgentInstanceModel::headerData(int section, Qt::Orientation orientation, int role) const { if (orientation == Qt::Vertical) { return QVariant(); } if (role != Qt::DisplayRole) { return QVariant(); } switch (section) { case 0: return i18nc("@title:column, name of a thing", "Name"); default: return QVariant(); } } QModelIndex AgentInstanceModel::index(int row, int column, const QModelIndex &) const { if (row < 0 || row >= d->mInstances.count()) { return QModelIndex(); } if (column != 0) { return QModelIndex(); } return createIndex(row, column); } QModelIndex AgentInstanceModel::parent(const QModelIndex &) const { return QModelIndex(); } Qt::ItemFlags AgentInstanceModel::flags(const QModelIndex &index) const { if (!index.isValid() || index.row() < 0 || index.row() >= d->mInstances.count()) { return QAbstractItemModel::flags(index); } return QAbstractItemModel::flags(index) | Qt::ItemIsEditable; } bool AgentInstanceModel::setData(const QModelIndex &index, const QVariant &value, int role) { if (!index.isValid()) { return false; } if (index.row() < 0 || index.row() >= d->mInstances.count()) { return false; } AgentInstance &instance = d->mInstances[index.row()]; switch (role) { case OnlineRole: instance.setIsOnline(value.toBool()); Q_EMIT dataChanged(index, index); return true; default: return false; } return false; } #include "moc_agentinstancemodel.cpp" diff --git a/src/core/models/agentinstancemodel.h b/src/core/models/agentinstancemodel.h index eb760715a..6332e12a3 100644 --- a/src/core/models/agentinstancemodel.h +++ b/src/core/models/agentinstancemodel.h @@ -1,107 +1,105 @@ /* Copyright (c) 2006-2008 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 AKONADI_AGENTINSTANCEMODEL_H #define AKONADI_AGENTINSTANCEMODEL_H #include "akonadicore_export.h" #include namespace Akonadi { /** * @short Provides a data model for agent instances. * * This class provides the interface of a QAbstractItemModel to * access all available agent instances: their name, identifier, * supported mimetypes and capabilities. * * @code * * Akonadi::AgentInstanceModel *model = new Akonadi::AgentInstanceModel( this ); * * QListView *view = new QListView( this ); * view->setModel( model ); * * @endcode * * To show only agent instances that match a given mime type or special * capabilities, use the AgentFilterProxyModel on top of this model. * * @author Tobias Koenig */ class AKONADICORE_EXPORT AgentInstanceModel : public QAbstractItemModel { Q_OBJECT public: /** * Describes the roles of this model. */ enum Roles { TypeRole = Qt::UserRole + 1, ///< The agent type itself TypeIdentifierRole, ///< The identifier of the agent type DescriptionRole, ///< A description of the agent type MimeTypesRole, ///< A list of supported mimetypes CapabilitiesRole, ///< A list of supported capabilities InstanceRole, ///< The agent instance itself InstanceIdentifierRole, ///< The identifier of the agent instance StatusRole, ///< The current status (numerical) of the instance StatusMessageRole, ///< A textual presentation of the current status ProgressRole, ///< The current progress (numerical in percent) of an operation OnlineRole, ///< The current online/offline status UserRole = Qt::UserRole + 42 ///< Role for user extensions }; /** * Creates a new agent instance model. * * @param parent The parent object. */ explicit AgentInstanceModel(QObject *parent = nullptr); /** * Destroys the agent instance model. */ ~AgentInstanceModel() override; Q_REQUIRED_RESULT QHash roleNames() const override; Q_REQUIRED_RESULT int columnCount(const QModelIndex &parent = QModelIndex()) const override; Q_REQUIRED_RESULT int rowCount(const QModelIndex &parent = QModelIndex()) const override; Q_REQUIRED_RESULT QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; Q_REQUIRED_RESULT QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; Q_REQUIRED_RESULT QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override; Q_REQUIRED_RESULT QModelIndex parent(const QModelIndex &index) const override; Q_REQUIRED_RESULT Qt::ItemFlags flags(const QModelIndex &index) const override; Q_REQUIRED_RESULT bool setData(const QModelIndex &index, const QVariant &value, int role) override; private: //@cond PRIVATE class Private; Private *const d; - - Q_PRIVATE_SLOT(d, void instanceChanged(const Akonadi::AgentInstance &)) //@endcond }; } #endif diff --git a/src/core/models/entitytreemodel.cpp b/src/core/models/entitytreemodel.cpp index 762bc51e0..4208bfd11 100644 --- a/src/core/models/entitytreemodel.cpp +++ b/src/core/models/entitytreemodel.cpp @@ -1,1142 +1,1144 @@ /* Copyright (c) 2008 Stephen Kelly 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 "entitytreemodel.h" #include "entitytreemodel_p.h" #include "akonadicore_debug.h" #include "monitor_p.h" #include #include #include #include #include #include #include #include "attributefactory.h" #include "monitor.h" #include "collectionmodifyjob.h" #include "entitydisplayattribute.h" #include "transactionsequence.h" #include "itemmodifyjob.h" #include "session.h" #include "collectionfetchscope.h" #include "collectionutils.h" #include "pastehelper_p.h" +// clazy:excludeall=old-style-connect + Q_DECLARE_METATYPE(QSet) using namespace Akonadi; EntityTreeModel::EntityTreeModel(Monitor *monitor, QObject *parent) : QAbstractItemModel(parent) , d_ptr(new EntityTreeModelPrivate(this)) { Q_D(EntityTreeModel); d->init(monitor); } EntityTreeModel::EntityTreeModel(Monitor *monitor, EntityTreeModelPrivate *d, QObject *parent) : QAbstractItemModel(parent) , d_ptr(d) { d->init(monitor); } EntityTreeModel::~EntityTreeModel() { Q_D(EntityTreeModel); for (const QList &list : qAsConst(d->m_childEntities)) { qDeleteAll(list); } delete d_ptr; } CollectionFetchScope::ListFilter EntityTreeModel::listFilter() const { Q_D(const EntityTreeModel); return d->m_listFilter; } void EntityTreeModel::setListFilter(CollectionFetchScope::ListFilter filter) { Q_D(EntityTreeModel); d->beginResetModel(); d->m_listFilter = filter; d->m_monitor->setAllMonitored(filter == CollectionFetchScope::NoFilter); d->endResetModel(); } void EntityTreeModel::setCollectionsMonitored(const Collection::List &collections) { Q_D(EntityTreeModel); d->beginResetModel(); const Akonadi::Collection::List lstCols = d->m_monitor->collectionsMonitored(); for (const Akonadi::Collection &col : lstCols) { d->m_monitor->setCollectionMonitored(col, false); } for (const Akonadi::Collection &col : collections) { d->m_monitor->setCollectionMonitored(col, true); } d->endResetModel(); } void EntityTreeModel::setCollectionMonitored(const Collection &col, bool monitored) { Q_D(EntityTreeModel); d->m_monitor->setCollectionMonitored(col, monitored); } bool EntityTreeModel::systemEntitiesShown() const { Q_D(const EntityTreeModel); return d->m_showSystemEntities; } void EntityTreeModel::setShowSystemEntities(bool show) { Q_D(EntityTreeModel); d->m_showSystemEntities = show; } void EntityTreeModel::clearAndReset() { Q_D(EntityTreeModel); d->beginResetModel(); d->endResetModel(); } QHash EntityTreeModel::roleNames() const { return { { Qt::DecorationRole, "decoration" }, { Qt::DisplayRole, "display" }, { EntityTreeModel::ItemIdRole, "itemId" }, { EntityTreeModel::CollectionIdRole, "collectionId" }, { EntityTreeModel::UnreadCountRole, "unreadCount" }, // TODO: expose when states for reporting of fetching payload parts of items is changed // { EntityTreeModel::FetchStateRole, "fetchState" }, { EntityTreeModel::EntityUrlRole, "url" }, { EntityTreeModel::RemoteIdRole, "remoteId" }, { EntityTreeModel::IsPopulatedRole, "isPopulated" }, { EntityTreeModel::CollectionRole, "collection" } }; } int EntityTreeModel::columnCount(const QModelIndex &parent) const { // TODO: Statistics? if (parent.isValid() && parent.column() != 0) { return 0; } return qMax(entityColumnCount(CollectionTreeHeaders), entityColumnCount(ItemListHeaders)); } QVariant EntityTreeModel::entityData(const Item &item, int column, int role) const { Q_D(const EntityTreeModel); if (column == 0) { switch (role) { case Qt::DisplayRole: case Qt::EditRole: if (const auto *attr = item.attribute(); attr && !attr->displayName().isEmpty()) { return attr->displayName(); } else if (!item.remoteId().isEmpty()) { return item.remoteId(); } return QString(QLatin1Char('<') + QString::number(item.id()) + QLatin1Char('>')); case Qt::DecorationRole: if (const auto *attr = item.attribute(); attr && !attr->iconName().isEmpty()) { return d->iconForName(attr->iconName()); } break; default: break; } } return QVariant(); } QVariant EntityTreeModel::entityData(const Collection &collection, int column, int role) const { Q_D(const EntityTreeModel); if (column > 0) { return QString(); } if (collection == Collection::root()) { // Only display the root collection. It may not be edited. if (role == Qt::DisplayRole) { return d->m_rootCollectionDisplayName; } else if (role == Qt::EditRole) { return QVariant(); } } switch (role) { case Qt::DisplayRole: case Qt::EditRole: if (column == 0) { if (const QString displayName = collection.displayName(); !displayName.isEmpty()) { return displayName; } else { return i18nc("@info:status", "Loading..."); } } break; case Qt::DecorationRole: if (const auto attr = collection.attribute(); attr && !attr->iconName().isEmpty()) { return d->iconForName(attr->iconName()); } return d->iconForName(CollectionUtils::defaultIconName(collection)); default: break; } return QVariant(); } QVariant EntityTreeModel::data(const QModelIndex &index, int role) const { Q_D(const EntityTreeModel); if (role == SessionRole) { return QVariant::fromValue(qobject_cast(d->m_session)); } // Ugly, but at least the API is clean. const HeaderGroup headerGroup = static_cast((role / static_cast(TerminalUserRole))); role %= TerminalUserRole; if (!index.isValid()) { if (ColumnCountRole != role) { return QVariant(); } return entityColumnCount(headerGroup); } if (ColumnCountRole == role) { return entityColumnCount(headerGroup); } const Node *node = reinterpret_cast(index.internalPointer()); if (ParentCollectionRole == role && d->m_collectionFetchStrategy != FetchNoCollections) { const Collection parentCollection = d->m_collections.value(node->parent); Q_ASSERT(parentCollection.isValid()); return QVariant::fromValue(parentCollection); } if (Node::Collection == node->type) { const Collection collection = d->m_collections.value(node->id); if (!collection.isValid()) { return QVariant(); } switch (role) { case MimeTypeRole: return collection.mimeType(); case RemoteIdRole: return collection.remoteId(); case CollectionIdRole: return collection.id(); case ItemIdRole: // QVariant().toInt() is 0, not -1, so we have to handle the ItemIdRole // and CollectionIdRole (below) specially return -1; case CollectionRole: return QVariant::fromValue(collection); case EntityUrlRole: return collection.url().url(); case UnreadCountRole: return collection.statistics().unreadCount(); case FetchStateRole: return d->m_pendingCollectionRetrieveJobs.contains(collection.id()) ? FetchingState : IdleState; case IsPopulatedRole: return d->m_populatedCols.contains(collection.id()); case OriginalCollectionNameRole: return entityData(collection, index.column(), Qt::DisplayRole); case PendingCutRole: return d->m_pendingCutCollections.contains(node->id); case Qt::BackgroundRole: if (const auto attr = collection.attribute(); attr && attr->backgroundColor().isValid()) { return attr->backgroundColor(); } Q_FALLTHROUGH(); default: return entityData(collection, index.column(), role); } } else if (Node::Item == node->type) { const Item item = d->m_items.value(node->id); if (!item.isValid()) { return QVariant(); } switch (role) { case ParentCollectionRole: return QVariant::fromValue(item.parentCollection()); case MimeTypeRole: return item.mimeType(); case RemoteIdRole: return item.remoteId(); case ItemRole: return QVariant::fromValue(item); case ItemIdRole: return item.id(); case CollectionIdRole: return -1; case LoadedPartsRole: return QVariant::fromValue(item.loadedPayloadParts()); case AvailablePartsRole: return QVariant::fromValue(item.availablePayloadParts()); case EntityUrlRole: return item.url(Akonadi::Item::UrlWithMimeType).url(); case PendingCutRole: return d->m_pendingCutItems.contains(node->id); case Qt::BackgroundRole: if (const auto attr = item.attribute(); attr && attr->backgroundColor().isValid()) { return attr->backgroundColor(); } Q_FALLTHROUGH(); default: return entityData(item, index.column(), role); } } return QVariant(); } Qt::ItemFlags EntityTreeModel::flags(const QModelIndex &index) const { Q_D(const EntityTreeModel); // Pass modeltest. if (!index.isValid()) { return {}; } Qt::ItemFlags flags = QAbstractItemModel::flags(index); const Node *node = reinterpret_cast(index.internalPointer()); if (Node::Collection == node->type) { const Collection collection = d->m_collections.value(node->id); if (collection.isValid()) { if (collection == Collection::root()) { // Selectable and displayable only. return flags; } const int rights = collection.rights(); if (rights & Collection::CanChangeCollection) { if (index.column() == 0) { flags |= Qt::ItemIsEditable; } // Changing the collection includes changing the metadata (child entityordering). // Need to allow this by drag and drop. flags |= Qt::ItemIsDropEnabled; } if (rights & (Collection::CanCreateCollection | Collection::CanCreateItem | Collection::CanLinkItem)) { // Can we drop new collections and items into this collection? flags |= Qt::ItemIsDropEnabled; } // dragging is always possible, even for read-only objects, but they can only be copied, not moved. flags |= Qt::ItemIsDragEnabled; } } else if (Node::Item == node->type) { // cut out entities are shown as disabled // TODO: Not sure this is wanted, it prevents any interaction with them, better // solution would be to move this to the delegate, as was done for collections. if (d->m_pendingCutItems.contains(node->id)) { return Qt::ItemIsSelectable; } // Rights come from the parent collection. Collection parentCollection; if (!index.parent().isValid()) { parentCollection = d->m_rootCollection; } else { const Node *parentNode = reinterpret_cast(index.parent().internalPointer()); parentCollection = d->m_collections.value(parentNode->id); } if (parentCollection.isValid()) { const int rights = parentCollection.rights(); // Can't drop onto items. if (rights & Collection::CanChangeItem && index.column() == 0) { flags |= Qt::ItemIsEditable; } // dragging is always possible, even for read-only objects, but they can only be copied, not moved. flags |= Qt::ItemIsDragEnabled; } } return flags; } Qt::DropActions EntityTreeModel::supportedDropActions() const { return (Qt::CopyAction | Qt::MoveAction | Qt::LinkAction); } QStringList EntityTreeModel::mimeTypes() const { // TODO: Should this return the mimetypes that the items provide? Allow dragging a contact from here for example. return {QStringLiteral("text/uri-list")}; } bool EntityTreeModel::dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent) { Q_UNUSED(row); Q_UNUSED(column); Q_D(EntityTreeModel); // Can't drop onto Collection::root. if (!parent.isValid()) { return false; } // TODO Use action and collection rights and return false if necessary // if row and column are -1, then the drop was on parent directly. // data should then be appended on the end of the items of the collections as appropriate. // That will mean begin insert rows etc. // Otherwise it was a sibling of the row^th item of parent. // Needs to be handled when ordering is accounted for. // Handle dropping between items as well as on items. // if ( row != -1 && column != -1 ) // { // } if (action == Qt::IgnoreAction) { return true; } // Shouldn't do this. Need to be able to drop vcards for example. // if ( !data->hasFormat( "text/uri-list" ) ) // return false; Node *node = reinterpret_cast(parent.internalId()); Q_ASSERT(node); if (Node::Item == node->type) { if (!parent.parent().isValid()) { // The drop is somehow on an item with no parent (shouldn't happen) // The drop should be considered handled anyway. qCWarning(AKONADICORE_LOG) << "Dropped onto item with no parent collection"; return true; } // A drop onto an item should be considered as a drop onto its parent collection node = reinterpret_cast(parent.parent().internalId()); } if (Node::Collection == node->type) { const Collection destCollection = d->m_collections.value(node->id); // Applications can't create new collections in root. Only resources can. if (destCollection == Collection::root()) { // Accept the event so that it doesn't propagate. return true; } if (data->hasFormat(QStringLiteral("text/uri-list"))) { MimeTypeChecker mimeChecker; mimeChecker.setWantedMimeTypes(destCollection.contentMimeTypes()); const QList urls = data->urls(); for (const QUrl &url : urls) { const Collection collection = d->m_collections.value(Collection::fromUrl(url).id()); if (collection.isValid()) { if (collection.parentCollection().id() == destCollection.id() && action != Qt::CopyAction) { qCWarning(AKONADICORE_LOG) << "Error: source and destination of move are the same."; return false; } if (!mimeChecker.isWantedCollection(collection)) { qCDebug(AKONADICORE_LOG) << "unwanted collection" << mimeChecker.wantedMimeTypes() << collection.contentMimeTypes(); return false; } QUrlQuery query(url); if (query.hasQueryItem(QStringLiteral("name"))) { const QString collectionName = query.queryItemValue(QStringLiteral("name")); const QStringList collectionNames = d->childCollectionNames(destCollection); if (collectionNames.contains(collectionName)) { QMessageBox::critical(nullptr, i18nc("@window:title", "Error"), i18n("The target collection '%1' contains already\na collection with name '%2'.", destCollection.name(), collection.name())); return false; } } } else { const Item item = d->m_items.value(Item::fromUrl(url).id()); if (item.isValid()) { if (item.parentCollection().id() == destCollection.id() && action != Qt::CopyAction) { qCWarning(AKONADICORE_LOG) << "Error: source and destination of move are the same."; return false; } if (!mimeChecker.isWantedItem(item)) { qCDebug(AKONADICORE_LOG) << "unwanted item" << mimeChecker.wantedMimeTypes() << item.mimeType(); return false; } } } } KJob *job = PasteHelper::pasteUriList(data, destCollection, action, d->m_session); if (!job) { return false; } connect(job, SIGNAL(result(KJob*)), SLOT(pasteJobDone(KJob*))); // Accept the event so that it doesn't propagate. return true; } else { // not a set of uris. Maybe vcards etc. Check if the parent supports them, and maybe do // fromMimeData for them. Hmm, put it in the same transaction with the above? // TODO: This should be handled first, not last. } } return false; } QModelIndex EntityTreeModel::index(int row, int column, const QModelIndex &parent) const { Q_D(const EntityTreeModel); if (parent.column() > 0) { return QModelIndex(); } //TODO: don't use column count here? Use some d-> func. if (column >= columnCount() || column < 0) { return QModelIndex(); } QList childEntities; const Node *parentNode = reinterpret_cast(parent.internalPointer()); if (!parentNode || !parent.isValid()) { if (d->m_showRootCollection) { childEntities << d->m_childEntities.value(-1); } else { childEntities = d->m_childEntities.value(d->m_rootCollection.id()); } } else if (parentNode->id >= 0) { childEntities = d->m_childEntities.value(parentNode->id); } const int size = childEntities.size(); if (row < 0 || row >= size) { return QModelIndex(); } Node *node = childEntities.at(row); return createIndex(row, column, reinterpret_cast(node)); } QModelIndex EntityTreeModel::parent(const QModelIndex &index) const { Q_D(const EntityTreeModel); if (!index.isValid()) { return QModelIndex(); } if (d->m_collectionFetchStrategy == InvisibleCollectionFetch || d->m_collectionFetchStrategy == FetchNoCollections) { return QModelIndex(); } const Node *node = reinterpret_cast(index.internalPointer()); if (!node) { return QModelIndex(); } const Collection collection = d->m_collections.value(node->parent); if (!collection.isValid()) { return QModelIndex(); } if (collection.id() == d->m_rootCollection.id()) { if (!d->m_showRootCollection) { return QModelIndex(); } else { return createIndex(0, 0, reinterpret_cast(d->m_rootNode)); } } Q_ASSERT(collection.parentCollection().isValid()); const int row = d->indexOf(d->m_childEntities.value(collection.parentCollection().id()), collection.id()); Q_ASSERT(row >= 0); Node *parentNode = d->m_childEntities.value(collection.parentCollection().id()).at(row); return createIndex(row, 0, reinterpret_cast(parentNode)); } int EntityTreeModel::rowCount(const QModelIndex &parent) const { Q_D(const EntityTreeModel); if (d->m_collectionFetchStrategy == InvisibleCollectionFetch || d->m_collectionFetchStrategy == FetchNoCollections) { if (parent.isValid()) { return 0; } else { return d->m_items.size(); } } if (!parent.isValid()) { // If we're showing the root collection then it will be the only child of the root. if (d->m_showRootCollection) { return d->m_childEntities.value(-1).size(); } return d->m_childEntities.value(d->m_rootCollection.id()).size(); } if (parent.column() != 0) { return 0; } const Node *node = reinterpret_cast(parent.internalPointer()); if (!node) { return 0; } if (Node::Item == node->type) { return 0; } Q_ASSERT(parent.isValid()); return d->m_childEntities.value(node->id).size(); } int EntityTreeModel::entityColumnCount(HeaderGroup headerGroup) const { // Not needed in this model. Q_UNUSED(headerGroup); return 1; } QVariant EntityTreeModel::entityHeaderData(int section, Qt::Orientation orientation, int role, HeaderGroup headerGroup) const { Q_D(const EntityTreeModel); // Not needed in this model. Q_UNUSED(headerGroup); if (section == 0 && orientation == Qt::Horizontal && role == Qt::DisplayRole) { if (d->m_rootCollection == Collection::root()) { return i18nc("@title:column Name of a thing", "Name"); } return d->m_rootCollection.name(); } return QAbstractItemModel::headerData(section, orientation, role); } QVariant EntityTreeModel::headerData(int section, Qt::Orientation orientation, int role) const { const HeaderGroup headerGroup = static_cast((role / static_cast(TerminalUserRole))); role %= TerminalUserRole; return entityHeaderData(section, orientation, role, headerGroup); } QMimeData *EntityTreeModel::mimeData(const QModelIndexList &indexes) const { Q_D(const EntityTreeModel); QMimeData *data = new QMimeData(); QList urls; for (const QModelIndex &index : indexes) { if (index.column() != 0) { continue; } if (!index.isValid()) { continue; } const Node *node = reinterpret_cast(index.internalPointer()); if (Node::Collection == node->type) { urls << d->m_collections.value(node->id).url(Collection::UrlWithName); } else if (Node::Item == node->type) { QUrl url = d->m_items.value(node->id).url(Item::Item::UrlWithMimeType); QUrlQuery query(url); query.addQueryItem(QStringLiteral("parent"), QString::number(node->parent)); url.setQuery(query); urls << url; } else { // if that happens something went horrible wrong Q_ASSERT(false); } } data->setUrls(urls); return data; } // Always return false for actions which take place asynchronously, eg via a Job. bool EntityTreeModel::setData(const QModelIndex &index, const QVariant &value, int role) { Q_D(EntityTreeModel); const Node *node = reinterpret_cast(index.internalPointer()); if (role == PendingCutRole) { if (index.isValid() && value.toBool()) { if (Node::Collection == node->type) { d->m_pendingCutCollections.append(node->id); } else if (Node::Item == node->type) { d->m_pendingCutItems.append(node->id); } } else { d->m_pendingCutCollections.clear(); d->m_pendingCutItems.clear(); } return true; } if (index.isValid() && node->type == Node::Collection && (role == CollectionRefRole || role == CollectionDerefRole)) { const Collection collection = index.data(CollectionRole).value(); Q_ASSERT(collection.isValid()); if (role == CollectionDerefRole) { d->deref(collection.id()); } else if (role == CollectionRefRole) { d->ref(collection.id()); } return true; } if (index.column() == 0 && (role & (Qt::EditRole | ItemRole | CollectionRole))) { if (Node::Collection == node->type) { Collection collection = d->m_collections.value(node->id); if (!collection.isValid() || !value.isValid()) { return false; } if (Qt::EditRole == role) { collection.setName(value.toString()); if (collection.hasAttribute()) { EntityDisplayAttribute *displayAttribute = collection.attribute(); displayAttribute->setDisplayName(value.toString()); } } else if (Qt::BackgroundRole == role) { QColor color = value.value(); if (!color.isValid()) { return false; } EntityDisplayAttribute *eda = collection.attribute(Collection::AddIfMissing); eda->setBackgroundColor(color); } else if (CollectionRole == role) { collection = value.value(); } CollectionModifyJob *job = new CollectionModifyJob(collection, d->m_session); connect(job, SIGNAL(result(KJob*)), SLOT(updateJobDone(KJob*))); return false; } else if (Node::Item == node->type) { Item item = d->m_items.value(node->id); if (!item.isValid() || !value.isValid()) { return false; } if (Qt::EditRole == role) { if (item.hasAttribute()) { EntityDisplayAttribute *displayAttribute = item.attribute(Item::AddIfMissing); displayAttribute->setDisplayName(value.toString()); } } else if (Qt::BackgroundRole == role) { QColor color = value.value(); if (!color.isValid()) { return false; } EntityDisplayAttribute *eda = item.attribute(Item::AddIfMissing); eda->setBackgroundColor(color); } else if (ItemRole == role) { item = value.value(); Q_ASSERT(item.id() == node->id); } ItemModifyJob *itemModifyJob = new ItemModifyJob(item, d->m_session); connect(itemModifyJob, SIGNAL(result(KJob*)), SLOT(updateJobDone(KJob*))); return false; } } return QAbstractItemModel::setData(index, value, role); } bool EntityTreeModel::canFetchMore(const QModelIndex &parent) const { Q_UNUSED(parent) return false; } void EntityTreeModel::fetchMore(const QModelIndex &parent) { Q_D(EntityTreeModel); if (!d->canFetchMore(parent)) { return; } if (d->m_collectionFetchStrategy == InvisibleCollectionFetch) { return; } if (d->m_itemPopulation == ImmediatePopulation) { // Nothing to do. The items are already in the model. return; } else if (d->m_itemPopulation == LazyPopulation) { const Collection collection = parent.data(CollectionRole).value(); if (!collection.isValid()) { return; } d->fetchItems(collection); } } bool EntityTreeModel::hasChildren(const QModelIndex &parent) const { Q_D(const EntityTreeModel); if (d->m_collectionFetchStrategy == InvisibleCollectionFetch || d->m_collectionFetchStrategy == FetchNoCollections) { return parent.isValid() ? false : !d->m_items.isEmpty(); } // TODO: Empty collections right now will return true and get a little + to expand. // There is probably no way to tell if a collection // has child items in akonadi without first attempting an itemFetchJob... // Figure out a way to fix this. (Statistics) return ((rowCount(parent) > 0) || (canFetchMore(parent) && d->m_itemPopulation == LazyPopulation)); } bool EntityTreeModel::isCollectionTreeFetched() const { Q_D(const EntityTreeModel); return d->m_collectionTreeFetched; } bool EntityTreeModel::isCollectionPopulated(Collection::Id id) const { Q_D(const EntityTreeModel); return d->m_populatedCols.contains(id); } bool EntityTreeModel::isFullyPopulated() const { Q_D(const EntityTreeModel); return d->m_collectionTreeFetched && d->m_pendingCollectionRetrieveJobs.isEmpty(); } QModelIndexList EntityTreeModel::match(const QModelIndex &start, int role, const QVariant &value, int hits, Qt::MatchFlags flags) const { Q_D(const EntityTreeModel); if (role == CollectionIdRole || role == CollectionRole) { Collection::Id id; if (role == CollectionRole) { const Collection collection = value.value(); id = collection.id(); } else { id = value.toLongLong(); } const Collection collection = d->m_collections.value(id); if (!collection.isValid()) { return {}; } const QModelIndex collectionIndex = d->indexForCollection(collection); Q_ASSERT(collectionIndex.isValid()); return {collectionIndex}; } else if (role == ItemIdRole || role == ItemRole) { Item::Id id; if (role == ItemRole) { id = value.value().id(); } else { id = value.toLongLong(); } const Item item = d->m_items.value(id); if (!item.isValid()) { return {}; } return d->indexesForItem(item); } else if (role == EntityUrlRole) { const QUrl url(value.toString()); const Item item = Item::fromUrl(url); if (item.isValid()) { return d->indexesForItem(d->m_items.value(item.id())); } const Collection collection = Collection::fromUrl(url); if (!collection.isValid()) { return {}; } return {d->indexForCollection(collection)}; } return QAbstractItemModel::match(start, role, value, hits, flags); } bool EntityTreeModel::insertRows(int, int, const QModelIndex &) { return false; } bool EntityTreeModel::insertColumns(int, int, const QModelIndex &) { return false; } bool EntityTreeModel::removeRows(int, int, const QModelIndex &) { return false; } bool EntityTreeModel::removeColumns(int, int, const QModelIndex &) { return false; } void EntityTreeModel::setItemPopulationStrategy(ItemPopulationStrategy strategy) { Q_D(EntityTreeModel); d->beginResetModel(); d->m_itemPopulation = strategy; if (strategy == NoItemPopulation) { disconnect(d->m_monitor, SIGNAL(itemAdded(Akonadi::Item,Akonadi::Collection)), this, SLOT(monitoredItemAdded(Akonadi::Item,Akonadi::Collection))); disconnect(d->m_monitor, SIGNAL(itemChanged(Akonadi::Item,QSet)), this, SLOT(monitoredItemChanged(Akonadi::Item,QSet))); disconnect(d->m_monitor, SIGNAL(itemRemoved(Akonadi::Item)), this, SLOT(monitoredItemRemoved(Akonadi::Item))); disconnect(d->m_monitor, SIGNAL(itemMoved(Akonadi::Item,Akonadi::Collection,Akonadi::Collection)), this, SLOT(monitoredItemMoved(Akonadi::Item,Akonadi::Collection,Akonadi::Collection))); disconnect(d->m_monitor, SIGNAL(itemLinked(Akonadi::Item,Akonadi::Collection)), this, SLOT(monitoredItemLinked(Akonadi::Item,Akonadi::Collection))); disconnect(d->m_monitor, SIGNAL(itemUnlinked(Akonadi::Item,Akonadi::Collection)), this, SLOT(monitoredItemUnlinked(Akonadi::Item,Akonadi::Collection))); } d->m_monitor->d_ptr->useRefCounting = (strategy == LazyPopulation); d->endResetModel(); } EntityTreeModel::ItemPopulationStrategy EntityTreeModel::itemPopulationStrategy() const { Q_D(const EntityTreeModel); return d->m_itemPopulation; } void EntityTreeModel::setIncludeRootCollection(bool include) { Q_D(EntityTreeModel); d->beginResetModel(); d->m_showRootCollection = include; d->endResetModel(); } bool EntityTreeModel::includeRootCollection() const { Q_D(const EntityTreeModel); return d->m_showRootCollection; } void EntityTreeModel::setRootCollectionDisplayName(const QString &displayName) { Q_D(EntityTreeModel); d->m_rootCollectionDisplayName = displayName; // TODO: Emit datachanged if it is being shown. } QString EntityTreeModel::rootCollectionDisplayName() const { Q_D(const EntityTreeModel); return d->m_rootCollectionDisplayName; } void EntityTreeModel::setCollectionFetchStrategy(CollectionFetchStrategy strategy) { Q_D(EntityTreeModel); d->beginResetModel(); d->m_collectionFetchStrategy = strategy; if (strategy == FetchNoCollections || strategy == InvisibleCollectionFetch) { disconnect(d->m_monitor, SIGNAL(collectionChanged(Akonadi::Collection)), this, SLOT(monitoredCollectionChanged(Akonadi::Collection))); disconnect(d->m_monitor, SIGNAL(collectionAdded(Akonadi::Collection,Akonadi::Collection)), this, SLOT(monitoredCollectionAdded(Akonadi::Collection,Akonadi::Collection))); disconnect(d->m_monitor, SIGNAL(collectionRemoved(Akonadi::Collection)), this, SLOT(monitoredCollectionRemoved(Akonadi::Collection))); disconnect(d->m_monitor, SIGNAL(collectionMoved(Akonadi::Collection,Akonadi::Collection,Akonadi::Collection)), this, SLOT(monitoredCollectionMoved(Akonadi::Collection,Akonadi::Collection,Akonadi::Collection))); d->m_monitor->fetchCollection(false); } else { d->m_monitor->fetchCollection(true); } d->endResetModel(); } EntityTreeModel::CollectionFetchStrategy EntityTreeModel::collectionFetchStrategy() const { Q_D(const EntityTreeModel); return d->m_collectionFetchStrategy; } static QPair, const EntityTreeModel *> proxiesAndModel(const QAbstractItemModel *model) { QList proxyChain; const QAbstractProxyModel *proxy = qobject_cast(model); const QAbstractItemModel *_model = model; while (proxy) { proxyChain.prepend(proxy); _model = proxy->sourceModel(); proxy = qobject_cast(_model); } const EntityTreeModel *etm = qobject_cast(_model); return qMakePair(proxyChain, etm); } static QModelIndex proxiedIndex(const QModelIndex &idx, const QList &proxyChain) { QModelIndex _idx = idx; for (const auto *proxy : proxyChain) { _idx = proxy->mapFromSource(_idx); } return _idx; } QModelIndex EntityTreeModel::modelIndexForCollection(const QAbstractItemModel *model, const Collection &collection) { const auto &[proxy, etm] = proxiesAndModel(model); if (!etm) { qCWarning(AKONADICORE_LOG) << "Model" << model << "is not derived from ETM or a proxy model on top of ETM."; return {}; } QModelIndex idx = etm->d_ptr->indexForCollection(collection); return proxiedIndex(idx, proxy); } QModelIndexList EntityTreeModel::modelIndexesForItem(const QAbstractItemModel *model, const Item &item) { const auto &[proxy, etm] = proxiesAndModel(model); if (!etm) { qCWarning(AKONADICORE_LOG) << "Model" << model << "is not derived from ETM or a proxy model on top of ETM."; return QModelIndexList(); } const QModelIndexList list = etm->d_ptr->indexesForItem(item); QModelIndexList proxyList; for (const QModelIndex &idx : list) { const QModelIndex pIdx = proxiedIndex(idx, proxy); if (pIdx.isValid()) { proxyList.push_back(pIdx); } } return proxyList; } Collection EntityTreeModel::updatedCollection(const QAbstractItemModel *model, qint64 collectionId) { const QAbstractProxyModel *proxy = qobject_cast(model); const QAbstractItemModel *_model = model; while (proxy) { _model = proxy->sourceModel(); proxy = qobject_cast(_model); } auto etm = qobject_cast(_model); if (etm) { return etm->d_ptr->m_collections.value(collectionId); } else { return Collection{ collectionId }; } } Collection EntityTreeModel::updatedCollection(const QAbstractItemModel *model, const Collection &collection) { return updatedCollection(model, collection.id()); } #include "moc_entitytreemodel.cpp" diff --git a/src/core/models/entitytreemodel_p.cpp b/src/core/models/entitytreemodel_p.cpp index 5d2b72067..e11e125b5 100644 --- a/src/core/models/entitytreemodel_p.cpp +++ b/src/core/models/entitytreemodel_p.cpp @@ -1,1896 +1,1900 @@ /* Copyright (c) 2008 Stephen Kelly 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 "entitytreemodel_p.h" #include "entitytreemodel.h" #include "agentmanagerinterface.h" #include "monitor_p.h" // For friend ref/deref #include "servermanager.h" #include "akranges.h" #include #include "agentmanager.h" #include "agenttype.h" #include "monitor.h" #include "changerecorder.h" #include "collectioncopyjob.h" #include "collectionfetchjob.h" #include "collectionfetchscope.h" #include "collectionmovejob.h" #include "collectionstatistics.h" #include "collectionstatisticsjob.h" #include "entityhiddenattribute.h" #include "itemcopyjob.h" #include "itemfetchjob.h" #include "itemmodifyjob.h" #include "itemmovejob.h" #include "linkjob.h" #include "session.h" #include "private/protocol_p.h" #include "akonadicore_debug.h" #include #include #include #include +// clazy:excludeall=old-style-connect + QHash jobTimeTracker; Q_LOGGING_CATEGORY(DebugETM, "org.kde.pim.akonadi.ETM", QtInfoMsg) using namespace Akonadi; using namespace AkRanges; static CollectionFetchJob::Type getFetchType(EntityTreeModel::CollectionFetchStrategy strategy) { switch (strategy) { case EntityTreeModel::FetchFirstLevelChildCollections: return CollectionFetchJob::FirstLevel; case EntityTreeModel::InvisibleCollectionFetch: case EntityTreeModel::FetchCollectionsRecursive: default: break; } return CollectionFetchJob::Recursive; } EntityTreeModelPrivate::EntityTreeModelPrivate(EntityTreeModel *parent) : q_ptr(parent) { // using collection as a parameter of a queued call in runItemFetchJob() qRegisterMetaType(); Akonadi::AgentManager *agentManager = Akonadi::AgentManager::self(); QObject::connect(agentManager, SIGNAL(instanceRemoved(Akonadi::AgentInstance)), q_ptr, SLOT(agentInstanceRemoved(Akonadi::AgentInstance))); } EntityTreeModelPrivate::~EntityTreeModelPrivate() { if (m_needDeleteRootNode) { delete m_rootNode; } m_rootNode = nullptr; } void EntityTreeModelPrivate::init(Monitor *monitor) { Q_Q(EntityTreeModel); Q_ASSERT(!m_monitor); m_monitor = monitor; // The default is to FetchCollectionsRecursive, so we tell the monitor to fetch collections // That way update signals from the monitor will contain the full collection. // This may be updated if the CollectionFetchStrategy is changed. m_monitor->fetchCollection(true); m_session = m_monitor->session(); m_rootCollectionDisplayName = QStringLiteral("[*]"); if (Akonadi::ChangeRecorder *cr = qobject_cast(m_monitor)) { cr->setChangeRecordingEnabled(false); } m_includeStatistics = true; m_monitor->fetchCollectionStatistics(true); m_monitor->collectionFetchScope().setAncestorRetrieval(Akonadi::CollectionFetchScope::All); q->connect(monitor, SIGNAL(mimeTypeMonitored(QString,bool)), SLOT(monitoredMimeTypeChanged(QString,bool))); q->connect(monitor, SIGNAL(collectionMonitored(Akonadi::Collection,bool)), SLOT(monitoredCollectionsChanged(Akonadi::Collection,bool))); q->connect(monitor, SIGNAL(itemMonitored(Akonadi::Item,bool)), SLOT(monitoredItemsChanged(Akonadi::Item,bool))); q->connect(monitor, SIGNAL(resourceMonitored(QByteArray,bool)), SLOT(monitoredResourcesChanged(QByteArray,bool))); // monitor collection changes q->connect(monitor, SIGNAL(collectionChanged(Akonadi::Collection)), SLOT(monitoredCollectionChanged(Akonadi::Collection))); q->connect(monitor, SIGNAL(collectionAdded(Akonadi::Collection,Akonadi::Collection)), SLOT(monitoredCollectionAdded(Akonadi::Collection,Akonadi::Collection))); q->connect(monitor, SIGNAL(collectionRemoved(Akonadi::Collection)), SLOT(monitoredCollectionRemoved(Akonadi::Collection))); q->connect(monitor, SIGNAL(collectionMoved(Akonadi::Collection,Akonadi::Collection,Akonadi::Collection)), SLOT(monitoredCollectionMoved(Akonadi::Collection,Akonadi::Collection,Akonadi::Collection))); // Monitor item changes. q->connect(monitor, SIGNAL(itemAdded(Akonadi::Item,Akonadi::Collection)), SLOT(monitoredItemAdded(Akonadi::Item,Akonadi::Collection))); q->connect(monitor, SIGNAL(itemChanged(Akonadi::Item,QSet)), SLOT(monitoredItemChanged(Akonadi::Item,QSet))); q->connect(monitor, SIGNAL(itemRemoved(Akonadi::Item)), SLOT(monitoredItemRemoved(Akonadi::Item))); q->connect(monitor, SIGNAL(itemMoved(Akonadi::Item,Akonadi::Collection,Akonadi::Collection)), SLOT(monitoredItemMoved(Akonadi::Item,Akonadi::Collection,Akonadi::Collection))); q->connect(monitor, SIGNAL(itemLinked(Akonadi::Item,Akonadi::Collection)), SLOT(monitoredItemLinked(Akonadi::Item,Akonadi::Collection))); q->connect(monitor, SIGNAL(itemUnlinked(Akonadi::Item,Akonadi::Collection)), SLOT(monitoredItemUnlinked(Akonadi::Item,Akonadi::Collection))); q->connect(monitor, SIGNAL(collectionStatisticsChanged(Akonadi::Collection::Id,Akonadi::CollectionStatistics)), SLOT(monitoredCollectionStatisticsChanged(Akonadi::Collection::Id,Akonadi::CollectionStatistics))); Akonadi::ServerManager *serverManager = Akonadi::ServerManager::self(); q->connect(serverManager, SIGNAL(started()), SLOT(serverStarted())); fillModel(); } void EntityTreeModelPrivate::prependNode(Node *node) { m_childEntities[node->parent].prepend(node); } void EntityTreeModelPrivate::appendNode(Node *node) { m_childEntities[node->parent].append(node); } void EntityTreeModelPrivate::serverStarted() { // Don't emit about to be reset. Too late for that endResetModel(); } void EntityTreeModelPrivate::changeFetchState(const Collection &parent) { Q_Q(EntityTreeModel); const QModelIndex collectionIndex = indexForCollection(parent); if (!collectionIndex.isValid()) { // Because we are called delayed, it is possible that @p parent has been deleted. return; } Q_EMIT q->dataChanged(collectionIndex, collectionIndex); } void EntityTreeModelPrivate::agentInstanceRemoved(const Akonadi::AgentInstance &instance) { Q_Q(EntityTreeModel); if (!instance.type().capabilities().contains(QLatin1String("Resource"))) { return; } if (m_rootCollection.isValid()) { if (m_rootCollection != Collection::root()) { if (m_rootCollection.resource() == instance.identifier()) { q->clearAndReset(); } return; } const auto &children = m_childEntities[Collection::root().id()]; for (const Node *node : children) { Q_ASSERT(node->type == Node::Collection); const Collection collection = m_collections[node->id]; if (collection.resource() == instance.identifier()) { monitoredCollectionRemoved(collection); } } } } void EntityTreeModelPrivate::fetchItems(const Collection &parent) { Q_Q(const EntityTreeModel); Q_ASSERT(parent.isValid()); Q_ASSERT(m_collections.contains(parent.id())); // TODO: Use a more specific fetch scope to get only the envelope for mails etc. ItemFetchJob *itemFetchJob = new Akonadi::ItemFetchJob(parent, m_session); itemFetchJob->setFetchScope(m_monitor->itemFetchScope()); itemFetchJob->fetchScope().setAncestorRetrieval(ItemFetchScope::All); itemFetchJob->fetchScope().setIgnoreRetrievalErrors(true); itemFetchJob->setDeliveryOption(ItemFetchJob::EmitItemsInBatches); if (m_showRootCollection || parent != m_rootCollection) { m_pendingCollectionRetrieveJobs.insert(parent.id()); // If collections are not in the model, there will be no valid index for them. if ((m_collectionFetchStrategy != EntityTreeModel::InvisibleCollectionFetch) && (m_collectionFetchStrategy != EntityTreeModel::FetchNoCollections)) { // We need to invoke this delayed because we would otherwise be emitting a sequence like // - beginInsertRows // - dataChanged // - endInsertRows // which would confuse proxies. QMetaObject::invokeMethod(const_cast(q), "changeFetchState", Qt::QueuedConnection, Q_ARG(Akonadi::Collection, parent)); } } q->connect(itemFetchJob, &ItemFetchJob::itemsReceived, q, [this, parentId = parent.id()](const Item::List &items) { itemsFetched(parentId, items); }); q->connect(itemFetchJob, &ItemFetchJob::result, q, [this, parentId = parent.id()](KJob *job) { itemFetchJobDone(parentId, job); }); qCDebug(DebugETM) << "collection:" << parent.name(); jobTimeTracker[itemFetchJob].start(); } void EntityTreeModelPrivate::fetchCollections(Akonadi::CollectionFetchJob *job) { Q_Q(EntityTreeModel); job->fetchScope().setListFilter(m_listFilter); job->fetchScope().setContentMimeTypes(m_monitor->mimeTypesMonitored()); m_pendingCollectionFetchJobs.insert(static_cast(job)); if (m_collectionFetchStrategy == EntityTreeModel::InvisibleCollectionFetch) { // This is invisible fetch, so no model signals are emitted q->connect(job, &CollectionFetchJob::collectionsReceived, q, [this](const Collection::List &collections) { for (const auto &collection : collections) { if (isHidden(collection)) { continue; } m_collections.insert(collection.id(), collection); prependNode(new Node{Node::Collection, collection.id(), -1}); fetchItems(collection); } }); } else { job->fetchScope().setIncludeStatistics(m_includeStatistics); job->fetchScope().setAncestorRetrieval(Akonadi::CollectionFetchScope::All); q->connect(job, SIGNAL(collectionsReceived(Akonadi::Collection::List)), q, SLOT(collectionsFetched(Akonadi::Collection::List))); } q->connect(job, SIGNAL(result(KJob*)), q, SLOT(collectionFetchJobDone(KJob*))); jobTimeTracker[job].start(); } void EntityTreeModelPrivate::fetchCollections(const Collection::List &collections, CollectionFetchJob::Type type) { fetchCollections(new CollectionFetchJob(collections, type, m_session)); } void EntityTreeModelPrivate::fetchCollections(const Collection &collection, CollectionFetchJob::Type type) { Q_ASSERT(collection.isValid()); CollectionFetchJob *job = new CollectionFetchJob(collection, type, m_session); fetchCollections(job); } namespace Akonadi { template inline bool EntityTreeModelPrivate::isHiddenImpl(const T &entity, Node::Type type) const { if (m_showSystemEntities) { return false; } if (type == Node::Collection && entity.id() == m_rootCollection.id()) { return false; } // entity.hasAttribute() does not compile w/ GCC for // some reason if (entity.hasAttribute(EntityHiddenAttribute().type())) { return true; } const Collection parent = entity.parentCollection(); if (parent.isValid()) { return isHiddenImpl(parent, Node::Collection); } return false; } } bool EntityTreeModelPrivate::isHidden(const Akonadi::Collection &collection) const { return isHiddenImpl(collection, Node::Collection); } bool EntityTreeModelPrivate::isHidden(const Akonadi::Item &item) const { return isHiddenImpl(item, Node::Item); } static QSet getChildren(Collection::Id parent, const std::unordered_map &childParentMap) { QSet children; for (const auto &[childId, parentId] : childParentMap) { if (parentId == parent) { children.insert(childId); children.unite(getChildren(childId, childParentMap)); } } return children; } void EntityTreeModelPrivate::collectionsFetched(const Akonadi::Collection::List &collections) { Q_Q(EntityTreeModel); QElapsedTimer t; t.start(); QVectorIterator it(collections); QHash collectionsToInsert; while (it.hasNext()) { const Collection collection = it.next(); const Collection::Id collectionId = collection.id(); if (isHidden(collection)) { continue; } auto collectionIt = m_collections.find(collectionId); if (collectionIt != m_collections.end()) { // This is probably the result of a parent of a previous collection already being in the model. // Replace the dummy collection with the real one and move on. // This could also be the result of a monitor signal having already inserted the collection // into this model. There's no way to tell, so we just emit dataChanged. *collectionIt = collection; const QModelIndex collectionIndex = indexForCollection(collection); dataChanged(collectionIndex, collectionIndex); Q_EMIT q->collectionFetched(collectionId); continue; } //If we're monitoring collections somewhere in the tree we need to retrieve their ancestors now if (collection.parentCollection() != m_rootCollection && m_monitor->collectionsMonitored().contains(collection)) { retrieveAncestors(collection, false); } collectionsToInsert.insert(collectionId, collection); } //Build a list of subtrees to insert, with the root of the subtree on the left, and the complete subtree including root on the right std::unordered_map > subTreesToInsert; { //Build a child-parent map that allows us to build the subtrees afterwards std::unordered_map childParentMap; for (const auto &col : collectionsToInsert) { childParentMap.insert({col.id(), col.parentCollection().id()}); //Complete the subtree up to the last known parent Collection parent = col.parentCollection(); while (parent.isValid() && parent != m_rootCollection && !m_collections.contains(parent.id())) { childParentMap.insert({parent.id(), parent.parentCollection().id()}); if (!collectionsToInsert.contains(parent.id())) { collectionsToInsert.insert(parent.id(), parent); } parent = parent.parentCollection(); } } QSet parents; //Find toplevel parents of the subtrees for (const auto &[childId, parentId] : childParentMap) { //The child has a parent without parent (it's a toplevel node that is not yet in m_collections) if (childParentMap.find(parentId) == childParentMap.cend()) { Q_ASSERT(!m_collections.contains(childId)); parents.insert(childId); } } //Find children of each subtree for (const auto parentId : parents) { QSet children; //We add the parent itself as well so it can be inserted below as part of the same loop children << parentId; children += getChildren(parentId, childParentMap); subTreesToInsert.insert_or_assign(parentId, std::move(children)); } } const int row = 0; for (const auto &[topCollectionId, subtree] : subTreesToInsert) { qCDebug(DebugETM) << "Subtree: " << topCollectionId << subtree; Q_ASSERT(!m_collections.contains(topCollectionId)); Collection topCollection = collectionsToInsert.value(topCollectionId); Q_ASSERT(topCollection.isValid()); //The toplevels parent must already be part of the model Q_ASSERT(m_collections.contains(topCollection.parentCollection().id())); const QModelIndex parentIndex = indexForCollection(topCollection.parentCollection()); q->beginInsertRows(parentIndex, row, row); Q_ASSERT(!subtree.empty()); for (const auto collectionId : subtree) { const Collection collection = collectionsToInsert.take(collectionId); Q_ASSERT(collection.isValid()); m_collections.insert(collectionId, collection); Q_ASSERT(collection.parentCollection().isValid()); prependNode(new Node{Node::Collection, collectionId, collection.parentCollection().id()}); } q->endInsertRows(); if (m_itemPopulation == EntityTreeModel::ImmediatePopulation) { for (const auto collectionId : subtree) { const auto col = m_collections.value(collectionId); if (!m_mimeChecker.hasWantedMimeTypes() || m_mimeChecker.isWantedCollection(col)) { fetchItems(col); } else { // Consider collections that don't contain relevant mimetypes to be populated m_populatedCols.insert(collectionId); Q_EMIT q_ptr->collectionPopulated(collectionId); const auto idx = indexForCollection(Collection(collectionId)); Q_ASSERT(idx.isValid()); dataChanged(idx, idx); } } } } } void EntityTreeModelPrivate::itemsFetched(const Collection::Id collectionId, const Akonadi::Item::List &items) { Q_Q(EntityTreeModel); if (!m_collections.contains(collectionId)) { qCWarning(AKONADICORE_LOG) << "Collection has been removed while fetching items"; return; } const Collection collection = m_collections.value(collectionId); Q_ASSERT(collection.isValid()); // if there are any items at all, remove from set of collections known to be empty if (!items.isEmpty()) { m_collectionsWithoutItems.remove(collectionId); } Item::List itemsToInsert; for (const auto &item : items) { if (isHidden(item)) { continue; } if ((!m_mimeChecker.hasWantedMimeTypes() || m_mimeChecker.isWantedItem(item))) { // When listing virtual collections we might get results for items which are already in // the model if their concrete collection has already been listed. // In that case the collectionId should be different though. // As an additional complication, new items might be both part of fetch job results and // part of monitor notifications. We only insert items which are not already in the model // considering their (possibly virtual) parent. bool isNewItem = true; auto itemIt = m_items.find(item.id()); if (itemIt != m_items.end()) { const Akonadi::Collection::List parents = getParentCollections(item); for (const Akonadi::Collection &parent : parents) { if (parent.id() == collectionId) { qCWarning(AKONADICORE_LOG) << "Fetched an item which is already in the model"; // Update it in case the revision changed; itemIt->value.apply(item); isNewItem = false; break; } } } if (isNewItem) { itemsToInsert << item; } } } if (!itemsToInsert.isEmpty()) { const Collection::Id colId = m_collectionFetchStrategy == EntityTreeModel::InvisibleCollectionFetch ? m_rootCollection.id() : m_collectionFetchStrategy == EntityTreeModel::FetchNoCollections ? m_rootCollection.id() : collectionId; const int startRow = m_childEntities.value(colId).size(); Q_ASSERT(m_collections.contains(colId)); const QModelIndex parentIndex = indexForCollection(m_collections.value(colId)); q->beginInsertRows(parentIndex, startRow, startRow + itemsToInsert.size() - 1); - for (const Item &item : itemsToInsert) { + for (const Item &item : qAsConst(itemsToInsert)) { const Item::Id itemId = item.id(); m_items.ref(itemId, item); m_childEntities[colId].append(new Node{Node::Item, itemId, collectionId}); } q->endInsertRows(); } } void EntityTreeModelPrivate::monitoredMimeTypeChanged(const QString &mimeType, bool monitored) { beginResetModel(); if (monitored) { m_mimeChecker.addWantedMimeType(mimeType); } else { m_mimeChecker.removeWantedMimeType(mimeType); } endResetModel(); } void EntityTreeModelPrivate::monitoredCollectionsChanged(const Akonadi::Collection &collection, bool monitored) { if (monitored) { const CollectionFetchJob::Type fetchType = getFetchType(m_collectionFetchStrategy); fetchCollections(collection, CollectionFetchJob::Base); fetchCollections(collection, fetchType); } else { //If a collection is dereferenced and no longer explicitly monitored it might still match other filters if (!shouldBePartOfModel(collection)) { monitoredCollectionRemoved(collection); } } } void EntityTreeModelPrivate::monitoredItemsChanged(const Akonadi::Item &item, bool monitored) { Q_UNUSED(item) Q_UNUSED(monitored) beginResetModel(); endResetModel(); } void EntityTreeModelPrivate::monitoredResourcesChanged(const QByteArray &resource, bool monitored) { Q_UNUSED(resource) Q_UNUSED(monitored) beginResetModel(); endResetModel(); } bool EntityTreeModelPrivate::retrieveAncestors(const Akonadi::Collection &collection, bool insertBaseCollection) { Q_Q(EntityTreeModel); Collection parentCollection = collection.parentCollection(); Q_ASSERT(parentCollection.isValid()); Q_ASSERT(parentCollection != Collection::root()); Collection::List ancestors; while (parentCollection != Collection::root() && !m_collections.contains(parentCollection.id())) { // Put a temporary node in the tree later. ancestors.prepend(parentCollection); parentCollection = parentCollection.parentCollection(); // If we got here through Collection added notification, the parent chain may be incomplete // and if the model is still populating or the collection belongs to a yet-unknown subtree // this will break here if (!parentCollection.isValid()) { break; } } // if m_rootCollection is Collection::root(), we always have common ancestor and do the retrieval // if we traversed up to Collection::root() but are looking at a subtree only (m_rootCollection != Collection::root()) // we have no common ancestor, and we don't have to retrieve anything if (parentCollection == Collection::root() && m_rootCollection != Collection::root()) { return true; } if (ancestors.isEmpty() && !insertBaseCollection) { //Nothing to do, avoid emitting insert signals return true; } CollectionFetchJob *job = nullptr; // We were unable to reach the top of the tree due to an incomplete ancestor chain, we will have // to retrieve it from the server. if (!parentCollection.isValid()) { if (insertBaseCollection) { job = new CollectionFetchJob(collection, CollectionFetchJob::Recursive, m_session); } else { job = new CollectionFetchJob(collection.parentCollection(), CollectionFetchJob::Recursive, m_session); } } else if (!ancestors.isEmpty()) { // Fetch the real ancestors job = new CollectionFetchJob(ancestors, CollectionFetchJob::Base, m_session); } if (job) { job->fetchScope().setListFilter(m_listFilter); job->fetchScope().setIncludeStatistics(m_includeStatistics); q->connect(job, SIGNAL(collectionsReceived(Akonadi::Collection::List)), q, SLOT(ancestorsFetched(Akonadi::Collection::List))); q->connect(job, SIGNAL(result(KJob*)), q, SLOT(collectionFetchJobDone(KJob*))); } if (!parentCollection.isValid()) { // We can't proceed to insert the fake collections to complete the tree because // we do not have the complete ancestor chain. However, once the fetch job is // finished the tree will be populated accordingly. return false; } // Q_ASSERT( parentCollection != m_rootCollection ); const QModelIndex parent = indexForCollection(parentCollection); // Still prepending all collections for now. int row = 0; // Although we insert several Collections here, we only need to notify though the model // about the top-level one. The rest will be found automatically by the view. q->beginInsertRows(parent, row, row); - for (const auto &ancestor : ancestors) { + for (const auto &ancestor : qAsConst(ancestors)) { Q_ASSERT(ancestor.parentCollection().isValid()); m_collections.insert(ancestor.id(), ancestor); prependNode(new Node{Node::Collection, ancestor.id(), ancestor.parentCollection().id()}); } if (insertBaseCollection) { m_collections.insert(collection.id(), collection); // Can't just use parentCollection because that doesn't necessarily refer to collection. prependNode(new Node{Node::Collection, collection.id(), collection.parentCollection().id()}); } q->endInsertRows(); return true; } void EntityTreeModelPrivate::ancestorsFetched(const Akonadi::Collection::List &collectionList) { for (const Collection &collection : collectionList) { m_collections[collection.id()] = collection; const QModelIndex index = indexForCollection(collection); Q_ASSERT(index.isValid()); dataChanged(index, index); } } void EntityTreeModelPrivate::insertCollection(const Akonadi::Collection &collection, const Akonadi::Collection &parent) { Q_ASSERT(collection.isValid()); Q_ASSERT(parent.isValid()); Q_Q(EntityTreeModel); const int row = 0; const QModelIndex parentIndex = indexForCollection(parent); q->beginInsertRows(parentIndex, row, row); m_collections.insert(collection.id(), collection); prependNode(new Node{Node::Collection, collection.id(), parent.id()}); q->endInsertRows(); } bool EntityTreeModelPrivate::hasChildCollection(const Collection &collection) const { const auto &children = m_childEntities[collection.id()]; for (const Node *node : children) { if (node->type == Node::Collection) { const Collection subcol = m_collections[node->id]; if (shouldBePartOfModel(subcol)) { return true; } } } return false; } bool EntityTreeModelPrivate::isAncestorMonitored(const Collection &collection) const { Akonadi::Collection parent = collection.parentCollection(); while (parent.isValid()) { if (m_monitor->collectionsMonitored().contains(parent)) { return true; } parent = parent.parentCollection(); } return false; } bool EntityTreeModelPrivate::shouldBePartOfModel(const Collection &collection) const { if (isHidden(collection)) { return false; } // We want a parent collection if it has at least one child that matches the // wanted mimetype if (hasChildCollection(collection)) { return true; } //Explicitly monitored collection if (m_monitor->collectionsMonitored().contains(collection)) { return true; } //We're explicitly monitoring collections, but didn't match the filter if (!m_mimeChecker.hasWantedMimeTypes() && !m_monitor->collectionsMonitored().isEmpty()) { //The collection should be included if one of the parents is monitored if (isAncestorMonitored(collection)) { return true; } return false; } // Some collection trees contain multiple mimetypes. Even though server side filtering ensures we // only get the ones we're interested in from the job, we have to filter on collections received through signals too. if (m_mimeChecker.hasWantedMimeTypes() && !m_mimeChecker.isWantedCollection(collection)) { return false; } if (m_listFilter == CollectionFetchScope::Enabled) { if (!collection.enabled()) { return false; } } else if (m_listFilter == CollectionFetchScope::Display) { if (!collection.shouldList(Collection::ListDisplay)) { return false; } } else if (m_listFilter == CollectionFetchScope::Sync) { if (!collection.shouldList(Collection::ListSync)) { return false; } } else if (m_listFilter == CollectionFetchScope::Index) { if (!collection.shouldList(Collection::ListIndex)) { return false; } } return true; } void EntityTreeModelPrivate::removeChildEntities(Collection::Id collectionId) { const QList childList = m_childEntities.value(collectionId); for (const Node *node : childList) { if (node->type == Node::Item) { m_items.unref(node->id); } else { removeChildEntities(node->id); m_collections.remove(node->id); m_populatedCols.remove(node->id); } } qDeleteAll(m_childEntities.take(collectionId)); } QStringList EntityTreeModelPrivate::childCollectionNames(const Collection &collection) const { return m_childEntities[collection.id()] | Views::filter([](const Node *node) { return node->type == Node::Collection; }) | Views::transform([this](const Node *node) { return m_collections.value(node->id).name(); }) | Actions::toQList; } void EntityTreeModelPrivate::monitoredCollectionAdded(const Akonadi::Collection &collection, const Akonadi::Collection &parent) { // If the resource is removed while populating the model with it, we might still // get some monitor signals. These stale/out-of-order signals can't be completely eliminated // in the akonadi server due to implementation details, so we also handle such signals in the model silently // in all the monitored slots. // Stephen Kelly, 28, July 2009 // If a fetch job is started and a collection is added to akonadi after the fetch job is started, the // new collection will be added to the fetch job results. It will also be notified through the monitor. // We return early here in that case. if (m_collections.contains(collection.id())) { return; } //If the resource is explicitly monitored all other checks are skipped. topLevelCollectionsFetched still checks the hidden attribute. if (m_monitor->resourcesMonitored().contains(collection.resource().toUtf8()) && collection.parentCollection() == Collection::root()) { - return topLevelCollectionsFetched(Collection::List() << collection); + topLevelCollectionsFetched({collection}); + return; } if (!shouldBePartOfModel(collection)) { return; } if (!m_collections.contains(parent.id())) { // The collection we're interested in is contained in a collection we're not interested in. // We download the ancestors of the collection we're interested in to complete the tree. if (collection != Collection::root()) { if (!retrieveAncestors(collection)) { return; } } } else { insertCollection(collection, parent); } if (m_itemPopulation == EntityTreeModel::ImmediatePopulation) { fetchItems(collection); } } void EntityTreeModelPrivate::monitoredCollectionRemoved(const Akonadi::Collection &collection) { //if an explicitly monitored collection is removed, we would also have to remove collections which were included to show it (as in the move case) if ((collection == m_rootCollection) || m_monitor->collectionsMonitored().contains(collection)) { beginResetModel(); endResetModel(); return; } Collection::Id parentId = collection.parentCollection().id(); if (parentId < 0) { parentId = -1; } if (!m_collections.contains(parentId)) { return; } // This may be a signal for a collection we've already removed by removing its ancestor. // Or the collection may have been hidden. if (!m_collections.contains(collection.id())) { return; } Q_Q(EntityTreeModel); Q_ASSERT(m_childEntities.contains(parentId)); const int row = indexOf(m_childEntities.value(parentId), collection.id()); Q_ASSERT(row >= 0); Q_ASSERT(m_collections.contains(parentId)); const Collection parentCollection = m_collections.value(parentId); m_populatedCols.remove(collection.id()); const QModelIndex parentIndex = indexForCollection(parentCollection); q->beginRemoveRows(parentIndex, row, row); // Delete all descendant collections and items. removeChildEntities(collection.id()); // Remove deleted collection from its parent. delete m_childEntities[parentId].takeAt(row); // Remove deleted collection itself. m_collections.remove(collection.id()); q->endRemoveRows(); // After removing a collection, check whether it's parent should be removed too if (!shouldBePartOfModel(parentCollection)) { monitoredCollectionRemoved(parentCollection); } } void EntityTreeModelPrivate::monitoredCollectionMoved(const Akonadi::Collection &collection, const Akonadi::Collection &sourceCollection, const Akonadi::Collection &destCollection) { if (isHidden(collection)) { return; } if (isHidden(sourceCollection)) { if (isHidden(destCollection)) { return; } monitoredCollectionAdded(collection, destCollection); return; } else if (isHidden(destCollection)) { monitoredCollectionRemoved(collection); return; } if (!m_collections.contains(collection.id())) { return; } if (m_monitor->collectionsMonitored().contains(collection)) { //if we don't reset here, we would have to make sure that destination collection is actually available, //and remove the sources parents if they were only included as parents of the moved collection beginResetModel(); endResetModel(); return; } Q_Q(EntityTreeModel); const QModelIndex srcParentIndex = indexForCollection(sourceCollection); const QModelIndex destParentIndex = indexForCollection(destCollection); Q_ASSERT(collection.parentCollection().isValid()); Q_ASSERT(destCollection.isValid()); Q_ASSERT(collection.parentCollection() == destCollection); const int srcRow = indexOf(m_childEntities.value(sourceCollection.id()), collection.id()); const int destRow = 0; // Prepend collections if (!q->beginMoveRows(srcParentIndex, srcRow, srcRow, destParentIndex, destRow)) { qCWarning(AKONADICORE_LOG) << "Cannot move collection" << collection.id() << " from collection" << sourceCollection.id() << "to" << destCollection.id(); return; } Node *node = m_childEntities[sourceCollection.id()].takeAt(srcRow); // collection has the correct parentCollection etc. We need to set it on the // internal data structure to not corrupt things. m_collections.insert(collection.id(), collection); node->parent = destCollection.id(); m_childEntities[destCollection.id()].prepend(node); q->endMoveRows(); } void EntityTreeModelPrivate::monitoredCollectionChanged(const Akonadi::Collection &collection) { if (!m_collections.contains(collection.id())) { // This can happen if // * we get a change notification after removing the collection. // * a collection of a non-monitored mimetype is changed elsewhere. Monitor does not // filter by content mimetype of Collections so we get notifications for all of them. //We might match the filter now, retry adding the collection monitoredCollectionAdded(collection, collection.parentCollection()); return; } if (!shouldBePartOfModel(collection)) { monitoredCollectionRemoved(collection); return; } m_collections[collection.id()] = collection; if (!m_showRootCollection && collection == m_rootCollection) { // If the root of the model is not Collection::root it might be modified. // But it doesn't exist in the accessible model structure, so we need to early return return; } const QModelIndex index = indexForCollection(collection); Q_ASSERT(index.isValid()); dataChanged(index, index); } void EntityTreeModelPrivate::monitoredCollectionStatisticsChanged(Akonadi::Collection::Id id, const Akonadi::CollectionStatistics &statistics) { if (!m_collections.contains(id)) { return; } m_collections[id].setStatistics(statistics); // if the item count becomes 0, add to set of collections we know to be empty // otherwise remove if in there if (statistics.count() == 0) { m_collectionsWithoutItems.insert(id); } else { m_collectionsWithoutItems.remove(id); } if (!m_showRootCollection && id == m_rootCollection.id()) { // If the root of the model is not Collection::root it might be modified. // But it doesn't exist in the accessible model structure, so we need to early return return; } const QModelIndex index = indexForCollection(m_collections[id]); dataChanged(index, index); } void EntityTreeModelPrivate::monitoredItemAdded(const Akonadi::Item &item, const Akonadi::Collection &collection) { Q_Q(EntityTreeModel); if (isHidden(item)) { return; } if (m_collectionFetchStrategy != EntityTreeModel::InvisibleCollectionFetch && !m_collections.contains(collection.id())) { qCWarning(AKONADICORE_LOG) << "Got a stale 'added' notification for an item whose collection was already removed." << item.id() << item.remoteId(); return; } if (m_items.contains(item.id())) { return; } Q_ASSERT(m_collectionFetchStrategy != EntityTreeModel::InvisibleCollectionFetch ? m_collections.contains(collection.id()) : true); if (m_mimeChecker.hasWantedMimeTypes() && !m_mimeChecker.isWantedItem(item)) { return; } //Adding items to not yet populated collections would block fetchMore, resulting in only new items showing up in the collection //This is only a problem with lazy population, otherwise fetchMore is not used at all if ((m_itemPopulation == EntityTreeModel::LazyPopulation) && !m_populatedCols.contains(collection.id())) { return; } int row; QModelIndex parentIndex; if (m_collectionFetchStrategy != EntityTreeModel::InvisibleCollectionFetch) { row = m_childEntities.value(collection.id()).size(); parentIndex = indexForCollection(m_collections.value(collection.id())); } else { row = q->rowCount(); } q->beginInsertRows(parentIndex, row, row); m_items.ref(item.id(), item); Node *node = new Node{Node::Item, item.id(), collection.id()}; if (m_collectionFetchStrategy != EntityTreeModel::InvisibleCollectionFetch) { m_childEntities[collection.id()].append(node); } else { m_childEntities[m_rootCollection.id()].append(node); } q->endInsertRows(); } void EntityTreeModelPrivate::monitoredItemRemoved(const Akonadi::Item &item, const Akonadi::Collection &parentCollection) { Q_Q(EntityTreeModel); if (isHidden(item)) { return; } if ((m_itemPopulation == EntityTreeModel::LazyPopulation) && !m_populatedCols.contains(parentCollection.isValid() ? parentCollection.id() : item.parentCollection().id())) { return; } const Collection::List parents = getParentCollections(item); if (parents.isEmpty()) { return; } if (!m_items.contains(item.id())) { qCWarning(AKONADICORE_LOG) << "Got a stale 'removed' notification for an item which was already removed." << item.id() << item.remoteId(); return; } for (const auto &collection : parents) { Q_ASSERT(m_collections.contains(collection.id())); Q_ASSERT(m_childEntities.contains(collection.id())); const int row = indexOf(m_childEntities.value(collection.id()), item.id()); Q_ASSERT(row >= 0); const QModelIndex parentIndex = indexForCollection(m_collections.value(collection.id())); q->beginRemoveRows(parentIndex, row, row); m_items.unref(item.id()); delete m_childEntities[collection.id()].takeAt(row); q->endRemoveRows(); } } void EntityTreeModelPrivate::monitoredItemChanged(const Akonadi::Item &item, const QSet &) { if (isHidden(item)) { return; } if ((m_itemPopulation == EntityTreeModel::LazyPopulation) && !m_populatedCols.contains(item.parentCollection().id())) { return; } auto itemIt = m_items.find(item.id()); if (itemIt == m_items.end()) { qCWarning(AKONADICORE_LOG) << "Got a stale 'changed' notification for an item which was already removed." << item.id() << item.remoteId(); return; } itemIt->value.apply(item); // Notifications about itemChange are always dispatched for real collection // and also all virtual collections the item belongs to. In order to preserve // the original storage collection when we need to have special handling for // notifications for virtual collections if (item.parentCollection().isVirtual()) { const Collection originalParent = itemIt->value.parentCollection(); itemIt->value.setParentCollection(originalParent); } const QModelIndexList indexes = indexesForItem(item); for (const QModelIndex &index : indexes) { if (index.isValid()) { dataChanged(index, index); } else { qCWarning(AKONADICORE_LOG) << "item has invalid index:" << item.id() << item.remoteId(); } } } void EntityTreeModelPrivate::monitoredItemMoved(const Akonadi::Item &item, const Akonadi::Collection &sourceCollection, const Akonadi::Collection &destCollection) { if (isHidden(item)) { return; } if (isHidden(sourceCollection)) { if (isHidden(destCollection)) { return; } monitoredItemAdded(item, destCollection); return; } else if (isHidden(destCollection)) { monitoredItemRemoved(item, sourceCollection); return; } else { monitoredItemRemoved(item, sourceCollection); monitoredItemAdded(item, destCollection); return; } // "Temporarily" commented out as it's likely the best course to // avoid the dreaded "reset storm" (or layoutChanged storm). The // whole itemMoved idea is great but not practical until all the // other proxy models play nicely with it, right now they just // transform moved signals in layout changed, which explodes into // a reset of the source model inside of the message list (ouch!) #if 0 if (!m_items.contains(item.id())) { qCWarning(AKONADICORE_LOG) << "Got a stale 'moved' notification for an item which was already removed." << item.id() << item.remoteId(); return; } Q_ASSERT(m_collections.contains(sourceCollection.id())); Q_ASSERT(m_collections.contains(destCollection.id())); const QModelIndex srcIndex = indexForCollection(sourceCollection); const QModelIndex destIndex = indexForCollection(destCollection); // Where should it go? Always append items and prepend collections and reorganize them with separate reactions to Attributes? const Item::Id itemId = item.id(); const int srcRow = indexOf(m_childEntities.value(sourceCollection.id()), itemId); const int destRow = q->rowCount(destIndex); Q_ASSERT(srcRow >= 0); Q_ASSERT(destRow >= 0); if (!q->beginMoveRows(srcIndex, srcRow, srcRow, destIndex, destRow)) { qCWarning(AKONADICORE_LOG) << "Invalid move"; return; } Q_ASSERT(m_childEntities.contains(sourceCollection.id())); Q_ASSERT(m_childEntities[sourceCollection.id()].size() > srcRow); Node *node = m_childEntities[sourceCollection.id()].takeAt(srcRow); m_items.insert(item.id(), item); node->parent = destCollection.id(); m_childEntities[destCollection.id()].append(node); q->endMoveRows(); #endif } void EntityTreeModelPrivate::monitoredItemLinked(const Akonadi::Item &item, const Akonadi::Collection &collection) { Q_Q(EntityTreeModel); if (isHidden(item)) { return; } const Collection::Id collectionId = collection.id(); const Item::Id itemId = item.id(); if (m_collectionFetchStrategy != EntityTreeModel::InvisibleCollectionFetch && !m_collections.contains(collection.id())) { qCWarning(AKONADICORE_LOG) << "Got a stale 'linked' notification for an item whose collection was already removed." << item.id() << item.remoteId(); return; } Q_ASSERT(m_collectionFetchStrategy != EntityTreeModel::InvisibleCollectionFetch ? m_collections.contains(collectionId) : true); if (m_mimeChecker.hasWantedMimeTypes() && !m_mimeChecker.isWantedItem(item)) { return; } //Adding items to not yet populated collections would block fetchMore, resullting in only new items showing up in the collection //This is only a problem with lazy population, otherwise fetchMore is not used at all if ((m_itemPopulation == EntityTreeModel::LazyPopulation) && !m_populatedCols.contains(collectionId)) { return; } QList &collectionEntities = m_childEntities[collectionId]; const int existingPosition = indexOf(collectionEntities, itemId); if (existingPosition > 0) { qCWarning(AKONADICORE_LOG) << "Item with id " << itemId << " already in virtual collection with id " << collectionId; return; } const int row = collectionEntities.size(); const QModelIndex parentIndex = indexForCollection(m_collections.value(collectionId)); q->beginInsertRows(parentIndex, row, row); m_items.ref(itemId, item); collectionEntities.append(new Node{Node::Item, itemId, collectionId}); q->endInsertRows(); } void EntityTreeModelPrivate::monitoredItemUnlinked(const Akonadi::Item &item, const Akonadi::Collection &collection) { Q_Q(EntityTreeModel); if (isHidden(item)) { return; } if ((m_itemPopulation == EntityTreeModel::LazyPopulation) && !m_populatedCols.contains(item.parentCollection().id())) { return; } if (!m_items.contains(item.id())) { qCWarning(AKONADICORE_LOG) << "Got a stale 'unlinked' notification for an item which was already removed." << item.id() << item.remoteId(); return; } Q_ASSERT(m_collectionFetchStrategy != EntityTreeModel::InvisibleCollectionFetch ? m_collections.contains(collection.id()) : true); const int row = indexOf(m_childEntities.value(collection.id()), item.id()); if (row < 0 || row >= m_childEntities[ collection.id() ].size()) { qCWarning(AKONADICORE_LOG) << "couldn't find index of unlinked item " << item.id() << collection.id() << row; Q_ASSERT(false); return; } const QModelIndex parentIndex = indexForCollection(m_collections.value(collection.id())); q->beginRemoveRows(parentIndex, row, row); delete m_childEntities[collection.id()].takeAt(row); m_items.unref(item.id()); q->endRemoveRows(); } void EntityTreeModelPrivate::collectionFetchJobDone(KJob *job) { m_pendingCollectionFetchJobs.remove(job); CollectionFetchJob *cJob = static_cast(job); if (job->error()) { qCWarning(AKONADICORE_LOG) << "Job error: " << job->errorString() << "for collection:" << cJob->collections(); return; } if (!m_collectionTreeFetched && m_pendingCollectionFetchJobs.isEmpty()) { m_collectionTreeFetched = true; Q_EMIT q_ptr->collectionTreeFetched(m_collections | Views::values | Actions::toQVector); } qCDebug(DebugETM) << "Fetch job took " << jobTimeTracker.take(job).elapsed() << "msec"; qCDebug(DebugETM) << "was collection fetch job: collections:" << cJob->collections().size(); if (!cJob->collections().isEmpty()) { qCDebug(DebugETM) << "first fetched collection:" << cJob->collections().at(0).name(); } } void EntityTreeModelPrivate::itemFetchJobDone(Collection::Id collectionId, KJob *job) { m_pendingCollectionRetrieveJobs.remove(collectionId); if (job->error()) { qCWarning(AKONADICORE_LOG) << "Job error: " << job->errorString() << "for collection:" << collectionId; return; } if (!m_collections.contains(collectionId)) { qCWarning(AKONADICORE_LOG) << "Collection has been removed while fetching items"; return; } ItemFetchJob *iJob = static_cast(job); qCDebug(DebugETM) << "Fetch job took " << jobTimeTracker.take(job).elapsed() << "msec"; qCDebug(DebugETM) << "was item fetch job: items:" << iJob->count(); if (iJob->count() == 0) { m_collectionsWithoutItems.insert(collectionId); } else { m_collectionsWithoutItems.remove(collectionId); } m_populatedCols.insert(collectionId); Q_EMIT q_ptr->collectionPopulated(collectionId); // If collections are not in the model, there will be no valid index for them. if ((m_collectionFetchStrategy != EntityTreeModel::InvisibleCollectionFetch) && (m_collectionFetchStrategy != EntityTreeModel::FetchNoCollections) && (m_showRootCollection || collectionId != m_rootCollection.id())) { const QModelIndex index = indexForCollection(Collection(collectionId)); Q_ASSERT(index.isValid()); //To notify about the changed fetch and population state dataChanged(index, index); } } void EntityTreeModelPrivate::pasteJobDone(KJob *job) { if (job->error()) { QString errorMsg; if (qobject_cast(job)) { errorMsg = i18nc("@info", "Could not copy item: %1", job->errorString()); } else if (qobject_cast(job)) { errorMsg = i18nc("@info", "Could not copy collection: %1", job->errorString()); } else if (qobject_cast(job)) { errorMsg = i18nc("@info", "Could not move item: %1", job->errorString()); } else if (qobject_cast(job)) { errorMsg = i18nc("@info", "Could not move collection: %1", job->errorString()); } else if (qobject_cast(job)) { errorMsg = i18nc("@info", "Could not link entity: %1", job->errorString()); } QMessageBox::critical(nullptr, i18nc("@title:window", "Error"), errorMsg); } } void EntityTreeModelPrivate::updateJobDone(KJob *job) { if (job->error()) { // TODO: handle job errors qCWarning(AKONADICORE_LOG) << "Job error:" << job->errorString(); } } void EntityTreeModelPrivate::rootFetchJobDone(KJob *job) { if (job->error()) { qCWarning(AKONADICORE_LOG) << job->errorString(); return; } CollectionFetchJob *collectionJob = qobject_cast(job); const Collection::List list = collectionJob->collections(); Q_ASSERT(list.size() == 1); m_rootCollection = list.first(); startFirstListJob(); } void EntityTreeModelPrivate::startFirstListJob() { Q_Q(EntityTreeModel); if (!m_collections.isEmpty()) { return; } // Even if the root collection is the invalid collection, we still need to start // the first list job with Collection::root. auto node = new Node{Node::Collection, m_rootCollection.id(), -1}; if (m_showRootCollection) { // Notify the outside that we're putting collection::root into the model. q->beginInsertRows(QModelIndex(), 0, 0); m_collections.insert(m_rootCollection.id(), m_rootCollection); delete m_rootNode; appendNode(node); q->endInsertRows(); } else { // Otherwise store it silently because it's not part of the usable model. delete m_rootNode; m_rootNode = node; m_collections.insert(m_rootCollection.id(), m_rootCollection); } const bool noMimetypes = !m_mimeChecker.hasWantedMimeTypes(); const bool noResources = m_monitor->resourcesMonitored().isEmpty(); const bool multipleCollections = m_monitor->collectionsMonitored().size() > 1; const bool generalPopulation = !noMimetypes || noResources; const CollectionFetchJob::Type fetchType = getFetchType(m_collectionFetchStrategy); //Collections can only be monitored if no resources and no mimetypes are monitored if (multipleCollections && noMimetypes && noResources) { fetchCollections(m_monitor->collectionsMonitored(), CollectionFetchJob::Base); fetchCollections(m_monitor->collectionsMonitored(), fetchType); return; } qCDebug(DebugETM) << "GEN" << generalPopulation << noMimetypes << noResources; if (generalPopulation) { fetchCollections(m_rootCollection, fetchType); } // If the root collection is not collection::root, then it could have items, and they will need to be // retrieved now. // Only fetch items NOT if there is NoItemPopulation, or if there is Lazypopulation and the root is visible // (if the root is not visible the lazy population can not be triggered) if ((m_itemPopulation != EntityTreeModel::NoItemPopulation) && !((m_itemPopulation == EntityTreeModel::LazyPopulation) && m_showRootCollection)) { if (m_rootCollection != Collection::root()) { fetchItems(m_rootCollection); } } // Resources which are explicitly monitored won't have appeared yet if their mimetype didn't match. // We fetch the top level collections and examine them for whether to add them. // This fetches virtual collections into the tree. if (!m_monitor->resourcesMonitored().isEmpty()) { fetchTopLevelCollections(); } } void EntityTreeModelPrivate::fetchTopLevelCollections() const { Q_Q(const EntityTreeModel); CollectionFetchJob *job = new CollectionFetchJob(Collection::root(), CollectionFetchJob::FirstLevel, m_session); q->connect(job, SIGNAL(collectionsReceived(Akonadi::Collection::List)), q, SLOT(topLevelCollectionsFetched(Akonadi::Collection::List))); q->connect(job, SIGNAL(result(KJob*)), q, SLOT(collectionFetchJobDone(KJob*))); qCDebug(DebugETM) << "EntityTreeModelPrivate::fetchTopLevelCollections"; jobTimeTracker[job].start(); } void EntityTreeModelPrivate::topLevelCollectionsFetched(const Akonadi::Collection::List &list) { Q_Q(EntityTreeModel); for (const Collection &collection : list) { // These collections have been explicitly shown in the Monitor, // but hidden trumps that for now. This may change in the future if we figure out a use for it. if (isHidden(collection)) { continue; } if (m_monitor->resourcesMonitored().contains(collection.resource().toUtf8()) && !m_collections.contains(collection.id())) { const QModelIndex parentIndex = indexForCollection(collection.parentCollection()); // Prepending new collections. const int row = 0; q->beginInsertRows(parentIndex, row, row); m_collections.insert(collection.id(), collection); Q_ASSERT(collection.parentCollection() == Collection::root()); prependNode(new Node{Node::Collection, collection.id(), collection.parentCollection().id()}); q->endInsertRows(); if (m_itemPopulation == EntityTreeModel::ImmediatePopulation) { fetchItems(collection); } Q_ASSERT(collection.isValid()); fetchCollections(collection, CollectionFetchJob::Recursive); } } } Akonadi::Collection::List EntityTreeModelPrivate::getParentCollections(const Item &item) const { Collection::List list; for (auto it = m_childEntities.constKeyValueBegin(), end = m_childEntities.constKeyValueEnd(); it != end; ++it) { const auto &[parentId, childNodes] = *it; int nodeIndex = indexOf(childNodes, item.id()); if (nodeIndex != -1 && childNodes.at(nodeIndex)->type == Node::Item) { list.push_back(m_collections.value(parentId)); } } return list; } void EntityTreeModelPrivate::ref(Collection::Id id) { m_monitor->d_ptr->ref(id); } bool EntityTreeModelPrivate::shouldPurge(Collection::Id id) { // reference counted collections should never be purged // they first have to be deref'ed until they reach 0. // if the collection is buffered, keep it. if (m_monitor->d_ptr->isMonitored(id)) { return false; } // otherwise we can safely purge this item return true; } bool EntityTreeModelPrivate::isMonitored(Collection::Id id) { return m_monitor->d_ptr->isMonitored(id); } bool EntityTreeModelPrivate::isBuffered(Collection::Id id) { return m_monitor->d_ptr->m_buffer.isBuffered(id); } void EntityTreeModelPrivate::deref(Collection::Id id) { const Collection::Id bumpedId = m_monitor->d_ptr->deref(id); if (bumpedId < 0) { return; } //The collection has already been removed, don't purge if (!m_collections.contains(bumpedId)) { return; } if (shouldPurge(bumpedId)) { purgeItems(bumpedId); } } -QList::iterator EntityTreeModelPrivate::skipCollections(QList::iterator it, QList::iterator end, int *pos) +QList::iterator EntityTreeModelPrivate::skipCollections(QList::iterator it, const QList::iterator &end, int *pos) { for (; it != end; ++it) { if ((*it)->type == Node::Item) { break; } ++(*pos); } return it; } -QList::iterator EntityTreeModelPrivate::removeItems(QList::iterator it, QList::iterator end, int *pos, const Collection &collection) +QList::iterator EntityTreeModelPrivate::removeItems(QList::iterator it, const QList::iterator &end, int *pos, const Collection &collection) { Q_Q(EntityTreeModel); QList::iterator startIt = it; // figure out how many items we will delete int start = *pos; for (; it != end; ++it) { if ((*it)->type != Node::Item) { break; } ++(*pos); } it = startIt; const QModelIndex parentIndex = indexForCollection(collection); q->beginRemoveRows(parentIndex, start, (*pos) - 1); const int toDelete = (*pos) - start; Q_ASSERT(toDelete > 0); QList &es = m_childEntities[collection.id()]; //NOTE: .erase will invalidate all iterators besides "it"! for (int i = 0; i < toDelete; ++i) { Q_ASSERT(es.count(*it) == 1); // don't keep implicitly shared data alive Q_ASSERT(m_items.contains((*it)->id)); m_items.unref((*it)->id); // delete actual node delete *it; it = es.erase(it); } q->endRemoveRows(); return it; } void EntityTreeModelPrivate::purgeItems(Collection::Id id) { QList &childEntities = m_childEntities[id]; const Collection collection = m_collections.value(id); Q_ASSERT(collection.isValid()); QList::iterator begin = childEntities.begin(); QList::iterator end = childEntities.end(); int pos = 0; while ((begin = skipCollections(begin, end, &pos)) != end) { begin = removeItems(begin, end, &pos, collection); end = childEntities.end(); } m_populatedCols.remove(id); //if an empty collection is purged and we leave it in here, itemAdded will be ignored for the collection //and the collection is never populated by fetchMore (but maybe by statistics changed?) m_collectionsWithoutItems.remove(id); } void EntityTreeModelPrivate::dataChanged(const QModelIndex &top, const QModelIndex &bottom) { Q_Q(EntityTreeModel); QModelIndex rightIndex; const Node *node = static_cast(bottom.internalPointer()); if (!node) { return; } if (node->type == Node::Collection) { rightIndex = bottom.sibling(bottom.row(), q->entityColumnCount(EntityTreeModel::CollectionTreeHeaders) - 1); } if (node->type == Node::Item) { rightIndex = bottom.sibling(bottom.row(), q->entityColumnCount(EntityTreeModel::ItemListHeaders) - 1); } Q_EMIT q->dataChanged(top, rightIndex); } QModelIndex EntityTreeModelPrivate::indexForCollection(const Collection &collection) const { Q_Q(const EntityTreeModel); if (!collection.isValid()) { return QModelIndex(); } if (m_collectionFetchStrategy == EntityTreeModel::InvisibleCollectionFetch) { return QModelIndex(); } // The id of the parent of Collection::root is not guaranteed to be -1 as assumed by startFirstListJob, // we ensure that we use -1 for the invalid Collection. Collection::Id parentId = -1; if ((collection == m_rootCollection)) { if (m_showRootCollection) { return q->createIndex(0, 0, static_cast(m_rootNode)); } return QModelIndex(); } if (collection == Collection::root()) { parentId = -1; } else if (collection.parentCollection().isValid()) { parentId = collection.parentCollection().id(); } else { for (const auto &children : m_childEntities) { const int row = indexOf(children, collection.id()); if (row < 0) { continue; } Node *node = children.at(row); return q->createIndex(row, 0, static_cast(node)); } return QModelIndex(); } const int row = indexOf(m_childEntities.value(parentId), collection.id()); if (row < 0) { return QModelIndex(); } Node *node = m_childEntities.value(parentId).at(row); return q->createIndex(row, 0, static_cast(node)); } QModelIndexList EntityTreeModelPrivate::indexesForItem(const Item &item) const { Q_Q(const EntityTreeModel); QModelIndexList indexes; if (m_collectionFetchStrategy == EntityTreeModel::FetchNoCollections) { Q_ASSERT(m_childEntities.contains(m_rootCollection.id())); QList nodeList = m_childEntities.value(m_rootCollection.id()); const int row = indexOf(nodeList, item.id()); Q_ASSERT(row >= 0); Q_ASSERT(row < nodeList.size()); Node *node = nodeList.at(row); indexes << q->createIndex(row, 0, static_cast(node)); return indexes; } const Collection::List collections = getParentCollections(item); indexes.reserve(collections.size()); for (const Collection &collection : collections) { const int row = indexOf(m_childEntities.value(collection.id()), item.id()); Q_ASSERT(row >= 0); Q_ASSERT(m_childEntities.contains(collection.id())); QList nodeList = m_childEntities.value(collection.id()); Q_ASSERT(row < nodeList.size()); Node *node = nodeList.at(row); indexes << q->createIndex(row, 0, static_cast(node)); } return indexes; } void EntityTreeModelPrivate::beginResetModel() { Q_Q(EntityTreeModel); q->beginResetModel(); } void EntityTreeModelPrivate::endResetModel() { Q_Q(EntityTreeModel); - for (Akonadi::Job *job : m_session->findChildren()) { + auto subjobs = m_session->findChildren(); + for (auto *job : subjobs) { job->disconnect(q); } m_collections.clear(); m_collectionsWithoutItems.clear(); m_populatedCols.clear(); m_items.clear(); m_pendingCollectionFetchJobs.clear(); m_pendingCollectionRetrieveJobs.clear(); m_collectionTreeFetched = false; - for (const QList &list : m_childEntities) { + for (const QList &list : qAsConst(m_childEntities)) { qDeleteAll(list); } m_childEntities.clear(); if (m_needDeleteRootNode) { m_needDeleteRootNode = false; delete m_rootNode; } m_rootNode = nullptr; q->endResetModel(); fillModel(); } void EntityTreeModelPrivate::monitoredItemsRetrieved(KJob *job) { if (job->error()) { qCWarning(AKONADICORE_LOG) << job->errorString(); return; } Q_Q(EntityTreeModel); ItemFetchJob *fetchJob = qobject_cast(job); Q_ASSERT(fetchJob); Item::List list = fetchJob->items(); q->beginResetModel(); for (const Item &item : list) { m_childEntities[-1].append(new Node{Node::Item, item.id(), m_rootCollection.id()}); m_items.ref(item.id(), item); } q->endResetModel(); } void EntityTreeModelPrivate::fillModel() { Q_Q(EntityTreeModel); m_mimeChecker.setWantedMimeTypes(m_monitor->mimeTypesMonitored()); const Collection::List collections = m_monitor->collectionsMonitored(); if (collections.isEmpty() && m_monitor->numMimeTypesMonitored() == 0 && m_monitor->numResourcesMonitored() == 0 && m_monitor->numItemsMonitored() != 0) { m_rootCollection = Collection(-1); m_collectionTreeFetched = true; Q_EMIT q_ptr->collectionTreeFetched(collections); // there are no collections to fetch const auto items = m_monitor->itemsMonitoredEx() | Views::transform([](const auto id) { return Item{id}; }) | Actions::toQVector; ItemFetchJob *itemFetch = new ItemFetchJob(items, m_session); itemFetch->setFetchScope(m_monitor->itemFetchScope()); itemFetch->fetchScope().setIgnoreRetrievalErrors(true); q->connect(itemFetch, SIGNAL(finished(KJob*)), q, SLOT(monitoredItemsRetrieved(KJob*))); return; } // In case there is only a single collection monitored, we can use this // collection as root of the node tree, in all other cases // Collection::root() is used if (collections.size() == 1) { m_rootCollection = collections.first(); } else { m_rootCollection = Collection::root(); } if (m_rootCollection == Collection::root()) { QTimer::singleShot(0, q, SLOT(startFirstListJob())); } else { Q_ASSERT(m_rootCollection.isValid()); CollectionFetchJob *rootFetchJob = new CollectionFetchJob(m_rootCollection, CollectionFetchJob::Base, m_session); q->connect(rootFetchJob, SIGNAL(result(KJob*)), SLOT(rootFetchJobDone(KJob*))); qCDebug(DebugETM) << ""; jobTimeTracker[rootFetchJob].start(); } } bool EntityTreeModelPrivate::canFetchMore(const QModelIndex &parent) const { const Item item = parent.data(EntityTreeModel::ItemRole).value(); if (m_collectionFetchStrategy == EntityTreeModel::InvisibleCollectionFetch) { return false; } if (item.isValid()) { // items can't have more rows. // TODO: Should I use this for fetching more of an item, ie more payload parts? return false; } else { // but collections can... const Collection::Id colId = parent.data(EntityTreeModel::CollectionIdRole).toULongLong(); // But the root collection can't... if (Collection::root().id() == colId) { return false; } // Collections which contain no items at all can't contain more if (m_collectionsWithoutItems.contains(colId)) { return false; } // Don't start the same job multiple times. if (m_pendingCollectionRetrieveJobs.contains(colId)) { return false; } // Can't fetch more if the collection's items have already been fetched if (m_populatedCols.contains(colId)) { return false; } // Only try to fetch more from a collection if we don't already have items in it. // Otherwise we'd spend all the time listing items in collections. return m_childEntities.value(colId) | Actions::none(Node::isItem); } } QIcon EntityTreeModelPrivate::iconForName(const QString &name) const { if (m_iconThemeName != QIcon::themeName()) { m_iconThemeName = QIcon::themeName(); m_iconCache.clear(); } QIcon &icon = m_iconCache[name]; if (icon.isNull()) { icon = QIcon::fromTheme(name); } return icon; } diff --git a/src/core/models/entitytreemodel_p.h b/src/core/models/entitytreemodel_p.h index a1d4579a7..1283c7d00 100644 --- a/src/core/models/entitytreemodel_p.h +++ b/src/core/models/entitytreemodel_p.h @@ -1,382 +1,385 @@ /* Copyright (c) 2008 Stephen Kelly 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 ENTITYTREEMODELPRIVATE_H #define ENTITYTREEMODELPRIVATE_H #include #include "item.h" #include "collectionfetchjob.h" #include "itemfetchscope.h" #include "mimetypechecker.h" #include "entitytreemodel.h" #include "akonaditests_export.h" #include Q_DECLARE_LOGGING_CATEGORY(DebugETM) namespace Akonadi { class Monitor; class AgentInstance; } struct Node { using Id = qint64; enum Type : char { Item, Collection }; explicit Node(Type type, Id id, Akonadi::Collection::Id parentId) : id(id), parent(parentId), type(type) {} static bool isItem(Node *node) { return node->type == Node::Item; } static bool isCollection(Node *node) { return node->type == Node::Collection; } Id id; Akonadi::Collection::Id parent; Type type; }; template class RefCountedHash { mutable Value *defaultValue = nullptr; public: - inline ~RefCountedHash() + explicit RefCountedHash() = default; + Q_DISABLE_COPY_MOVE(RefCountedHash) + + ~RefCountedHash() { delete defaultValue; } inline auto begin() { return mHash.begin(); } inline auto end() { return mHash.end(); } inline auto begin() const { return mHash.begin(); } inline auto end() const { return mHash.end(); } inline auto find(const Key &key) const { return mHash.find(key); } inline auto find(const Key &key) { return mHash.find(key); } inline bool size() const { return mHash.size(); } inline bool isEmpty() const { return mHash.isEmpty(); } inline void clear() { mHash.clear(); } inline bool contains(const Key &key) const { return mHash.contains(key); } inline const Value &value(const Key &key) const { auto it = mHash.find(key); if (it == mHash.end()) { return defaultValue ? *defaultValue : *(defaultValue = new Value()); } return it->value; } inline const Value &operator[](const Key &key) const { return value(key); } inline auto ref(const Key &key, const Value &value) { auto it = mHash.find(key); if (it != mHash.end()) { ++(it->refCnt); return it; } else { return mHash.insert(key, {1, std::move(value)}); } } inline void unref(const Key &key) { auto it = mHash.find(key); if (it == mHash.end()) { return; } --(it->refCnt); if (it->refCnt == 0) { mHash.erase(it); } } private: template struct RefCountedValue { uint8_t refCnt = 0; V value; }; QHash> mHash; }; namespace Akonadi { /** * @internal */ class AKONADI_TESTS_EXPORT EntityTreeModelPrivate { public: explicit EntityTreeModelPrivate(EntityTreeModel *parent); ~EntityTreeModelPrivate(); EntityTreeModel *const q_ptr; enum RetrieveDepth { Base, Recursive }; void init(Monitor *monitor); void prependNode(Node *node); void appendNode(Node *node); void fetchCollections(const Collection &collection, CollectionFetchJob::Type type = CollectionFetchJob::FirstLevel); void fetchCollections(const Collection::List &collections, CollectionFetchJob::Type type = CollectionFetchJob::FirstLevel); void fetchCollections(Akonadi::CollectionFetchJob *job); void fetchItems(const Collection &collection); void collectionsFetched(const Akonadi::Collection::List &collections); void itemsFetched(const Collection::Id collectionId, const Akonadi::Item::List &items); void monitoredCollectionAdded(const Akonadi::Collection &collection, const Akonadi::Collection &parent); void monitoredCollectionRemoved(const Akonadi::Collection &collection); void monitoredCollectionChanged(const Akonadi::Collection &collection); void monitoredCollectionStatisticsChanged(Akonadi::Collection::Id, const Akonadi::CollectionStatistics &statistics); void monitoredCollectionMoved(const Akonadi::Collection &collection, const Akonadi::Collection &sourceCollection, const Akonadi::Collection &destCollection); void monitoredItemAdded(const Akonadi::Item &item, const Akonadi::Collection &collection); void monitoredItemRemoved(const Akonadi::Item &item, const Akonadi::Collection &collection = Akonadi::Collection()); void monitoredItemChanged(const Akonadi::Item &item, const QSet &); void monitoredItemMoved(const Akonadi::Item &item, const Akonadi::Collection &, const Akonadi::Collection &); void monitoredItemLinked(const Akonadi::Item &item, const Akonadi::Collection &); void monitoredItemUnlinked(const Akonadi::Item &item, const Akonadi::Collection &); void monitoredMimeTypeChanged(const QString &mimeType, bool monitored); void monitoredCollectionsChanged(const Akonadi::Collection &collection, bool monitored); void monitoredItemsChanged(const Akonadi::Item &item, bool monitored); void monitoredResourcesChanged(const QByteArray &resource, bool monitored); Collection::List getParentCollections(const Item &item) const; void removeChildEntities(Collection::Id collectionId); /** * Returns the list of names of the child collections of @p collection. */ QStringList childCollectionNames(const Collection &collection) const; /** * Fetch parent collections and insert this @p collection and its parents into the node tree * * Returns whether the ancestor chain was complete and the parent collections were inserted into * the tree. */ bool retrieveAncestors(const Akonadi::Collection &collection, bool insertBaseCollection = true); void ancestorsFetched(const Akonadi::Collection::List &collectionList); void insertCollection(const Akonadi::Collection &collection, const Akonadi::Collection &parent); void beginResetModel(); void endResetModel(); /** * Start function for filling the Model, finds and fetches the root of the node tree * Next relevant function for filling the model is startFirstListJob() */ void fillModel(); void changeFetchState(const Collection &parent); void agentInstanceRemoved(const Akonadi::AgentInstance &instance); QIcon iconForName(const QString &name) const; QHash m_collections; RefCountedHash m_items; QHash > m_childEntities; QSet m_populatedCols; QSet m_collectionsWithoutItems; QVector m_pendingCutItems; QVector m_pendingCutCollections; mutable QSet m_pendingCollectionRetrieveJobs; mutable QSet m_pendingCollectionFetchJobs; // Icon cache to workaround QIcon::fromTheme being very slow (bug #346644) mutable QHash m_iconCache; mutable QString m_iconThemeName; Monitor *m_monitor = nullptr; Collection m_rootCollection; Node *m_rootNode = nullptr; bool m_needDeleteRootNode = false; QString m_rootCollectionDisplayName; QStringList m_mimeTypeFilter; MimeTypeChecker m_mimeChecker; EntityTreeModel::CollectionFetchStrategy m_collectionFetchStrategy = EntityTreeModel::FetchCollectionsRecursive; EntityTreeModel::ItemPopulationStrategy m_itemPopulation = EntityTreeModel::ImmediatePopulation; CollectionFetchScope::ListFilter m_listFilter = CollectionFetchScope::NoFilter; bool m_includeStatistics = false; bool m_showRootCollection = false; bool m_collectionTreeFetched = false; bool m_showSystemEntities = false; Session *m_session = nullptr; /** * Called after the root collection was fetched by fillModel * * Initiates further fetching of collections depending on the monitored collections * (in the monitor) and the m_collectionFetchStrategy. * * Further collections are either fetched directly with fetchCollections and * fetchItems or, in case that collections or resources are monitored explicitly * via fetchTopLevelCollections */ void startFirstListJob(); void serverStarted(); void monitoredItemsRetrieved(KJob *job); void rootFetchJobDone(KJob *job); void collectionFetchJobDone(KJob *job); void itemFetchJobDone(Collection::Id collectionId, KJob *job); void updateJobDone(KJob *job); void pasteJobDone(KJob *job); /** * Returns the index of the node in @p list with the id @p id. Returns -1 if not found. */ template int indexOf(const QList &nodes, Node::Id id) const { int i = 0; for (const Node *node : nodes) { if (node->id == id && node->type == Type) { return i; } i++; } return -1; } Q_DECLARE_PUBLIC(EntityTreeModel) void fetchTopLevelCollections() const; void topLevelCollectionsFetched(const Akonadi::Collection::List &collectionList); /** @returns True if @p item or one of its descendants is hidden. */ bool isHidden(const Item &item) const; bool isHidden(const Collection &collection) const; template bool isHiddenImpl(const T &entity, Node::Type type) const; void ref(Collection::Id id); void deref(Collection::Id id); /** * @returns true if the collection is actively monitored (referenced or buffered with refcounting enabled) * * purely for testing */ bool isMonitored(Collection::Id id); /** * @returns true if the collection is buffered * * purely for testing */ bool isBuffered(Collection::Id id); /** @returns true if the Collection with the id of @p id should be purged. */ bool shouldPurge(Collection::Id id); /** Purges the items in the Collection @p id */ void purgeItems(Collection::Id id); /** Removes the items starting from @p it and up to a maximum of @p end in Collection @p col. @p pos should be the index of @p it in the m_childEntities before calling, and is updated to the position of the next Collection in m_childEntities afterward. This is required to emit model remove signals properly. @returns an iterator pointing to the next Collection after @p it, or at @p end */ - QList::iterator removeItems(QList::iterator it, QList::iterator end, + QList::iterator removeItems(QList::iterator it, const QList::iterator &end, int *pos, const Collection &col); /** Skips over Collections in m_childEntities up to a maximum of @p end. @p it is an iterator pointing to the first Collection in a block of Collections, and @p pos initially describes the index of @p it in m_childEntities and is updated to point to the index of the next Item in the list. @returns an iterator pointing to the next Item after @p it, or at @p end */ - QList::iterator skipCollections(QList::iterator it, QList::iterator end, int *pos); + QList::iterator skipCollections(QList::iterator it, const QList::iterator &end, int *pos); /** Emits the data changed signal for the entire row as in the subclass, instead of just for the first column. */ void dataChanged(const QModelIndex &top, const QModelIndex &bottom); /** * Returns the model index for the given @p collection. */ QModelIndex indexForCollection(const Collection &collection) const; /** * Returns the model indexes for the given @p item. */ QModelIndexList indexesForItem(const Item &item) const; bool canFetchMore(const QModelIndex &parent) const; /** * Returns true if the collection matches all filters and should be part of the model. * This method checks all properties that could change by modifying the collection. * Currently that includes: * * hidden attribute * * content mime types */ bool shouldBePartOfModel(const Collection &collection) const; bool hasChildCollection(const Collection &collection) const; bool isAncestorMonitored(const Collection &collection) const; }; } #endif diff --git a/src/core/models/favoritecollectionsmodel.cpp b/src/core/models/favoritecollectionsmodel.cpp index c4fa2b805..3a0bc62df 100644 --- a/src/core/models/favoritecollectionsmodel.cpp +++ b/src/core/models/favoritecollectionsmodel.cpp @@ -1,498 +1,498 @@ /* Copyright (c) 2009 Kevin Ottens 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 "favoritecollectionsmodel.h" #include "akonadicore_debug.h" #include #include #include #include #include #include #include #include "entitytreemodel.h" #include "mimetypechecker.h" #include "pastehelper_p.h" #include "favoritecollectionattribute.h" #include "collectionmodifyjob.h" using namespace Akonadi; /** * @internal */ class Q_DECL_HIDDEN FavoriteCollectionsModel::Private { public: Private(const KConfigGroup &group, FavoriteCollectionsModel *parent) : q(parent) , configGroup(group) { } QString labelForCollection(Collection::Id collectionId) const { if (labelMap.contains(collectionId)) { return labelMap[collectionId]; } return q->defaultFavoriteLabel(Collection{collectionId}); } void insertIfAvailable(Collection::Id col) { if (collectionIds.contains(col)) { select(col); if (!referencedCollections.contains(col)) { reference(col); } auto idx = EntityTreeModel::modelIndexForCollection(q, Collection{ col }); if (idx.isValid()) { auto c = q->data(idx, EntityTreeModel::CollectionRole).value(); if (c.isValid() && !c.hasAttribute()) { c.addAttribute(new FavoriteCollectionAttribute()); new CollectionModifyJob(c, q); } } } } void insertIfAvailable(const QModelIndex &idx) { insertIfAvailable(idx.data(EntityTreeModel::CollectionIdRole).value()); } /** * Stuff changed (e.g. new rows inserted into sorted model), reload everything. */ void reload() { //don't clear the selection model here. Otherwise we mess up the users selection as collections get removed and re-inserted. for (const Collection::Id &collectionId : qAsConst(collectionIds)) { insertIfAvailable(collectionId); } // If a favorite folder was removed then surely it's gone from the selection model, so no need to do anything about that. } void rowsInserted(const QModelIndex &parent, int begin, int end) { for (int row = begin; row <= end; row++) { const QModelIndex child = q->sourceModel()->index(row, 0, parent); if (!child.isValid()) { continue; } insertIfAvailable(child); const int childRows = q->sourceModel()->rowCount(child); if (childRows > 0) { rowsInserted(child, 0, childRows - 1); } } } void dataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight) { for (int row = topLeft.row(); row <= bottomRight.row(); row++) { const QModelIndex idx = topLeft.sibling(row, 0); insertIfAvailable(idx); } } /** * Selects the index in the internal selection model to make the collection visible in the model */ - void select(const Collection::Id &collectionId) + void select(Collection::Id collectionId) { const QModelIndex index = EntityTreeModel::modelIndexForCollection(q->sourceModel(), Collection(collectionId)); if (index.isValid()) { q->selectionModel()->select(index, QItemSelectionModel::Select); } } - void deselect(const Collection::Id &collectionId) + void deselect(Collection::Id collectionId) { const QModelIndex idx = EntityTreeModel::modelIndexForCollection(q->sourceModel(), Collection(collectionId)); if (idx.isValid()) { q->selectionModel()->select(idx, QItemSelectionModel::Deselect); } } - void reference(const Collection::Id &collectionId) + void reference(Collection::Id collectionId) { if (referencedCollections.contains(collectionId)) { qCWarning(AKONADICORE_LOG) << "already referenced " << collectionId; return; } const QModelIndex index = EntityTreeModel::modelIndexForCollection(q->sourceModel(), Collection(collectionId)); if (index.isValid()) { if (q->sourceModel()->setData(index, QVariant(), EntityTreeModel::CollectionRefRole)) { referencedCollections << collectionId; } else { qCWarning(AKONADICORE_LOG) << "failed to reference collection"; } q->sourceModel()->fetchMore(index); } } - void dereference(const Collection::Id &collectionId) + void dereference(Collection::Id collectionId) { if (!referencedCollections.contains(collectionId)) { qCWarning(AKONADICORE_LOG) << "not referenced " << collectionId; return; } const QModelIndex index = EntityTreeModel::modelIndexForCollection(q->sourceModel(), Collection(collectionId)); if (index.isValid()) { q->sourceModel()->setData(index, QVariant(), EntityTreeModel::CollectionDerefRole); referencedCollections.remove(collectionId); } } void clearReferences() { for (const Collection::Id &collectionId : qAsConst(referencedCollections)) { dereference(collectionId); } } /** * Adds a collection to the favorite collections */ - void add(const Collection::Id &collectionId) + void add(Collection::Id collectionId) { if (collectionIds.contains(collectionId)) { qCDebug(AKONADICORE_LOG) << "already in model " << collectionId; return; } collectionIds << collectionId; reference(collectionId); select(collectionId); const auto idx = EntityTreeModel::modelIndexForCollection(q, Collection{ collectionId }); if (idx.isValid()) { auto col = q->data(idx, EntityTreeModel::CollectionRole).value(); if (col.isValid() && !col.hasAttribute()) { col.addAttribute(new FavoriteCollectionAttribute()); new CollectionModifyJob(col, q); } } } - void remove(const Collection::Id &collectionId) + void remove(Collection::Id collectionId) { collectionIds.removeAll(collectionId); labelMap.remove(collectionId); dereference(collectionId); deselect(collectionId); const auto idx = EntityTreeModel::modelIndexForCollection(q, Collection{ collectionId }); if (idx.isValid()) { auto col = q->data(idx, EntityTreeModel::CollectionRole).value(); if (col.isValid() && col.hasAttribute()) { col.removeAttribute(); new CollectionModifyJob(col, q); } } } void set(const QList &collections) { QList colIds = collectionIds; for (const Collection::Id &col : collections) { const int removed = colIds.removeAll(col); const bool isNewCollection = removed <= 0; if (isNewCollection) { add(col); } } //Remove what's left for (Akonadi::Collection::Id colId : qAsConst(colIds)) { remove(colId); } } void set(const Akonadi::Collection::List &collections) { QList colIds; colIds.reserve(collections.count()); for (const Akonadi::Collection &col : collections) { colIds << col.id(); } set(colIds); } void loadConfig() { const QList collections = configGroup.readEntry("FavoriteCollectionIds", QList()); const QStringList labels = configGroup.readEntry("FavoriteCollectionLabels", QStringList()); const int numberOfLabels(labels.size()); for (int i = 0; i < collections.size(); ++i) { if (i < numberOfLabels) { labelMap[collections[i]] = labels[i]; } add(collections[i]); } } void saveConfig() { QStringList labels; labels.reserve(collectionIds.count()); for (const Collection::Id &collectionId : qAsConst(collectionIds)) { labels << labelForCollection(collectionId); } configGroup.writeEntry("FavoriteCollectionIds", collectionIds); configGroup.writeEntry("FavoriteCollectionLabels", labels); configGroup.config()->sync(); } FavoriteCollectionsModel *const q; QList collectionIds; QSet referencedCollections; QHash labelMap; KConfigGroup configGroup; }; /* Implementation note: * * We use KSelectionProxyModel in order to make a flat list of selected folders from the folder tree. * * Attempts to use QSortFilterProxyModel make code somewhat simpler, * but don't work since we then get a filtered tree, not a flat list. Stacking a KDescendantsProxyModel * on top would likely remove explicitly selected parents when one of their child is selected too. */ FavoriteCollectionsModel::FavoriteCollectionsModel(QAbstractItemModel *source, const KConfigGroup &group, QObject *parent) : KSelectionProxyModel(new QItemSelectionModel(source, parent), parent) , d(new Private(group, this)) { setSourceModel(source); setFilterBehavior(ExactSelection); d->loadConfig(); //React to various changes in the source model connect(source, &QAbstractItemModel::modelReset, this, [this]() { d->reload(); }); connect(source, &QAbstractItemModel::layoutChanged, this, [this]() { d->reload(); }); connect(source, &QAbstractItemModel::rowsInserted, this, [this](const QModelIndex &parent, int begin, int end) { d->rowsInserted(parent, begin, end); }); connect(source, &QAbstractItemModel::dataChanged, this, [this](const QModelIndex &tl, const QModelIndex &br) { d->dataChanged(tl, br); }); } FavoriteCollectionsModel::~FavoriteCollectionsModel() { delete d; } void FavoriteCollectionsModel::setCollections(const Collection::List &collections) { d->set(collections); d->saveConfig(); } void FavoriteCollectionsModel::addCollection(const Collection &collection) { d->add(collection.id()); d->saveConfig(); } void FavoriteCollectionsModel::removeCollection(const Collection &collection) { d->remove(collection.id()); d->saveConfig(); } Akonadi::Collection::List FavoriteCollectionsModel::collections() const { Collection::List cols; cols.reserve(d->collectionIds.count()); for (const Collection::Id &colId : qAsConst(d->collectionIds)) { const QModelIndex idx = EntityTreeModel::modelIndexForCollection(sourceModel(), Collection(colId)); const Collection collection = sourceModel()->data(idx, EntityTreeModel::CollectionRole).value(); cols << collection; } return cols; } QList FavoriteCollectionsModel::collectionIds() const { return d->collectionIds; } void Akonadi::FavoriteCollectionsModel::setFavoriteLabel(const Collection &collection, const QString &label) { Q_ASSERT(d->collectionIds.contains(collection.id())); d->labelMap[collection.id()] = label; d->saveConfig(); const QModelIndex idx = EntityTreeModel::modelIndexForCollection(sourceModel(), collection); if (!idx.isValid()) { return; } const QModelIndex index = mapFromSource(idx); Q_EMIT dataChanged(index, index); } QVariant Akonadi::FavoriteCollectionsModel::data(const QModelIndex &index, int role) const { if (index.column() == 0 && (role == Qt::DisplayRole || role == Qt::EditRole)) { const QModelIndex sourceIndex = mapToSource(index); const Collection::Id collectionId = sourceModel()->data(sourceIndex, EntityTreeModel::CollectionIdRole).toLongLong(); return d->labelForCollection(collectionId); } else { return KSelectionProxyModel::data(index, role); } } bool FavoriteCollectionsModel::setData(const QModelIndex &index, const QVariant &value, int role) { if (index.isValid() && index.column() == 0 && role == Qt::EditRole) { const QString newLabel = value.toString(); if (newLabel.isEmpty()) { return false; } const QModelIndex sourceIndex = mapToSource(index); const Collection collection = sourceModel()->data(sourceIndex, EntityTreeModel::CollectionRole).value(); setFavoriteLabel(collection, newLabel); return true; } return KSelectionProxyModel::setData(index, value, role); } QString Akonadi::FavoriteCollectionsModel::favoriteLabel(const Akonadi::Collection &collection) { if (!collection.isValid()) { return QString(); } return d->labelForCollection(collection.id()); } QString Akonadi::FavoriteCollectionsModel::defaultFavoriteLabel(const Akonadi::Collection &collection) { if (!collection.isValid()) { return QString(); } const auto colIdx = EntityTreeModel::modelIndexForCollection(sourceModel(), Collection(collection.id())); const QString nameOfCollection = colIdx.data().toString(); QModelIndex idx = colIdx.parent(); QString accountName; while (idx != QModelIndex()) { accountName = idx.data(EntityTreeModel::OriginalCollectionNameRole).toString(); idx = idx.parent(); } if (accountName.isEmpty()) { return nameOfCollection; } else { return nameOfCollection + QStringLiteral(" (") + accountName + QLatin1Char(')'); } } QVariant FavoriteCollectionsModel::headerData(int section, Qt::Orientation orientation, int role) const { if (section == 0 && orientation == Qt::Horizontal && role == Qt::DisplayRole) { return i18n("Favorite Folders"); } else { return KSelectionProxyModel::headerData(section, orientation, role); } } bool FavoriteCollectionsModel::dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent) { Q_UNUSED(action); Q_UNUSED(row); Q_UNUSED(column); if (data->hasFormat(QStringLiteral("text/uri-list"))) { const QList urls = data->urls(); const QModelIndex sourceIndex = mapToSource(parent); const Collection destCollection = sourceModel()->data(sourceIndex, EntityTreeModel::CollectionRole).value(); MimeTypeChecker mimeChecker; mimeChecker.setWantedMimeTypes(destCollection.contentMimeTypes()); for (const QUrl &url : urls) { const Collection col = Collection::fromUrl(url); if (col.isValid()) { addCollection(col); } else { const Item item = Item::fromUrl(url); if (item.isValid()) { if (item.parentCollection().id() == destCollection.id() && action != Qt::CopyAction) { qCDebug(AKONADICORE_LOG) << "Error: source and destination of move are the same."; return false; } #if 0 if (!mimeChecker.isWantedItem(item)) { qCDebug(AKONADICORE_LOG) << "unwanted item" << mimeChecker.wantedMimeTypes() << item.mimeType(); return false; } #endif KJob *job = PasteHelper::pasteUriList(data, destCollection, action); if (!job) { return false; } connect(job, &KJob::result, this, &FavoriteCollectionsModel::pasteJobDone); // Accept the event so that it doesn't propagate. return true; } } } return true; } return false; } QStringList FavoriteCollectionsModel::mimeTypes() const { QStringList mts = KSelectionProxyModel::mimeTypes(); if (!mts.contains(QLatin1String("text/uri-list"))) { mts.append(QStringLiteral("text/uri-list")); } return mts; } Qt::ItemFlags FavoriteCollectionsModel::flags(const QModelIndex &index) const { Qt::ItemFlags fs = KSelectionProxyModel::flags(index); if (!index.isValid()) { fs |= Qt::ItemIsDropEnabled; } return fs; } void FavoriteCollectionsModel::pasteJobDone(KJob *job) { if (job->error()) { qCDebug(AKONADICORE_LOG) << "Paste job error:" << job->errorString(); } } #include "moc_favoritecollectionsmodel.cpp" diff --git a/src/core/models/favoritecollectionsmodel.h b/src/core/models/favoritecollectionsmodel.h index ed136c53b..329543fe8 100644 --- a/src/core/models/favoritecollectionsmodel.h +++ b/src/core/models/favoritecollectionsmodel.h @@ -1,164 +1,164 @@ /* Copyright (c) 2009 Kevin Ottens 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_FAVORITECOLLECTIONSMODEL_H #define AKONADI_FAVORITECOLLECTIONSMODEL_H #include "akonadicore_export.h" #include #include "collection.h" class KConfigGroup; class KJob; namespace Akonadi { class EntityTreeModel; /** * @short A model that lists a set of favorite collections. * * In some applications you want to provide fast access to a list * of often used collections (e.g. Inboxes from different email accounts * in a mail application). Therefore you can use the FavoriteCollectionsModel * which stores the list of favorite collections in a given configuration * file. * * Example: * * @code * * using namespace Akonadi; * * EntityTreeModel *sourceModel = new EntityTreeModel( ... ); * * const KConfigGroup group = KGlobal::config()->group( "Favorite Collections" ); * * FavoriteCollectionsModel *model = new FavoriteCollectionsModel( sourceModel, group, this ); * * EntityListView *view = new EntityListView( this ); * view->setModel( model ); * * @endcode * * @author Kevin Ottens * @since 4.4 */ class AKONADICORE_EXPORT FavoriteCollectionsModel : public KSelectionProxyModel { Q_OBJECT public: /** * Creates a new favorite collections model. * * @param model The source model where the favorite collections * come from. * @param group The config group that shall be used to save the * selection of favorite collections. * @param parent The parent object. */ FavoriteCollectionsModel(QAbstractItemModel *model, const KConfigGroup &group, QObject *parent = nullptr); /** * Destroys the favorite collections model. */ ~FavoriteCollectionsModel() override; /** * Returns the list of favorite collections. * @deprecated Use collectionIds instead. */ Q_REQUIRED_RESULT AKONADICORE_DEPRECATED Collection::List collections() const; /** * Returns the list of ids of favorite collections set on the FavoriteCollectionsModel. * * Note that if you want Collections with actual data * you should use something like this instead: * * @code * FavoriteCollectionsModel* favs = getFavsModel(); * Collection::List cols; * const int rowCount = favs->rowCount(); * for (int row = 0; row < rowcount; ++row) { * static const int column = 0; * const QModelIndex index = favs->index(row, column); * const Collection col = index.data(EntityTreeModel::CollectionRole).value(); * cols << col; * } * @endcode * * Note that due to the asynchronous nature of the model, this method returns collection ids * of collections which may not be in the model yet. If you want the ids of the collections * that are actually in the model, use a loop similar to above with the CollectionIdRole. */ Q_REQUIRED_RESULT QList collectionIds() const; /** * Return associate label for collection */ Q_REQUIRED_RESULT QString favoriteLabel(const Akonadi::Collection &col); Q_REQUIRED_RESULT QString defaultFavoriteLabel(const Akonadi::Collection &col); Q_REQUIRED_RESULT QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; Q_REQUIRED_RESULT bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override; Q_REQUIRED_RESULT QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; Q_REQUIRED_RESULT bool dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent) override; Q_REQUIRED_RESULT QStringList mimeTypes() const override; Q_REQUIRED_RESULT Qt::ItemFlags flags(const QModelIndex &index) const override; public Q_SLOTS: /** * Sets the @p collections as favorite collections. */ - void setCollections(const Collection::List &collections); + void setCollections(const Akonadi::Collection::List &collections); /** * Adds a @p collection to the list of favorite collections. */ - void addCollection(const Collection &collection); + void addCollection(const Akonadi::Collection &collection); /** * Removes a @p collection from the list of favorite collections. */ - void removeCollection(const Collection &collection); + void removeCollection(const Akonadi::Collection &collection); /** * Sets a custom @p label that will be used when showing the * favorite @p collection. */ - void setFavoriteLabel(const Collection &collection, const QString &label); + void setFavoriteLabel(const Akonadi::Collection &collection, const QString &label); private Q_SLOTS: void pasteJobDone(KJob *job); private: //@cond PRIVATE using KSelectionProxyModel::setSourceModel; class Private; Private *const d; //@endcond }; } #endif diff --git a/src/core/models/selectionproxymodel.cpp b/src/core/models/selectionproxymodel.cpp index 5f88e59ea..460e8fb72 100644 --- a/src/core/models/selectionproxymodel.cpp +++ b/src/core/models/selectionproxymodel.cpp @@ -1,89 +1,89 @@ /* Copyright (c) 2009 Stephen Kelly 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 "selectionproxymodel.h" #include "entitytreemodel.h" using namespace Akonadi; namespace Akonadi { class SelectionProxyModelPrivate { public: SelectionProxyModelPrivate(SelectionProxyModel *selectionProxyModel) : q_ptr(selectionProxyModel) { Q_Q(SelectionProxyModel); const auto indexes = q->sourceRootIndexes(); for (const auto &rootIndex : indexes) { rootIndexAdded(rootIndex); } } ~SelectionProxyModelPrivate() { Q_Q(SelectionProxyModel); const auto indexes = q->sourceRootIndexes(); for (const auto &rootIndex : indexes) { rootIndexAboutToBeRemoved(rootIndex); } } /** Increases the refcount of the Collection in @p newRootIndex */ void rootIndexAdded(const QModelIndex &newRootIndex) { Q_Q(SelectionProxyModel); // newRootIndex is already in the sourceModel. q->sourceModel()->setData(newRootIndex, QVariant(), EntityTreeModel::CollectionRefRole); q->sourceModel()->fetchMore(newRootIndex); } /** Decreases the refcount of the Collection in @p removedRootIndex */ void rootIndexAboutToBeRemoved(const QModelIndex &removedRootIndex) { Q_Q(SelectionProxyModel); q->sourceModel()->setData(removedRootIndex, QVariant(), EntityTreeModel::CollectionDerefRole); } Q_DECLARE_PUBLIC(SelectionProxyModel) SelectionProxyModel *q_ptr; }; } SelectionProxyModel::SelectionProxyModel(QItemSelectionModel *selectionModel, QObject *parent) : KSelectionProxyModel(selectionModel, parent) , d_ptr(new SelectionProxyModelPrivate(this)) { - connect(this, SIGNAL(rootIndexAdded(QModelIndex)), SLOT(rootIndexAdded(QModelIndex))); - connect(this, SIGNAL(rootIndexAboutToBeRemoved(QModelIndex)), SLOT(rootIndexAboutToBeRemoved(QModelIndex))); + connect(this, SIGNAL(rootIndexAdded(QModelIndex)), SLOT(rootIndexAdded(QModelIndex))); // clazy:exclude=old-style-connect + connect(this, SIGNAL(rootIndexAboutToBeRemoved(QModelIndex)), SLOT(rootIndexAboutToBeRemoved(QModelIndex))); // clazy:exclude=old-style-connect } SelectionProxyModel::~SelectionProxyModel() { delete d_ptr; } #include "moc_selectionproxymodel.cpp" diff --git a/src/core/models/statisticsproxymodel.cpp b/src/core/models/statisticsproxymodel.cpp index f6d294038..76653a8d1 100644 --- a/src/core/models/statisticsproxymodel.cpp +++ b/src/core/models/statisticsproxymodel.cpp @@ -1,344 +1,344 @@ /* Copyright (c) 2009 Kevin Ottens 2016 David Faure 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 "statisticsproxymodel.h" #include "akonadicore_debug.h" #include "kitemmodels_version.h" #include "entitytreemodel.h" #include "collectionutils.h" #include "collectionquotaattribute.h" #include "collectionstatistics.h" #include "entitydisplayattribute.h" #include #include #include #include #include #include using namespace Akonadi; /** * @internal */ class Q_DECL_HIDDEN StatisticsProxyModel::Private { public: Private(StatisticsProxyModel *parent) : q(parent), mToolTipEnabled(false), mExtraColumnsEnabled(false) { } void getCountRecursive(const QModelIndex &index, qint64 &totalSize) const { Collection collection = qvariant_cast(index.data(EntityTreeModel::CollectionRole)); // Do not assert on invalid collections, since a collection may be deleted // in the meantime and deleted collections are invalid. if (collection.isValid()) { CollectionStatistics statistics = collection.statistics(); totalSize += qMax(0LL, statistics.size()); if (index.model()->hasChildren(index)) { const int rowCount = index.model()->rowCount(index); for (int row = 0; row < rowCount; row++) { static const int column = 0; getCountRecursive(index.model()->index(row, column, index), totalSize); } } } } int sourceColumnCount() const { return q->sourceModel()->columnCount(); } QString toolTipForCollection(const QModelIndex &index, const Collection &collection) { const QString bckColor = QApplication::palette().color(QPalette::ToolTipBase).name(); const QString txtColor = QApplication::palette().color(QPalette::ToolTipText).name(); QString tip = QStringLiteral( "\n" ); const QString textDirection = (QApplication::layoutDirection() == Qt::LeftToRight) ? QStringLiteral("left") : QStringLiteral("right"); tip += QStringLiteral( " \n" " \n" " \n" ).arg(txtColor, bckColor, index.data(Qt::DisplayRole).toString(), textDirection); tip += QStringLiteral( " \n" " \n" ).arg(iconPath).arg(icon_size_found); if (QApplication::layoutDirection() == Qt::LeftToRight) { tip += tipInfo + QStringLiteral("" \ "
\n" "
\n" " %3\n" "
\n" "
\n" ).arg(textDirection); QString tipInfo = QStringLiteral( " %1: %2
\n" " %3: %4

\n" ).arg(i18n("Total Messages")).arg(collection.statistics().count()) .arg(i18n("Unread Messages")).arg(collection.statistics().unreadCount()); if (collection.hasAttribute()) { const CollectionQuotaAttribute *quota = collection.attribute(); if (quota->currentValue() > -1 && quota->maximumValue() > 0) { qreal percentage = (100.0 * quota->currentValue()) / quota->maximumValue(); if (qAbs(percentage) >= 0.01) { QString percentStr = QString::number(percentage, 'f', 2); tipInfo += QStringLiteral( " %1: %2%
\n" ).arg(i18n("Quota"), percentStr); } } } KFormat formatter; qint64 currentFolderSize(collection.statistics().size()); tipInfo += QStringLiteral( " %1: %2
\n" ).arg(i18n("Storage Size"), formatter.formatByteSize(currentFolderSize)); qint64 totalSize = 0; getCountRecursive(index, totalSize); totalSize -= currentFolderSize; if (totalSize > 0) { tipInfo += QStringLiteral( "%1: %2
" ).arg(i18n("Subfolder Storage Size"), formatter.formatByteSize(totalSize)); } QString iconName = CollectionUtils::defaultIconName(collection); if (collection.hasAttribute() && !collection.attribute()->iconName().isEmpty()) { if (!collection.attribute()->activeIconName().isEmpty() && collection.statistics().unreadCount() > 0) { iconName = collection.attribute()->activeIconName(); } else { iconName = collection.attribute()->iconName(); } } int iconSizes[] = { 32, 22 }; int icon_size_found = 32; QString iconPath; for (int i = 0; i < 2; ++i) { iconPath = KIconLoader::global()->iconPath(iconName, -iconSizes[ i ], true); if (!iconPath.isEmpty()) { icon_size_found = iconSizes[ i ]; break; } } if (iconPath.isEmpty()) { iconPath = KIconLoader::global()->iconPath(QStringLiteral("folder"), -32, false); } QString tipIcon = QStringLiteral( "
\n" " \n" "
\n" "
").arg(textDirection) + tipIcon; } else { tip += tipIcon + QStringLiteral("").arg(textDirection) + tipInfo; } tip += QLatin1String( "
" ); return tip; } void _k_sourceDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QVector &roles); StatisticsProxyModel *q = nullptr; bool mToolTipEnabled; bool mExtraColumnsEnabled; }; void StatisticsProxyModel::Private::_k_sourceDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QVector &roles) { QModelIndex proxyTopLeft(q->mapFromSource(topLeft)); QModelIndex proxyBottomRight(q->mapFromSource(bottomRight)); // Emit data changed for the whole row (bug #222292) if (mExtraColumnsEnabled && topLeft.column() == 0) { // in theory we could filter on roles, but ETM doesn't set any yet const int lastColumn = q->columnCount() - 1; proxyBottomRight = proxyBottomRight.sibling(proxyBottomRight.row(), lastColumn); } Q_EMIT q->dataChanged(proxyTopLeft, proxyBottomRight, roles); } void StatisticsProxyModel::setSourceModel(QAbstractItemModel *model) { if (sourceModel()) { - disconnect(sourceModel(), SIGNAL(dataChanged(QModelIndex,QModelIndex,QVector)), - this, SLOT(_k_sourceDataChanged(QModelIndex,QModelIndex,QVector))); + disconnect(sourceModel(), &QAbstractItemModel::dataChanged, this, nullptr); } KExtraColumnsProxyModel::setSourceModel(model); if (model) { // Disconnect the default handling of dataChanged in QIdentityProxyModel, so we can extend it to the whole row - disconnect(model, SIGNAL(dataChanged(QModelIndex,QModelIndex,QVector)), + disconnect(model, SIGNAL(dataChanged(QModelIndex,QModelIndex,QVector)), // clazy:exclude=old-style-connect this, SLOT(_q_sourceDataChanged(QModelIndex,QModelIndex,QVector))); - connect(model, SIGNAL(dataChanged(QModelIndex,QModelIndex,QVector)), - this, SLOT(_k_sourceDataChanged(QModelIndex,QModelIndex,QVector))); + connect(model, &QAbstractItemModel::dataChanged, this, [this](const auto &tl, const auto &br, const auto &roles) { + d->_k_sourceDataChanged(tl, br, roles); + }); } } StatisticsProxyModel::StatisticsProxyModel(QObject *parent) : KExtraColumnsProxyModel(parent), d(new Private(this)) { setExtraColumnsEnabled(true); } StatisticsProxyModel::~StatisticsProxyModel() { delete d; } void StatisticsProxyModel::setToolTipEnabled(bool enable) { d->mToolTipEnabled = enable; } bool StatisticsProxyModel::isToolTipEnabled() const { return d->mToolTipEnabled; } void StatisticsProxyModel::setExtraColumnsEnabled(bool enable) { if (d->mExtraColumnsEnabled == enable) { return; } d->mExtraColumnsEnabled = enable; if (enable) { KExtraColumnsProxyModel::appendColumn(i18nc("number of unread entities in the collection", "Unread")); KExtraColumnsProxyModel::appendColumn(i18nc("number of entities in the collection", "Total")); KExtraColumnsProxyModel::appendColumn(i18nc("collection size", "Size")); } else { KExtraColumnsProxyModel::removeExtraColumn(2); KExtraColumnsProxyModel::removeExtraColumn(1); KExtraColumnsProxyModel::removeExtraColumn(0); } } bool StatisticsProxyModel::isExtraColumnsEnabled() const { return d->mExtraColumnsEnabled; } QVariant StatisticsProxyModel::extraColumnData(const QModelIndex &parent, int row, int extraColumn, int role) const { switch (role) { case Qt::DisplayRole: { const QModelIndex firstColumn = index(row, 0, parent); const Collection collection = data(firstColumn, EntityTreeModel::CollectionRole).value(); if (collection.isValid() && collection.statistics().count() >= 0) { const CollectionStatistics stats = collection.statistics(); if (extraColumn == 2) { KFormat formatter; return formatter.formatByteSize(stats.size()); } else if (extraColumn == 1) { return stats.count(); } else if (extraColumn == 0) { if (stats.unreadCount() > 0) { return stats.unreadCount(); } else { return QString(); } } else { qCWarning(AKONADICORE_LOG) << "We shouldn't get there for a column which is not total, unread or size."; } } } break; case Qt::TextAlignmentRole: { return Qt::AlignRight; } default: break; } return QVariant(); } QVariant StatisticsProxyModel::data(const QModelIndex &index, int role) const { if (role == Qt::ToolTipRole && d->mToolTipEnabled) { const QModelIndex firstColumn = index.sibling(index.row(), 0); const Collection collection = data(firstColumn, EntityTreeModel::CollectionRole).value(); if (collection.isValid()) { return d->toolTipForCollection(firstColumn, collection); } } return KExtraColumnsProxyModel::data(index, role); } Qt::ItemFlags StatisticsProxyModel::flags(const QModelIndex &index_) const { if (index_.column() >= d->sourceColumnCount()) { const QModelIndex firstColumn = index_.sibling(index_.row(), 0); return KExtraColumnsProxyModel::flags(firstColumn) & (Qt::ItemIsSelectable | Qt::ItemIsDragEnabled // Allowed flags | Qt::ItemIsDropEnabled | Qt::ItemIsEnabled); } return KExtraColumnsProxyModel::flags(index_); } // Not sure this is still necessary.... QModelIndexList StatisticsProxyModel::match(const QModelIndex &start, int role, const QVariant &value, int hits, Qt::MatchFlags flags) const { if (role < Qt::UserRole) { return KExtraColumnsProxyModel::match(start, role, value, hits, flags); } QModelIndexList list; QModelIndex proxyIndex; const auto matches = sourceModel()->match(mapToSource(start), role, value, hits, flags); for (const auto &idx : matches) { proxyIndex = mapFromSource(idx); if (proxyIndex.isValid()) { list.push_back(proxyIndex); } } return list; } #include "moc_statisticsproxymodel.cpp" diff --git a/src/core/models/statisticsproxymodel.h b/src/core/models/statisticsproxymodel.h index b071c2300..a5a438d54 100644 --- a/src/core/models/statisticsproxymodel.h +++ b/src/core/models/statisticsproxymodel.h @@ -1,111 +1,109 @@ /* Copyright (c) 2009 Kevin Ottens 2016 David Faure s 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_STATISTICSPROXYMODEL_H #define AKONADI_STATISTICSPROXYMODEL_H #include "akonadicore_export.h" #include namespace Akonadi { /** * @short A proxy model that exposes collection statistics through extra columns. * * This class can be used on top of an EntityTreeModel to display extra columns * summarizing statistics of collections. * * @code * * Akonadi::EntityTreeModel *model = new Akonadi::EntityTreeModel( ... ); * * Akonadi::StatisticsProxyModel *proxy = new Akonadi::StatisticsProxyModel(); * proxy->setSourceModel( model ); * * Akonadi::EntityTreeView *view = new Akonadi::EntityTreeView( this ); * view->setModel( proxy ); * * @endcode * * @author Kevin Ottens , now maintained by David Faure * @since 4.4 */ class AKONADICORE_EXPORT StatisticsProxyModel : public KExtraColumnsProxyModel { Q_OBJECT public: /** * Creates a new statistics proxy model. * * @param parent The parent object. */ explicit StatisticsProxyModel(QObject *parent = nullptr); /** * Destroys the statistics proxy model. */ ~StatisticsProxyModel() override; /** * @param enable Display tooltips * By default, tooltips are disabled. */ void setToolTipEnabled(bool enable); /** * Return true if we display tooltips, otherwise false */ Q_REQUIRED_RESULT bool isToolTipEnabled() const; /** * @param enable Display extra statistics columns * By default, the extra columns are enabled. */ void setExtraColumnsEnabled(bool enable); /** * Return true if we display extra statistics columns, otherwise false */ Q_REQUIRED_RESULT bool isExtraColumnsEnabled() const; Q_REQUIRED_RESULT QVariant extraColumnData(const QModelIndex &parent, int row, int extraColumn, int role) const override; Q_REQUIRED_RESULT QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; Q_REQUIRED_RESULT Qt::ItemFlags flags(const QModelIndex &index) const override; Q_REQUIRED_RESULT virtual QModelIndexList match(const QModelIndex &start, int role, const QVariant &value, int hits = 1, Qt::MatchFlags flags = Qt::MatchFlags(Qt::MatchStartsWith | Qt::MatchWrap)) const override; void setSourceModel(QAbstractItemModel *model) override; private: //@cond PRIVATE class Private; Private *const d; - - Q_PRIVATE_SLOT(d, void _k_sourceDataChanged(QModelIndex, QModelIndex, QVector)) //@endcond }; } #endif diff --git a/src/core/models/tagmodel.h b/src/core/models/tagmodel.h index 05261d38a..c13ae9e84 100644 --- a/src/core/models/tagmodel.h +++ b/src/core/models/tagmodel.h @@ -1,97 +1,90 @@ /* 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. */ #ifndef AKONADI_TAGMODEL_H #define AKONADI_TAGMODEL_H #include #include "akonadicore_export.h" #include "tag.h" class KJob; namespace Akonadi { class Monitor; class TagModelPrivate; class AKONADICORE_EXPORT TagModel : public QAbstractItemModel { Q_OBJECT public: enum Roles { IdRole = Qt::UserRole + 1, NameRole, TypeRole, GIDRole, ParentRole, TagRole, UserRole = Qt::UserRole + 500, TerminalUserRole = 2000, EndRole = 65535 }; explicit TagModel(Monitor *recorder, QObject *parent = nullptr); ~TagModel() override; Q_REQUIRED_RESULT int columnCount(const QModelIndex &parent = QModelIndex()) const override; Q_REQUIRED_RESULT int rowCount(const QModelIndex &parent = QModelIndex()) const override; Q_REQUIRED_RESULT QVariant data(const QModelIndex &index, int role) const override; Q_REQUIRED_RESULT QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; Q_REQUIRED_RESULT Qt::ItemFlags flags(const QModelIndex &index) const override; /* virtual Qt::DropActions supportedDropActions() const; virtual QMimeData* mimeData( const QModelIndexList &indexes ) const; virtual bool dropMimeData( const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent ); */ Q_REQUIRED_RESULT QModelIndex parent(const QModelIndex &child) const override; Q_REQUIRED_RESULT QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override; protected: Q_DECLARE_PRIVATE(TagModel) TagModelPrivate *d_ptr; TagModel(Monitor *recorder, TagModelPrivate *dd, QObject *parent); Q_SIGNALS: void populated(); private: bool insertRows(int row, int count, const QModelIndex &index = QModelIndex()) override; bool insertColumns(int column, int count, const QModelIndex &index = QModelIndex()) override; bool removeColumns(int column, int count, const QModelIndex &index = QModelIndex()) override; bool removeRows(int row, int count, const QModelIndex &index = QModelIndex()) override; - - Q_PRIVATE_SLOT(d_func(), void fillModel()) - Q_PRIVATE_SLOT(d_func(), void tagsFetched(const Akonadi::Tag::List &tags)) - Q_PRIVATE_SLOT(d_func(), void tagsFetchDone(KJob *job)) - Q_PRIVATE_SLOT(d_func(), void monitoredTagAdded(const Akonadi::Tag &tag)) - Q_PRIVATE_SLOT(d_func(), void monitoredTagRemoved(const Akonadi::Tag &tag)) - Q_PRIVATE_SLOT(d_func(), void monitoredTagChanged(const Akonadi::Tag &tag)) }; } #endif // AKONADI_TAGMODEL_H diff --git a/src/core/models/tagmodel_p.cpp b/src/core/models/tagmodel_p.cpp index aea5321cc..f15ae91a9 100644 --- a/src/core/models/tagmodel_p.cpp +++ b/src/core/models/tagmodel_p.cpp @@ -1,247 +1,238 @@ /* 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 "tagmodel_p.h" #include "tagmodel.h" #include "monitor.h" #include "session.h" #include "tagfetchjob.h" #include "akonadicore_debug.h" #include using namespace Akonadi; TagModelPrivate::TagModelPrivate(TagModel *parent) : mMonitor(nullptr) , mSession(nullptr) , q_ptr(parent) { // Root tag mTags.insert(-1, Tag()); } -TagModelPrivate::~TagModelPrivate() -{ -} - void TagModelPrivate::init(Monitor *monitor) { Q_Q(TagModel); mMonitor = monitor; mSession = mMonitor->session(); - q->connect(mMonitor, SIGNAL(tagAdded(Akonadi::Tag)), - q, SLOT(monitoredTagAdded(Akonadi::Tag))); - q->connect(mMonitor, SIGNAL(tagChanged(Akonadi::Tag)), - q, SLOT(monitoredTagChanged(Akonadi::Tag))); - q->connect(mMonitor, SIGNAL(tagRemoved(Akonadi::Tag)), - q, SLOT(monitoredTagRemoved(Akonadi::Tag))); + q->connect(mMonitor, &Monitor::tagAdded, q, [this](const Tag &tag) { monitoredTagAdded(tag); }); + q->connect(mMonitor, &Monitor::tagChanged, q, [this](const Tag &tag) { monitoredTagChanged(tag); }); + q->connect(mMonitor, &Monitor::tagRemoved, q, [this](const Tag &tag) { monitoredTagRemoved(tag); }); // Delay starting the job to allow unit-tests to set up fake stuff QTimer::singleShot(0, q, [this] { fillModel(); }); } void TagModelPrivate::fillModel() { Q_Q(TagModel); TagFetchJob *fetchJob = new TagFetchJob(mSession); fetchJob->setFetchScope(mMonitor->tagFetchScope()); - q->connect(fetchJob, SIGNAL(tagsReceived(Akonadi::Tag::List)), - q, SLOT(tagsFetched(Akonadi::Tag::List))); - q->connect(fetchJob, SIGNAL(finished(KJob*)), - q, SLOT(tagsFetchDone(KJob*))); + q->connect(fetchJob, &TagFetchJob::tagsReceived, q, [this](const auto &tags) { tagsFetched(tags); }); + q->connect(fetchJob, &KJob::result, q, [this](KJob *job) { tagsFetchDone(job); }); } QModelIndex TagModelPrivate::indexForTag(const qint64 tagId) const { Q_Q(const TagModel); if (!mTags.contains(tagId)) { return QModelIndex(); } const Tag tag = mTags.value(tagId); if (!tag.isValid()) { return QModelIndex(); } const Tag::Id parentId = tag.parent().id(); const int row = mChildTags.value(parentId).indexOf(tag); if (row != -1) { return q->createIndex(row, 0, (int) parentId); } return QModelIndex(); } Tag TagModelPrivate::tagForIndex(const QModelIndex &index) const { if (!index.isValid()) { return Tag(); } const Tag::Id parentId = index.internalId(); const Tag::List &children = mChildTags.value(parentId); return children.value(index.row()); } void TagModelPrivate::monitoredTagAdded(const Tag &tag) { Q_Q(TagModel); const qint64 parentId = tag.parent().id(); // Parent not yet in model, defer for later if (!mTags.contains(parentId)) { Tag::List &list = mPendingTags[parentId]; list.append(tag); return; } Tag::List &children = mChildTags[parentId]; q->beginInsertRows(indexForTag(parentId), children.count(), children.count()); mTags.insert(tag.id(), tag); children.append(tag); q->endInsertRows(); // If there are any child tags waiting for this parent, insert them if (mPendingTags.contains(tag.id())) { const Tag::List pendingChildren = mPendingTags.take(tag.id()); for (const Tag &pendingTag : pendingChildren) { monitoredTagAdded(pendingTag); } } } void TagModelPrivate::removeTagsRecursively(qint64 tagId) { const Tag tag = mTags.value(tagId); // Remove all children first const Tag::List childTags = mChildTags.take(tagId); for (const Tag &child : childTags) { removeTagsRecursively(child.id()); } // Remove the actual tag Tag::List &siblings = mChildTags[tag.parent().id()]; siblings.removeOne(tag); mTags.remove(tag.id()); } void TagModelPrivate::monitoredTagRemoved(const Tag &tag) { Q_Q(TagModel); if (!tag.isValid()) { qCWarning(AKONADICORE_LOG) << "Attempting to remove root tag?"; return; } // Better lookup parent in our cache auto iter = mTags.constFind(tag.id()); if (iter == mTags.cend()) { qCWarning(AKONADICORE_LOG) << "Got removal notification for unknown tag" << tag.id(); return; } const qint64 parentId = iter->parent().id(); const Tag::List &siblings = mChildTags[parentId]; const int pos = siblings.indexOf(tag); Q_ASSERT(pos != -1); q->beginRemoveRows(indexForTag(parentId), pos, pos); removeTagsRecursively(tag.id()); q->endRemoveRows(); } void TagModelPrivate::monitoredTagChanged(const Tag &tag) { Q_Q(TagModel); if (!mTags.contains(tag.id())) { qCWarning(AKONADICORE_LOG) << "Got change notifications for unknown tag" << tag.id(); return; } const Tag oldTag = mTags.value(tag.id()); // Replace existing tag in cache mTags.insert(tag.id(), tag); // Check whether the tag has been reparented const qint64 oldParent = oldTag.parent().id(); const qint64 newParent = tag.parent().id(); if (oldParent != newParent) { const QModelIndex sourceParent = indexForTag(oldParent); const int sourcePos = mChildTags.value(oldParent).indexOf(oldTag); const QModelIndex destParent = indexForTag(newParent); const int destPos = mChildTags.value(newParent).count(); q->beginMoveRows(sourceParent, sourcePos, sourcePos, destParent, destPos); Tag::List &oldSiblings = mChildTags[oldParent]; oldSiblings.removeAt(sourcePos); Tag::List &newSiblings = mChildTags[newParent]; newSiblings.append(tag); q->endMoveRows(); } else { Tag::List &children = mChildTags[oldParent]; const int sourcePos = children.indexOf(oldTag); if (sourcePos != -1) { children[sourcePos] = tag; } const QModelIndex index = indexForTag(tag.id()); Q_EMIT q->dataChanged(index, index); } } void TagModelPrivate::tagsFetched(const Tag::List &tags) { for (const Tag &tag : tags) { monitoredTagAdded(tag); } } void TagModelPrivate::tagsFetchDone(KJob *job) { Q_Q(TagModel); if (job->error()) { qCWarning(AKONADICORE_LOG) << job->errorString(); return; } if (!mPendingTags.isEmpty()) { qCWarning(AKONADICORE_LOG) << "Fetched all tags from server, but there are still" << mPendingTags.count() << "orphan tags:"; for (auto it = mPendingTags.cbegin(), e = mPendingTags.cend(); it != e; ++it) { qCWarning(AKONADICORE_LOG) << "tagId = " << it.key() << "; with list count =" << it.value().count(); } return; } Q_EMIT q->populated(); } diff --git a/src/core/models/tagmodel_p.h b/src/core/models/tagmodel_p.h index 4422a1994..a65fa4e01 100644 --- a/src/core/models/tagmodel_p.h +++ b/src/core/models/tagmodel_p.h @@ -1,70 +1,69 @@ /* 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. */ #ifndef AKONADI_TAGMODELPRIVATE_H #define AKONADI_TAGMODELPRIVATE_H #include "tag.h" class QModelIndex; class KJob; namespace Akonadi { class Monitor; class TagModel; class Session; class TagModelPrivate { public: explicit TagModelPrivate(TagModel *parent); - virtual ~TagModelPrivate(); void init(Monitor *recorder); void fillModel(); void tagsFetchDone(KJob *job); void tagsFetched(const Akonadi::Tag::List &tags); void monitoredTagAdded(const Akonadi::Tag &tag); void monitoredTagChanged(const Akonadi::Tag &tag); void monitoredTagRemoved(const Akonadi::Tag &tag); Q_REQUIRED_RESULT QModelIndex indexForTag(qint64 tagId) const; Q_REQUIRED_RESULT Tag tagForIndex(const QModelIndex &index) const; void removeTagsRecursively(qint64 parentTag); Monitor *mMonitor = nullptr; Session *mSession = nullptr; QHash mChildTags; QHash mTags; QHash mPendingTags; protected: Q_DECLARE_PUBLIC(TagModel) TagModel *q_ptr; }; } #endif // AKONADI_TAGMODELPRIVATE_H diff --git a/src/core/monitor.cpp b/src/core/monitor.cpp index 39bc4500a..b6008a9d8 100644 --- a/src/core/monitor.cpp +++ b/src/core/monitor.cpp @@ -1,387 +1,387 @@ /* 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; using namespace AkRanges; 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(); } } Q_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(); } } Q_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(); } } Q_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(); } } Q_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(); } } Q_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(); } } Q_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(); Q_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*))); + connect(session, &Session::destroyed, this, [d](QObject *o) { d->slotSessionDestroyed(o); }); 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 | Actions::toQList; } int Monitor::numMimeTypesMonitored() const { Q_D(const Monitor); return d->mimetypes.count(); } QList Monitor::resourcesMonitored() const { Q_D(const Monitor); return d->resources | Actions::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.h b/src/core/monitor.h index 63276beeb..4f551654f 100644 --- a/src/core/monitor.h +++ b/src/core/monitor.h @@ -1,840 +1,834 @@ /* 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. */ #ifndef AKONADI_MONITOR_H #define AKONADI_MONITOR_H #include "akonadicore_export.h" #include "tag.h" #include "collection.h" #include "item.h" #include "relation.h" #include namespace Akonadi { class CollectionFetchScope; class CollectionStatistics; class Item; class ItemFetchScope; class MonitorPrivate; class Session; class TagFetchScope; class NotificationSubscriber; class ChangeNotification; namespace Protocol { class Command; } /** * @short Monitors an item or collection for changes. * * The Monitor emits signals if some of these objects are changed or * removed or new ones are added to the Akonadi storage. * * There are various ways to filter these notifications. There are three types of filter * evaluation: * - (-) removal-only filter, ie. if the filter matches the notification is dropped, * if not filter evaluation continues with the next one * - (+) pass-exit filter, ie. if the filter matches the notification is delivered, * if not evaluation is continued * - (f) final filter, ie. evaluation ends here if the corresponding filter criteria is set, * the notification is delivered depending on the result, evaluation is only continued * if no filter criteria is defined * * The following filter are available, listed in evaluation order: * (1) ignored sessions (-) * (2) monitor everything (+) * (3a) resource and mimetype filters (f) (items only) * (3b) resource filters (f) (collections only) * (4) item is monitored (+) * (5) collection is monitored (+) * * Optionally, the changed objects can be fetched automatically from the server. * To enable this, see itemFetchScope() and collectionFetchScope(). * * Note that as a consequence of rule 3a, it is not possible to monitor (more than zero resources * OR more than zero mimetypes) AND more than zero collections. * * @todo Distinguish between monitoring collection properties and collection content. * @todo Special case for collection content counts changed * * @author Volker Krause */ class AKONADICORE_EXPORT Monitor : public QObject { Q_OBJECT public: enum Type { /** * @internal This must be kept in sync with Akonadi::NotificationMessageV2::Type */ Collections = 1, Items, Tags, Relations, /** * Listen to subscription changes of other Monitors connected to Akonadi. * This is only for debugging purposes and should not be used in real * applications. * @since 5.4 */ Subscribers, /** * Listens to all notifications being emitted by the server and provides * additional information about them. This is only for debugging purposes * and should not be used in real applications. * * @note Enabling monitoring this type has performance impact on the * Akonadi Server. * * @since 5.4 */ Notifications }; /** * Creates a new monitor. * * @param parent The parent object. */ explicit Monitor(QObject *parent = nullptr); /** * Destroys the monitor. */ ~Monitor() override; /** * Sets whether the specified collection shall be monitored for changes. If * monitoring is turned on for the collection, all notifications for items * in that collection will be emitted, and its child collections will also * be monitored. Note that move notifications will be emitted if either one * of the collections involved is being monitored. * * Note that if a session is being ignored, this takes precedence over * setCollectionMonitored() on that session. * * @param collection The collection to monitor. * If this collection is Collection::root(), all collections * in the Akonadi storage will be monitored. * @param monitored Whether to monitor the collection. */ void setCollectionMonitored(const Collection &collection, bool monitored = true); /** * Sets whether the specified item shall be monitored for changes. * * Note that if a session is being ignored, this takes precedence over * setItemMonitored() on that session. * * @param item The item to monitor. * @param monitored Whether to monitor the item. */ void setItemMonitored(const Item &item, bool monitored = true); /** * Sets whether the specified resource shall be monitored for changes. If * monitoring is turned on for the resource, all notifications for * collections and items in that resource will be emitted. * * Note that if a session is being ignored, this takes precedence over * setResourceMonitored() on that session. * * @param resource The resource identifier. * @param monitored Whether to monitor the resource. */ void setResourceMonitored(const QByteArray &resource, bool monitored = true); /** * Sets whether items of the specified mime type shall be monitored for changes. * If monitoring is turned on for the mime type, all notifications for items * matching that mime type will be emitted, but notifications for collections * matching that mime type will only be emitted if this is otherwise specified, * for example by setCollectionMonitored(). * * Note that if a session is being ignored, this takes precedence over * setMimeTypeMonitored() on that session. * * @param mimetype The mime type to monitor. * @param monitored Whether to monitor the mime type. */ void setMimeTypeMonitored(const QString &mimetype, bool monitored = true); /** * Sets whether the specified tag shall be monitored for changes. * * Same rules as for item monitoring apply. * * @param tag Tag to monitor. * @param monitored Whether to monitor the tag. * @since 4.13 */ void setTagMonitored(const Tag &tag, bool monitored = true); /** * Sets whether given type (Collection, Item, Tag should be monitored). * * By default all types are monitored, but once you change one, you have * to explicitly enable all other types you want to monitor. * * @param type Type to monitor. * @param monitored Whether to monitor the type * @since 4.13 */ void setTypeMonitored(Type type, bool monitored = true); /** * Sets whether all items shall be monitored. * @param monitored sets all items as monitored if set as @c true * Note that if a session is being ignored, this takes precedence over * setAllMonitored() on that session. */ void setAllMonitored(bool monitored = true); void setExclusive(bool exclusive = true); Q_REQUIRED_RESULT bool exclusive() const; /** * Ignores all change notifications caused by the given session. This * overrides all other settings on this session. * * @param session The session you want to ignore. */ void ignoreSession(Session *session); /** * Enables automatic fetching of changed collections from the Akonadi storage. * * @param enable @c true enables automatic fetching, @c false disables automatic fetching. */ void fetchCollection(bool enable); /** * Enables automatic fetching of changed collection statistics information from * the Akonadi storage. * * @param enable @c true to enables automatic fetching, @c false disables automatic fetching. */ void fetchCollectionStatistics(bool enable); /** * Sets the item fetch scope. * * Controls how much of an item's data is fetched from the server, e.g. * whether to fetch the full item payload or only meta data. * * @param fetchScope The new scope for item fetch operations. * * @see itemFetchScope() */ void setItemFetchScope(const ItemFetchScope &fetchScope); /** * Instructs the monitor to fetch only those parts that were changed and * were requested in the fetch scope. * * This is taken in account only for item modifications. * Example usage: * @code * monitor->itemFetchScope().fetchFullPayload( true ); * monitor->fetchChangedOnly(true); * @endcode * * In the example if an item was changed, but its payload was not, the full * payload will not be retrieved. * If the item's payload was changed, the monitor retrieves the changed * payload as well. * * The default is to fetch everything requested. * * @since 4.8 * * @param enable @c true to enable the feature, @c false means everything * that was requested will be fetched. */ void fetchChangedOnly(bool enable); /** * Returns the item fetch scope. * * Since this returns a reference it can be used to conveniently modify the * current scope in-place, i.e. by calling a method on the returned reference * without storing it in a local variable. See the ItemFetchScope documentation * for an example. * * @return a reference to the current item fetch scope * * @see setItemFetchScope() for replacing the current item fetch scope */ ItemFetchScope &itemFetchScope(); /** * Sets the collection fetch scope. * * Controls which collections are monitored and how much of a collection's data * is fetched from the server. * * @param fetchScope The new scope for collection fetch operations. * * @see collectionFetchScope() * @since 4.4 */ void setCollectionFetchScope(const CollectionFetchScope &fetchScope); /** * Returns the collection fetch scope. * * Since this returns a reference it can be used to conveniently modify the * current scope in-place, i.e. by calling a method on the returned reference * without storing it in a local variable. See the CollectionFetchScope documentation * for an example. * * @return a reference to the current collection fetch scope * * @see setCollectionFetchScope() for replacing the current collection fetch scope * @since 4.4 */ CollectionFetchScope &collectionFetchScope(); /** * Sets the tag fetch scope. * * Controls how much of an tag's data is fetched from the server. * * @param fetchScope The new scope for tag fetch operations. * * @see tagFetchScope() */ void setTagFetchScope(const TagFetchScope &fetchScope); /** * Returns the tag fetch scope. * * Since this returns a reference it can be used to conveniently modify the * current scope in-place, i.e. by calling a method on the returned reference * without storing it in a local variable. * * @return a reference to the current tag fetch scope * * @see setTagFetchScope() for replacing the current tag fetch scope */ TagFetchScope &tagFetchScope(); /** * Returns the list of collections being monitored. * * @since 4.3 */ Q_REQUIRED_RESULT Collection::List collectionsMonitored() const; /** * Returns the set of items being monitored. * * Faster version (at least on 32-bit systems) of itemsMonitored(). * * @since 4.6 */ Q_REQUIRED_RESULT QVector itemsMonitoredEx() const; /** * Returns the number of items being monitored. * Optimization. * @since 4.14.3 */ Q_REQUIRED_RESULT int numItemsMonitored() const; /** * Returns the set of mimetypes being monitored. * * @since 4.3 */ Q_REQUIRED_RESULT QStringList mimeTypesMonitored() const; /** * Returns the number of mimetypes being monitored. * Optimization. * @since 4.14.3 */ Q_REQUIRED_RESULT int numMimeTypesMonitored() const; /** * Returns the set of tags being monitored. * * @since 4.13 */ Q_REQUIRED_RESULT QVector tagsMonitored() const; /** * Returns the set of types being monitored. * * @since 4.13 */ Q_REQUIRED_RESULT QVector typesMonitored() const; /** * Returns the set of identifiers for resources being monitored. * * @since 4.3 */ Q_REQUIRED_RESULT QList resourcesMonitored() const; /** * Returns the number of resources being monitored. * Optimization. * @since 4.14.3 */ Q_REQUIRED_RESULT int numResourcesMonitored() const; /** * Returns true if everything is being monitored. * * @since 4.3 */ Q_REQUIRED_RESULT bool isAllMonitored() const; /** * Sets the session used by the Monitor to communicate with the %Akonadi server. * If not set, the Akonadi::Session::defaultSession is used. * @param session the session to be set * @since 4.4 */ void setSession(Akonadi::Session *session); /** * Returns the Session used by the monitor to communicate with Akonadi. * * @since 4.4 */ Q_REQUIRED_RESULT Session *session() const; /** * Allows to enable/disable collection move translation. If enabled (the default), move * notifications are automatically translated into add/remove notifications if the source/destination * is outside of the monitored collection hierarchy. * @param enabled enables collection move translation if set as @c true * @since 4.9 */ void setCollectionMoveTranslationEnabled(bool enabled); Q_SIGNALS: /** * This signal is emitted if a monitored item has changed, e.g. item parts have been modified. * * @param item The changed item. * @param partIdentifiers The identifiers of the item parts that has been changed. */ void itemChanged(const Akonadi::Item &item, const QSet &partIdentifiers); /** * This signal is emitted if flags of monitored items have changed. * * @param items Items that were changed * @param addedFlags Flags that have been added to each item in @p items * @param removedFlags Flags that have been removed from each item in @p items * @since 4.11 */ void itemsFlagsChanged(const Akonadi::Item::List &items, const QSet &addedFlags, const QSet &removedFlags); /** * This signal is emitted if tags of monitored items have changed. * * @param items Items that were changed * @param addedTags Tags that have been added to each item in @p items. * @param removedTags Tags that have been removed from each item in @p items * @since 4.13 */ void itemsTagsChanged(const Akonadi::Item::List &items, const QSet &addedTags, const QSet &removedTags); /** * This signal is emitted if relations of monitored items have changed. * * @param items Items that were changed * @param addedRelations Relations that have been added to each item in @p items. * @param removedRelations Relations that have been removed from each item in @p items * @since 4.15 */ void itemsRelationsChanged(const Akonadi::Item::List &items, const Akonadi::Relation::List &addedRelations, const Akonadi::Relation::List &removedRelations); /** * This signal is emitted if a monitored item has been moved between two collections * * @param item The moved item. * @param collectionSource The collection the item has been moved from. * @param collectionDestination The collection the item has been moved to. */ void itemMoved(const Akonadi::Item &item, const Akonadi::Collection &collectionSource, const Akonadi::Collection &collectionDestination); /** * This is signal is emitted when multiple monitored items have been moved between two collections * * @param items Moved items * @param collectionSource The collection the items have been moved from. * @param collectionDestination The collection the items have been moved to. * * @since 4.11 */ void itemsMoved(const Akonadi::Item::List &items, const Akonadi::Collection &collectionSource, const Akonadi::Collection &collectionDestination); /** * This signal is emitted if an item has been added to a monitored collection in the Akonadi storage. * * @param item The new item. * @param collection The collection the item has been added to. */ void itemAdded(const Akonadi::Item &item, const Akonadi::Collection &collection); /** * This signal is emitted if * - a monitored item has been removed from the Akonadi storage * or * - a item has been removed from a monitored collection. * * @param item The removed item. */ void itemRemoved(const Akonadi::Item &item); /** * This signal is emitted if monitored items have been removed from Akonadi * storage of items have been removed from a monitored collection. * * @param items Removed items * * @since 4.11 */ void itemsRemoved(const Akonadi::Item::List &items); /** * This signal is emitted if a reference to an item is added to a virtual collection. * @param item The linked item. * @param collection The collection the item is linked to. * * @since 4.2 */ void itemLinked(const Akonadi::Item &item, const Akonadi::Collection &collection); /** * This signal is emitted if a reference to multiple items is added to a virtual collection * * @param items The linked items * @param collection The collections the items are linked to * * @since 4.11 */ void itemsLinked(const Akonadi::Item::List &items, const Akonadi::Collection &collection); /** * This signal is emitted if a reference to an item is removed from a virtual collection. * @param item The unlinked item. * @param collection The collection the item is unlinked from. * * @since 4.2 */ void itemUnlinked(const Akonadi::Item &item, const Akonadi::Collection &collection); /** * This signal is emitted if a reference to items is removed from a virtual collection * * @param items The unlinked items * @param collection The collections the items are unlinked from * * @since 4.11 */ void itemsUnlinked(const Akonadi::Item::List &items, const Akonadi::Collection &collection); /** * This signal is emitted if a new collection has been added to a monitored collection in the Akonadi storage. * * @param collection The new collection. * @param parent The parent collection. */ void collectionAdded(const Akonadi::Collection &collection, const Akonadi::Collection &parent); /** * This signal is emitted if a monitored collection has been changed (properties or content). * * @param collection The changed collection. */ void collectionChanged(const Akonadi::Collection &collection); /** * This signal is emitted if a monitored collection has been changed (properties or attributes). * * @param collection The changed collection. * @param attributeNames The names of the collection attributes that have been changed. * * @since 4.4 */ void collectionChanged(const Akonadi::Collection &collection, const QSet &attributeNames); /** * This signals is emitted if a monitored collection has been moved. * * @param collection The moved collection. * @param source The previous parent collection. * @param destination The new parent collection. * * @since 4.4 */ void collectionMoved(const Akonadi::Collection &collection, const Akonadi::Collection &source, const Akonadi::Collection &destination); /** * This signal is emitted if a monitored collection has been removed from the Akonadi storage. * * @param collection The removed collection. */ void collectionRemoved(const Akonadi::Collection &collection); /** * This signal is emitted if a collection has been subscribed to by the user. * It will be emitted even for unmonitored collections as the check for whether to * monitor it has not been applied yet. * * @param collection The subscribed collection * @param parent The parent collection of the subscribed collection. * * @since 4.6 */ void collectionSubscribed(const Akonadi::Collection &collection, const Akonadi::Collection &parent); /** * This signal is emitted if a user unsubscribes from a collection. * * @param collection The unsubscribed collection * * @since 4.6 */ void collectionUnsubscribed(const Akonadi::Collection &collection); /** * This signal is emitted if the statistics information of a monitored collection * has changed. * * @param id The collection identifier of the changed collection. * @param statistics The updated collection statistics, invalid if automatic * fetching of statistics changes is disabled. */ void collectionStatisticsChanged(Akonadi::Collection::Id id, const Akonadi::CollectionStatistics &statistics); /** * This signal is emitted if a tag has been added to Akonadi storage. * * @param tag The added tag * @since 4.13 */ void tagAdded(const Akonadi::Tag &tag); /** * This signal is emitted if a monitored tag is changed on the server. * * @param tag The changed tag. * @since 4.13 */ void tagChanged(const Akonadi::Tag &tag); /** * This signal is emitted if a monitored tag is removed from the server storage. * * The monitor will also emit itemTagsChanged() signal for all monitored items * (if any) that were tagged by @p tag. * * @param tag The removed tag. * @since 4.13 */ void tagRemoved(const Akonadi::Tag &tag); /** * This signal is emitted if a relation has been added to Akonadi storage. * * The monitor will also emit itemRelationsChanged() signal for all monitored items * hat are affected by @p relation. * * @param relation The added relation * @since 4.13 */ void relationAdded(const Akonadi::Relation &relation); /** * This signal is emitted if a monitored relation is removed from the server storage. * * The monitor will also emit itemRelationsChanged() signal for all monitored items * that were affected by @p relation. * * @param relation The removed relation. * @since 4.13 */ void relationRemoved(const Akonadi::Relation &relation); /** * This signal is emitted when Subscribers are monitored and a new subscriber * subscribers to the server. * * @param subscriber The new subscriber * @since 5.4 * * @note Monitoring for subscribers and listening to this signal only makes * sense if you want to globally debug Monitors. There is no reason to use * this in regular applications. */ void notificationSubscriberAdded(const Akonadi::NotificationSubscriber &subscriber); /** * This signal is emitted when Subscribers are monitored and an existing * subscriber changes its subscription. * * @param subscriber The changed subscriber * @since 5.4 * * @note Monitoring for subscribers and listening to this signal only makes * sense if you want to globally debug Monitors. There is no reason to use * this in regular applications. */ void notificationSubscriberChanged(const Akonadi::NotificationSubscriber &subscriber); /** * This signal is emitted when Subscribers are monitored and an existing * subscriber unsubscribes from the server. * * @param subscriber The removed subscriber * @since 5.4 * * @note Monitoring for subscribers and listening to this signal only makes * sense if you want to globally debug Monitors. There is no reason to use * this in regular applications. */ void notificationSubscriberRemoved(const Akonadi::NotificationSubscriber &subscriber); /** * This signal is emitted when Notifications are monitored and the server emits * any change notification. * * @since 5.4 * * @note Getting introspection into all change notifications only makes sense * if you want to globally debug Notifications. There is no reason to use * this in regular applications. */ void debugNotification(const Akonadi::ChangeNotification ¬ification); /** * This signal is emitted if the Monitor starts or stops monitoring @p collection explicitly. * @param collection The collection * @param monitored Whether the collection is now being monitored or not. * * @since 4.3 */ void collectionMonitored(const Akonadi::Collection &collection, bool monitored); /** * This signal is emitted if the Monitor starts or stops monitoring @p item explicitly. * @param item The item * @param monitored Whether the item is now being monitored or not. * * @since 4.3 */ void itemMonitored(const Akonadi::Item &item, bool monitored); /** * This signal is emitted if the Monitor starts or stops monitoring the resource with the identifier @p identifier explicitly. * @param identifier The identifier of the resource. * @param monitored Whether the resource is now being monitored or not. * * @since 4.3 */ void resourceMonitored(const QByteArray &identifier, bool monitored); /** * This signal is emitted if the Monitor starts or stops monitoring @p mimeType explicitly. * @param mimeType The mimeType. * @param monitored Whether the mimeType is now being monitored or not. * * @since 4.3 */ void mimeTypeMonitored(const QString &mimeType, bool monitored); /** * This signal is emitted if the Monitor starts or stops monitoring everything. * @param monitored Whether everything is now being monitored or not. * * @since 4.3 */ void allMonitored(bool monitored); /** * This signal is emitted if the Monitor starts or stops monitoring @p tag explicitly. * @param tag The tag. * @param monitored Whether the tag is now being monitored or not. * @since 4.13 */ void tagMonitored(const Akonadi::Tag &tag, bool monitored); /** * This signal is emitted if the Monitor starts or stops monitoring @p type explicitly * @param type The type. * @param monitored Whether the type is now being monitored or not. * @since 4.13 */ void typeMonitored(const Akonadi::Monitor::Type type, bool monitored); void monitorReady(); protected: //@cond PRIVATE void connectNotify(const QMetaMethod &signal) override; void disconnectNotify(const QMetaMethod &signal) override; friend class EntityTreeModel; friend class EntityTreeModelPrivate; MonitorPrivate *d_ptr; explicit Monitor(MonitorPrivate *d, QObject *parent = nullptr); //@endcond private: Q_DECLARE_PRIVATE(Monitor) //@cond PRIVATE - Q_PRIVATE_SLOT(d_ptr, void slotSessionDestroyed(QObject *)) - Q_PRIVATE_SLOT(d_ptr, void slotStatisticsChangedFinished(KJob *)) - Q_PRIVATE_SLOT(d_ptr, void slotFlushRecentlyChangedCollections()) - Q_PRIVATE_SLOT(d_ptr, void slotUpdateSubscription()) Q_PRIVATE_SLOT(d_ptr, void handleCommands()) - Q_PRIVATE_SLOT(d_ptr, void dataAvailable()) - Q_PRIVATE_SLOT(d_ptr, void serverStateChanged(Akonadi::ServerManager::State)) Q_PRIVATE_SLOT(d_ptr, void invalidateCollectionCache(qint64)) Q_PRIVATE_SLOT(d_ptr, void invalidateItemCache(qint64)) Q_PRIVATE_SLOT(d_ptr, void invalidateTagCache(qint64)) friend class ResourceBasePrivate; //@endcond }; } #endif diff --git a/src/core/monitor_p.cpp b/src/core/monitor_p.cpp index 3730167a3..3902db931 100644 --- a/src/core/monitor_p.cpp +++ b/src/core/monitor_p.cpp @@ -1,1404 +1,1399 @@ /* 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; using namespace AkRanges; 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))); + QObject::connect(collectionCache, &CollectionCache::dataAvailable, q_ptr, [this]() { dataAvailable(); }); + QObject::connect(itemCache, &ItemCache::dataAvailable, q_ptr, [this]() { dataAvailable(); }); + QObject::connect(tagCache, &TagCache::dataAvailable, q_ptr, [this]() { dataAvailable(); }); + QObject::connect(ServerManager::self(), &ServerManager::stateChanged, q_ptr, [this](auto state) { serverStateChanged(state); }); statisticsCompressionTimer.setSingleShot(true); statisticsCompressionTimer.setInterval(500); - QObject::connect(&statisticsCompressionTimer, SIGNAL(timeout()), q_ptr, SLOT(slotFlushRecentlyChangedCollections())); + QObject::connect(&statisticsCompressionTimer, &QTimer::timeout, q_ptr, [this]() { 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(q_ptr); pendingModificationTimer->setSingleShot(true); pendingModificationTimer->setInterval(0); pendingModificationTimer->start(); - q_ptr->connect(pendingModificationTimer, SIGNAL(timeout()), - q_ptr, SLOT(slotUpdateSubscription())); + q_ptr->connect(pendingModificationTimer, &QTimer::timeout, q_ptr, [this]() { 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()) | Actions::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()); Q_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; Q_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 | Actions::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() | Actions::toQList); removedTags = tagCache->retrieve(msg.removedTags() | Actions::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, addedTags | Actions::toQSet, removedTags | Actions::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: return emitToListeners(&Monitor::collectionSubscribed, collection, parent); case Protocol::CollectionChangeNotification::Unsubscribe: return 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) +void MonitorPrivate::updateListeners(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/core/monitor_p.h b/src/core/monitor_p.h index 221abcadc..0d0bab267 100644 --- a/src/core/monitor_p.h +++ b/src/core/monitor_p.h @@ -1,413 +1,413 @@ /* 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. */ #ifndef AKONADI_MONITOR_P_H #define AKONADI_MONITOR_P_H #include "akonadicore_export.h" #include "monitor.h" #include "collection.h" #include "collectionstatisticsjob.h" #include "collectionfetchscope.h" #include "item.h" #include "itemfetchscope.h" #include "tagfetchscope.h" #include "job.h" #include "entitycache_p.h" #include "servermanager.h" #include "changenotificationdependenciesfactory_p.h" #include "connection_p.h" #include "commandbuffer_p.h" #include "private/protocol_p.h" #include #include #include #include #include namespace Akonadi { class Monitor; class ChangeNotification; // A helper struct to wrap pointer to member function (which cannot be contained // in a regular pointer) struct SignalId { constexpr SignalId() = default; using Unit = uint; static constexpr int Size = sizeof(&Monitor::itemAdded) / sizeof(Unit); Unit data[sizeof(&Monitor::itemAdded) / sizeof(Unit)] = { 0 }; - inline bool operator==(const SignalId &other) const { + inline bool operator==(SignalId other) const { for (int i = Size - 1; i >= 0; --i) { if (data[i] != other.data[i]) { return false; } } return true; } }; -inline uint qHash(const SignalId &sig) +inline uint qHash(SignalId sig) { // The 4 LSBs of the address should be enough to give us a good hash return sig.data[SignalId::Size - 1]; } /** * @internal */ class AKONADICORE_EXPORT MonitorPrivate { public: enum ListenerAction { AddListener, RemoveListener }; MonitorPrivate(ChangeNotificationDependenciesFactory *dependenciesFactory_, Monitor *parent); virtual ~MonitorPrivate(); void init(); Monitor *q_ptr; Q_DECLARE_PUBLIC(Monitor) ChangeNotificationDependenciesFactory *dependenciesFactory = nullptr; QPointer ntfConnection; Collection::List collections; QSet resources; QSet items; QSet tags; QSet types; QSet mimetypes; bool monitorAll; bool exclusive; QList sessions; ItemFetchScope mItemFetchScope; TagFetchScope mTagFetchScope; CollectionFetchScope mCollectionFetchScope; bool mFetchChangedOnly; Session *session = nullptr; CollectionCache *collectionCache = nullptr; ItemListCache *itemCache = nullptr; TagListCache *tagCache = nullptr; QMimeDatabase mimeDatabase; QHash listeners; CommandBuffer mCommandBuffer; Protocol::ModifySubscriptionCommand::ModifiedParts pendingModificationChanges; Protocol::ModifySubscriptionCommand pendingModification; QTimer *pendingModificationTimer; bool monitorReady; // The waiting list QQueue pendingNotifications; // The messages for which data is currently being fetched QQueue pipeline; // In a pure Monitor, the pipeline contains items that were dequeued from pendingNotifications. // The ordering [pipeline] [pendingNotifications] is kept at all times. // [] [A B C] -> [A B] [C] -> [B] [C] -> [B C] [] -> [C] [] -> [] // In a ChangeRecorder, the pipeline contains one item only, and not dequeued yet. // [] [A B C] -> [A] [A B C] -> [] [A B C] -> (changeProcessed) [] [B C] -> [B] [B C] etc... bool fetchCollection; bool fetchCollectionStatistics; bool collectionMoveTranslationEnabled; // Virtual methods for ChangeRecorder virtual void notificationsEnqueued(int) { } virtual void notificationsErased() { } // Virtual so it can be overridden in FakeMonitor. virtual bool connectToNotificationManager(); void disconnectFromNotificationManager(); void dispatchNotifications(); void flushPipeline(); bool ensureDataAvailable(const Protocol::ChangeNotificationPtr &msg); /** * Sends out the change notification @p msg. * @param msg the change notification to send * @return @c true if the notification was actually send to someone, @c false if no one was listening. */ virtual bool emitNotification(const Protocol::ChangeNotificationPtr &msg); void updatePendingStatistics(const Protocol::ChangeNotificationPtr &msg); void invalidateCaches(const Protocol::ChangeNotificationPtr &msg); /** Used by ResourceBase to inform us about collection changes before the notifications are emitted, needed to avoid the missing RID race on change replay. */ void invalidateCache(const Collection &col); /// Virtual so that ChangeRecorder can set it to 0 and handle the pipeline itself virtual int pipelineSize() const; // private Q_SLOTS void dataAvailable(); void slotSessionDestroyed(QObject *object); void slotStatisticsChangedFinished(KJob *job); void slotFlushRecentlyChangedCollections(); /** Returns whether a message was appended to @p notificationQueue */ int translateAndCompress(QQueue ¬ificationQueue, const Protocol::ChangeNotificationPtr &msg); void handleCommands(); virtual void slotNotify(const Protocol::ChangeNotificationPtr &msg); /** * Sends out a change notification for an item. * @return @c true if the notification was actually send to someone, @c false if no one was listening. */ bool emitItemsNotification(const Protocol::ItemChangeNotification &msg, const Item::List &items = Item::List(), const Collection &collection = Collection(), const Collection &collectionDest = Collection()); /** * Sends out a change notification for a collection. * @return @c true if the notification was actually send to someone, @c false if no one was listening. */ bool emitCollectionNotification(const Protocol::CollectionChangeNotification &msg, const Collection &col = Collection(), const Collection &par = Collection(), const Collection &dest = Collection()); bool emitTagNotification(const Protocol::TagChangeNotification &msg, const Tag &tags); bool emitRelationNotification(const Protocol::RelationChangeNotification &msg, const Relation &relation); bool emitSubscriptionChangeNotification(const Protocol::SubscriptionChangeNotification &msg, const NotificationSubscriber &subscriber); bool emitDebugChangeNotification(const Protocol::DebugChangeNotification &msg, const ChangeNotification &ntf); void serverStateChanged(Akonadi::ServerManager::State state); /** * This method is called by the ChangeMediator to enforce an invalidation of the passed collection. */ void invalidateCollectionCache(qint64 collectionId); /** * This method is called by the ChangeMediator to enforce an invalidation of the passed item. */ void invalidateItemCache(qint64 itemId); /** * This method is called by the ChangeMediator to enforce an invalidation of the passed tag. */ void invalidateTagCache(qint64 tagId); void scheduleSubscriptionUpdate(); void slotUpdateSubscription(); - void updateListeners(const QMetaMethod &signal, ListenerAction action); + void updateListeners(QMetaMethod signal, ListenerAction action); template void updateListener(Signal signal, ListenerAction action) { auto it = listeners.find(signalId(signal)); if (action == AddListener) { if (it == listeners.end()) { it = listeners.insert(signalId(signal), 0); } ++(*it); } else { if (--(*it) == 0) { listeners.erase(it); } } } static Protocol::ModifySubscriptionCommand::ChangeType monitorTypeToProtocol(Monitor::Type type); /** @brief Class used to determine when to purge items in a Collection The buffer method can be used to buffer a Collection. This may cause another Collection to be purged if it is removed from the buffer. The purge method is used to purge a Collection from the buffer, but not the model. This is used for example, to not buffer Collections anymore if they get referenced, and to ensure that one Collection does not appear twice in the buffer. Check whether a Collection is buffered using the isBuffered method. */ class AKONADI_TESTS_EXPORT PurgeBuffer { // Buffer the most recent 10 unreferenced Collections static const int MAXBUFFERSIZE = 10; public: explicit PurgeBuffer() { } /** Adds @p id to the Collections to be buffered @returns The collection id which was removed form the buffer or -1 if none. */ Collection::Id buffer(Collection::Id id); /** Removes @p id from the Collections being buffered */ void purge(Collection::Id id); bool isBuffered(Collection::Id id) const { return m_buffer.contains(id); } static int buffersize(); private: QQueue m_buffer; } m_buffer; QHash refCountMap; bool useRefCounting; void ref(Collection::Id id); Collection::Id deref(Collection::Id id); /** * Returns true if the collection is monitored by monitor. * * A collection is always monitored if useRefCounting is false. * If ref counting is used, the collection is only monitored, * if the collection is either in refCountMap or m_buffer. * If ref counting is used and the collection is not in refCountMap or m_buffer, * no updates for the contained items are emitted, because they are lazily ignored. */ bool isMonitored(Collection::Id colId) const; private: // collections that need a statistics update QSet recentlyChangedCollections; QTimer statisticsCompressionTimer; /** @returns True if @p msg should be ignored. Otherwise appropriate signals are emitted for it. */ bool isLazilyIgnored(const Protocol::ChangeNotificationPtr &msg, bool allowModifyFlagsConversion = false) const; /** Sets @p needsSplit to True when @p msg contains more than one item and there's at least one listener that does not support batch operations. Sets @p batchSupported to True when there's at least one listener that supports batch operations. */ void checkBatchSupport(const Protocol::ChangeNotificationPtr &msg, bool &needsSplit, bool &batchSupported) const; Protocol::ChangeNotificationList splitMessage(const Protocol::ItemChangeNotification &msg, bool legacy) const; bool isCollectionMonitored(Collection::Id collection) const { if (collection < 0) { return false; } if (collections.contains(Collection(collection))) { return true; } if (collections.contains(Collection::root())) { return true; } return false; } bool isMimeTypeMonitored(const QString &mimetype) const { if (mimetypes.contains(mimetype)) { return true; } const QMimeType mimeType = mimeDatabase.mimeTypeForName(mimetype); if (!mimeType.isValid()) { return false; } for (const QString &mt : mimetypes) { if (mimeType.inherits(mt)) { return true; } } return false; } template bool isMoveDestinationResourceMonitored(const T &msg) const { if (msg.operation() != T::Move) { return false; } return resources.contains(msg.destinationResource()); } void fetchStatistics(Collection::Id colId) { CollectionStatisticsJob *job = new CollectionStatisticsJob(Collection(colId), session); - QObject::connect(job, SIGNAL(result(KJob*)), q_ptr, SLOT(slotStatisticsChangedFinished(KJob*))); + QObject::connect(job, &KJob::result, q_ptr, [this](KJob *job) { slotStatisticsChangedFinished(job); }); } void notifyCollectionStatisticsWatchers(Collection::Id collection, const QByteArray &resource); bool fetchCollections() const; bool fetchItems() const; // A hack to "cast" pointer to member function to something we can easily // use as a key in the hashtable template constexpr SignalId signalId(Signal signal) const { union { Signal in; SignalId out; } h = {signal}; return h.out; } template bool hasListeners(Signal signal) const { auto it = listeners.find(signalId(signal)); return it != listeners.end(); } template bool emitToListeners(Signal signal, Args ... args) { if (hasListeners(signal)) { Q_EMIT (q_ptr->*signal)(std::forward(args) ...); return true; } return false; } }; } #endif diff --git a/src/core/partfetcher.cpp b/src/core/partfetcher.cpp index b698fae0f..25e07fc5d 100644 --- a/src/core/partfetcher.cpp +++ b/src/core/partfetcher.cpp @@ -1,184 +1,182 @@ /* Copyright (c) 2009 Stephen Kelly 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 "partfetcher.h" #include "entitytreemodel.h" #include "session.h" #include "itemfetchjob.h" #include "itemfetchscope.h" #include Q_DECLARE_METATYPE(QSet) using namespace Akonadi; namespace Akonadi { class PartFetcherPrivate { PartFetcherPrivate(PartFetcher *partFetcher, const QModelIndex &index, const QByteArray &partName) : m_persistentIndex(index) , m_partName(partName) , q_ptr(partFetcher) { } void fetchJobDone(KJob *job); void modelDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight); QPersistentModelIndex m_persistentIndex; QByteArray m_partName; Item m_item; Q_DECLARE_PUBLIC(PartFetcher) PartFetcher *q_ptr; }; } void PartFetcherPrivate::fetchJobDone(KJob *job) { Q_Q(PartFetcher); if (job->error()) { q->setError(KJob::UserDefinedError); q->setErrorText(i18n("Unable to fetch item for index")); q->emitResult(); return; } ItemFetchJob *fetchJob = qobject_cast(job); const Item::List list = fetchJob->items(); Q_ASSERT(list.size() == 1); // If m_persistentIndex comes from a selection proxy model, it could become // invalid if the user clicks around a lot. if (!m_persistentIndex.isValid()) { q->setError(KJob::UserDefinedError); q->setErrorText(i18n("Index is no longer available")); q->emitResult(); return; } const QSet loadedParts = m_persistentIndex.data(EntityTreeModel::LoadedPartsRole).value >(); Q_ASSERT(!loadedParts.contains(m_partName)); Item item = m_persistentIndex.data(EntityTreeModel::ItemRole).value(); item.apply(list.at(0)); QAbstractItemModel *model = const_cast(m_persistentIndex.model()); Q_ASSERT(model); QVariant itemVariant = QVariant::fromValue(item); model->setData(m_persistentIndex, itemVariant, EntityTreeModel::ItemRole); m_item = item; q->emitResult(); } PartFetcher::PartFetcher(const QModelIndex &index, const QByteArray &partName, QObject *parent) : KJob(parent) , d_ptr(new PartFetcherPrivate(this, index, partName)) { } PartFetcher::~PartFetcher() { delete d_ptr; } void PartFetcher::start() { Q_D(PartFetcher); const QModelIndex index = d->m_persistentIndex; const QSet loadedParts = index.data(EntityTreeModel::LoadedPartsRole).value >(); if (loadedParts.contains(d->m_partName)) { d->m_item = d->m_persistentIndex.data(EntityTreeModel::ItemRole).value(); emitResult(); return; } const QSet availableParts = index.data(EntityTreeModel::AvailablePartsRole).value >(); if (!availableParts.contains(d->m_partName)) { setError(UserDefinedError); setErrorText(i18n("Payload part '%1' is not available for this index", QString::fromLatin1(d->m_partName))); emitResult(); return; } Akonadi::Session *session = qobject_cast(qvariant_cast(index.data(EntityTreeModel::SessionRole))); if (!session) { setError(UserDefinedError); setErrorText(i18n("No session available for this index")); emitResult(); return; } const Akonadi::Item item = index.data(EntityTreeModel::ItemRole).value(); if (!item.isValid()) { setError(UserDefinedError); setErrorText(i18n("No item available for this index")); emitResult(); return; } ItemFetchScope scope; scope.fetchPayloadPart(d->m_partName); ItemFetchJob *itemFetchJob = new Akonadi::ItemFetchJob(item, session); itemFetchJob->setFetchScope(scope); - - connect(itemFetchJob, SIGNAL(result(KJob*)), - this, SLOT(fetchJobDone(KJob*))); + connect(itemFetchJob, &KJob::result, this, [d](KJob *job) { d->fetchJobDone(job); }); } QModelIndex PartFetcher::index() const { Q_D(const PartFetcher); return d->m_persistentIndex; } QByteArray PartFetcher::partName() const { Q_D(const PartFetcher); return d->m_partName; } Item PartFetcher::item() const { Q_D(const PartFetcher); return d->m_item; } #include "moc_partfetcher.cpp" diff --git a/src/core/partfetcher.h b/src/core/partfetcher.h index 32d9f8d3c..a1429d0a4 100644 --- a/src/core/partfetcher.h +++ b/src/core/partfetcher.h @@ -1,123 +1,122 @@ /* Copyright (c) 2009 Stephen Kelly 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_PARTFETCHER_H #define AKONADI_PARTFETCHER_H #include #include "akonadicore_export.h" class QModelIndex; namespace Akonadi { class Item; class PartFetcherPrivate; /** * @short Convenience class for getting payload parts from an Akonadi Model. * * This class can be used to retrieve individual payload parts from an EntityTreeModel, * and fetch them asynchronously from the Akonadi storage if necessary. * * The requested part is emitted though the partFetched signal. * * Example: * * @code * * const QModelIndex index = view->selectionModel()->currentIndex(); * * PartFetcher *fetcher = new PartFetcher( index, Akonadi::MessagePart::Envelope ); * connect( fetcher, SIGNAL(result(KJob*)), SLOT(fetchResult(KJob*)) ); * fetcher->start(); * * ... * * MyClass::fetchResult( KJob *job ) * { * if ( job->error() ) { * qDebug() << job->errorText(); * return; * } * * PartFetcher *fetcher = qobject_cast( job ); * * const Item item = fetcher->item(); * // do something with the item * } * * @endcode * * @author Stephen Kelly * @since 4.4 */ class AKONADICORE_EXPORT PartFetcher : public KJob { Q_OBJECT public: /** * Creates a new part fetcher. * * @param index The index of the item to fetch the part from. * @param partName The name of the payload part to fetch. * @param parent The parent object. */ PartFetcher(const QModelIndex &index, const QByteArray &partName, QObject *parent = nullptr); /** * Destroys the part fetcher. */ ~PartFetcher() override; /** * Starts the fetch operation. */ void start() override; /** * Returns the index of the item the part was fetched from. */ QModelIndex index() const; /** * Returns the name of the part that has been fetched. */ QByteArray partName() const; /** * Returns the item that contains the fetched payload part. */ Item item() const; private: //@cond PRIVATE Q_DECLARE_PRIVATE(Akonadi::PartFetcher) PartFetcherPrivate *const d_ptr; - Q_PRIVATE_SLOT(d_func(), void fetchJobDone(KJob *job)) //@endcond }; } #endif diff --git a/src/core/protocolhelper.cpp b/src/core/protocolhelper.cpp index adfb9ae7e..e9aa07544 100644 --- a/src/core/protocolhelper.cpp +++ b/src/core/protocolhelper.cpp @@ -1,780 +1,780 @@ /* Copyright (c) 2008 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 "protocolhelper_p.h" #include "akonadicore_debug.h" #include "attributefactory.h" #include "collectionstatistics.h" #include "item_p.h" #include "collection_p.h" #include "tag_p.h" #include "exceptionbase.h" #include "itemserializer_p.h" #include "itemserializerplugin.h" #include "servermanager.h" #include "tagfetchscope.h" #include "persistentsearchattribute.h" #include "itemfetchscope.h" #include "private/protocol_p.h" #include "private/externalpartstorage_p.h" #include #include #include using namespace Akonadi; using namespace AkRanges; CachePolicy ProtocolHelper::parseCachePolicy(const Protocol::CachePolicy &policy) { CachePolicy cp; cp.setCacheTimeout(policy.cacheTimeout()); cp.setIntervalCheckTime(policy.checkInterval()); cp.setInheritFromParent(policy.inherit()); cp.setSyncOnDemand(policy.syncOnDemand()); cp.setLocalParts(policy.localParts()); return cp; } Protocol::CachePolicy ProtocolHelper::cachePolicyToProtocol(const CachePolicy &policy) { Protocol::CachePolicy proto; proto.setCacheTimeout(policy.cacheTimeout()); proto.setCheckInterval(policy.intervalCheckTime()); proto.setInherit(policy.inheritFromParent()); proto.setSyncOnDemand(policy.syncOnDemand()); proto.setLocalParts(policy.localParts()); return proto; } template inline static void parseAttributesImpl(const Protocol::Attributes &attributes, T *entity) { for (auto iter = attributes.cbegin(), end = attributes.cend(); iter != end; ++iter) { Attribute *attribute = AttributeFactory::createAttribute(iter.key()); if (!attribute) { qCWarning(AKONADICORE_LOG) << "Warning: unknown attribute" << iter.key(); continue; } attribute->deserialize(iter.value()); entity->addAttribute(attribute); } } template inline static void parseAncestorsCachedImpl(const QVector &ancestors, T *entity, Collection::Id parentCollection, ProtocolHelperValuePool *pool) { if (!pool || parentCollection == -1) { // if no pool or parent collection id is provided we can't cache anything, so continue as usual ProtocolHelper::parseAncestors(ancestors, entity); return; } if (pool->ancestorCollections.contains(parentCollection)) { // ancestor chain is cached already, so use the cached value entity->setParentCollection(pool->ancestorCollections.value(parentCollection)); } else { // not cached yet, parse the chain ProtocolHelper::parseAncestors(ancestors, entity); pool->ancestorCollections.insert(parentCollection, entity->parentCollection()); } } template inline static Protocol::Attributes attributesToProtocolImpl(const T &entity, bool ns) { Protocol::Attributes attributes; const auto attrs = entity.attributes(); for (const auto *attr : attrs) { attributes.insert(ProtocolHelper::encodePartIdentifier(ns ? ProtocolHelper::PartAttribute : ProtocolHelper::PartGlobal, attr->type()), attr->serialized()); } return attributes; } void ProtocolHelper::parseAncestorsCached(const QVector &ancestors, Item *item, Collection::Id parentCollection, ProtocolHelperValuePool *pool) { parseAncestorsCachedImpl(ancestors, item, parentCollection, pool); } void ProtocolHelper::parseAncestorsCached(const QVector &ancestors, Collection *collection, Collection::Id parentCollection, ProtocolHelperValuePool *pool) { parseAncestorsCachedImpl(ancestors, collection, parentCollection, pool); } void ProtocolHelper::parseAncestors(const QVector &ancestors, Item *item) { Collection fakeCollection; parseAncestors(ancestors, &fakeCollection); item->setParentCollection(fakeCollection.parentCollection()); } void ProtocolHelper::parseAncestors(const QVector &ancestors, Collection *collection) { static const Collection::Id rootCollectionId = Collection::root().id(); Collection *current = collection; for (const Protocol::Ancestor &ancestor : ancestors) { if (ancestor.id() == rootCollectionId) { current->setParentCollection(Collection::root()); break; } Akonadi::Collection parentCollection(ancestor.id()); parentCollection.setName(ancestor.name()); parentCollection.setRemoteId(ancestor.remoteId()); parseAttributesImpl(ancestor.attributes(), &parentCollection); current->setParentCollection(parentCollection); current = ¤t->parentCollection(); } } static Collection::ListPreference parsePreference(Tristate value) { switch (value) { case Tristate::True: return Collection::ListEnabled; case Tristate::False: return Collection::ListDisabled; case Tristate::Undefined: return Collection::ListDefault; } Q_ASSERT(false); return Collection::ListDefault; } CollectionStatistics ProtocolHelper::parseCollectionStatistics(const Protocol::FetchCollectionStatsResponse &stats) { CollectionStatistics cs; cs.setCount(stats.count()); cs.setSize(stats.size()); cs.setUnreadCount(stats.unseen()); return cs; } void ProtocolHelper::parseAttributes(const Protocol::Attributes &attributes, Item *item) { parseAttributesImpl(attributes, item); } void ProtocolHelper::parseAttributes(const Protocol::Attributes &attributes, Collection *collection) { parseAttributesImpl(attributes, collection); } void ProtocolHelper::parseAttributes(const Protocol::Attributes &attributes, Tag *tag) { parseAttributesImpl(attributes, tag); } Protocol::Attributes ProtocolHelper::attributesToProtocol(const Item &item, bool ns) { return attributesToProtocolImpl(item, ns); } Protocol::Attributes ProtocolHelper::attributesToProtocol(const Collection &collection, bool ns) { return attributesToProtocolImpl(collection, ns); } Protocol::Attributes ProtocolHelper::attributesToProtocol(const Tag &tag, bool ns) { return attributesToProtocolImpl(tag, ns); } Protocol::Attributes ProtocolHelper::attributesToProtocol(const std::vector &modifiedAttributes, bool ns) { Protocol::Attributes attributes; for (const Attribute *attr : modifiedAttributes) { attributes.insert(ProtocolHelper::encodePartIdentifier(ns ? ProtocolHelper::PartAttribute : ProtocolHelper::PartGlobal, attr->type()), attr->serialized()); } return attributes; } Collection ProtocolHelper::parseCollection(const Protocol::FetchCollectionsResponse &data, bool requireParent) { Collection collection(data.id()); if (requireParent) { collection.setParentCollection(Collection(data.parentId())); } collection.setName(data.name()); collection.setRemoteId(data.remoteId()); collection.setRemoteRevision(data.remoteRevision()); collection.setResource(data.resource()); collection.setContentMimeTypes(data.mimeTypes()); collection.setVirtual(data.isVirtual()); collection.setStatistics(parseCollectionStatistics(data.statistics())); collection.setCachePolicy(parseCachePolicy(data.cachePolicy())); parseAncestors(data.ancestors(), &collection); collection.setEnabled(data.enabled()); collection.setLocalListPreference(Collection::ListDisplay, parsePreference(data.displayPref())); collection.setLocalListPreference(Collection::ListIndex, parsePreference(data.indexPref())); collection.setLocalListPreference(Collection::ListSync, parsePreference(data.syncPref())); if (!data.searchQuery().isEmpty()) { auto attr = collection.attribute(Collection::AddIfMissing); attr->setQueryString(data.searchQuery()); const auto cols = data.searchCollections() | Views::transform([](const auto id) { return Collection{id}; }) | Actions::toQVector; attr->setQueryCollections(cols); } parseAttributes(data.attributes(), &collection); collection.d_ptr->resetChangeLog(); return collection; } Tag ProtocolHelper::parseTag(const Protocol::FetchTagsResponse &data) { Tag tag(data.id()); tag.setRemoteId(data.remoteId()); tag.setGid(data.gid()); tag.setType(data.type()); tag.setParent(Tag(data.parentId())); parseAttributes(data.attributes(), &tag); tag.d_ptr->resetChangeLog(); return tag; } QByteArray ProtocolHelper::encodePartIdentifier(PartNamespace ns, const QByteArray &label) { switch (ns) { case PartGlobal: return label; case PartPayload: return "PLD:" + label; case PartAttribute: return "ATR:" + label; default: Q_ASSERT(false); } return QByteArray(); } QByteArray ProtocolHelper::decodePartIdentifier(const QByteArray &data, PartNamespace &ns) { if (data.startsWith("PLD:")) { //krazy:exclude=strings ns = PartPayload; return data.mid(4); } else if (data.startsWith("ATR:")) { //krazy:exclude=strings ns = PartAttribute; return data.mid(4); } else { ns = PartGlobal; return data; } } Protocol::ScopeContext ProtocolHelper::commandContextToProtocol(const Akonadi::Collection &collection, const Akonadi::Tag &tag, const Item::List &requestedItems) { Protocol::ScopeContext ctx; if (tag.isValid()) { ctx.setContext(Protocol::ScopeContext::Tag, tag.id()); } if (collection == Collection::root()) { if (requestedItems.isEmpty() && !tag.isValid()) { // collection content listing throw Exception("Cannot perform item operations on root collection."); } } else { if (collection.isValid()) { ctx.setContext(Protocol::ScopeContext::Collection, collection.id()); } else if (!collection.remoteId().isEmpty()) { ctx.setContext(Protocol::ScopeContext::Collection, collection.remoteId()); } } return ctx; } Scope ProtocolHelper::hierarchicalRidToScope(const Collection &col) { if (col == Collection::root()) { return Scope({ Scope::HRID(0) }); } if (col.remoteId().isEmpty()) { return Scope(); } QVector chain; Collection c = col; while (!c.remoteId().isEmpty()) { chain.append(Scope::HRID(c.id(), c.remoteId())); c = c.parentCollection(); } return Scope(chain + QVector { Scope::HRID(0) }); } Scope ProtocolHelper::hierarchicalRidToScope(const Item &item) { return Scope(QVector({ Scope::HRID(item.id(), item.remoteId()) }) + hierarchicalRidToScope(item.parentCollection()).hridChain()); } Protocol::ItemFetchScope ProtocolHelper::itemFetchScopeToProtocol(const ItemFetchScope &fetchScope) { Protocol::ItemFetchScope fs; QVector parts; parts.reserve(fetchScope.payloadParts().size() + fetchScope.attributes().size()); parts += fetchScope.payloadParts() | Views::transform(std::bind(encodePartIdentifier, PartPayload, std::placeholders::_1)) | Actions::toQVector; parts += fetchScope.attributes() | Views::transform(std::bind(encodePartIdentifier, PartAttribute, std::placeholders::_1)) | Actions::toQVector; fs.setRequestedParts(parts); // The default scope fs.setFetch(Protocol::ItemFetchScope::Flags | Protocol::ItemFetchScope::Size | Protocol::ItemFetchScope::RemoteID | Protocol::ItemFetchScope::RemoteRevision | Protocol::ItemFetchScope::MTime); fs.setFetch(Protocol::ItemFetchScope::FullPayload, fetchScope.fullPayload()); fs.setFetch(Protocol::ItemFetchScope::AllAttributes, fetchScope.allAttributes()); fs.setFetch(Protocol::ItemFetchScope::CacheOnly, fetchScope.cacheOnly()); fs.setFetch(Protocol::ItemFetchScope::CheckCachedPayloadPartsOnly, fetchScope.checkForCachedPayloadPartsOnly()); fs.setFetch(Protocol::ItemFetchScope::IgnoreErrors, fetchScope.ignoreRetrievalErrors()); switch (fetchScope.ancestorRetrieval()) { case ItemFetchScope::Parent: fs.setAncestorDepth(Protocol::ItemFetchScope::ParentAncestor); break; case ItemFetchScope::All: fs.setAncestorDepth(Protocol::ItemFetchScope::AllAncestors); break; case ItemFetchScope::None: fs.setAncestorDepth(Protocol::ItemFetchScope::NoAncestor); break; default: Q_ASSERT(false); break; } if (fetchScope.fetchChangedSince().isValid()) { fs.setChangedSince(fetchScope.fetchChangedSince()); } fs.setFetch(Protocol::ItemFetchScope::RemoteID, fetchScope.fetchRemoteIdentification()); fs.setFetch(Protocol::ItemFetchScope::RemoteRevision, fetchScope.fetchRemoteIdentification()); fs.setFetch(Protocol::ItemFetchScope::GID, fetchScope.fetchGid()); fs.setFetch(Protocol::ItemFetchScope::Tags, fetchScope.fetchTags()); fs.setFetch(Protocol::ItemFetchScope::VirtReferences, fetchScope.fetchVirtualReferences()); fs.setFetch(Protocol::ItemFetchScope::MTime, fetchScope.fetchModificationTime()); fs.setFetch(Protocol::ItemFetchScope::Relations, fetchScope.fetchRelations()); return fs; } ItemFetchScope ProtocolHelper::parseItemFetchScope(const Protocol::ItemFetchScope &fetchScope) { ItemFetchScope ifs; const auto parts = fetchScope.requestedParts(); - for (const auto &part : fetchScope.requestedParts()) { + for (const auto &part : parts) { if (part.startsWith("PLD:")) { ifs.fetchPayloadPart(part.mid(4), true); } else if (part.startsWith("ATR:")) { ifs.fetchAttribute(part.mid(4), true); } } if (fetchScope.fetch(Protocol::ItemFetchScope::FullPayload)) { ifs.fetchFullPayload(true); } if (fetchScope.fetch(Protocol::ItemFetchScope::AllAttributes)) { ifs.fetchAllAttributes(true); } if (fetchScope.fetch(Protocol::ItemFetchScope::CacheOnly)) { ifs.setCacheOnly(true); } if (fetchScope.fetch(Protocol::ItemFetchScope::CheckCachedPayloadPartsOnly)) { ifs.setCheckForCachedPayloadPartsOnly(true); } if (fetchScope.fetch(Protocol::ItemFetchScope::IgnoreErrors)) { ifs.setIgnoreRetrievalErrors(true); } switch (fetchScope.ancestorDepth()) { case Protocol::ItemFetchScope::ParentAncestor: ifs.setAncestorRetrieval(ItemFetchScope::Parent); break; case Protocol::ItemFetchScope::AllAncestors: ifs.setAncestorRetrieval(ItemFetchScope::All); break; default: ifs.setAncestorRetrieval(ItemFetchScope::None); break; } if (fetchScope.changedSince().isValid()) { ifs.setFetchChangedSince(fetchScope.changedSince()); } if (fetchScope.fetch(Protocol::ItemFetchScope::RemoteID) || fetchScope.fetch(Protocol::ItemFetchScope::RemoteRevision)) { ifs.setFetchRemoteIdentification(true); } if (fetchScope.fetch(Protocol::ItemFetchScope::GID)) { ifs.setFetchGid(true); } if (fetchScope.fetch(Protocol::ItemFetchScope::Tags)) { ifs.setFetchTags(true); } if (fetchScope.fetch(Protocol::ItemFetchScope::VirtReferences)) { ifs.setFetchVirtualReferences(true); } if (fetchScope.fetch(Protocol::ItemFetchScope::MTime)) { ifs.setFetchModificationTime(true); } if (fetchScope.fetch(Protocol::ItemFetchScope::Relations)) { ifs.setFetchRelations(true); } return ifs; } Protocol::CollectionFetchScope ProtocolHelper::collectionFetchScopeToProtocol(const CollectionFetchScope &fetchScope) { Protocol::CollectionFetchScope cfs; switch (fetchScope.listFilter()) { case CollectionFetchScope::NoFilter: cfs.setListFilter(Protocol::CollectionFetchScope::NoFilter); break; case CollectionFetchScope::Display: cfs.setListFilter(Protocol::CollectionFetchScope::Display); break; case CollectionFetchScope::Sync: cfs.setListFilter(Protocol::CollectionFetchScope::Sync); break; case CollectionFetchScope::Index: cfs.setListFilter(Protocol::CollectionFetchScope::Index); break; case CollectionFetchScope::Enabled: cfs.setListFilter(Protocol::CollectionFetchScope::Enabled); break; } cfs.setIncludeStatistics(fetchScope.includeStatistics()); cfs.setResource(fetchScope.resource()); cfs.setContentMimeTypes(fetchScope.contentMimeTypes()); cfs.setAttributes(fetchScope.attributes()); cfs.setFetchIdOnly(fetchScope.fetchIdOnly()); switch (fetchScope.ancestorRetrieval()) { case CollectionFetchScope::None: cfs.setAncestorRetrieval(Protocol::CollectionFetchScope::None); break; case CollectionFetchScope::Parent: cfs.setAncestorRetrieval(Protocol::CollectionFetchScope::Parent); break; case CollectionFetchScope::All: cfs.setAncestorRetrieval(Protocol::CollectionFetchScope::All); break; } if (cfs.ancestorRetrieval() != Protocol::CollectionFetchScope::None) { cfs.setAncestorAttributes(fetchScope.ancestorFetchScope().attributes()); cfs.setAncestorFetchIdOnly(fetchScope.ancestorFetchScope().fetchIdOnly()); } cfs.setIgnoreRetrievalErrors(fetchScope.ignoreRetrievalErrors()); return cfs; } CollectionFetchScope ProtocolHelper::parseCollectionFetchScope(const Protocol::CollectionFetchScope &fetchScope) { CollectionFetchScope cfs; switch (fetchScope.listFilter()) { case Protocol::CollectionFetchScope::NoFilter: cfs.setListFilter(CollectionFetchScope::NoFilter); break; case Protocol::CollectionFetchScope::Display: cfs.setListFilter(CollectionFetchScope::Display); break; case Protocol::CollectionFetchScope::Sync: cfs.setListFilter(CollectionFetchScope::Sync); break; case Protocol::CollectionFetchScope::Index: cfs.setListFilter(CollectionFetchScope::Index); break; case Protocol::CollectionFetchScope::Enabled: cfs.setListFilter(CollectionFetchScope::Enabled); break; } cfs.setIncludeStatistics(fetchScope.includeStatistics()); cfs.setResource(fetchScope.resource()); cfs.setContentMimeTypes(fetchScope.contentMimeTypes()); switch (fetchScope.ancestorRetrieval()) { case Protocol::CollectionFetchScope::None: cfs.setAncestorRetrieval(CollectionFetchScope::None); break; case Protocol::CollectionFetchScope::Parent: cfs.setAncestorRetrieval(CollectionFetchScope::Parent); break; case Protocol::CollectionFetchScope::All: cfs.setAncestorRetrieval(CollectionFetchScope::All); break; } if (cfs.ancestorRetrieval() != CollectionFetchScope::None) { cfs.ancestorFetchScope().setFetchIdOnly(fetchScope.ancestorFetchIdOnly()); const auto attrs = fetchScope.ancestorAttributes(); for (const auto &attr : attrs) { cfs.ancestorFetchScope().fetchAttribute(attr, true); } } const auto attrs = fetchScope.attributes(); for (const auto &attr : attrs) { cfs.fetchAttribute(attr, true); } cfs.setFetchIdOnly(fetchScope.fetchIdOnly()); cfs.setIgnoreRetrievalErrors(fetchScope.ignoreRetrievalErrors()); return cfs; } Protocol::TagFetchScope ProtocolHelper::tagFetchScopeToProtocol(const TagFetchScope &fetchScope) { Protocol::TagFetchScope tfs; tfs.setFetchIdOnly(fetchScope.fetchIdOnly()); tfs.setAttributes(fetchScope.attributes()); tfs.setFetchRemoteID(fetchScope.fetchRemoteId()); tfs.setFetchAllAttributes(fetchScope.fetchAllAttributes()); return tfs; } TagFetchScope ProtocolHelper::parseTagFetchScope(const Protocol::TagFetchScope &fetchScope) { TagFetchScope tfs; tfs.setFetchIdOnly(fetchScope.fetchIdOnly()); tfs.setFetchRemoteId(fetchScope.fetchRemoteID()); tfs.setFetchAllAttributes(fetchScope.fetchAllAttributes()); const auto attrs = fetchScope.attributes(); for (const auto &attr : attrs) { tfs.fetchAttribute(attr, true); } return tfs; } static Item::Flags convertFlags(const QVector &flags, ProtocolHelperValuePool *valuePool) { #if __cplusplus >= 201103L || defined(__GNUC__) || defined(__clang__) // When the compiler supports thread-safe static initialization (mandated by the C++11 memory model) // then use it to share the common case of a single-item set only containing the \SEEN flag. // NOTE: GCC and clang has threadsafe static initialization for some time now, even without C++11. if (flags.size() == 1 && flags.first() == "\\SEEN") { static const Item::Flags sharedSeen = Item::Flags() << QByteArray("\\SEEN"); return sharedSeen; } #endif Item::Flags convertedFlags; convertedFlags.reserve(flags.size()); for (const QByteArray &flag : flags) { if (valuePool) { convertedFlags.insert(valuePool->flagPool.sharedValue(flag)); } else { convertedFlags.insert(flag); } } return convertedFlags; } Item ProtocolHelper::parseItemFetchResult(const Protocol::FetchItemsResponse &data, const Akonadi::ItemFetchScope *fetchScope, ProtocolHelperValuePool *valuePool) { Item item; item.setId(data.id()); item.setRevision(data.revision()); if (!fetchScope || fetchScope->fetchRemoteIdentification()) { item.setRemoteId(data.remoteId()); item.setRemoteRevision(data.remoteRevision()); } item.setGid(data.gid()); item.setStorageCollectionId(data.parentId()); if (valuePool) { item.setMimeType(valuePool->mimeTypePool.sharedValue(data.mimeType())); } else { item.setMimeType(data.mimeType()); } if (!item.isValid()) { return Item(); } item.setFlags(convertFlags(data.flags(), valuePool)); if ((!fetchScope || fetchScope->fetchTags()) && !data.tags().isEmpty()) { Tag::List tags; tags.reserve(data.tags().size()); Q_FOREACH (const Protocol::FetchTagsResponse &tag, data.tags()) { tags.append(parseTagFetchResult(tag)); } item.setTags(tags); } if ((!fetchScope || fetchScope->fetchRelations()) && !data.relations().isEmpty()) { Relation::List relations; relations.reserve(data.relations().size()); Q_FOREACH (const Protocol::FetchRelationsResponse &rel, data.relations()) { relations.append(parseRelationFetchResult(rel)); } item.d_ptr->mRelations = relations; } if ((!fetchScope || fetchScope->fetchVirtualReferences()) && !data.virtualReferences().isEmpty()) { Collection::List virtRefs; virtRefs.reserve(data.virtualReferences().size()); Q_FOREACH (qint64 colId, data.virtualReferences()) { virtRefs.append(Collection(colId)); } item.setVirtualReferences(virtRefs); } if (!data.cachedParts().isEmpty()) { QSet cp; cp.reserve(data.cachedParts().size()); Q_FOREACH (const QByteArray &ba, data.cachedParts()) { cp.insert(ba); } item.setCachedPayloadParts(cp); } item.setSize(data.size()); item.setModificationTime(data.mTime()); parseAncestorsCached(data.ancestors(), &item, data.parentId(), valuePool); Q_FOREACH (const Protocol::StreamPayloadResponse &part, data.parts()) { ProtocolHelper::PartNamespace ns; const QByteArray plainKey = decodePartIdentifier(part.payloadName(), ns); const auto metaData = part.metaData(); switch (ns) { case ProtocolHelper::PartPayload: if (fetchScope && !fetchScope->fullPayload() && !fetchScope->payloadParts().contains(plainKey)) { continue; } ItemSerializer::deserialize(item, plainKey, part.data(), metaData.version(), static_cast(metaData.storageType())); if (metaData.storageType() == Protocol::PartMetaData::Foreign) { item.d_ptr->mPayloadPath = QString::fromUtf8(part.data()); } break; case ProtocolHelper::PartAttribute: { if (fetchScope && !fetchScope->allAttributes() && !fetchScope->attributes().contains(plainKey)) { continue; } Attribute *attr = AttributeFactory::createAttribute(plainKey); Q_ASSERT(attr); if (metaData.storageType() == Protocol::PartMetaData::External) { const QString filename = ExternalPartStorage::resolveAbsolutePath(part.data()); QFile file(filename); if (file.open(QFile::ReadOnly)) { attr->deserialize(file.readAll()); } else { qCWarning(AKONADICORE_LOG) << "Failed to open attribute file: " << filename; delete attr; attr = nullptr; } } else { attr->deserialize(part.data()); } if (attr) { item.addAttribute(attr); } break; } case ProtocolHelper::PartGlobal: default: qCWarning(AKONADICORE_LOG) << "Unknown item part type:" << part.payloadName(); } } item.d_ptr->resetChangeLog(); return item; } Tag ProtocolHelper::parseTagFetchResult(const Protocol::FetchTagsResponse &data) { Tag tag; tag.setId(data.id()); tag.setGid(data.gid()); tag.setRemoteId(data.remoteId()); tag.setType(data.type()); tag.setParent(data.parentId() > 0 ? Tag(data.parentId()) : Tag()); parseAttributes(data.attributes(), &tag); tag.d_ptr->resetChangeLog(); return tag; } Relation ProtocolHelper::parseRelationFetchResult(const Protocol::FetchRelationsResponse &data) { Relation relation; relation.setLeft(Item(data.left())); relation.setRight(Item(data.right())); relation.setRemoteId(data.remoteId()); relation.setType(data.type()); return relation; } bool ProtocolHelper::streamPayloadToFile(const QString &fileName, const QByteArray &data, QByteArray &error) { const QString filePath = ExternalPartStorage::resolveAbsolutePath(fileName); //qCDebug(AKONADICORE_LOG) << filePath << fileName; if (!filePath.startsWith(ExternalPartStorage::akonadiStoragePath())) { qCWarning(AKONADICORE_LOG) << "Invalid file path" << fileName; error = "Invalid file path"; return false; } QFile file(filePath); if (!file.open(QIODevice::WriteOnly | QIODevice::Truncate)) { qCWarning(AKONADICORE_LOG) << "Failed to open destination payload file" << file.errorString(); error = "Failed to store payload into file"; return false; } if (file.write(data) != data.size()) { qCWarning(AKONADICORE_LOG) << "Failed to write all payload data to file"; error = "Failed to store payload into file"; return false; } //qCDebug(AKONADICORE_LOG) << "Wrote" << data.size() << "bytes to " << file.fileName(); // Make sure stuff is written to disk file.close(); return true; } Akonadi::Tristate ProtocolHelper::listPreference(Collection::ListPreference pref) { switch (pref) { case Collection::ListEnabled: return Tristate::True; case Collection::ListDisabled: return Tristate::False; case Collection::ListDefault: return Tristate::Undefined; } Q_ASSERT(false); return Tristate::Undefined; } diff --git a/src/core/qtest_akonadi.h b/src/core/qtest_akonadi.h index fbf0422e3..a82bd6c54 100644 --- a/src/core/qtest_akonadi.h +++ b/src/core/qtest_akonadi.h @@ -1,216 +1,216 @@ /* This file is based on qtest_kde.h from kdelibs Copyright (C) 2006 David Faure Copyright (C) 2009 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 version 2 as published by the Free Software Foundation. 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 QTEST_AKONADI_H #define QTEST_AKONADI_H #include "agentinstance.h" #include "agentmanager.h" #include "servermanager.h" #include "collectionpathresolver.h" #include "monitor.h" #include "collectionfetchscope.h" #include "itemfetchscope.h" #include #include #include #include #include #include /** * \short Akonadi Replacement for QTEST_MAIN from QTestLib * * This macro should be used for classes that run inside the Akonadi Testrunner. * So instead of writing QTEST_MAIN( TestClass ) you write * QTEST_AKONADIMAIN( TestClass ). * * Unlike QTEST_MAIN, this macro actually does call QApplication::exec() so * that the application is running during test execution. This is needed * for proper clean up of Sessions. * * \param TestObject The class you use for testing. * * \see QTestLib * \see QTEST_KDEMAIN */ #define QTEST_AKONADIMAIN(TestObject) \ int main(int argc, char *argv[]) \ { \ qputenv("LC_ALL", "C"); \ qunsetenv("KDE_COLOR_DEBUG"); \ QApplication app(argc, argv); \ app.setApplicationName(QStringLiteral("qttest")); \ app.setOrganizationDomain(QStringLiteral("kde.org")); \ app.setOrganizationName(QStringLiteral("KDE")); \ QGuiApplication::setQuitOnLastWindowClosed(false); \ qRegisterMetaType>(); \ int result = 0; \ - QTimer::singleShot(0, [argc, argv, &result]() { \ + QTimer::singleShot(0, &app, [argc, argv, &result]() { \ TestObject tc; \ result = QTest::qExec(&tc, argc, argv); \ qApp->quit(); \ }); \ app.exec(); \ return result; \ } namespace AkonadiTest { /** * Checks that the test is running in the proper test environment */ void checkTestIsIsolated() { if (qEnvironmentVariableIsEmpty("TESTRUNNER_DB_ENVIRONMENT")) qFatal("This test must be run using ctest, in order to use the testrunner environment. Aborting, to avoid messing up your real akonadi"); if (!qgetenv("XDG_DATA_HOME").contains("testrunner")) qFatal("Did you forget to run the test using QTEST_AKONADIMAIN?"); } /** * Switch all resources offline to reduce interference from them */ void setAllResourcesOffline() { // switch all resources offline to reduce interference from them const auto lst = Akonadi::AgentManager::self()->instances(); for (Akonadi::AgentInstance agent : lst) { agent.setIsOnline(false); } } template bool akWaitForSignal(Object sender, Func member, int timeout = 1000) { QSignalSpy spy(sender, member); bool ok = false; [&]() { QTRY_VERIFY_WITH_TIMEOUT(spy.count() > 0, timeout); ok = true; }(); return ok; } bool akWaitForSignal(const QObject *sender, const char *member, int timeout = 1000) { QSignalSpy spy(sender, member); bool ok = false; [&]() { QTRY_VERIFY_WITH_TIMEOUT(spy.count() > 0, timeout); ok = true; }(); return ok; } qint64 collectionIdFromPath(const QString &path) { Akonadi::CollectionPathResolver *resolver = new Akonadi::CollectionPathResolver(path); bool success = resolver->exec(); if (!success) { qDebug() << "path resolution for " << path << " failed: " << resolver->errorText(); return -1; } qint64 id = resolver->collection(); return id; } QString testrunnerServiceName() { const QString pid = QString::fromLocal8Bit(qgetenv("AKONADI_TESTRUNNER_PID")); Q_ASSERT(!pid.isEmpty()); return QStringLiteral("org.kde.Akonadi.Testrunner-") + pid; } bool restartAkonadiServer() { QDBusInterface testrunnerIface(testrunnerServiceName(), QStringLiteral("/"), QStringLiteral("org.kde.Akonadi.Testrunner"), QDBusConnection::sessionBus()); if (!testrunnerIface.isValid()) { qWarning() << "Unable to get a dbus interface to the testrunner!"; } QDBusReply reply = testrunnerIface.call(QStringLiteral("restartAkonadiServer")); if (!reply.isValid()) { qWarning() << reply.error(); return false; } else if (Akonadi::ServerManager::isRunning()) { return true; } else { bool ok = false; [&]() { QSignalSpy spy(Akonadi::ServerManager::self(), &Akonadi::ServerManager::started); QTRY_VERIFY_WITH_TIMEOUT(spy.count() > 0, 10000); ok = true; }(); return ok; } } bool trackAkonadiProcess(bool track) { QDBusInterface testrunnerIface(testrunnerServiceName(), QStringLiteral("/"), QStringLiteral("org.kde.Akonadi.Testrunner"), QDBusConnection::sessionBus()); if (!testrunnerIface.isValid()) { qWarning() << "Unable to get a dbus interface to the testrunner!"; } QDBusReply reply = testrunnerIface.call(QStringLiteral("trackAkonadiProcess"), track); if (!reply.isValid()) { qWarning() << reply.error(); return false; } else { return true; } } std::unique_ptr getTestMonitor() { auto m = new Akonadi::Monitor(); m->fetchCollection(true); m->setCollectionMonitored(Akonadi::Collection::root(), true); m->setAllMonitored(true); auto &itemFS = m->itemFetchScope(); itemFS.setAncestorRetrieval(Akonadi::ItemFetchScope::All); auto &colFS = m->collectionFetchScope(); colFS.setAncestorRetrieval(Akonadi::CollectionFetchScope::All); QSignalSpy readySpy(m, &Akonadi::Monitor::monitorReady); readySpy.wait(); return std::unique_ptr(m); } } // namespace /** * Runs an Akonadi::Job synchronously and aborts if the job failed. * Similar to QVERIFY( job->exec() ) but includes the job error message * in the output in case of a failure. */ #define AKVERIFYEXEC( job ) \ QVERIFY2( job->exec(), job->errorString().toUtf8().constData() ) #endif diff --git a/src/core/searchquery.cpp b/src/core/searchquery.cpp index 1fd1c9498..ed326799a 100644 --- a/src/core/searchquery.cpp +++ b/src/core/searchquery.cpp @@ -1,415 +1,415 @@ /* 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 "searchquery.h" #include "akonadicore_debug.h" #include #include #include using namespace Akonadi; class SearchTerm::Private : public QSharedData { public: Private() : QSharedData() , condition(SearchTerm::CondEqual) , relation(SearchTerm::RelAnd) { } Private(const Private &other) : QSharedData(other) , key(other.key) , value(other.value) , condition(other.condition) , relation(other.relation) , terms(other.terms) , isNegated(other.isNegated) { } bool operator==(const Private &other) const { return relation == other.relation && isNegated == other.isNegated && terms == other.terms && key == other.key && value == other.value && condition == other.condition; } QString key; QVariant value; Condition condition; Relation relation; QList terms; bool isNegated = false; }; class SearchQuery::Private : public QSharedData { public: Private() : QSharedData() , limit(-1) { } Private(const Private &other) : QSharedData(other) , rootTerm(other.rootTerm) , limit(other.limit) { } bool operator==(const Private &other) const { return rootTerm == other.rootTerm && limit == other.limit; } static QVariantMap termToJSON(const SearchTerm &term) { const QList &subTerms = term.subTerms(); QVariantMap termJSON; termJSON.insert(QStringLiteral("negated"), term.isNegated()); if (subTerms.isEmpty()) { if (!term.isNull()) { termJSON.insert(QStringLiteral("key"), term.key()); termJSON.insert(QStringLiteral("value"), term.value()); termJSON.insert(QStringLiteral("cond"), static_cast(term.condition())); } } else { termJSON.insert(QStringLiteral("rel"), static_cast(term.relation())); QVariantList subTermsJSON; subTermsJSON.reserve(subTerms.count()); for (const SearchTerm &term : qAsConst(subTerms)) { subTermsJSON.append(termToJSON(term)); } termJSON.insert(QStringLiteral("subTerms"), subTermsJSON); } return termJSON; } static SearchTerm JSONToTerm(const QVariantMap &map) { if (map.isEmpty()) { return SearchTerm(); - } else if (map.contains(QLatin1String("key"))) { + } else if (map.contains(QStringLiteral("key"))) { SearchTerm term(map[QStringLiteral("key")].toString(), map[QStringLiteral("value")], static_cast(map[QStringLiteral("cond")].toInt())); term.setIsNegated(map[QStringLiteral("negated")].toBool()); return term; - } else if (map.contains(QLatin1String("rel"))) { + } else if (map.contains(QStringLiteral("rel"))) { SearchTerm term(static_cast(map[QStringLiteral("rel")].toInt())); term.setIsNegated(map[QStringLiteral("negated")].toBool()); const QList list = map[QStringLiteral("subTerms")].toList(); for (const QVariant &var : list) { term.addSubTerm(JSONToTerm(var.toMap())); } return term; } else { qCWarning(AKONADICORE_LOG) << "Invalid JSON for term: " << map; return SearchTerm(); } } SearchTerm rootTerm; int limit; }; SearchTerm::SearchTerm(SearchTerm::Relation relation) : d(new Private) { d->relation = relation; } SearchTerm::SearchTerm(const QString &key, const QVariant &value, SearchTerm::Condition condition) : d(new Private) { d->relation = RelAnd; d->key = key; d->value = value; d->condition = condition; } SearchTerm::SearchTerm(const SearchTerm &other) : d(other.d) { } SearchTerm::~SearchTerm() { } SearchTerm &SearchTerm::operator=(const SearchTerm &other) { d = other.d; return *this; } bool SearchTerm::operator==(const SearchTerm &other) const { return *d == *other.d; } bool SearchTerm::isNull() const { return d->key.isEmpty() && d->value.isNull() && d->terms.isEmpty(); } QString SearchTerm::key() const { return d->key; } QVariant SearchTerm::value() const { return d->value; } SearchTerm::Condition SearchTerm::condition() const { return d->condition; } void SearchTerm::setIsNegated(bool negated) { d->isNegated = negated; } bool SearchTerm::isNegated() const { return d->isNegated; } void SearchTerm::addSubTerm(const SearchTerm &term) { d->terms << term; } QList< SearchTerm > SearchTerm::subTerms() const { return d->terms; } SearchTerm::Relation SearchTerm::relation() const { return d->relation; } SearchQuery::SearchQuery(SearchTerm::Relation rel) : d(new Private) { d->rootTerm = SearchTerm(rel); } SearchQuery::SearchQuery(const SearchQuery &other) : d(other.d) { } SearchQuery::~SearchQuery() { } SearchQuery &SearchQuery::operator=(const SearchQuery &other) { d = other.d; return *this; } bool SearchQuery::operator==(const SearchQuery &other) const { return *d == *other.d; } bool SearchQuery::isNull() const { return d->rootTerm.isNull(); } SearchTerm SearchQuery::term() const { return d->rootTerm; } void SearchQuery::addTerm(const QString &key, const QVariant &value, SearchTerm::Condition condition) { addTerm(SearchTerm(key, value, condition)); } void SearchQuery::addTerm(const SearchTerm &term) { d->rootTerm.addSubTerm(term); } void SearchQuery::setTerm(const SearchTerm &term) { d->rootTerm = term; } void SearchQuery::setLimit(int limit) { d->limit = limit; } int SearchQuery::limit() const { return d->limit; } QByteArray SearchQuery::toJSON() const { QVariantMap root; if (!d->rootTerm.isNull()) { root = Private::termToJSON(d->rootTerm); root.insert(QStringLiteral("limit"), d->limit); } QJsonObject jo = QJsonObject::fromVariantMap(root); QJsonDocument jdoc; jdoc.setObject(jo); return jdoc.toJson(); } SearchQuery SearchQuery::fromJSON(const QByteArray &jsonData) { QJsonParseError error; const QJsonDocument json = QJsonDocument::fromJson(jsonData, &error); if (error.error != QJsonParseError::NoError || json.isNull()) { return SearchQuery(); } SearchQuery query; const QJsonObject obj = json.object(); query.d->rootTerm = Private::JSONToTerm(obj.toVariantMap()); if (obj.contains(QLatin1String("limit"))) { query.d->limit = obj.value(QStringLiteral("limit")).toInt(); } return query; } static QMap emailSearchFieldMapping() { static QMap mapping; if (mapping.isEmpty()) { mapping.insert(EmailSearchTerm::Body, QStringLiteral("body")); mapping.insert(EmailSearchTerm::Headers, QStringLiteral("headers")); mapping.insert(EmailSearchTerm::Subject, QStringLiteral("subject")); mapping.insert(EmailSearchTerm::Message, QStringLiteral("message")); mapping.insert(EmailSearchTerm::HeaderFrom, QStringLiteral("from")); mapping.insert(EmailSearchTerm::HeaderTo, QStringLiteral("to")); mapping.insert(EmailSearchTerm::HeaderCC, QStringLiteral("cc")); mapping.insert(EmailSearchTerm::HeaderBCC, QStringLiteral("bcc")); mapping.insert(EmailSearchTerm::HeaderReplyTo, QStringLiteral("replyto")); mapping.insert(EmailSearchTerm::HeaderOrganization, QStringLiteral("organization")); mapping.insert(EmailSearchTerm::HeaderListId, QStringLiteral("listid")); mapping.insert(EmailSearchTerm::HeaderResentFrom, QStringLiteral("resentfrom")); mapping.insert(EmailSearchTerm::HeaderXLoop, QStringLiteral("xloop")); mapping.insert(EmailSearchTerm::HeaderXMailingList, QStringLiteral("xmailinglist")); mapping.insert(EmailSearchTerm::HeaderXSpamFlag, QStringLiteral("xspamflag")); mapping.insert(EmailSearchTerm::HeaderDate, QStringLiteral("date")); mapping.insert(EmailSearchTerm::HeaderOnlyDate, QStringLiteral("onlydate")); mapping.insert(EmailSearchTerm::MessageStatus, QStringLiteral("messagestatus")); mapping.insert(EmailSearchTerm::MessageTag, QStringLiteral("messagetag")); mapping.insert(EmailSearchTerm::ByteSize, QStringLiteral("size")); mapping.insert(EmailSearchTerm::Attachment, QStringLiteral("attachment")); } return mapping; } EmailSearchTerm::EmailSearchTerm(EmailSearchTerm::EmailSearchField field, const QVariant &value, SearchTerm::Condition condition) : SearchTerm(toKey(field), value, condition) { } QString EmailSearchTerm::toKey(EmailSearchTerm::EmailSearchField field) { return emailSearchFieldMapping().value(field); } EmailSearchTerm::EmailSearchField EmailSearchTerm::fromKey(const QString &key) { return emailSearchFieldMapping().key(key); } static QMap contactSearchFieldMapping() { static QMap mapping; if (mapping.isEmpty()) { mapping.insert(ContactSearchTerm::Name, QStringLiteral("name")); mapping.insert(ContactSearchTerm::Nickname, QStringLiteral("nickname")); mapping.insert(ContactSearchTerm::Email, QStringLiteral("email")); mapping.insert(ContactSearchTerm::Uid, QStringLiteral("uid")); mapping.insert(ContactSearchTerm::All, QStringLiteral("all")); } return mapping; } ContactSearchTerm::ContactSearchTerm(ContactSearchTerm::ContactSearchField field, const QVariant &value, SearchTerm::Condition condition) : SearchTerm(toKey(field), value, condition) { } QString ContactSearchTerm::toKey(ContactSearchTerm::ContactSearchField field) { return contactSearchFieldMapping().value(field); } ContactSearchTerm::ContactSearchField ContactSearchTerm::fromKey(const QString &key) { return contactSearchFieldMapping().key(key); } QMap incidenceSearchFieldMapping() { QMap mapping; if (mapping.isEmpty()) { mapping.insert(IncidenceSearchTerm::All, QStringLiteral("all")); mapping.insert(IncidenceSearchTerm::PartStatus, QStringLiteral("partstatus")); mapping.insert(IncidenceSearchTerm::Organizer, QStringLiteral("organizer")); mapping.insert(IncidenceSearchTerm::Summary, QStringLiteral("summary")); mapping.insert(IncidenceSearchTerm::Location, QStringLiteral("location")); } return mapping; } IncidenceSearchTerm::IncidenceSearchTerm(IncidenceSearchTerm::IncidenceSearchField field, const QVariant &value, SearchTerm::Condition condition) : SearchTerm(toKey(field), value, condition) { } QString IncidenceSearchTerm::toKey(IncidenceSearchTerm::IncidenceSearchField field) { return incidenceSearchFieldMapping().value(field); } IncidenceSearchTerm::IncidenceSearchField IncidenceSearchTerm::fromKey(const QString &key) { return incidenceSearchFieldMapping().key(key); } diff --git a/src/core/servermanager.cpp b/src/core/servermanager.cpp index f0386668e..4877a39ee 100644 --- a/src/core/servermanager.cpp +++ b/src/core/servermanager.cpp @@ -1,405 +1,406 @@ /* Copyright (c) 2008 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 "servermanager.h" #include "servermanager_p.h" #include "agenttype.h" #include "agentmanager.h" #include "session_p.h" #include "firstrun_p.h" #include "akonadicore_debug.h" #include #include #include "private/protocol_p.h" #include "private/standarddirs_p.h" #include "private/dbus_p.h" #include "private/instance_p.h" #include #include #include #include #include #include #include #include #include using namespace Akonadi; class Akonadi::ServerManagerPrivate { public: ServerManagerPrivate() : instance(new ServerManager(this)) , mState(ServerManager::NotRunning) , mSafetyTimer(new QTimer) { mState = instance->state(); mSafetyTimer->setSingleShot(true); mSafetyTimer->setInterval(30000); QObject::connect(mSafetyTimer.data(), &QTimer::timeout, instance, [this]() { timeout();}); if (mState == ServerManager::Running && Internal::clientType() == Internal::User && !ServerManager::hasInstanceIdentifier()) { mFirstRunner = new Firstrun(instance); } } ~ServerManagerPrivate() { delete instance; } void checkStatusChanged() { setState(instance->state()); } void setState(ServerManager::State state) { if (mState != state) { mState = state; Q_EMIT instance->stateChanged(state); if (state == ServerManager::Running) { Q_EMIT instance->started(); if (!mFirstRunner && Internal::clientType() == Internal::User && !ServerManager::hasInstanceIdentifier()) { mFirstRunner = new Firstrun(instance); } } else if (state == ServerManager::NotRunning || state == ServerManager::Broken) { Q_EMIT instance->stopped(); } if (state == ServerManager::Starting || state == ServerManager::Stopping) { QMetaObject::invokeMethod(mSafetyTimer.data(), QOverload<>::of(&QTimer::start), Qt::QueuedConnection); // in case we are in a different thread } else { QMetaObject::invokeMethod(mSafetyTimer.data(), &QTimer::stop, Qt::QueuedConnection); // in case we are in a different thread } } } void timeout() { if (mState == ServerManager::Starting || mState == ServerManager::Stopping) { setState(ServerManager::Broken); } } ServerManager *instance = nullptr; static int serverProtocolVersion; static uint generation; ServerManager::State mState; QScopedPointer mSafetyTimer; Firstrun *mFirstRunner = nullptr; static Internal::ClientType clientType; QString mBrokenReason; std::unique_ptr serviceWatcher; }; int ServerManagerPrivate::serverProtocolVersion = -1; uint ServerManagerPrivate::generation = 0; Internal::ClientType ServerManagerPrivate::clientType = Internal::User; Q_GLOBAL_STATIC(ServerManagerPrivate, sInstance) ServerManager::ServerManager(ServerManagerPrivate *dd) : d(dd) { Kdelibs4ConfigMigrator migrate(QStringLiteral("servermanager")); migrate.setConfigFiles(QStringList() << QStringLiteral("akonadi-firstrunrc")); migrate.migrate(); qRegisterMetaType(); d->serviceWatcher = std::make_unique( ServerManager::serviceName(ServerManager::Server), QDBusConnection::sessionBus(), QDBusServiceWatcher::WatchForRegistration | QDBusServiceWatcher::WatchForUnregistration); d->serviceWatcher->addWatchedService(ServerManager::serviceName(ServerManager::Control)); d->serviceWatcher->addWatchedService(ServerManager::serviceName(ServerManager::ControlLock)); d->serviceWatcher->addWatchedService(ServerManager::serviceName(ServerManager::UpgradeIndicator)); // this (and also the two connects below) are queued so that they trigger after AgentManager is done loading // the current agent types and instances // this ensures the invariant of AgentManager reporting a consistent state if ServerManager::state() == Running // that's the case with direct connections as well, but only after you enter the event loop once connect(d->serviceWatcher.get(), &QDBusServiceWatcher::serviceRegistered, this, [this]() { d->serverProtocolVersion = -1; d->checkStatusChanged(); }, Qt::QueuedConnection); connect(d->serviceWatcher.get(), &QDBusServiceWatcher::serviceUnregistered, this, [this](const QString &name) { if (name == ServerManager::serviceName(ServerManager::ControlLock) && d->mState == ServerManager::Starting) { // Control.Lock has disappeared during startup, which means that akonadi_control // has terminated, most probably because it was not able to start akonadiserver // process. Don't wait 30 seconds for sefetyTimeout, but go into Broken state // immediately. d->setState(ServerManager::Broken); return; } d->serverProtocolVersion = -1; d->checkStatusChanged(); }, Qt::QueuedConnection); // AgentManager is dangerous to use for agents themselves if (Internal::clientType() != Internal::User) { return; } - connect(AgentManager::self(), SIGNAL(typeAdded(Akonadi::AgentType)), SLOT(checkStatusChanged()), Qt::QueuedConnection); - connect(AgentManager::self(), SIGNAL(typeRemoved(Akonadi::AgentType)), SLOT(checkStatusChanged()), Qt::QueuedConnection); + + connect(AgentManager::self(), &AgentManager::typeAdded, this, [this]() { d->checkStatusChanged(); }, Qt::QueuedConnection); + connect(AgentManager::self(), &AgentManager::typeRemoved, this, [this]() { d->checkStatusChanged(); }, Qt::QueuedConnection); } ServerManager *Akonadi::ServerManager::self() { return sInstance->instance; } bool ServerManager::start() { const bool controlRegistered = QDBusConnection::sessionBus().interface()->isServiceRegistered(ServerManager::serviceName(ServerManager::Control)); const bool serverRegistered = QDBusConnection::sessionBus().interface()->isServiceRegistered(ServerManager::serviceName(ServerManager::Server)); if (controlRegistered && serverRegistered) { return true; } const bool controlLockRegistered = QDBusConnection::sessionBus().interface()->isServiceRegistered(ServerManager::serviceName(ServerManager::ControlLock)); if (controlLockRegistered || controlRegistered) { qCDebug(AKONADICORE_LOG) << "Akonadi server is already starting up"; sInstance->setState(Starting); return true; } qCDebug(AKONADICORE_LOG) << "executing akonadi_control"; QStringList args; if (hasInstanceIdentifier()) { args << QStringLiteral("--instance") << instanceIdentifier(); } const bool ok = QProcess::startDetached(QStringLiteral("akonadi_control"), args); if (!ok) { qCWarning(AKONADICORE_LOG) << "Unable to execute akonadi_control, falling back to D-Bus auto-launch"; QDBusReply reply = QDBusConnection::sessionBus().interface()->startService(ServerManager::serviceName(ServerManager::Control)); if (!reply.isValid()) { qCDebug(AKONADICORE_LOG) << "Akonadi server could not be started via D-Bus either: " << reply.error().message(); return false; } } sInstance->setState(Starting); return true; } bool ServerManager::stop() { QDBusInterface iface(ServerManager::serviceName(ServerManager::Control), QStringLiteral("/ControlManager"), QStringLiteral("org.freedesktop.Akonadi.ControlManager")); if (!iface.isValid()) { return false; } iface.call(QDBus::NoBlock, QStringLiteral("shutdown")); sInstance->setState(Stopping); return true; } void ServerManager::showSelfTestDialog(QWidget *parent) { Q_UNUSED(parent); QProcess::startDetached(QStringLiteral("akonadiselftest"), QStringList()); } bool ServerManager::isRunning() { return state() == Running; } ServerManager::State ServerManager::state() { ServerManager::State previousState = NotRunning; if (sInstance.exists()) { // be careful, this is called from the ServerManager::Private ctor, so using sInstance unprotected can cause infinite recursion previousState = sInstance->mState; sInstance->mBrokenReason.clear(); } const bool serverUpgrading = QDBusConnection::sessionBus().interface()->isServiceRegistered(ServerManager::serviceName(ServerManager::UpgradeIndicator)); if (serverUpgrading) { return Upgrading; } const bool controlRegistered = QDBusConnection::sessionBus().interface()->isServiceRegistered(ServerManager::serviceName(ServerManager::Control)); const bool serverRegistered = QDBusConnection::sessionBus().interface()->isServiceRegistered(ServerManager::serviceName(ServerManager::Server)); if (controlRegistered && serverRegistered) { // check if the server protocol is recent enough if (sInstance.exists()) { if (Internal::serverProtocolVersion() >= 0 && Internal::serverProtocolVersion() != Protocol::version()) { sInstance->mBrokenReason = i18n("The Akonadi server protocol version differs from the protocol version used by this application.\n" "If you recently updated your system please log out and back in to make sure all applications use the " "correct protocol version."); return Broken; } } // AgentManager is dangerous to use for agents themselves if (Internal::clientType() == Internal::User) { // besides the running server processes we also need at least one resource to be operational const AgentType::List agentTypes = AgentManager::self()->types(); for (const AgentType &type : agentTypes) { if (type.capabilities().contains(QLatin1String("Resource"))) { return Running; } } if (sInstance.exists()) { sInstance->mBrokenReason = i18n("There are no Akonadi Agents available. Please verify your KDE PIM installation."); } return Broken; } else { return Running; } } const bool controlLockRegistered = QDBusConnection::sessionBus().interface()->isServiceRegistered(ServerManager::serviceName(ServerManager::ControlLock)); if (controlLockRegistered || controlRegistered) { qCDebug(AKONADICORE_LOG) << "Akonadi server is only partially running. Server:" << serverRegistered << "ControlLock:" << controlLockRegistered << "Control:" << controlRegistered; if (previousState == Running) { return NotRunning; // we don't know if it's starting or stopping, probably triggered by someone else } return previousState; } if (serverRegistered) { qCWarning(AKONADICORE_LOG) << "Akonadi server running without control process!"; return Broken; } if (previousState == Starting) { // valid case where nothing is running (yet) return previousState; } return NotRunning; } QString ServerManager::brokenReason() { if (sInstance.exists()) { return sInstance->mBrokenReason; } return QString(); } QString ServerManager::instanceIdentifier() { return Instance::identifier(); } bool ServerManager::hasInstanceIdentifier() { return Instance::hasIdentifier(); } QString ServerManager::serviceName(ServerManager::ServiceType serviceType) { switch (serviceType) { case Server: return DBus::serviceName(DBus::Server); case Control: return DBus::serviceName(DBus::Control); case ControlLock: return DBus::serviceName(DBus::ControlLock); case UpgradeIndicator: return DBus::serviceName(DBus::UpgradeIndicator); } Q_ASSERT(!"WTF?"); return QString(); } QString ServerManager::agentServiceName(ServiceAgentType agentType, const QString &identifier) { switch (agentType) { case Agent: return DBus::agentServiceName(identifier, DBus::Agent); case Resource: return DBus::agentServiceName(identifier, DBus::Resource); case Preprocessor: return DBus::agentServiceName(identifier, DBus::Preprocessor); } Q_ASSERT(!"WTF?"); return QString(); } QString ServerManager::serverConfigFilePath(OpenMode openMode) { return StandardDirs::serverConfigFile( openMode == Akonadi::ServerManager::ReadOnly ? StandardDirs::ReadOnly : StandardDirs::ReadWrite); } QString ServerManager::agentConfigFilePath(const QString &identifier) { return StandardDirs::agentConfigFile(identifier); } QString ServerManager::addNamespace(const QString &string) { if (Instance::hasIdentifier()) { return string % QLatin1Char('_') % Instance::identifier(); } return string; } uint ServerManager::generation() { return Internal::generation(); } int Internal::serverProtocolVersion() { return ServerManagerPrivate::serverProtocolVersion; } void Internal::setServerProtocolVersion(int version) { ServerManagerPrivate::serverProtocolVersion = version; if (sInstance.exists()) { sInstance->checkStatusChanged(); } } uint Internal::generation() { return ServerManagerPrivate::generation; } void Internal::setGeneration(uint generation) { ServerManagerPrivate::generation = generation; } Internal::ClientType Internal::clientType() { return ServerManagerPrivate::clientType; } void Internal::setClientType(ClientType type) { ServerManagerPrivate::clientType = type; } #include "moc_servermanager.cpp" diff --git a/src/core/servermanager.h b/src/core/servermanager.h index b4088ce2f..dd72b1284 100644 --- a/src/core/servermanager.h +++ b/src/core/servermanager.h @@ -1,243 +1,241 @@ /* Copyright (c) 2008 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. */ #ifndef AKONADI_SERVERMANAGER_H #define AKONADI_SERVERMANAGER_H #include "akonadicore_export.h" #include #include namespace Akonadi { class ServerManagerPrivate; /** * @short Provides methods to control the Akonadi server process. * * Asynchronous, low-level control of the Akonadi server. * Akonadi::Control provides a synchronous interface to some of the methods in here. * * @author Volker Krause * @see Akonadi::Control * @since 4.2 */ class AKONADICORE_EXPORT ServerManager : public QObject { Q_OBJECT public: /** * Enum for the various states the server can be in. * @since 4.5 */ enum State { NotRunning, ///< Server is not running, could be no one started it yet or it failed to start. Starting, ///< Server was started but is not yet running. Running, ///< Server is running and operational. Stopping, ///< Server is shutting down. Broken, ///< Server is not operational and an error has been detected. Upgrading ///< Server is performing a database upgrade as part of a new startup. }; /** * Starts the server. This method returns immediately and does not wait * until the server is actually up and running. * @return @c true if the start was possible (which not necessarily means * the server is really running though) and @c false if an immediate error occurred. * @see Akonadi::Control::start() */ static bool start(); /** * Stops the server. This methods returns immediately after the shutdown * command has been send and does not wait until the server is actually * shut down. * @return @c true if the shutdown command was sent successfully, @c false * otherwise */ static bool stop(); /** * Shows the Akonadi self test dialog, which tests Akonadi for various problems * and reports these to the user if. * @param parent the parent widget for the dialog */ static void showSelfTestDialog(QWidget *parent); /** * Checks if the server is available currently. For more detailed status information * see state(). * @see state() */ Q_REQUIRED_RESULT static bool isRunning(); /** * Returns the state of the server. * @since 4.5 */ Q_REQUIRED_RESULT static State state(); /** * Returns the reason why the Server is broken, if known. * * If state() is @p Broken, then you can use this method to obtain a more * detailed description of the problem and present it to users. Note that * the message can be empty if the reason is not known. * * @since 5.6 */ Q_REQUIRED_RESULT static QString brokenReason(); /** * Returns the identifier of the Akonadi instance we are connected to. This is usually * an empty string (representing the default instance), unless you have explicitly set * the AKONADI_INSTANCE environment variable to connect to a different one. * @since 4.10 */ Q_REQUIRED_RESULT static QString instanceIdentifier(); /** * Returns @c true if we are connected to a non-default Akonadi server instance. * @since 4.10 */ Q_REQUIRED_RESULT static bool hasInstanceIdentifier(); /** * Types of known D-Bus services. * @since 4.10 */ enum ServiceType { Server, Control, ControlLock, UpgradeIndicator }; /** * Returns the namespaced D-Bus service name for @p serviceType. * Use this rather the raw service name strings in order to support usage of a non-default * instance of the Akonadi server. * @param serviceType the service type for which to return the D-Bus name * @since 4.10 */ static QString serviceName(ServiceType serviceType); /** * Known agent types. * @since 4.10 */ enum ServiceAgentType { Agent, Resource, Preprocessor }; /** * Returns the namespaced D-Bus service name for an agent of type @p agentType with agent * identifier @p identifier. * @param agentType the agent type to use for D-Bus base name * @param identifier the agent identifier to include in the D-Bus name * @since 4.10 */ Q_REQUIRED_RESULT static QString agentServiceName(ServiceAgentType agentType, const QString &identifier); /** * Adds the multi-instance namespace to @p string if required (with '_' as separator). * Use whenever a multi-instance safe name is required (configfiles, identifiers, ...). * @param string the string to adapt * @since 4.10 */ Q_REQUIRED_RESULT static QString addNamespace(const QString &string); /** * Returns the singleton instance of this class, for connecting to its * signals */ static ServerManager *self(); enum OpenMode { ReadOnly, ReadWrite }; /** * Returns absolute path to akonadiserverrc file with Akonadi server * configuration. */ Q_REQUIRED_RESULT static QString serverConfigFilePath(OpenMode openMode); /** * Returns absolute path to configuration file of an agent identified by * given @p identifier. */ Q_REQUIRED_RESULT static QString agentConfigFilePath(const QString &identifier); /** * Returns current Akonadi database generation identifier * * Generation is guaranteed to never change unless as long as the database * backend is not removed and re-created. In such case it is guaranteed that * the new generation number will be higher than the previous one. * * Generation can be used by applications to detect when Akonadi database * has been recreated and thus some of the configuration (for example * collection IDs stored in a config file) must be invalidated. * * @note Note that the generation number is only available if the server * is running. If this function is called before the server starts it will * return 0. * * @since 5.4 */ Q_REQUIRED_RESULT static uint generation(); Q_SIGNALS: /** * Emitted whenever the server becomes fully operational. */ void started(); /** * Emitted whenever the server becomes unavailable. */ void stopped(); /** * Emitted whenever the server state changes. * @param state the new server state * @since 4.5 */ void stateChanged(Akonadi::ServerManager::State state); private: //@cond PRIVATE friend class ServerManagerPrivate; ServerManager(ServerManagerPrivate *dd); ServerManagerPrivate *const d; - Q_PRIVATE_SLOT(d, void checkStatusChanged()) - Q_PRIVATE_SLOT(d, void timeout()) //@endcond }; } Q_DECLARE_METATYPE(Akonadi::ServerManager::State) #endif diff --git a/src/core/session.cpp b/src/core/session.cpp index b922f36fd..856cb7c1e 100644 --- a/src/core/session.cpp +++ b/src/core/session.cpp @@ -1,445 +1,442 @@ /* Copyright (c) 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 "session.h" #include "session_p.h" #include "job.h" #include "job_p.h" #include "servermanager.h" #include "servermanager_p.h" #include "protocolhelper_p.h" #include "sessionthread_p.h" #include "private/standarddirs_p.h" #include "private/protocol_p.h" #include "akonadicore_debug.h" #include #include #include #include #include #include #include #include // ### FIXME pipelining got broken by switching result emission in JobPrivate::handleResponse to delayed emission // in order to work around exec() deadlocks. As a result of that Session knows to late about a finished job and still // sends responses for the next one to the already finished one #define PIPELINE_LENGTH 0 //#define PIPELINE_LENGTH 2 using namespace Akonadi; //@cond PRIVATE void SessionPrivate::startNext() { QTimer::singleShot(0, mParent, [this]() { doStartNext(); }); } void SessionPrivate::reconnect() { if (!connection) { connection = new Connection(Connection::CommandConnection, sessionId, &mCommandBuffer); sessionThread()->addConnection(connection); mParent->connect(connection, &Connection::reconnected, mParent, &Session::reconnected, Qt::QueuedConnection); - mParent->connect(connection, SIGNAL(socketDisconnected()), mParent, SLOT(socketDisconnected()), - Qt::QueuedConnection); - mParent->connect(connection, SIGNAL(socketError(QString)), mParent, SLOT(socketError(QString)), - Qt::QueuedConnection); + mParent->connect(connection, &Connection::socketDisconnected, mParent, [this]() { socketDisconnected(); }, Qt::QueuedConnection); + mParent->connect(connection, &Connection::socketError, mParent, [this](const QString &error) { socketError(error); }, Qt::QueuedConnection); } connection->reconnect(); } void SessionPrivate::socketError(const QString &error) { qCWarning(AKONADICORE_LOG) << "Socket error occurred:" << error; socketDisconnected(); } void SessionPrivate::socketDisconnected() { if (currentJob) { currentJob->d_ptr->lostConnection(); } connected = false; } bool SessionPrivate::handleCommands() { CommandBufferLocker lock(&mCommandBuffer); CommandBufferNotifyBlocker notify(&mCommandBuffer); while (!mCommandBuffer.isEmpty()) { const auto command = mCommandBuffer.dequeue(); lock.unlock(); const auto cmd = command.command; const auto tag = command.tag; // Handle Hello response -> send Login if (cmd->type() == Protocol::Command::Hello) { const auto &hello = Protocol::cmdCast(cmd); if (hello.isError()) { qCWarning(AKONADICORE_LOG) << "Error when establishing connection with Akonadi server:" << hello.errorMessage(); connection->closeConnection(); QTimer::singleShot(1000, connection, &Connection::reconnect); return false; } qCDebug(AKONADICORE_LOG) << "Connected to" << hello.serverName() << ", using protocol version" << hello.protocolVersion(); qCDebug(AKONADICORE_LOG) << "Server generation:" << hello.generation(); qCDebug(AKONADICORE_LOG) << "Server says:" << hello.message(); // Version mismatch is handled in SessionPrivate::startJob() so that // we can report the error out via KJob API protocolVersion = hello.protocolVersion(); Internal::setServerProtocolVersion(protocolVersion); Internal::setGeneration(hello.generation()); sendCommand(nextTag(), Protocol::LoginCommandPtr::create(sessionId)); } else if (cmd->type() == Protocol::Command::Login) { const auto &login = Protocol::cmdCast(cmd); if (login.isError()) { qCWarning(AKONADICORE_LOG) << "Unable to login to Akonadi server:" << login.errorMessage(); connection->closeConnection(); - QTimer::singleShot(1000, mParent, SLOT(reconnect())); + QTimer::singleShot(1000, mParent, [this]() { reconnect(); }); return false; } connected = true; startNext(); } else if (currentJob) { currentJob->d_ptr->handleResponse(tag, cmd); } lock.relock(); } return true; } bool SessionPrivate::canPipelineNext() { if (queue.isEmpty() || pipeline.count() >= PIPELINE_LENGTH) { return false; } if (pipeline.isEmpty() && currentJob) { return currentJob->d_ptr->mWriteFinished; } if (!pipeline.isEmpty()) { return pipeline.last()->d_ptr->mWriteFinished; } return false; } void SessionPrivate::doStartNext() { if (!connected || (queue.isEmpty() && pipeline.isEmpty())) { return; } if (canPipelineNext()) { Akonadi::Job *nextJob = queue.dequeue(); pipeline.enqueue(nextJob); startJob(nextJob); } if (jobRunning) { return; } jobRunning = true; if (!pipeline.isEmpty()) { currentJob = pipeline.dequeue(); } else { currentJob = queue.dequeue(); startJob(currentJob); } } void SessionPrivate::startJob(Job *job) { if (protocolVersion != Protocol::version()) { job->setError(Job::ProtocolVersionMismatch); if (protocolVersion < Protocol::version()) { job->setErrorText(i18n("Protocol version mismatch. Server version is older (%1) than ours (%2). " "If you updated your system recently please restart the Akonadi server.", protocolVersion, Protocol::version())); qCWarning(AKONADICORE_LOG) << "Protocol version mismatch. Server version is older (" << protocolVersion << ") than ours (" << Protocol::version() << "). " "If you updated your system recently please restart the Akonadi server."; } else { job->setErrorText(i18n("Protocol version mismatch. Server version is newer (%1) than ours (%2). " "If you updated your system recently please restart all KDE PIM applications.", protocolVersion, Protocol::version())); qCWarning(AKONADICORE_LOG) << "Protocol version mismatch. Server version is newer (" << protocolVersion << ") than ours (" << Protocol::version() << "). " "If you updated your system recently please restart all KDE PIM applications."; } job->emitResult(); } else { job->d_ptr->startQueued(); } } void SessionPrivate::endJob(Job *job) { job->emitResult(); } void SessionPrivate::jobDone(KJob *job) { // ### careful, this method can be called from the QObject dtor of job (see jobDestroyed() below) // so don't call any methods on job itself if (job == currentJob) { if (pipeline.isEmpty()) { jobRunning = false; currentJob = nullptr; } else { currentJob = pipeline.dequeue(); } startNext(); } else { // non-current job finished, likely canceled while still in the queue queue.removeAll(static_cast(job)); // ### likely not enough to really cancel already running jobs pipeline.removeAll(static_cast(job)); } } void SessionPrivate::jobWriteFinished(Akonadi::Job *job) { Q_ASSERT((job == currentJob && pipeline.isEmpty()) || (job = pipeline.last())); Q_UNUSED(job); startNext(); } void SessionPrivate::jobDestroyed(QObject *job) { // careful, accessing non-QObject methods of job will fail here already jobDone(static_cast(job)); } void SessionPrivate::addJob(Job *job) { queue.append(job); - QObject::connect(job, SIGNAL(finished(KJob*)), mParent, SLOT(jobDone(KJob*))); - QObject::connect(job, SIGNAL(writeFinished(Akonadi::Job*)), mParent, SLOT(jobWriteFinished(Akonadi::Job*))); - QObject::connect(job, SIGNAL(destroyed(QObject*)), mParent, SLOT(jobDestroyed(QObject*))); + QObject::connect(job, &KJob::result, mParent, [this](KJob *job) { jobDone(job); }); + QObject::connect(job, &Job::writeFinished, mParent, [this](Job *job) { jobWriteFinished(job); }); + QObject::connect(job, &QObject::destroyed, mParent, [this](QObject *o) { jobDestroyed(o); }); startNext(); } void SessionPrivate::publishOtherJobs(Job *thanThisJob) { int count = 0; - for (const auto& job : queue) { + for (const auto& job : qAsConst(queue)) { if (job != thanThisJob) { job->d_ptr->publishJob(); ++count; } } if (count > 0) { qCDebug(AKONADICORE_LOG) << "published" << count << "pending jobs to the job tracker"; } if (currentJob && currentJob != thanThisJob) { currentJob->d_ptr->signalStartedToJobTracker(); } } qint64 SessionPrivate::nextTag() { return theNextTag++; } void SessionPrivate::sendCommand(qint64 tag, const Protocol::CommandPtr &command) { connection->sendCommand(tag, command); } void SessionPrivate::serverStateChanged(ServerManager::State state) { if (state == ServerManager::Running && !connected) { reconnect(); } else if (!connected && state == ServerManager::Broken) { // If the server is broken, cancel all pending jobs, otherwise they will be // blocked forever and applications waiting for them to finish would be stuck for (Job *job : qAsConst(queue)) { job->setError(Job::ConnectionFailed); job->kill(KJob::EmitResult); } } else if (state == ServerManager::Stopping) { sessionThread()->destroyConnection(connection); connection = nullptr; } } void SessionPrivate::itemRevisionChanged(Akonadi::Item::Id itemId, int oldRevision, int newRevision) { // only deal with the queue, for the guys in the pipeline it's too late already anyway // and they shouldn't have gotten there if they depend on a preceding job anyway. for (Job *job : qAsConst(queue)) { job->d_ptr->updateItemRevision(itemId, oldRevision, newRevision); } } //@endcond SessionPrivate::SessionPrivate(Session *parent) : mParent(parent) , mSessionThread(new SessionThread) , connection(nullptr) , protocolVersion(0) , mCommandBuffer(parent, "handleCommands") , currentJob(nullptr) { // Shutdown the thread before QApplication event loop quits - the // thread()->wait() mechanism in Connection dtor crashes sometimes // when called from QApplication destructor connThreadCleanUp = QObject::connect(qApp, &QCoreApplication::aboutToQuit, [this]() { delete mSessionThread; mSessionThread = nullptr; }); } SessionPrivate::~SessionPrivate() { QObject::disconnect(connThreadCleanUp); delete mSessionThread; } void SessionPrivate::init(const QByteArray &id) { if (!id.isEmpty()) { sessionId = id; } else { sessionId = QCoreApplication::instance()->applicationName().toUtf8() + '-' + QByteArray::number(qrand()); } qCDebug(AKONADICORE_LOG) << "Initializing session with ID" << id; connected = false; theNextTag = 2; jobRunning = false; if (ServerManager::state() == ServerManager::NotRunning) { ServerManager::start(); } - mParent->connect(ServerManager::self(), SIGNAL(stateChanged(Akonadi::ServerManager::State)), - SLOT(serverStateChanged(Akonadi::ServerManager::State))); - + QObject::connect(ServerManager::self(), &ServerManager::stateChanged, + mParent, [this](ServerManager::State state) { serverStateChanged(state); }); reconnect(); } void SessionPrivate::forceReconnect() { jobRunning = false; connected = false; if (connection) { connection->forceReconnect(); } QMetaObject::invokeMethod(mParent, [this]() { reconnect(); }, Qt::QueuedConnection); } Session::Session(const QByteArray &sessionId, QObject *parent) : QObject(parent) , d(new SessionPrivate(this)) { d->init(sessionId); } Session::Session(SessionPrivate *dd, const QByteArray &sessionId, QObject *parent) : QObject(parent) , d(dd) { d->mParent = this; d->init(sessionId); } Session::~Session() { d->clear(false); delete d; } QByteArray Session::sessionId() const { return d->sessionId; } Q_GLOBAL_STATIC(QThreadStorage>, instances) void SessionPrivate::createDefaultSession(const QByteArray &sessionId) { Q_ASSERT_X(!sessionId.isEmpty(), "SessionPrivate::createDefaultSession", "You tried to create a default session with empty session id!"); Q_ASSERT_X(!instances()->hasLocalData(), "SessionPrivate::createDefaultSession", "You tried to create a default session twice!"); Session *session = new Session(sessionId); setDefaultSession(session); } void SessionPrivate::setDefaultSession(Session *session) { instances()->setLocalData({ session }); QObject::connect(qApp, &QCoreApplication::aboutToQuit, []() { instances()->setLocalData({}); }); } Session *Session::defaultSession() { if (!instances()->hasLocalData()) { Session *session = new Session(); SessionPrivate::setDefaultSession(session); } return instances()->localData().data(); } void Session::clear() { d->clear(true); } void SessionPrivate::clear(bool forceReconnect) { for (Job *job : qAsConst(queue)) { job->kill(KJob::EmitResult); // safe, not started yet } queue.clear(); for (Job *job : qAsConst(pipeline)) { job->d_ptr->mStarted = false; // avoid killing/reconnect loops job->kill(KJob::EmitResult); } pipeline.clear(); if (currentJob) { currentJob->d_ptr->mStarted = false; // avoid killing/reconnect loops currentJob->kill(KJob::EmitResult); } if (forceReconnect) { this->forceReconnect(); } } #include "moc_session.cpp" diff --git a/src/core/session.h b/src/core/session.h index 1f59a2b19..a0317cbff 100644 --- a/src/core/session.h +++ b/src/core/session.h @@ -1,150 +1,142 @@ /* Copyright (c) 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. */ #ifndef AKONADI_SESSION_H #define AKONADI_SESSION_H #include "akonadicore_export.h" #include class KJob; class FakeSession; class FakeNotificationConnection; namespace Akonadi { namespace Protocol { class Command; using CommandPtr = QSharedPointer; } class Job; class SessionPrivate; class ChangeNotificationDependenciesFactory; /** * @short A communication session with the Akonadi storage. * * Every Job object has to be associated with a Session. * The session is responsible of scheduling its jobs. * For now only a simple serial execution is implemented (the IMAP-like * protocol to communicate with the storage backend is capable of parallel * execution on a single session though). * * @code * * using namespace Akonadi; * * Session *session = new Session( "mySession" ); * * CollectionFetchJob *job = new CollectionFetchJob( Collection::root(), * CollectionFetchJob::Recursive, * session ); * * connect( job, SIGNAL(result(KJob*)), this, SLOT(slotResult(KJob*)) ); * * @endcode * * @author Volker Krause */ class AKONADICORE_EXPORT Session : public QObject { Q_OBJECT friend class Job; friend class JobPrivate; friend class SessionPrivate; public: /** * Creates a new session. * * @param sessionId The identifier for this session, will be a * random value if empty. * @param parent The parent object. * * @see defaultSession() */ explicit Session(const QByteArray &sessionId = QByteArray(), QObject *parent = nullptr); /** * Destroys the session. */ ~Session(); /** * Returns the session identifier. */ Q_REQUIRED_RESULT QByteArray sessionId() const; /** * Returns the default session for this thread. */ static Session *defaultSession(); /** * Stops all jobs queued for execution. */ void clear(); Q_SIGNALS: /** * This signal is emitted whenever the session has been reconnected * to the server (e.g. after a server crash). * * @since 4.6 */ void reconnected(); protected: /** * Creates a new session with shared private object. * * @param d The private object. * @param sessionId The identifier for this session, will be a * random value if empty. * @param parent The parent object. * * @note This constructor is needed for unit testing only. */ explicit Session(SessionPrivate *d, const QByteArray &sessionId = QByteArray(), QObject *parent = nullptr); private: //@cond PRIVATE SessionPrivate *const d; friend class ::FakeSession; friend class ::FakeNotificationConnection; friend class ChangeNotificationDependenciesFactory; - Q_PRIVATE_SLOT(d, void reconnect()) - Q_PRIVATE_SLOT(d, void socketError(const QString &error)) - Q_PRIVATE_SLOT(d, void socketDisconnected()) Q_PRIVATE_SLOT(d, bool handleCommands()) - Q_PRIVATE_SLOT(d, void doStartNext()) - Q_PRIVATE_SLOT(d, void jobDone(KJob *)) - Q_PRIVATE_SLOT(d, void jobWriteFinished(Akonadi::Job *)) - Q_PRIVATE_SLOT(d, void jobDestroyed(QObject *)) - Q_PRIVATE_SLOT(d, void serverStateChanged(Akonadi::ServerManager::State)) //@endcond PRIVATE }; } #endif diff --git a/src/server/notificationmanager.cpp b/src/server/notificationmanager.cpp index 93b8db9e1..64f8b0352 100644 --- a/src/server/notificationmanager.cpp +++ b/src/server/notificationmanager.cpp @@ -1,223 +1,223 @@ /* Copyright (c) 2006 - 2007 Volker Krause Copyright (c) 2010 Michael Jansen 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 "notificationmanager.h" #include "notificationsubscriber.h" #include "storage/notificationcollector.h" #include "tracer.h" #include "akonadiserver_debug.h" #include "aggregatedfetchscope.h" #include "storage/collectionstatistics.h" #include "handlerhelper.h" #include #include #include #include #include #include #include using namespace Akonadi; using namespace Akonadi::Server; using namespace AkRanges; NotificationManager::NotificationManager(StartMode startMode) : AkThread(QStringLiteral("NotificationManager"), startMode) , mTimer(nullptr) , mNotifyThreadPool(nullptr) , mDebugNotifications(0) { } NotificationManager::~NotificationManager() { quitThread(); } void NotificationManager::init() { AkThread::init(); const QString serverConfigFile = StandardDirs::serverConfigFile(StandardDirs::ReadWrite); QSettings settings(serverConfigFile, QSettings::IniFormat); mTimer = new QTimer(this); mTimer->setInterval(settings.value(QStringLiteral("NotificationManager/Interval"), 50).toInt()); mTimer->setSingleShot(true); connect(mTimer, &QTimer::timeout, this, &NotificationManager::emitPendingNotifications); mNotifyThreadPool = new QThreadPool(this); mNotifyThreadPool->setMaxThreadCount(5); mCollectionFetchScope = new AggregatedCollectionFetchScope(); mItemFetchScope = new AggregatedItemFetchScope(); mTagFetchScope = new AggregatedTagFetchScope(); } void NotificationManager::quit() { mQuitting = true; mTimer->stop(); delete mTimer; mNotifyThreadPool->clear(); mNotifyThreadPool->waitForDone(); delete mNotifyThreadPool; qDeleteAll(mSubscribers); delete mCollectionFetchScope; delete mItemFetchScope; delete mTagFetchScope; AkThread::quit(); } void NotificationManager::registerConnection(quintptr socketDescriptor) { Q_ASSERT(thread() == QThread::currentThread()); NotificationSubscriber *subscriber = new NotificationSubscriber(this, socketDescriptor); qCInfo(AKONADISERVER_LOG) << "New notification connection (registered as" << subscriber << ")"; connect(subscriber, &NotificationSubscriber::notificationDebuggingChanged, this, [this](bool enabled) { if (enabled) { ++mDebugNotifications; } else { --mDebugNotifications; } Q_ASSERT(mDebugNotifications >= 0); Q_ASSERT(mDebugNotifications <= mSubscribers.count()); }); mSubscribers.push_back(subscriber); } void NotificationManager::forgetSubscriber(NotificationSubscriber *subscriber) { Q_ASSERT(QThread::currentThread() == thread()); mSubscribers.removeAll(subscriber); } void NotificationManager::slotNotify(const Protocol::ChangeNotificationList &msgs) { Q_ASSERT(QThread::currentThread() == thread()); for (const auto &msg : msgs) { switch (msg->type()) { case Protocol::Command::CollectionChangeNotification: Protocol::CollectionChangeNotification::appendAndCompress(mNotifications, msg); continue; case Protocol::Command::ItemChangeNotification: case Protocol::Command::TagChangeNotification: case Protocol::Command::RelationChangeNotification: case Protocol::Command::SubscriptionChangeNotification: case Protocol::Command::DebugChangeNotification: mNotifications.push_back(msg); continue; default: Q_ASSERT_X(false, "slotNotify", "Invalid notification type!"); continue; } } if (!mTimer->isActive()) { mTimer->start(); } } class NotifyRunnable : public QRunnable { public: explicit NotifyRunnable(NotificationSubscriber *subscriber, const Protocol::ChangeNotificationList ¬ifications) : mSubscriber(subscriber) , mNotifications(notifications) { } - ~NotifyRunnable() - { - } + ~NotifyRunnable() = default; void run() override { for (const auto &ntf : qAsConst(mNotifications)) { if (mSubscriber) { mSubscriber->notify(ntf); } else { break; } } } private: + Q_DISABLE_COPY_MOVE(NotifyRunnable); + QPointer mSubscriber; Protocol::ChangeNotificationList mNotifications; }; void NotificationManager::emitPendingNotifications() { Q_ASSERT(QThread::currentThread() == thread()); if (mNotifications.isEmpty()) { return; } if (mDebugNotifications == 0) { mSubscribers | Views::filter(IsNotNull) | Actions::forEach([this](const auto &subscriber) { mNotifyThreadPool->start(new NotifyRunnable(subscriber, mNotifications)); }); } else { // When debugging notification we have to use a non-threaded approach // so that we can work with return value of notify() for (const auto ¬ification : qAsConst(mNotifications)) { QVector listeners; for (NotificationSubscriber *subscriber : qAsConst(mSubscribers)) { if (subscriber && subscriber->notify(notification)) { listeners.push_back(subscriber->subscriber()); } } emitDebugNotification(notification, listeners); } } mNotifications.clear(); } void NotificationManager::emitDebugNotification(const Protocol::ChangeNotificationPtr &ntf, const QVector &listeners) { auto debugNtf = Protocol::DebugChangeNotificationPtr::create(); debugNtf->setNotification(ntf); debugNtf->setListeners(listeners); debugNtf->setTimestamp(QDateTime::currentMSecsSinceEpoch()); mSubscribers | Views::filter(IsNotNull) | Actions::forEach([this, &debugNtf](const auto &subscriber) { mNotifyThreadPool->start(new NotifyRunnable(subscriber, {debugNtf})); }); } diff --git a/src/server/notificationsubscriber.cpp b/src/server/notificationsubscriber.cpp index 7181e8549..1c5c8bc4a 100644 --- a/src/server/notificationsubscriber.cpp +++ b/src/server/notificationsubscriber.cpp @@ -1,690 +1,690 @@ /* Copyright (c) 2015 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 "notificationsubscriber.h" #include "akonadiserver_debug.h" #include "notificationmanager.h" #include "aggregatedfetchscope.h" #include "utils.h" #include #include #include #include #include #include using namespace Akonadi; using namespace Akonadi::Server; using namespace AkRanges; QMimeDatabase NotificationSubscriber::sMimeDatabase; #define TRACE_NTF(x) //#define TRACE_NTF(x) qCDebug(AKONADISERVER_LOG) << mSubscriber << x NotificationSubscriber::NotificationSubscriber(NotificationManager *manager) : QObject() , mManager(manager) , mSocket(nullptr) , mAllMonitored(false) , mExclusive(false) , mNotificationDebugging(false) { if (mManager) { mManager->itemFetchScope()->addSubscriber(); mManager->collectionFetchScope()->addSubscriber(); mManager->tagFetchScope()->addSubscriber(); } } NotificationSubscriber::NotificationSubscriber(NotificationManager *manager, quintptr socketDescriptor) : NotificationSubscriber(manager) { mSocket = new QLocalSocket(this); connect(mSocket, &QLocalSocket::readyRead, this, &NotificationSubscriber::handleIncomingData); connect(mSocket, &QLocalSocket::disconnected, this, &NotificationSubscriber::socketDisconnected); mSocket->setSocketDescriptor(socketDescriptor); - const SchemaVersion schema = SchemaVersion::retrieveAll().first(); + const SchemaVersion schema = SchemaVersion::retrieveAll().at(0); auto hello = Protocol::HelloResponsePtr::create(); hello->setServerName(QStringLiteral("Akonadi")); hello->setMessage(QStringLiteral("Not really IMAP server")); hello->setProtocolVersion(Protocol::version()); hello->setGeneration(schema.generation()); writeCommand(0, hello); } NotificationSubscriber::~NotificationSubscriber() { QMutexLocker locker(&mLock); if (mNotificationDebugging) { Q_EMIT notificationDebuggingChanged(false); } } void NotificationSubscriber::handleIncomingData() { while (mSocket->bytesAvailable() > (int) sizeof(qint64)) { Protocol::DataStream stream(mSocket); // Ignored atm qint64 tag = -1; stream >> tag; Protocol::CommandPtr cmd; try { cmd = Protocol::deserialize(mSocket); } catch (const Akonadi::ProtocolException &e) { qCWarning(AKONADISERVER_LOG) << "ProtocolException while reading from notification bus for" << mSubscriber << ":" << e.what(); disconnectSubscriber(); return; } catch (const std::exception &e) { qCWarning(AKONADISERVER_LOG) << "Unknown exception while reading from notification bus for" << mSubscriber << ":" << e.what(); disconnectSubscriber(); return; } if (cmd->type() == Protocol::Command::Invalid) { qCWarning(AKONADISERVER_LOG) << "Invalid command while reading from notification bus for " << mSubscriber << ", resetting connection"; disconnectSubscriber(); return; } switch (cmd->type()) { case Protocol::Command::CreateSubscription: registerSubscriber(Protocol::cmdCast(cmd)); writeCommand(tag, Protocol::CreateSubscriptionResponsePtr::create()); break; case Protocol::Command::ModifySubscription: if (mSubscriber.isEmpty()) { qCWarning(AKONADISERVER_LOG) << "Notification subscriber received ModifySubscription command before RegisterSubscriber"; disconnectSubscriber(); return; } modifySubscription(Protocol::cmdCast(cmd)); writeCommand(tag, Protocol::ModifySubscriptionResponsePtr::create()); break; case Protocol::Command::Logout: disconnectSubscriber(); break; default: qCWarning(AKONADISERVER_LOG) << "Notification subscriber for" << mSubscriber << "received an invalid command" << cmd->type(); disconnectSubscriber(); break; } } } void NotificationSubscriber::socketDisconnected() { qCInfo(AKONADISERVER_LOG) << "Subscriber" << mSubscriber << "disconnected"; disconnectSubscriber(); } void NotificationSubscriber::disconnectSubscriber() { QMutexLocker locker(&mLock); auto changeNtf = Protocol::SubscriptionChangeNotificationPtr::create(); changeNtf->setSubscriber(mSubscriber); changeNtf->setSessionId(mSession); changeNtf->setOperation(Protocol::SubscriptionChangeNotification::Remove); mManager->slotNotify({ changeNtf }); if (mSocket) { disconnect(mSocket, &QLocalSocket::disconnected, this, &NotificationSubscriber::socketDisconnected); mSocket->close(); } // Unregister ourselves from the aggregated collection fetch scope auto cfs = mManager->collectionFetchScope(); cfs->apply(mCollectionFetchScope, Protocol::CollectionFetchScope()); cfs->removeSubscriber(); auto tfs = mManager->tagFetchScope(); tfs->apply(mTagFetchScope, Protocol::TagFetchScope()); tfs->removeSubscriber(); auto ifs = mManager->itemFetchScope(); ifs->apply(mItemFetchScope, Protocol::ItemFetchScope()); ifs->removeSubscriber(); mManager->forgetSubscriber(this); deleteLater(); } void NotificationSubscriber::registerSubscriber(const Protocol::CreateSubscriptionCommand &command) { QMutexLocker locker(&mLock); qCInfo(AKONADISERVER_LOG) << "Subscriber" << this << "identified as" << command.subscriberName(); mSubscriber = command.subscriberName(); mSession = command.session(); auto changeNtf = Protocol::SubscriptionChangeNotificationPtr::create(); changeNtf->setSubscriber(mSubscriber); changeNtf->setSessionId(mSession); changeNtf->setOperation(Protocol::SubscriptionChangeNotification::Add); mManager->slotNotify({ changeNtf }); } void NotificationSubscriber::modifySubscription(const Protocol::ModifySubscriptionCommand &command) { QMutexLocker locker(&mLock); const auto modifiedParts = command.modifiedParts(); #define START_MONITORING(type) \ (modifiedParts & Protocol::ModifySubscriptionCommand::ModifiedParts( \ Protocol::ModifySubscriptionCommand::type | Protocol::ModifySubscriptionCommand::Add)) #define STOP_MONITORING(type) \ (modifiedParts & Protocol::ModifySubscriptionCommand::ModifiedParts( \ Protocol::ModifySubscriptionCommand::type | Protocol::ModifySubscriptionCommand::Remove)) #define APPEND(set, newItems) \ Q_FOREACH (const auto &entity, newItems) { \ set.insert(entity); \ } #define REMOVE(set, items) \ Q_FOREACH (const auto &entity, items) { \ set.remove(entity); \ } if (START_MONITORING(Types)) { APPEND(mMonitoredTypes, command.startMonitoringTypes()) } if (STOP_MONITORING(Types)) { REMOVE(mMonitoredTypes, command.stopMonitoringTypes()) } if (START_MONITORING(Collections)) { APPEND(mMonitoredCollections, command.startMonitoringCollections()) } if (STOP_MONITORING(Collections)) { REMOVE(mMonitoredCollections, command.stopMonitoringCollections()) } if (START_MONITORING(Items)) { APPEND(mMonitoredItems, command.startMonitoringItems()) } if (STOP_MONITORING(Items)) { REMOVE(mMonitoredItems, command.stopMonitoringItems()) } if (START_MONITORING(Tags)) { APPEND(mMonitoredTags, command.startMonitoringTags()) } if (STOP_MONITORING(Tags)) { REMOVE(mMonitoredTags, command.stopMonitoringTags()) } if (START_MONITORING(Resources)) { APPEND(mMonitoredResources, command.startMonitoringResources()) } if (STOP_MONITORING(Resources)) { REMOVE(mMonitoredResources, command.stopMonitoringResources()) } if (START_MONITORING(MimeTypes)) { APPEND(mMonitoredMimeTypes, command.startMonitoringMimeTypes()) } if (STOP_MONITORING(MimeTypes)) { REMOVE(mMonitoredMimeTypes, command.stopMonitoringMimeTypes()) } if (START_MONITORING(Sessions)) { APPEND(mIgnoredSessions, command.startIgnoringSessions()) } if (STOP_MONITORING(Sessions)) { REMOVE(mIgnoredSessions, command.stopIgnoringSessions()) } if (modifiedParts & Protocol::ModifySubscriptionCommand::AllFlag) { mAllMonitored = command.allMonitored(); } if (modifiedParts & Protocol::ModifySubscriptionCommand::ExclusiveFlag) { mExclusive = command.isExclusive(); } if (modifiedParts & Protocol::ModifySubscriptionCommand::ItemFetchScope) { const auto newScope = command.itemFetchScope(); mManager->itemFetchScope()->apply(mItemFetchScope, newScope); mItemFetchScope = newScope; } if (modifiedParts & Protocol::ModifySubscriptionCommand::CollectionFetchScope) { const auto newScope = command.collectionFetchScope(); mManager->collectionFetchScope()->apply(mCollectionFetchScope, newScope); mCollectionFetchScope = newScope; } if (modifiedParts & Protocol::ModifySubscriptionCommand::TagFetchScope) { const auto newScope = command.tagFetchScope(); mManager->tagFetchScope()->apply(mTagFetchScope, newScope); mTagFetchScope = newScope; if (!newScope.fetchIdOnly()) Q_ASSERT(!mManager->tagFetchScope()->fetchIdOnly()); } if (mManager) { if (modifiedParts & Protocol::ModifySubscriptionCommand::Types) { // Did the caller just subscribed to subscription changes? if (command.startMonitoringTypes().contains(Protocol::ModifySubscriptionCommand::SubscriptionChanges)) { // If yes, then send them list of all existing subscribers mManager->mSubscribers | Views::filter(IsNotNull) | Actions::forEach([this](const auto &subscriber) { QMetaObject::invokeMethod(this, "notify", Qt::QueuedConnection, Q_ARG(Akonadi::Protocol::ChangeNotificationPtr, subscriber->toChangeNotification())); }); } if (command.startMonitoringTypes().contains(Protocol::ModifySubscriptionCommand::ChangeNotifications)) { if (!mNotificationDebugging) { mNotificationDebugging = true; Q_EMIT notificationDebuggingChanged(true); } } else if (command.stopMonitoringTypes().contains(Protocol::ModifySubscriptionCommand::ChangeNotifications)) { if (mNotificationDebugging) { mNotificationDebugging = false; Q_EMIT notificationDebuggingChanged(false); } } } // Emit subscription change notification auto changeNtf = toChangeNotification(); changeNtf->setOperation(Protocol::SubscriptionChangeNotification::Modify); mManager->slotNotify({ changeNtf }); } #undef START_MONITORING #undef STOP_MONITORING #undef APPEND #undef REMOVE } Protocol::SubscriptionChangeNotificationPtr NotificationSubscriber::toChangeNotification() const { // Assumes mLock being locked by caller auto ntf = Protocol::SubscriptionChangeNotificationPtr::create(); ntf->setSessionId(mSession); ntf->setSubscriber(mSubscriber); ntf->setOperation(Protocol::SubscriptionChangeNotification::Add); ntf->setCollections(mMonitoredCollections); ntf->setItems(mMonitoredItems); ntf->setTags(mMonitoredTags); ntf->setTypes(mMonitoredTypes); ntf->setMimeTypes(mMonitoredMimeTypes); ntf->setResources(mMonitoredResources); ntf->setIgnoredSessions(mIgnoredSessions); ntf->setAllMonitored(mAllMonitored); ntf->setExclusive(mExclusive); ntf->setItemFetchScope(mItemFetchScope); ntf->setTagFetchScope(mTagFetchScope); ntf->setCollectionFetchScope(mCollectionFetchScope); return ntf; } bool NotificationSubscriber::isCollectionMonitored(Entity::Id id) const { // Assumes mLock being locked by caller if (id < 0) { return false; } else if (mMonitoredCollections.contains(id)) { return true; } else if (mMonitoredCollections.contains(0)) { return true; } return false; } bool NotificationSubscriber::isMimeTypeMonitored(const QString &mimeType) const { // Assumes mLock being locked by caller const QMimeType mt = sMimeDatabase.mimeTypeForName(mimeType); if (mMonitoredMimeTypes.contains(mimeType)) { return true; } const QStringList lst = mt.aliases(); return std::any_of(lst.cbegin(), lst.cend(), [this](const auto &mt) { return mMonitoredMimeTypes.contains(mt); }); } bool NotificationSubscriber::isMoveDestinationResourceMonitored(const Protocol::ItemChangeNotification &msg) const { // Assumes mLock being locked by caller if (msg.operation() != Protocol::ItemChangeNotification::Move) { return false; } return mMonitoredResources.contains(msg.destinationResource()); } bool NotificationSubscriber::isMoveDestinationResourceMonitored(const Protocol::CollectionChangeNotification &msg) const { // Assumes mLock being locked by caller if (msg.operation() != Protocol::CollectionChangeNotification::Move) { return false; } return mMonitoredResources.contains(msg.destinationResource()); } bool NotificationSubscriber::acceptsItemNotification(const Protocol::ItemChangeNotification &msg) const { // Assumes mLock being locked by caller if (msg.items().isEmpty()) { return false; } if (mAllMonitored) { TRACE_NTF("ACCEPTS ITEM: all monitored"); return true; } if (!mMonitoredTypes.isEmpty() && !mMonitoredTypes.contains(Protocol::ModifySubscriptionCommand::ItemChanges)) { TRACE_NTF("ACCEPTS ITEM: REJECTED - Item changes not monitored"); return false; } // we have a resource or mimetype filter if (!mMonitoredResources.isEmpty() || !mMonitoredMimeTypes.isEmpty()) { if (mMonitoredResources.contains(msg.resource())) { TRACE_NTF("ACCEPTS ITEM: ACCEPTED - resource monitored"); return true; } if (isMoveDestinationResourceMonitored(msg)) { TRACE_NTF("ACCEPTS ITEM: ACCEPTED: move destination monitored"); return true; } Q_FOREACH (const auto &item, msg.items()) { if (isMimeTypeMonitored(item.mimeType())) { TRACE_NTF("ACCEPTS ITEM: ACCEPTED - mimetype monitored"); return true; } } TRACE_NTF("ACCEPTS ITEM: REJECTED: resource nor mimetype monitored"); return false; } // we explicitly monitor that item or the collections it's in Q_FOREACH (const auto &item, msg.items()) { if (mMonitoredItems.contains(item.id())) { TRACE_NTF("ACCEPTS ITEM: ACCEPTED: item explicitly monitored"); return true; } } if (isCollectionMonitored(msg.parentCollection())) { TRACE_NTF("ACCEPTS ITEM: ACCEPTED: parent collection monitored"); return true; } if (isCollectionMonitored(msg.parentDestCollection())) { TRACE_NTF("ACCEPTS ITEM: ACCEPTED: destination collection monitored"); return true; } TRACE_NTF("ACCEPTS ITEM: REJECTED"); return false; } bool NotificationSubscriber::acceptsCollectionNotification(const Protocol::CollectionChangeNotification &msg) const { // Assumes mLock being locked by caller const auto &collection = msg.collection(); if (collection.id() < 0) { return false; } // HACK: We need to dispatch notifications about disabled collections to SOME // agents (that's what we have the exclusive subscription for) - but because // querying each Collection from database would be expensive, we use the // metadata hack to transfer this information from NotificationCollector if (msg.metadata().contains("DISABLED") && (msg.operation() != Protocol::CollectionChangeNotification::Unsubscribe) && !msg.changedParts().contains("ENABLED")) { // Exclusive subscriber always gets it if (mExclusive) { return true; } // If the subscriber is not exclusive (i.e. if we got here), then the subscriber does // not care about this one, so drop it return false; } if (mAllMonitored) { return true; } if (!mMonitoredTypes.isEmpty() && !mMonitoredTypes.contains(Protocol::ModifySubscriptionCommand::CollectionChanges)) { return false; } // we have a resource filter if (!mMonitoredResources.isEmpty()) { const bool resourceMatches = mMonitoredResources.contains(msg.resource()) || isMoveDestinationResourceMonitored(msg); // a bit hacky, but match the behaviour from the item case, // if resource is the only thing we are filtering on, stop here, and if the resource filter matched, of course if (mMonitoredMimeTypes.isEmpty() || resourceMatches) { return resourceMatches; } // else continue } // we explicitly monitor that collection, or all of them if (isCollectionMonitored(collection.id())) { return true; } return isCollectionMonitored(msg.parentCollection()) || isCollectionMonitored(msg.parentDestCollection()); } bool NotificationSubscriber::acceptsTagNotification(const Protocol::TagChangeNotification &msg) const { // Assumes mLock being locked by caller if (msg.tag().id() < 0) { return false; } // Special handling for Tag removal notifications: When a Tag is removed, // a notification is emitted for each Resource that owns the tag (i.e. // each resource that owns a Tag RID - Tag RIDs are resource-specific). // Additionally then we send one more notification without any RID that is // destined for regular applications (which don't know anything about Tag RIDs) if (msg.operation() == Protocol::TagChangeNotification::Remove) { // HACK: Since have no way to determine which resource this NotificationSource // belongs to, we are abusing the fact that each resource ignores it's own // main session, which is called the same name as the resource. // If there are any ignored sessions, but this notification does not have // a specific resource set, then we ignore it, as this notification is // for clients, not resources (does not have tag RID) if (!mIgnoredSessions.isEmpty() && msg.resource().isEmpty()) { return false; } // If this source ignores a session (i.e. we assume it is a resource), // but this notification is for another resource, then we ignore it if (!msg.resource().isEmpty() && !mIgnoredSessions.contains(msg.resource())) { return false; } // Now we got here, which means that this notification either has empty // resource, i.e. it is destined for a client applications, or it's // destined for resource that we *think* (see the hack above) this // NotificationSource belongs too. Which means we approve this notification, // but it can still be discarded in the generic Tag notification filter // below } if (mAllMonitored) { return true; } if (!mMonitoredTypes.isEmpty() && !mMonitoredTypes.contains(Protocol::ModifySubscriptionCommand::TagChanges)) { return false; } if (mMonitoredTags.isEmpty()) { return true; } if (mMonitoredTags.contains(msg.tag().id())) { return true; } return true; } bool NotificationSubscriber::acceptsRelationNotification(const Protocol::RelationChangeNotification &msg) const { // Assumes mLock being locked by caller Q_UNUSED(msg); if (mAllMonitored) { return true; } if (!mMonitoredTypes.isEmpty() && !mMonitoredTypes.contains(Protocol::ModifySubscriptionCommand::RelationChanges)) { return false; } return true; } bool NotificationSubscriber::acceptsSubscriptionNotification(const Protocol::SubscriptionChangeNotification &msg) const { // Assumes mLock being locked by caller Q_UNUSED(msg); // Unlike other types, subscription notifications must be explicitly enabled // by caller and are excluded from "monitor all" as well return mMonitoredTypes.contains(Protocol::ModifySubscriptionCommand::SubscriptionChanges); } bool NotificationSubscriber::acceptsDebugChangeNotification(const Protocol::DebugChangeNotification &msg) const { // Assumes mLock being locked by caller // We should never end up sending debug notification about a debug notification. // This could get very messy very quickly... Q_ASSERT(msg.notification()->type() != Protocol::Command::DebugChangeNotification); if (msg.notification()->type() == Protocol::Command::DebugChangeNotification) { return false; } // Unlike other types, debug change notifications must be explicitly enabled // by caller and are excluded from "monitor all" as well return mMonitoredTypes.contains(Protocol::ModifySubscriptionCommand::ChangeNotifications); } bool NotificationSubscriber::acceptsNotification(const Protocol::ChangeNotification &msg) const { // Assumes mLock being locked // Uninitialized subscriber gets nothing if (mSubscriber.isEmpty()) { return false; } // session is ignored // TODO: Should this affect SubscriptionChangeNotification and DebugChangeNotification? if (mIgnoredSessions.contains(msg.sessionId())) { return false; } switch (msg.type()) { case Protocol::Command::ItemChangeNotification: return acceptsItemNotification(static_cast(msg)); case Protocol::Command::CollectionChangeNotification: return acceptsCollectionNotification(static_cast(msg)); case Protocol::Command::TagChangeNotification: return acceptsTagNotification(static_cast(msg)); case Protocol::Command::RelationChangeNotification: return acceptsRelationNotification(static_cast(msg)); case Protocol::Command::SubscriptionChangeNotification: return acceptsSubscriptionNotification(static_cast(msg)); case Protocol::Command::DebugChangeNotification: return acceptsDebugChangeNotification(static_cast(msg)); default: qCWarning(AKONADISERVER_LOG) << "NotificationSubscriber" << mSubscriber << "received an invalid notification type" << msg.type(); return false; } } bool NotificationSubscriber::notify(const Protocol::ChangeNotificationPtr ¬ification) { // Guard against this object being deleted while we are waiting for the lock QPointer ptr(this); QMutexLocker locker(&mLock); if (!ptr) { return false; } if (acceptsNotification(*notification)) { QMetaObject::invokeMethod(this, "writeNotification", Qt::QueuedConnection, Q_ARG(Akonadi::Protocol::ChangeNotificationPtr, notification)); return true; } return false; } void NotificationSubscriber::writeNotification(const Protocol::ChangeNotificationPtr ¬ification) { // tag chosen by fair dice roll writeCommand(4, notification); } void NotificationSubscriber::writeCommand(qint64 tag, const Protocol::CommandPtr &cmd) { Q_ASSERT(QThread::currentThread() == thread()); Protocol::DataStream stream(mSocket); stream << tag; try { Protocol::serialize(stream, cmd); stream.flush(); if (!mSocket->waitForBytesWritten()) { if (mSocket->state() == QLocalSocket::ConnectedState) { qCWarning(AKONADISERVER_LOG) << "NotificationSubscriber for" << mSubscriber << ": timeout writing into stream"; } else { // client has disconnected, just discard the message } } } catch (const ProtocolException &e) { qCWarning(AKONADISERVER_LOG) << "ProtocolException while writing into stream for subscriber" << mSubscriber << ":" << e.what(); } } diff --git a/src/server/search/abstractsearchengine.h b/src/server/search/abstractsearchengine.h index 832834a46..1c6d1e232 100644 --- a/src/server/search/abstractsearchengine.h +++ b/src/server/search/abstractsearchengine.h @@ -1,58 +1,62 @@ /* Copyright (c) 2008 Tobias Koenig Copyright (c) 2010 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. */ #ifndef AKONADI_ABSTRACTSEARCHENGINE_H #define AKONADI_ABSTRACTSEARCHENGINE_H -#include +#include namespace Akonadi { namespace Server { class Collection; /** * Abstract interface for search engines. * Executed in the main thread. Must not block. */ class AbstractSearchEngine { public: - virtual ~AbstractSearchEngine() - { - } + virtual ~AbstractSearchEngine() = default; /** * Adds the given @p collection to the search. */ virtual void addSearch(const Collection &collection) = 0; /** * Removes the collection with the given @p id from the search. */ virtual void removeSearch(qint64 id) = 0; + +protected: + explicit AbstractSearchEngine() = default; + +private: + Q_DISABLE_COPY_MOVE(AbstractSearchEngine) }; } // namespace Server } // namespace Akonadi #endif diff --git a/src/server/search/abstractsearchplugin.h b/src/server/search/abstractsearchplugin.h index 5fccfe0c9..8da961166 100644 --- a/src/server/search/abstractsearchplugin.h +++ b/src/server/search/abstractsearchplugin.h @@ -1,69 +1,72 @@ /* Copyright (c) 2013 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_ABSTRACTSEARCHPLUGIN #define AKONADI_ABSTRACTSEARCHPLUGIN #include #include #include namespace Akonadi { /** * @class AbstractSearchPlugin * * 3rd party applications can install a search plugin for Akonadi server to * provide access to their search capability. * * When the server performs a search, it will send the query to all available * search plugins and merge the results. * * @since 1.12 */ class AbstractSearchPlugin { public: /** * Destructor. */ - virtual ~AbstractSearchPlugin() - { - }; + virtual ~AbstractSearchPlugin() = default; /** * Reimplement this method to provide the actual search capability. * * The implementation can block. * * @param query Search query to execute. * @return List of Akonadi Item IDs referring to items that are matching * the query. */ virtual QSet search(const QString &query, const QVector &collections, const QStringList &mimeTypes) = 0; +protected: + explicit AbstractSearchPlugin() = default; + +private: + Q_DISABLE_COPY_MOVE(AbstractSearchPlugin) }; } Q_DECLARE_INTERFACE(Akonadi::AbstractSearchPlugin, "org.freedesktop.Akonadi.AbstractSearchPlugin") #endif diff --git a/src/server/storage/datastore.cpp b/src/server/storage/datastore.cpp index 73cfeb2f6..6603db419 100644 --- a/src/server/storage/datastore.cpp +++ b/src/server/storage/datastore.cpp @@ -1,1520 +1,1520 @@ /*************************************************************************** * Copyright (C) 2006 by Andreas Gungl * * Copyright (C) 2007 by Robert Zwerus * * * * 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 "datastore.h" #include "akonadi.h" #include "collectionstatistics.h" #include "dbconfig.h" #include "dbinitializer.h" #include "dbupdater.h" #include "notificationmanager.h" #include "tracer.h" #include "transaction.h" #include "selectquerybuilder.h" #include "handlerhelper.h" #include "countquerybuilder.h" #include "parthelper.h" #include "handler.h" #include "collectionqueryhelper.h" #include "akonadischema.h" #include "parttypehelper.h" #include "querycache.h" #include "queryhelper.h" #include "akonadiserver_debug.h" #include "storagedebugger.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace Akonadi; using namespace Akonadi::Server; -static QMutex sTransactionMutex; bool DataStore::s_hasForeignKeyConstraints = false; +QMutex DataStore::sTransactionMutex = {}; static QThreadStorage sInstances; #define TRANSACTION_MUTEX_LOCK if ( DbType::isSystemSQLite( m_database ) ) sTransactionMutex.lock() #define TRANSACTION_MUTEX_UNLOCK if ( DbType::isSystemSQLite( m_database ) ) sTransactionMutex.unlock() #define setBoolPtr(ptr, val) \ { \ if ((ptr)) { \ *(ptr) = (val); \ } \ } std::unique_ptr DataStore::sFactory; void DataStore::setFactory(std::unique_ptr factory) { sFactory = std::move(factory); } /*************************************************************************** * DataStore * ***************************************************************************/ DataStore::DataStore(AkonadiServer &akonadi) : QObject() , m_akonadi(akonadi) , m_dbOpened(false) , m_transactionLevel(0) , m_keepAliveTimer(nullptr) { if (DbConfig::configuredDatabase()->driverName() == QLatin1String("QMYSQL")) { // Send a dummy query to MySQL every 1 hour to keep the connection alive, // otherwise MySQL just drops the connection and our subsequent queries fail // without properly reporting the error m_keepAliveTimer = new QTimer(this); m_keepAliveTimer->setInterval(3600 * 1000); QObject::connect(m_keepAliveTimer, &QTimer::timeout, this, &DataStore::sendKeepAliveQuery); } } DataStore::~DataStore() { if (m_dbOpened) { close(); } } void DataStore::open() { m_connectionName = QUuid::createUuid().toString() + QString::number(reinterpret_cast(QThread::currentThread())); Q_ASSERT(!QSqlDatabase::contains(m_connectionName)); m_database = QSqlDatabase::addDatabase(DbConfig::configuredDatabase()->driverName(), m_connectionName); DbConfig::configuredDatabase()->apply(m_database); if (!m_database.isValid()) { m_dbOpened = false; return; } m_dbOpened = m_database.open(); if (!m_dbOpened) { debugLastDbError("Cannot open database."); } else { qCDebug(AKONADISERVER_LOG) << "Database" << m_database.databaseName() << "opened using driver" << m_database.driverName(); } StorageDebugger::instance()->addConnection(reinterpret_cast(this), QThread::currentThread()->objectName()); connect(QThread::currentThread(), &QThread::objectNameChanged, this, [this](const QString &name) { if (!name.isEmpty()) { StorageDebugger::instance()->changeConnection(reinterpret_cast(this), name); } }); DbConfig::configuredDatabase()->initSession(m_database); if (m_keepAliveTimer) { m_keepAliveTimer->start(); } } QSqlDatabase DataStore::database() { if (!m_dbOpened) { open(); } return m_database; } void DataStore::close() { if (m_keepAliveTimer) { m_keepAliveTimer->stop(); } if (!m_dbOpened) { return; } if (inTransaction()) { // By setting m_transactionLevel to '1' here, we skip all nested transactions // and rollback the outermost transaction. m_transactionLevel = 1; rollbackTransaction(); } QueryCache::clear(); m_database.close(); m_database = QSqlDatabase(); QSqlDatabase::removeDatabase(m_connectionName); StorageDebugger::instance()->removeConnection(reinterpret_cast(this)); m_dbOpened = false; } bool DataStore::init() { Q_ASSERT(QThread::currentThread() == QCoreApplication::instance()->thread()); AkonadiSchema schema; DbInitializer::Ptr initializer = DbInitializer::createInstance(database(), &schema); if (!initializer->run()) { qCCritical(AKONADISERVER_LOG) << initializer->errorMsg(); return false; } s_hasForeignKeyConstraints = initializer->hasForeignKeyConstraints(); if (QFile::exists(QStringLiteral(":dbupdate.xml"))) { DbUpdater updater(database(), QStringLiteral(":dbupdate.xml")); if (!updater.run()) { return false; } } else { qCWarning(AKONADISERVER_LOG) << "Warning: dbupdate.xml not found, skipping updates"; } if (!initializer->updateIndexesAndConstraints()) { qCCritical(AKONADISERVER_LOG) << initializer->errorMsg(); return false; } // enable caching for some tables MimeType::enableCache(true); Flag::enableCache(true); Resource::enableCache(true); Collection::enableCache(true); PartType::enableCache(true); return true; } NotificationCollector *DataStore::notificationCollector() { if (!mNotificationCollector) { mNotificationCollector = std::make_unique(m_akonadi, this); } return mNotificationCollector.get(); } DataStore *DataStore::self() { if (!sInstances.hasLocalData()) { sInstances.setLocalData(sFactory->createStore()); } return sInstances.localData(); } bool DataStore::hasDataStore() { return sInstances.hasLocalData(); } /* --- ItemFlags ----------------------------------------------------- */ bool DataStore::setItemsFlags(const PimItem::List &items, const QVector &flags, bool *flagsChanged, const Collection &col_, bool silent) { QSet removedFlags; QSet addedFlags; QVariantList insIds; QVariantList insFlags; Query::Condition delConds(Query::Or); Collection col = col_; setBoolPtr(flagsChanged, false); for (const PimItem &item : items) { const Flag::List itemFlags = item.flags(); for (const Flag &flag : itemFlags) { if (!flags.contains(flag)) { removedFlags << flag.name(); Query::Condition cond; cond.addValueCondition(PimItemFlagRelation::leftFullColumnName(), Query::Equals, item.id()); cond.addValueCondition(PimItemFlagRelation::rightFullColumnName(), Query::Equals, flag.id()); delConds.addCondition(cond); } } for (const Flag &flag : flags) { if (!itemFlags.contains(flag)) { addedFlags << flag.name(); insIds << item.id(); insFlags << flag.id(); } } if (col.id() == -1) { col.setId(item.collectionId()); } else if (col.id() != item.collectionId()) { col.setId(-2); } } if (!removedFlags.empty()) { QueryBuilder qb(PimItemFlagRelation::tableName(), QueryBuilder::Delete); qb.addCondition(delConds); if (!qb.exec()) { return false; } } if (!addedFlags.empty()) { QueryBuilder qb2(PimItemFlagRelation::tableName(), QueryBuilder::Insert); qb2.setColumnValue(PimItemFlagRelation::leftColumn(), insIds); qb2.setColumnValue(PimItemFlagRelation::rightColumn(), insFlags); qb2.setIdentificationColumn(QString()); if (!qb2.exec()) { return false; } } if (!silent && (!addedFlags.isEmpty() || !removedFlags.isEmpty())) { QSet addedFlagsBa, removedFlagsBa; for (const auto &addedFlag : qAsConst(addedFlags)) { addedFlagsBa.insert(addedFlag.toLatin1()); } for (const auto &removedFlag : qAsConst(removedFlags)) { removedFlagsBa.insert(removedFlag.toLatin1()); } notificationCollector()->itemsFlagsChanged(items, addedFlagsBa, removedFlagsBa, col); } setBoolPtr(flagsChanged, (addedFlags != removedFlags)); return true; } bool DataStore::doAppendItemsFlag(const PimItem::List &items, const Flag &flag, const QSet &existing, const Collection &col_, bool silent) { Collection col = col_; QVariantList flagIds; QVariantList appendIds; PimItem::List appendItems; for (const PimItem &item : items) { if (existing.contains(item.id())) { continue; } flagIds << flag.id(); appendIds << item.id(); appendItems << item; if (col.id() == -1) { col.setId(item.collectionId()); } else if (col.id() != item.collectionId()) { col.setId(-2); } } if (appendItems.isEmpty()) { return true; // all items have the desired flags already } QueryBuilder qb2(PimItemFlagRelation::tableName(), QueryBuilder::Insert); qb2.setColumnValue(PimItemFlagRelation::leftColumn(), appendIds); qb2.setColumnValue(PimItemFlagRelation::rightColumn(), flagIds); qb2.setIdentificationColumn(QString()); if (!qb2.exec()) { qCWarning(AKONADISERVER_LOG) << "Failed to append flag" << flag.name() << "to Items" << appendIds; return false; } if (!silent) { notificationCollector()->itemsFlagsChanged(appendItems, {flag.name().toLatin1()}, {}, col); } return true; } bool DataStore::appendItemsFlags(const PimItem::List &items, const QVector &flags, bool *flagsChanged, bool checkIfExists, const Collection &col, bool silent) { QVariantList itemsIds; itemsIds.reserve(items.count()); for (const PimItem &item : items) { itemsIds.append(item.id()); } setBoolPtr(flagsChanged, false); for (const Flag &flag : flags) { QSet existing; if (checkIfExists) { QueryBuilder qb(PimItemFlagRelation::tableName(), QueryBuilder::Select); Query::Condition cond; cond.addValueCondition(PimItemFlagRelation::rightColumn(), Query::Equals, flag.id()); cond.addValueCondition(PimItemFlagRelation::leftColumn(), Query::In, itemsIds); qb.addColumn(PimItemFlagRelation::leftColumn()); qb.addCondition(cond); if (!qb.exec()) { qCWarning(AKONADISERVER_LOG) << "Failed to retrieve existing flags for Items " << itemsIds; return false; } QSqlQuery query = qb.query(); if (query.driver()->hasFeature(QSqlDriver::QuerySize)) { //The query size feature is not supported by the sqllite driver if (query.size() == items.count()) { continue; } setBoolPtr(flagsChanged, true); } while (query.next()) { existing << query.value(0).value(); } if (!query.driver()->hasFeature(QSqlDriver::QuerySize)) { if (existing.size() != items.count()) { setBoolPtr(flagsChanged, true); } } query.finish(); } if (!doAppendItemsFlag(items, flag, existing, col, silent)) { return false; } } return true; } bool DataStore::removeItemsFlags(const PimItem::List &items, const QVector &flags, bool *flagsChanged, const Collection &col_, bool silent) { Collection col = col_; QSet removedFlags; QVariantList itemsIds; QVariantList flagsIds; setBoolPtr(flagsChanged, false); itemsIds.reserve(items.count()); for (const PimItem &item : items) { itemsIds << item.id(); if (col.id() == -1) { col.setId(item.collectionId()); } else if (col.id() != item.collectionId()) { col.setId(-2); } for (int i = 0; i < flags.count(); ++i) { const QString flagName = flags[i].name(); if (!removedFlags.contains(flagName)) { flagsIds << flags[i].id(); removedFlags << flagName; } } } // Delete all given flags from all given items in one go QueryBuilder qb(PimItemFlagRelation::tableName(), QueryBuilder::Delete); Query::Condition cond(Query::And); cond.addValueCondition(PimItemFlagRelation::rightFullColumnName(), Query::In, flagsIds); cond.addValueCondition(PimItemFlagRelation::leftFullColumnName(), Query::In, itemsIds); qb.addCondition(cond); if (!qb.exec()) { qCWarning(AKONADISERVER_LOG) << "Failed to remove flags" << flags << "from Items" << itemsIds; return false; } if (qb.query().numRowsAffected() != 0) { setBoolPtr(flagsChanged, true); if (!silent) { QSet removedFlagsBa; for (const auto &remoteFlag : qAsConst(removedFlags)) { removedFlagsBa.insert(remoteFlag.toLatin1()); } notificationCollector()->itemsFlagsChanged(items, {}, removedFlagsBa, col); } } return true; } /* --- ItemTags ----------------------------------------------------- */ bool DataStore::setItemsTags(const PimItem::List &items, const Tag::List &tags, bool *tagsChanged, bool silent) { QSet removedTags; QSet addedTags; QVariantList insIds; QVariantList insTags; Query::Condition delConds(Query::Or); setBoolPtr(tagsChanged, false); for (const PimItem &item : items) { const Tag::List itemTags = item.tags(); for (const Tag &tag : itemTags) { if (!tags.contains(tag)) { // Remove tags from items that had it set removedTags << tag.id(); Query::Condition cond; cond.addValueCondition(PimItemTagRelation::leftFullColumnName(), Query::Equals, item.id()); cond.addValueCondition(PimItemTagRelation::rightFullColumnName(), Query::Equals, tag.id()); delConds.addCondition(cond); } } for (const Tag &tag : tags) { if (!itemTags.contains(tag)) { // Add tags to items that did not have the tag addedTags << tag.id(); insIds << item.id(); insTags << tag.id(); } } } if (!removedTags.empty()) { QueryBuilder qb(PimItemTagRelation::tableName(), QueryBuilder::Delete); qb.addCondition(delConds); if (!qb.exec()) { qCWarning(AKONADISERVER_LOG) << "Failed to remove tags" << removedTags << "from Items"; return false; } } if (!addedTags.empty()) { QueryBuilder qb2(PimItemTagRelation::tableName(), QueryBuilder::Insert); qb2.setColumnValue(PimItemTagRelation::leftColumn(), insIds); qb2.setColumnValue(PimItemTagRelation::rightColumn(), insTags); qb2.setIdentificationColumn(QString()); if (!qb2.exec()) { qCWarning(AKONADISERVER_LOG) << "Failed to add tags" << addedTags << "to Items"; return false; } } if (!silent && (!addedTags.empty() || !removedTags.empty())) { notificationCollector()->itemsTagsChanged(items, addedTags, removedTags); } setBoolPtr(tagsChanged, (addedTags != removedTags)); return true; } bool DataStore::doAppendItemsTag(const PimItem::List &items, const Tag &tag, const QSet &existing, const Collection &col, bool silent) { QVariantList tagIds; QVariantList appendIds; PimItem::List appendItems; for (const PimItem &item : items) { if (existing.contains(item.id())) { continue; } tagIds << tag.id(); appendIds << item.id(); appendItems << item; } if (appendItems.isEmpty()) { return true; // all items have the desired tags already } QueryBuilder qb2(PimItemTagRelation::tableName(), QueryBuilder::Insert); qb2.setColumnValue(PimItemTagRelation::leftColumn(), appendIds); qb2.setColumnValue(PimItemTagRelation::rightColumn(), tagIds); qb2.setIdentificationColumn(QString()); if (!qb2.exec()) { qCWarning(AKONADISERVER_LOG) << "Failed to append tag" << tag << "to Items" << appendItems; return false; } if (!silent) { notificationCollector()->itemsTagsChanged(appendItems, {tag.id()}, {}, col); } return true; } bool DataStore::appendItemsTags(const PimItem::List &items, const Tag::List &tags, bool *tagsChanged, bool checkIfExists, const Collection &col, bool silent) { QVariantList itemsIds; itemsIds.reserve(items.count()); for (const PimItem &item : items) { itemsIds.append(item.id()); } setBoolPtr(tagsChanged, false); for (const Tag &tag : tags) { QSet existing; if (checkIfExists) { QueryBuilder qb(PimItemTagRelation::tableName(), QueryBuilder::Select); Query::Condition cond; cond.addValueCondition(PimItemTagRelation::rightColumn(), Query::Equals, tag.id()); cond.addValueCondition(PimItemTagRelation::leftColumn(), Query::In, itemsIds); qb.addColumn(PimItemTagRelation::leftColumn()); qb.addCondition(cond); if (!qb.exec()) { qCWarning(AKONADISERVER_LOG) << "Failed to retrieve existing tag" << tag << "for Items" << itemsIds; return false; } QSqlQuery query = qb.query(); if (query.driver()->hasFeature(QSqlDriver::QuerySize)) { if (query.size() == items.count()) { continue; } setBoolPtr(tagsChanged, true); } while (query.next()) { existing << query.value(0).value(); } if (!query.driver()->hasFeature(QSqlDriver::QuerySize)) { if (existing.size() != items.count()) { setBoolPtr(tagsChanged, true); } } query.finish(); } if (!doAppendItemsTag(items, tag, existing, col, silent)) { return false; } } return true; } bool DataStore::removeItemsTags(const PimItem::List &items, const Tag::List &tags, bool *tagsChanged, bool silent) { QSet removedTags; QVariantList itemsIds; QVariantList tagsIds; setBoolPtr(tagsChanged, false); itemsIds.reserve(items.count()); for (const PimItem &item : items) { itemsIds << item.id(); for (int i = 0; i < tags.count(); ++i) { const qint64 tagId = tags[i].id(); if (!removedTags.contains(tagId)) { tagsIds << tagId; removedTags << tagId; } } } // Delete all given tags from all given items in one go QueryBuilder qb(PimItemTagRelation::tableName(), QueryBuilder::Delete); Query::Condition cond(Query::And); cond.addValueCondition(PimItemTagRelation::rightFullColumnName(), Query::In, tagsIds); cond.addValueCondition(PimItemTagRelation::leftFullColumnName(), Query::In, itemsIds); qb.addCondition(cond); if (!qb.exec()) { qCWarning(AKONADISERVER_LOG) << "Failed to remove tags" << tagsIds << "from Items" << itemsIds; return false; } if (qb.query().numRowsAffected() != 0) { setBoolPtr(tagsChanged, true); if (!silent) { notificationCollector()->itemsTagsChanged(items, QSet(), removedTags); } } return true; } bool DataStore::removeTags(const Tag::List &tags, bool silent) { // Currently the "silent" argument is only for API symmetry Q_UNUSED(silent); QVariantList removedTagsIds; QSet removedTags; removedTagsIds.reserve(tags.count()); removedTags.reserve(tags.count()); for (const Tag &tag : tags) { removedTagsIds << tag.id(); removedTags << tag.id(); } // Get all PIM items that we will untag SelectQueryBuilder itemsQuery; itemsQuery.addJoin(QueryBuilder::LeftJoin, PimItemTagRelation::tableName(), PimItemTagRelation::leftFullColumnName(), PimItem::idFullColumnName()); itemsQuery.addValueCondition(PimItemTagRelation::rightFullColumnName(), Query::In, removedTagsIds); if (!itemsQuery.exec()) { qCWarning(AKONADISERVER_LOG) << "Removing tags failed: failed to query Items for given tags" << removedTagsIds; return false; } const PimItem::List items = itemsQuery.result(); if (!items.isEmpty()) { notificationCollector()->itemsTagsChanged(items, QSet(), removedTags); } for (const Tag &tag : tags) { // Emit special tagRemoved notification for each resource that owns the tag QueryBuilder qb(TagRemoteIdResourceRelation::tableName(), QueryBuilder::Select); qb.addColumn(TagRemoteIdResourceRelation::remoteIdFullColumnName()); qb.addJoin(QueryBuilder::InnerJoin, Resource::tableName(), TagRemoteIdResourceRelation::resourceIdFullColumnName(), Resource::idFullColumnName()); qb.addColumn(Resource::nameFullColumnName()); qb.addValueCondition(TagRemoteIdResourceRelation::tagIdFullColumnName(), Query::Equals, tag.id()); if (!qb.exec()) { qCWarning(AKONADISERVER_LOG) << "Removing tags failed: failed to retrieve RIDs for tag" << tag.id(); return false; } // Emit specialized notifications for each resource QSqlQuery query = qb.query(); while (query.next()) { const QString rid = query.value(0).toString(); const QByteArray resource = query.value(1).toByteArray(); notificationCollector()->tagRemoved(tag, resource, rid); } query.finish(); // And one for clients - without RID notificationCollector()->tagRemoved(tag, QByteArray(), QString()); } // Just remove the tags, table constraints will take care of the rest QueryBuilder qb(Tag::tableName(), QueryBuilder::Delete); qb.addValueCondition(Tag::idColumn(), Query::In, removedTagsIds); if (!qb.exec()) { qCWarning(AKONADISERVER_LOG) << "Failed to remove tags" << removedTagsIds; return false; } return true; } /* --- ItemParts ----------------------------------------------------- */ bool DataStore::removeItemParts(const PimItem &item, const QSet &parts) { SelectQueryBuilder qb; qb.addJoin(QueryBuilder::InnerJoin, PartType::tableName(), Part::partTypeIdFullColumnName(), PartType::idFullColumnName()); qb.addValueCondition(Part::pimItemIdFullColumnName(), Query::Equals, item.id()); qb.addCondition(PartTypeHelper::conditionFromFqNames(parts)); if (!qb.exec()) { qCWarning(AKONADISERVER_LOG) << "Removing item parts failed: failed to query parts" << parts << "from Item " << item.id(); return false; } const Part::List existingParts = qb.result(); for (Part part : qAsConst(existingParts)) { //krazy:exclude=foreach if (!PartHelper::remove(&part)) { qCWarning(AKONADISERVER_LOG) << "Failed to remove part" << part.id() << "(" << part.partType().ns() << ":" << part.partType().name() << ") from Item" << item.id(); return false; } } notificationCollector()->itemChanged(item, parts); return true; } bool DataStore::invalidateItemCache(const PimItem &item) { // find all payload item parts SelectQueryBuilder qb; qb.addJoin(QueryBuilder::InnerJoin, PimItem::tableName(), PimItem::idFullColumnName(), Part::pimItemIdFullColumnName()); qb.addJoin(QueryBuilder::InnerJoin, PartType::tableName(), Part::partTypeIdFullColumnName(), PartType::idFullColumnName()); qb.addValueCondition(Part::pimItemIdFullColumnName(), Query::Equals, item.id()); qb.addValueCondition(Part::dataFullColumnName(), Query::IsNot, QVariant()); qb.addValueCondition(PartType::nsFullColumnName(), Query::Equals, QLatin1String("PLD")); qb.addValueCondition(PimItem::dirtyFullColumnName(), Query::Equals, false); if (!qb.exec()) { qCWarning(AKONADISERVER_LOG) << "Failed to invalidate cache for Item" << item.id(); return false; } const Part::List parts = qb.result(); // clear data field for (Part part : parts) { if (!PartHelper::truncate(part)) { qCWarning(AKONADISERVER_LOG) << "Failed to truncate payload part" << part.id() << "(" << part.partType().ns() << ":" << part.partType().name() << ") of Item" << item.id(); return false; } } return true; } /* --- Collection ------------------------------------------------------ */ bool DataStore::appendCollection(Collection &collection, const QStringList &mimeTypes, const QMap &attributes) { // no need to check for already existing collection with the same name, // a unique index on parent + name prevents that in the database if (!collection.insert()) { qCWarning(AKONADISERVER_LOG) << "Failed to append Collection" << collection.name() << "in resource" << collection.resource().name(); return false; } if (!appendMimeTypeForCollection(collection.id(), mimeTypes)) { qCWarning(AKONADISERVER_LOG) << "Failed to append mimetypes" << mimeTypes << "to new collection" << collection.name() << "(ID" << collection.id() << ") in resource" << collection.resource().name(); return false; } for (auto it = attributes.cbegin(), end = attributes.cend(); it != end; ++it) { if (!addCollectionAttribute(collection, it.key(), it.value(), true)) { qCWarning(AKONADISERVER_LOG) << "Failed to append attribute" << it.key() << "to new collection" << collection.name() << "(ID" << collection.id() << ") in resource" << collection.resource().name(); return false; } } notificationCollector()->collectionAdded(collection); return true; } bool DataStore::cleanupCollection(Collection &collection) { if (!s_hasForeignKeyConstraints) { return cleanupCollection_slow(collection); } // db will do most of the work for us, we just deal with notifications and external payload parts here Q_ASSERT(s_hasForeignKeyConstraints); // collect item deletion notifications const PimItem::List items = collection.items(); const QByteArray resource = collection.resource().name().toLatin1(); // generate the notification before actually removing the data // TODO: we should try to get rid of this, requires client side changes to resources and Monitor though notificationCollector()->itemsRemoved(items, collection, resource); // remove all external payload parts QueryBuilder qb(Part::tableName(), QueryBuilder::Select); qb.addColumn(Part::dataFullColumnName()); qb.addJoin(QueryBuilder::InnerJoin, PimItem::tableName(), Part::pimItemIdFullColumnName(), PimItem::idFullColumnName()); qb.addJoin(QueryBuilder::InnerJoin, Collection::tableName(), PimItem::collectionIdFullColumnName(), Collection::idFullColumnName()); qb.addValueCondition(Collection::idFullColumnName(), Query::Equals, collection.id()); qb.addValueCondition(Part::storageFullColumnName(), Query::Equals, Part::External); qb.addValueCondition(Part::dataFullColumnName(), Query::IsNot, QVariant()); if (!qb.exec()) { qCWarning(AKONADISERVER_LOG) << "Failed to cleanup collection" << collection.name() << "(ID" << collection.id() << "):" << "Failed to query existing payload parts"; return false; } try { while (qb.query().next()) { ExternalPartStorage::self()->removePartFile( ExternalPartStorage::resolveAbsolutePath(qb.query().value(0).toByteArray())); } } catch (const PartHelperException &e) { qb.query().finish(); qCWarning(AKONADISERVER_LOG) << "PartHelperException while cleaning up collection" << collection.name() << "(ID" << collection.id() << "):" << e.what(); return false; } qb.query().finish(); // delete the collection itself, referential actions will do the rest notificationCollector()->collectionRemoved(collection); return collection.remove(); } bool DataStore::cleanupCollection_slow(Collection &collection) { Q_ASSERT(!s_hasForeignKeyConstraints); // delete the content const PimItem::List items = collection.items(); const QByteArray resource = collection.resource().name().toLatin1(); notificationCollector()->itemsRemoved(items, collection, resource); for (const PimItem &item : items) { if (!item.clearFlags()) { // TODO: move out of loop and use only a single query qCWarning(AKONADISERVER_LOG) << "Slow cleanup of collection" << collection.name() << "(ID" << collection.id() <<")" << "failed: error clearing items flags"; return false; } if (!PartHelper::remove(Part::pimItemIdColumn(), item.id())) { // TODO: reduce to single query qCWarning(AKONADISERVER_LOG) << "Slow cleanup of collection" << collection.name() << "(ID" << collection.id() <<")" << "failed: error clearing item payload parts"; return false; } if (!PimItem::remove(PimItem::idColumn(), item.id())) { // TODO: move into single querya qCWarning(AKONADISERVER_LOG) << "Slow cleanup of collection" << collection.name() << "(ID" << collection.id() <<")" << "failed: error clearing items"; return false; } if (!Entity::clearRelation(item.id(), Entity::Right)) { // TODO: move into single query qCWarning(AKONADISERVER_LOG) << "Slow cleanup of collection" << collection.name() << "(ID" << collection.id() <<")" << "failed: error clearing linked items"; return false; } } // delete collection mimetypes collection.clearMimeTypes(); Collection::clearPimItems(collection.id()); // delete attributes Q_FOREACH (CollectionAttribute attr, collection.attributes()) { //krazy:exclude=foreach if (!attr.remove()) { qCWarning(AKONADISERVER_LOG) << "Slow cleanup of collection" << collection.name() << "(ID" << collection.id() << ")" << "failed: error clearing attribute" << attr.type(); return false; } } // delete the collection itself notificationCollector()->collectionRemoved(collection); return collection.remove(); } static bool recursiveSetResourceId(const Collection &collection, qint64 resourceId) { Transaction transaction(DataStore::self(), QStringLiteral("RECURSIVE SET RESOURCEID")); QueryBuilder qb(Collection::tableName(), QueryBuilder::Update); qb.addValueCondition(Collection::parentIdColumn(), Query::Equals, collection.id()); qb.setColumnValue(Collection::resourceIdColumn(), resourceId); qb.setColumnValue(Collection::remoteIdColumn(), QVariant()); qb.setColumnValue(Collection::remoteRevisionColumn(), QVariant()); if (!qb.exec()) { qCWarning(AKONADISERVER_LOG) << "Failed to set resource ID" << resourceId << "to collection" << collection.name() << "(ID" << collection.id() << ")"; return false; } // this is a cross-resource move, so also reset any resource-specific data (RID, RREV, etc) // as well as mark the items dirty to prevent cache purging before they have been written back qb = QueryBuilder(PimItem::tableName(), QueryBuilder::Update); qb.addValueCondition(PimItem::collectionIdColumn(), Query::Equals, collection.id()); qb.setColumnValue(PimItem::remoteIdColumn(), QVariant()); qb.setColumnValue(PimItem::remoteRevisionColumn(), QVariant()); const QDateTime now = QDateTime::currentDateTimeUtc(); qb.setColumnValue(PimItem::datetimeColumn(), now); qb.setColumnValue(PimItem::atimeColumn(), now); qb.setColumnValue(PimItem::dirtyColumn(), true); if (!qb.exec()) { qCWarning(AKONADISERVER_LOG) << "Failed reset RID/RREV for PimItems in Collection" << collection.name() << "(ID" << collection.id() << ")"; return false; } transaction.commit(); Q_FOREACH (const Collection &col, collection.children()) { if (!recursiveSetResourceId(col, resourceId)) { return false; } } return true; } bool DataStore::moveCollection(Collection &collection, const Collection &newParent) { if (collection.parentId() == newParent.id()) { return true; } if (!m_dbOpened) { return false; } if (!newParent.isValid()) { qCWarning(AKONADISERVER_LOG) << "Failed to move collection" << collection.name() << "(ID" << collection.id() << "): invalid destination"; return false; } const QByteArray oldResource = collection.resource().name().toLatin1(); int resourceId = collection.resourceId(); const Collection source = collection.parent(); if (newParent.id() > 0) { // not root resourceId = newParent.resourceId(); } if (!CollectionQueryHelper::canBeMovedTo(collection, newParent)) { return false; } collection.setParentId(newParent.id()); if (collection.resourceId() != resourceId) { collection.setResourceId(resourceId); collection.setRemoteId(QString()); collection.setRemoteRevision(QString()); if (!recursiveSetResourceId(collection, resourceId)) { return false; } } if (!collection.update()) { qCWarning(AKONADISERVER_LOG) << "Failed to move Collection" << collection.name() << "(ID" << collection.id() << ")" << "into Collection" << collection.name() << "(ID" << collection.id() << ")"; return false; } notificationCollector()->collectionMoved(collection, source, oldResource, newParent.resource().name().toLatin1()); return true; } bool DataStore::appendMimeTypeForCollection(qint64 collectionId, const QStringList &mimeTypes) { if (mimeTypes.isEmpty()) { return true; } for (const QString &mimeType : mimeTypes) { const auto &mt = MimeType::retrieveByNameOrCreate(mimeType); if (!mt.isValid()) { return false; } if (!Collection::addMimeType(collectionId, mt.id())) { qCWarning(AKONADISERVER_LOG) << "Failed to append mimetype" << mt.name() << "to Collection" << collectionId; return false; } } return true; } void DataStore::activeCachePolicy(Collection &col) { if (!col.cachePolicyInherit()) { return; } Collection parent = col; while (parent.parentId() != 0) { parent = parent.parent(); if (!parent.cachePolicyInherit()) { col.setCachePolicyCheckInterval(parent.cachePolicyCheckInterval()); col.setCachePolicyCacheTimeout(parent.cachePolicyCacheTimeout()); col.setCachePolicySyncOnDemand(parent.cachePolicySyncOnDemand()); col.setCachePolicyLocalParts(parent.cachePolicyLocalParts()); return; } } // ### system default col.setCachePolicyCheckInterval(-1); col.setCachePolicyCacheTimeout(-1); col.setCachePolicySyncOnDemand(false); col.setCachePolicyLocalParts(QStringLiteral("ALL")); } QVector DataStore::virtualCollections(const PimItem &item) { SelectQueryBuilder qb; qb.addJoin(QueryBuilder::InnerJoin, Collection::tableName(), Collection::idFullColumnName(), CollectionPimItemRelation::leftFullColumnName()); qb.addValueCondition(CollectionPimItemRelation::rightFullColumnName(), Query::Equals, item.id()); if (!qb.exec()) { qCWarning(AKONADISERVER_LOG) << "Failed to query virtual collections which PimItem" << item.id() << "belongs into"; return QVector(); } return qb.result(); } QMap > DataStore::virtualCollections(const PimItem::List &items) { QueryBuilder qb(CollectionPimItemRelation::tableName(), QueryBuilder::Select); qb.addJoin(QueryBuilder::InnerJoin, Collection::tableName(), Collection::idFullColumnName(), CollectionPimItemRelation::leftFullColumnName()); qb.addJoin(QueryBuilder::InnerJoin, PimItem::tableName(), PimItem::idFullColumnName(), CollectionPimItemRelation::rightFullColumnName()); qb.addColumn(Collection::idFullColumnName()); qb.addColumns(QStringList() << PimItem::idFullColumnName() << PimItem::remoteIdFullColumnName() << PimItem::remoteRevisionFullColumnName() << PimItem::mimeTypeIdFullColumnName()); qb.addSortColumn(Collection::idFullColumnName(), Query::Ascending); if (items.count() == 1) { qb.addValueCondition(CollectionPimItemRelation::rightFullColumnName(), Query::Equals, items.first().id()); } else { QVariantList ids; ids.reserve(items.count()); for (const PimItem &item : items) { ids << item.id(); } qb.addValueCondition(CollectionPimItemRelation::rightFullColumnName(), Query::In, ids); } if (!qb.exec()) { qCWarning(AKONADISERVER_LOG) << "Failed to query virtual Collections which PimItems" << items << "belong into"; return QMap >(); } QSqlQuery query = qb.query(); QMap > map; query.next(); while (query.isValid()) { const qlonglong collectionId = query.value(0).toLongLong(); QList &pimItems = map[collectionId]; do { PimItem item; item.setId(query.value(1).toLongLong()); item.setRemoteId(query.value(2).toString()); item.setRemoteRevision(query.value(3).toString()); item.setMimeTypeId(query.value(4).toLongLong()); pimItems << item; } while (query.next() && query.value(0).toLongLong() == collectionId); } query.finish(); return map; } /* --- PimItem ------------------------------------------------------- */ bool DataStore::appendPimItem(QVector &parts, const QVector &flags, const MimeType &mimetype, const Collection &collection, const QDateTime &dateTime, const QString &remote_id, const QString &remoteRevision, const QString &gid, PimItem &pimItem) { pimItem.setMimeTypeId(mimetype.id()); pimItem.setCollectionId(collection.id()); if (dateTime.isValid()) { pimItem.setDatetime(dateTime); } if (remote_id.isEmpty()) { // from application pimItem.setDirty(true); } else { // from resource pimItem.setRemoteId(remote_id); pimItem.setDirty(false); } pimItem.setRemoteRevision(remoteRevision); pimItem.setGid(gid); pimItem.setAtime(QDateTime::currentDateTimeUtc()); if (!pimItem.insert()) { qCWarning(AKONADISERVER_LOG) << "Failed to append new PimItem into Collection" << collection.name() << "(ID" << collection.id() << ")"; return false; } // insert every part if (!parts.isEmpty()) { //don't use foreach, the caller depends on knowing the part has changed, see the Append handler for (QVector::iterator it = parts.begin(); it != parts.end(); ++it) { (*it).setPimItemId(pimItem.id()); if ((*it).datasize() < (*it).data().size()) { (*it).setDatasize((*it).data().size()); } // qCDebug(AKONADISERVER_LOG) << "Insert from DataStore::appendPimItem"; if (!PartHelper::insert(&(*it))) { qCWarning(AKONADISERVER_LOG) << "Failed to add part" << it->partType().name() << "to new PimItem" << pimItem.id(); return false; } } } bool seen = false; for (const Flag &flag : flags) { seen |= (flag.name() == QLatin1String(AKONADI_FLAG_SEEN) || flag.name() == QLatin1String(AKONADI_FLAG_IGNORED)); if (!pimItem.addFlag(flag)) { qCWarning(AKONADISERVER_LOG) << "Failed to add flag" << flag.name() << "to new PimItem" << pimItem.id(); return false; } } // qCDebug(AKONADISERVER_LOG) << "appendPimItem: " << pimItem; notificationCollector()->itemAdded(pimItem, seen, collection); return true; } bool DataStore::unhidePimItem(PimItem &pimItem) { if (!m_dbOpened) { return false; } qCDebug(AKONADISERVER_LOG) << "DataStore::unhidePimItem(" << pimItem << ")"; // FIXME: This is inefficient. Using a bit on the PimItemTable record would probably be some orders of magnitude faster... return removeItemParts(pimItem, { AKONADI_ATTRIBUTE_HIDDEN }); } bool DataStore::unhideAllPimItems() { if (!m_dbOpened) { return false; } qCDebug(AKONADISERVER_LOG) << "DataStore::unhideAllPimItems()"; try { return PartHelper::remove(Part::partTypeIdFullColumnName(), PartTypeHelper::fromFqName(QStringLiteral("ATR"), QStringLiteral("HIDDEN")).id()); } catch (...) { } // we can live with this failing return false; } bool DataStore::cleanupPimItems(const PimItem::List &items, bool silent) { // generate relation removed notifications if (!silent) { for (const PimItem &item : items) { SelectQueryBuilder relationQuery; relationQuery.addValueCondition(Relation::leftIdFullColumnName(), Query::Equals, item.id()); relationQuery.addValueCondition(Relation::rightIdFullColumnName(), Query::Equals, item.id()); relationQuery.setSubQueryMode(Query::Or); if (!relationQuery.exec()) { throw HandlerException("Failed to obtain relations"); } const Relation::List relations = relationQuery.result(); for (const Relation &relation : relations) { notificationCollector()->relationRemoved(relation); } } // generate the notification before actually removing the data notificationCollector()->itemsRemoved(items); } // FIXME: Create a single query to do this for (const auto &item : items) { if (!item.clearFlags()) { qCWarning(AKONADISERVER_LOG) << "Failed to clean up flags from PimItem" << item.id(); return false; } if (!PartHelper::remove(Part::pimItemIdColumn(), item.id())) { qCWarning(AKONADISERVER_LOG) << "Failed to clean up parts from PimItem" << item.id(); return false; } if (!PimItem::remove(PimItem::idColumn(), item.id())) { qCWarning(AKONADISERVER_LOG) << "Failed to remove PimItem" << item.id(); return false; } if (!Entity::clearRelation(item.id(), Entity::Right)) { qCWarning(AKONADISERVER_LOG) << "Failed to remove PimItem" << item.id() << "from linked collections"; return false; } } return true; } bool DataStore::addCollectionAttribute(const Collection &col, const QByteArray &key, const QByteArray &value, bool silent) { SelectQueryBuilder qb; qb.addValueCondition(CollectionAttribute::collectionIdColumn(), Query::Equals, col.id()); qb.addValueCondition(CollectionAttribute::typeColumn(), Query::Equals, key); if (!qb.exec()) { qCWarning(AKONADISERVER_LOG) << "Failed to append attribute" << key << "to Collection" << col.name() << "(ID" << col.id() << "): Failed to query existing attribute"; return false; } if (!qb.result().isEmpty()) { qCWarning(AKONADISERVER_LOG) << "Failed to append attribute" << key << "to Collection" << col.name() << "(ID" << col.id() << "): Attribute already exists"; return false; } CollectionAttribute attr; attr.setCollectionId(col.id()); attr.setType(key); attr.setValue(value); if (!attr.insert()) { qCWarning(AKONADISERVER_LOG) << "Failed to append attribute" << key << "to Collection" << col.name() << "(ID" << col.id() << ")"; return false; } if (!silent) { notificationCollector()->collectionChanged(col, QList() << key); } return true; } bool DataStore::removeCollectionAttribute(const Collection &col, const QByteArray &key) { SelectQueryBuilder qb; qb.addValueCondition(CollectionAttribute::collectionIdColumn(), Query::Equals, col.id()); qb.addValueCondition(CollectionAttribute::typeColumn(), Query::Equals, key); if (!qb.exec()) { throw HandlerException("Unable to query for collection attribute"); } const QVector result = qb.result(); for (CollectionAttribute attr : result) { if (!attr.remove()) { throw HandlerException("Unable to remove collection attribute"); } } if (!result.isEmpty()) { notificationCollector()->collectionChanged(col, QList() << key); return true; } return false; } void DataStore::debugLastDbError(const char *actionDescription) const { qCCritical(AKONADISERVER_LOG) << "Database error:" << actionDescription; qCCritical(AKONADISERVER_LOG) << " Last driver error:" << m_database.lastError().driverText(); qCCritical(AKONADISERVER_LOG) << " Last database error:" << m_database.lastError().databaseText(); m_akonadi.tracer().error("DataStore (Database Error)", QStringLiteral("%1\nDriver said: %2\nDatabase said:%3") .arg(QString::fromLatin1(actionDescription), m_database.lastError().driverText(), m_database.lastError().databaseText())); } void DataStore::debugLastQueryError(const QSqlQuery &query, const char *actionDescription) const { qCCritical(AKONADISERVER_LOG) << "Query error:" << actionDescription; qCCritical(AKONADISERVER_LOG) << " Last error message:" << query.lastError().text(); qCCritical(AKONADISERVER_LOG) << " Last driver error:" << m_database.lastError().driverText(); qCCritical(AKONADISERVER_LOG) << " Last database error:" << m_database.lastError().databaseText(); m_akonadi.tracer().error("DataStore (Database Query Error)", QStringLiteral("%1: %2") .arg(QString::fromLatin1(actionDescription), query.lastError().text())); } // static QString DataStore::dateTimeFromQDateTime(const QDateTime &dateTime) { QDateTime utcDateTime = dateTime; if (utcDateTime.timeSpec() != Qt::UTC) { utcDateTime.toUTC(); } return utcDateTime.toString(QStringLiteral("yyyy-MM-dd hh:mm:ss")); } // static QDateTime DataStore::dateTimeToQDateTime(const QByteArray &dateTime) { return QDateTime::fromString(QString::fromLatin1(dateTime), QStringLiteral("yyyy-MM-dd hh:mm:ss")); } bool DataStore::doRollback() { QSqlDriver *driver = m_database.driver(); QElapsedTimer timer; timer.start(); driver->rollbackTransaction(); StorageDebugger::instance()->removeTransaction(reinterpret_cast(this), false, timer.elapsed(), m_database.lastError().text()); if (m_database.lastError().isValid()) { TRANSACTION_MUTEX_UNLOCK; debugLastDbError("DataStore::rollbackTransaction"); return false; } TRANSACTION_MUTEX_UNLOCK; return true; } void DataStore::transactionKilledByDB() { m_transactionKilledByDB = true; cleanupAfterRollback(); Q_EMIT transactionRolledBack(); } bool DataStore::beginTransaction(const QString &name) { if (!m_dbOpened) { return false; } if (m_transactionLevel == 0 || m_transactionKilledByDB) { m_transactionKilledByDB = false; QElapsedTimer timer; timer.start(); TRANSACTION_MUTEX_LOCK; if (DbType::type(m_database) == DbType::Sqlite) { m_database.exec(QStringLiteral("BEGIN IMMEDIATE TRANSACTION")); StorageDebugger::instance()->addTransaction(reinterpret_cast(this), name, timer.elapsed(), m_database.lastError().text()); if (m_database.lastError().isValid()) { debugLastDbError("DataStore::beginTransaction (SQLITE)"); TRANSACTION_MUTEX_UNLOCK; return false; } } else { m_database.driver()->beginTransaction(); StorageDebugger::instance()->addTransaction(reinterpret_cast(this), name, timer.elapsed(), m_database.lastError().text()); if (m_database.lastError().isValid()) { debugLastDbError("DataStore::beginTransaction"); TRANSACTION_MUTEX_UNLOCK; return false; } } if (DbType::type(m_database) == DbType::PostgreSQL) { // Make constraints check deferred in PostgreSQL. Allows for // INSERT INTO mimetypetable (name) VALUES ('foo') RETURNING id; // INSERT INTO collectionmimetyperelation (collection_id, mimetype_id) VALUES (x, y) // where "y" refers to the newly inserted mimetype m_database.exec(QStringLiteral("SET CONSTRAINTS ALL DEFERRED")); } } ++m_transactionLevel; return true; } bool DataStore::rollbackTransaction() { if (!m_dbOpened) { return false; } if (m_transactionLevel == 0) { qCWarning(AKONADISERVER_LOG) << "DataStore::rollbackTransaction(): No transaction in progress!"; return false; } --m_transactionLevel; if (m_transactionLevel == 0 && !m_transactionKilledByDB) { doRollback(); cleanupAfterRollback(); Q_EMIT transactionRolledBack(); } return true; } bool DataStore::commitTransaction() { if (!m_dbOpened) { return false; } if (m_transactionLevel == 0) { qCWarning(AKONADISERVER_LOG) << "DataStore::commitTransaction(): No transaction in progress!"; return false; } if (m_transactionLevel == 1) { if (m_transactionKilledByDB) { qCWarning(AKONADISERVER_LOG) << "DataStore::commitTransaction(): Cannot commit, transaction was killed by mysql deadlock handling!"; return false; } QSqlDriver *driver = m_database.driver(); QElapsedTimer timer; timer.start(); driver->commitTransaction(); StorageDebugger::instance()->removeTransaction(reinterpret_cast(this), true, timer.elapsed(), m_database.lastError().text()); if (m_database.lastError().isValid()) { debugLastDbError("DataStore::commitTransaction"); rollbackTransaction(); return false; } else { TRANSACTION_MUTEX_UNLOCK; m_transactionLevel--; Q_EMIT transactionCommitted(); } } else { m_transactionLevel--; } return true; } bool DataStore::inTransaction() const { return m_transactionLevel > 0; } void DataStore::sendKeepAliveQuery() { if (m_database.isOpen()) { QSqlQuery query(m_database); query.exec(QStringLiteral("SELECT 1")); } } void DataStore::cleanupAfterRollback() { MimeType::invalidateCompleteCache(); Flag::invalidateCompleteCache(); Resource::invalidateCompleteCache(); Collection::invalidateCompleteCache(); PartType::invalidateCompleteCache(); m_akonadi.collectionStatistics().expireCache(); QueryCache::clear(); } diff --git a/src/server/storage/datastore.h b/src/server/storage/datastore.h index 4bfbceaa4..53a2cf188 100644 --- a/src/server/storage/datastore.h +++ b/src/server/storage/datastore.h @@ -1,376 +1,380 @@ /*************************************************************************** * Copyright (C) 2006 by Andreas Gungl * * * * 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. * ***************************************************************************/ #ifndef DATASTORE_H #define DATASTORE_H #include #include #include #include #include +#include class QSqlQuery; class QTimer; #include "entities.h" #include "notificationcollector.h" #include namespace Akonadi { namespace Server { class DataStore; class DataStoreFactory { public: - explicit DataStoreFactory() = default; - virtual ~DataStoreFactory() = default; virtual DataStore *createStore() = 0; +protected: + explicit DataStoreFactory() = default; private: Q_DISABLE_COPY_MOVE(DataStoreFactory) }; class NotificationCollector; /** This class handles all the database access.

Database configuration

You can select between various database backends during runtime using the @c $HOME/.config/akonadi/akonadiserverrc configuration file. Example: @verbatim [%General] Driver=QMYSQL [QMYSQL_EMBEDDED] Name=akonadi Options=SERVER_DATADIR=/home/foo/.local/share/akonadi/db_data [QMYSQL] Name=akonadi Host=localhost User=foo Password=***** #Options=UNIX_SOCKET=/home/foo/.local/share/akonadi/socket-bar/mysql.socket StartServer=true ServerPath=/usr/sbin/mysqld [QSQLITE] Name=/home/foo/.local/share/akonadi/akonadi.db @endverbatim Use @c General/Driver to select the QSql driver to use for database access. The following drivers are currently supported, other might work but are untested: - QMYSQL - QMYSQL_EMBEDDED - QSQLITE The options for each driver are read from the corresponding group. The following options are supported, dependent on the driver not all of them might have an effect: - Name: Database name, for sqlite that's the file name of the database. - Host: Hostname of the database server - User: Username for the database server - Password: Password for the database server - Options: Additional options, format is driver-dependent - StartServer: Start the database locally just for Akonadi instead of using an existing one - ServerPath: Path to the server executable */ class DataStore : public QObject { Q_OBJECT public: const constexpr static bool Silent = true; static void setFactory(std::unique_ptr factory); /** Closes the database connection and destroys the DataStore object. */ ~DataStore() override; /** Opens the database connection. */ virtual void open(); /** Closes the database connection. */ void close(); /** Initializes the database. Should be called during startup by the main thread. */ virtual bool init(); /** Per thread singleton. */ static DataStore *self(); /** * Returns whether per thread DataStore has been created. */ static bool hasDataStore(); /* --- ItemFlags ----------------------------------------------------- */ virtual bool setItemsFlags(const PimItem::List &items, const QVector &flags, bool *flagsChanged = nullptr, const Collection &col = Collection(), bool silent = false); virtual bool appendItemsFlags(const PimItem::List &items, const QVector &flags, bool *flagsChanged = nullptr, bool checkIfExists = true, const Collection &col = Collection(), bool silent = false); virtual bool removeItemsFlags(const PimItem::List &items, const QVector &flags, bool *tagsChanged = nullptr, const Collection &collection = Collection(), bool silent = false); /* --- ItemTags ----------------------------------------------------- */ virtual bool setItemsTags(const PimItem::List &items, const Tag::List &tags, bool *tagsChanged = nullptr, bool silent = false); virtual bool appendItemsTags(const PimItem::List &items, const Tag::List &tags, bool *tagsChanged = nullptr, bool checkIfExists = true, const Collection &col = Collection(), bool silent = false); virtual bool removeItemsTags(const PimItem::List &items, const Tag::List &tags, bool *tagsChanged = nullptr, bool silent = false); virtual bool removeTags(const Tag::List &tags, bool silent = false); /* --- ItemParts ----------------------------------------------------- */ virtual bool removeItemParts(const PimItem &item, const QSet &parts); // removes all payload parts for this item. virtual bool invalidateItemCache(const PimItem &item); /* --- Collection ------------------------------------------------------ */ virtual bool appendCollection(Collection &collection, const QStringList &mimeTypes, const QMap &attributes); /// removes the given collection and all its content virtual bool cleanupCollection(Collection &collection); /// same as the above but for database backends without working referential actions on foreign keys virtual bool cleanupCollection_slow(Collection &collection); /// moves the collection @p collection to @p newParent. virtual bool moveCollection(Collection &collection, const Collection &newParent); virtual bool appendMimeTypeForCollection(qint64 collectionId, const QStringList &mimeTypes); static QString collectionDelimiter() { return QStringLiteral("/"); } /** Determines the active cache policy for this Collection. The active cache policy is set in the corresponding Collection fields. */ virtual void activeCachePolicy(Collection &col); /// Returns all virtual collections the @p item is linked to QVector virtualCollections(const PimItem &item); QMap< Server::Entity::Id, QList< PimItem > > virtualCollections(const Akonadi::Server::PimItem::List &items); /* --- PimItem ------------------------------------------------------- */ virtual bool appendPimItem(QVector &parts, const QVector &flags, const MimeType &mimetype, const Collection &collection, const QDateTime &dateTime, const QString &remote_id, const QString &remoteRevision, const QString &gid, PimItem &pimItem); /** * Removes the pim item and all referenced data ( e.g. flags ) */ virtual bool cleanupPimItems(const PimItem::List &items, bool silent = false); /** * Unhides the specified PimItem. Emits the itemAdded() notification as * the hidden flag is assumed to have been set by appendPimItem() before * pushing the item to the preprocessor chain. The hidden item had his * notifications disabled until now (so for the clients the "unhide" operation * is actually a new item arrival). * * This function does NOT verify if the item was *really* hidden: this is * responsibility of the caller. */ virtual bool unhidePimItem(PimItem &pimItem); /** * Unhides all the items which have the "hidden" flag set. * This function doesn't emit any notification about the items * being unhidden so it's meant to be called only in rare circumstances. * The most notable call to this function is at server startup * when we attempt to restore a clean state of the database. */ virtual bool unhideAllPimItems(); /* --- Collection attributes ------------------------------------------ */ virtual bool addCollectionAttribute(const Collection &col, const QByteArray &key, const QByteArray &value, bool silent = false); /** * Removes the given collection attribute for @p col. * @throws HandlerException on database errors * @returns @c true if the attribute existed, @c false otherwise */ virtual bool removeCollectionAttribute(const Collection &col, const QByteArray &key); /* --- Helper functions ---------------------------------------------- */ /** Begins a transaction. No changes will be written to the database and no notification signal will be emitted unless you call commitTransaction(). @return @c true if successful. */ virtual bool beginTransaction(const QString &name); /** Reverts all changes within the current transaction. */ virtual bool rollbackTransaction(); /** Commits all changes within the current transaction and emits all collected notfication signals. If committing fails, the transaction will be rolled back. */ virtual bool commitTransaction(); /** Returns true if there is a transaction in progress. */ bool inTransaction() const; /** Returns the notification collector of this DataStore object. Use this to listen to change notification signals. */ NotificationCollector *notificationCollector(); /** Returns the QSqlDatabase object. Use this for generating queries yourself. Will [re-]open the database, if it is closed. */ QSqlDatabase database(); /** Sets the current session id. */ void setSessionId(const QByteArray &sessionId) { mSessionId = sessionId; } /** Returns if the database is currently open */ bool isOpened() const { return m_dbOpened; } bool doRollback(); void transactionKilledByDB(); Q_SIGNALS: /** Emitted if a transaction has been successfully committed. */ void transactionCommitted(); /** Emitted if a transaction has been aborted. */ void transactionRolledBack(); protected: /** Creates a new DataStore object and opens it. */ DataStore(AkonadiServer &akonadi); void debugLastDbError(const char *actionDescription) const; void debugLastQueryError(const QSqlQuery &query, const char *actionDescription) const; private: bool doAppendItemsFlag(const PimItem::List &items, const Flag &flag, const QSet &existing, const Collection &col, bool silent); bool doAppendItemsTag(const PimItem::List &items, const Tag &tag, const QSet &existing, const Collection &col, bool silent); /** Converts the given date/time to the database format, i.e. "YYYY-MM-DD HH:MM:SS". @param dateTime the date/time in UTC @return the date/time in database format @see dateTimeToQDateTime */ static QString dateTimeFromQDateTime(const QDateTime &dateTime); /** Converts the given date/time from database format to QDateTime. @param dateTime the date/time in database format @return the date/time as QDateTime @see dateTimeFromQDateTime */ static QDateTime dateTimeToQDateTime(const QByteArray &dateTime); private Q_SLOTS: void sendKeepAliveQuery(); protected: static std::unique_ptr sFactory; std::unique_ptr mNotificationCollector; AkonadiServer &m_akonadi; private: + Q_DISABLE_COPY_MOVE(DataStore) + void cleanupAfterRollback(); QString m_connectionName; QSqlDatabase m_database; bool m_dbOpened; bool m_transactionKilledByDB = false; uint m_transactionLevel; struct TransactionQuery { QString query; QVector boundValues; bool isBatch; }; QByteArray mSessionId; QTimer *m_keepAliveTimer = nullptr; static bool s_hasForeignKeyConstraints; + static QMutex sTransactionMutex; friend class DataStoreFactory; }; } // namespace Server } // namespace Akonadi #endif diff --git a/src/server/storage/dbconfigmysql.cpp b/src/server/storage/dbconfigmysql.cpp index 846bee3d7..7f34035d8 100644 --- a/src/server/storage/dbconfigmysql.cpp +++ b/src/server/storage/dbconfigmysql.cpp @@ -1,635 +1,635 @@ /* 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. */ #include "dbconfigmysql.h" #include "utils.h" #include "akonadiserver_debug.h" #include #include #include #include #include #include #include #include #include #include using namespace Akonadi; using namespace Akonadi::Server; #define MYSQL_MIN_MAJOR 5 #define MYSQL_MIN_MINOR 1 #define MYSQL_VERSION_CHECK(major, minor, patch) ((major << 16) | (minor << 8) | patch) static const QString s_mysqlSocketFileName = QStringLiteral("mysql.socket"); DbConfigMysql::DbConfigMysql() : mInternalServer(true) , mDatabaseProcess(nullptr) { } QString DbConfigMysql::driverName() const { return QStringLiteral("QMYSQL"); } QString DbConfigMysql::databaseName() const { return mDatabaseName; } static QString findExecutable(const QString &bin) { static const QStringList mysqldSearchPath = { QStringLiteral("/usr/bin"), QStringLiteral("/usr/sbin"), QStringLiteral("/usr/local/sbin"), QStringLiteral("/usr/local/libexec"), QStringLiteral("/usr/libexec"), QStringLiteral("/opt/mysql/libexec"), QStringLiteral("/opt/local/lib/mysql5/bin"), QStringLiteral("/opt/mysql/sbin"), }; QString path = QStandardPaths::findExecutable(bin); if (path.isEmpty()) { // No results in PATH; fall back to hardcoded list. path = QStandardPaths::findExecutable(bin, mysqldSearchPath); } return path; } bool DbConfigMysql::init(QSettings &settings) { // determine default settings depending on the driver QString defaultHostName; QString defaultOptions; QString defaultServerPath; QString defaultCleanShutdownCommand; #ifndef Q_OS_WIN const QString socketDirectory = Utils::preferredSocketDirectory(StandardDirs::saveDir("data", QStringLiteral("db_misc")), s_mysqlSocketFileName.length()); #endif const bool defaultInternalServer = true; #ifdef MYSQLD_EXECUTABLE if (QFile::exists(QStringLiteral(MYSQLD_EXECUTABLE))) { defaultServerPath = QStringLiteral(MYSQLD_EXECUTABLE); } #endif if (defaultServerPath.isEmpty()) { defaultServerPath = findExecutable(QStringLiteral("mysqld")); } const QString mysqladminPath = findExecutable(QStringLiteral("mysqladmin")); if (!mysqladminPath.isEmpty()) { #ifndef Q_OS_WIN defaultCleanShutdownCommand = QStringLiteral("%1 --defaults-file=%2/mysql.conf --socket=%3/%4 shutdown") .arg(mysqladminPath, StandardDirs::saveDir("data"), socketDirectory, s_mysqlSocketFileName); #else defaultCleanShutdownCommand = QString::fromLatin1("%1 shutdown --shared-memory").arg(mysqladminPath); #endif } mMysqlInstallDbPath = findExecutable(QStringLiteral("mysql_install_db")); qCDebug(AKONADISERVER_LOG) << "Found mysql_install_db: " << mMysqlInstallDbPath; mMysqlCheckPath = findExecutable(QStringLiteral("mysqlcheck")); qCDebug(AKONADISERVER_LOG) << "Found mysqlcheck: " << mMysqlCheckPath; mInternalServer = settings.value(QStringLiteral("QMYSQL/StartServer"), defaultInternalServer).toBool(); #ifndef Q_OS_WIN if (mInternalServer) { defaultOptions = QStringLiteral("UNIX_SOCKET=%1/%2").arg(socketDirectory, s_mysqlSocketFileName); } #endif // read settings for current driver settings.beginGroup(driverName()); mDatabaseName = settings.value(QStringLiteral("Name"), defaultDatabaseName()).toString(); mHostName = settings.value(QStringLiteral("Host"), defaultHostName).toString(); mUserName = settings.value(QStringLiteral("User")).toString(); mPassword = settings.value(QStringLiteral("Password")).toString(); mConnectionOptions = settings.value(QStringLiteral("Options"), defaultOptions).toString(); mMysqldPath = settings.value(QStringLiteral("ServerPath"), defaultServerPath).toString(); mCleanServerShutdownCommand = settings.value(QStringLiteral("CleanServerShutdownCommand"), defaultCleanShutdownCommand).toString(); settings.endGroup(); // verify settings and apply permanent changes (written out below) if (mInternalServer) { mConnectionOptions = defaultOptions; // intentionally not namespaced as we are the only one in this db instance when using internal mode mDatabaseName = QStringLiteral("akonadi"); } if (mInternalServer && (mMysqldPath.isEmpty() || !QFile::exists(mMysqldPath))) { mMysqldPath = defaultServerPath; } qCDebug(AKONADISERVER_LOG) << "Using mysqld:" << mMysqldPath; // store back the default values settings.beginGroup(driverName()); settings.setValue(QStringLiteral("Name"), mDatabaseName); settings.setValue(QStringLiteral("Host"), mHostName); settings.setValue(QStringLiteral("Options"), mConnectionOptions); if (!mMysqldPath.isEmpty()) { settings.setValue(QStringLiteral("ServerPath"), mMysqldPath); } settings.setValue(QStringLiteral("StartServer"), mInternalServer); settings.endGroup(); settings.sync(); // apply temporary changes to the settings if (mInternalServer) { mHostName.clear(); mUserName.clear(); mPassword.clear(); } return true; } void DbConfigMysql::apply(QSqlDatabase &database) { if (!mDatabaseName.isEmpty()) { database.setDatabaseName(mDatabaseName); } if (!mHostName.isEmpty()) { database.setHostName(mHostName); } if (!mUserName.isEmpty()) { database.setUserName(mUserName); } if (!mPassword.isEmpty()) { database.setPassword(mPassword); } database.setConnectOptions(mConnectionOptions); // can we check that during init() already? Q_ASSERT(database.driver()->hasFeature(QSqlDriver::LastInsertId)); } bool DbConfigMysql::useInternalServer() const { return mInternalServer; } bool DbConfigMysql::startInternalServer() { bool success = true; const QString akDir = StandardDirs::saveDir("data"); const QString dataDir = StandardDirs::saveDir("data", QStringLiteral("db_data")); #ifndef Q_OS_WIN const QString socketDirectory = Utils::preferredSocketDirectory(StandardDirs::saveDir("data", QStringLiteral("db_misc")), s_mysqlSocketFileName.length()); const QString socketFile = QStringLiteral("%1/%2").arg(socketDirectory, s_mysqlSocketFileName); const QString pidFileName = QStringLiteral("%1/mysql.pid").arg(socketDirectory); #endif // generate config file const QString globalConfig = StandardDirs::locateResourceFile("config", QStringLiteral("mysql-global.conf")); const QString localConfig = StandardDirs::locateResourceFile("config", QStringLiteral("mysql-local.conf")); const QString actualConfig = StandardDirs::saveDir("data") + QLatin1String("/mysql.conf"); if (globalConfig.isEmpty()) { qCCritical(AKONADISERVER_LOG) << "Did not find MySQL server default configuration (mysql-global.conf)"; return false; } #ifdef Q_OS_LINUX // It is recommended to disable CoW feature when running on Btrfs to improve // database performance. Disabling CoW only has effect on empty directory (since // it affects only new files), so we check whether MySQL has not yet been initialized. QDir dir(dataDir + QDir::separator() + QLatin1String("mysql")); if (!dir.exists()) { if (Utils::getDirectoryFileSystem(dataDir) == QLatin1String("btrfs")) { Utils::disableCoW(dataDir); } } #endif if (mMysqldPath.isEmpty()) { qCCritical(AKONADISERVER_LOG) << "mysqld not found. Please verify your installation"; return false; } // Get the version of the mysqld server that we'll be using. // MySQL (but not MariaDB) deprecates and removes command line options in // patch version releases, so we need to adjust the command line options accordingly // when running the helper utilities or starting the server const unsigned int localVersion = parseCommandLineToolsVersion(); if (localVersion == 0x000000) { qCCritical(AKONADISERVER_LOG) << "Failed to detect mysqld version!"; } // TODO: Parse "MariaDB" or "MySQL" from the version string instead of relying // on the version numbers const bool isMariaDB = localVersion >= MYSQL_VERSION_CHECK(10, 0, 0); qCDebug(AKONADISERVER_LOG).nospace() << "mysqld reports version " << (localVersion >> 16) << "." << ((localVersion >> 8) & 0x0000FF) << "." << (localVersion & 0x0000FF) << " (" << (isMariaDB ? "MariaDB" : "Oracle MySQL") << ")"; bool confUpdate = false; QFile actualFile(actualConfig); // update conf only if either global (or local) is newer than actual if ((QFileInfo(globalConfig).lastModified() > QFileInfo(actualFile).lastModified()) || (QFileInfo(localConfig).lastModified() > QFileInfo(actualFile).lastModified())) { QFile globalFile(globalConfig); QFile localFile(localConfig); if (globalFile.open(QFile::ReadOnly) && actualFile.open(QFile::WriteOnly)) { actualFile.write(globalFile.readAll()); if (!localConfig.isEmpty()) { if (localFile.open(QFile::ReadOnly)) { actualFile.write(localFile.readAll()); localFile.close(); } } globalFile.close(); actualFile.close(); confUpdate = true; } else { qCCritical(AKONADISERVER_LOG) << "Unable to create MySQL server configuration file."; qCCritical(AKONADISERVER_LOG) << "This means that either the default configuration file (mysql-global.conf) was not readable"; qCCritical(AKONADISERVER_LOG) << "or the target file (mysql.conf) could not be written."; return false; } } // MySQL doesn't like world writeable config files (which makes sense), but // our config file somehow ends up being world-writable on some systems for no // apparent reason nevertheless, so fix that const QFile::Permissions allowedPerms = actualFile.permissions() & (QFile::ReadOwner | QFile::WriteOwner | QFile::ReadGroup | QFile::WriteGroup | QFile::ReadOther); if (allowedPerms != actualFile.permissions()) { actualFile.setPermissions(allowedPerms); } if (dataDir.isEmpty()) { qCCritical(AKONADISERVER_LOG) << "Akonadi server was not able to create database data directory"; return false; } if (akDir.isEmpty()) { qCCritical(AKONADISERVER_LOG) << "Akonadi server was not able to create database log directory"; return false; } #ifndef Q_OS_WIN if (socketDirectory.isEmpty()) { qCCritical(AKONADISERVER_LOG) << "Akonadi server was not able to create database misc directory"; return false; } // the socket path must not exceed 103 characters, so check for max dir length right away if (socketDirectory.length() >= 90) { qCCritical(AKONADISERVER_LOG) << "MySQL cannot deal with a socket path this long. Path was: " << socketDirectory; return false; } // If mysql socket file exists, check if also the server process is still running, // else we can safely remove the socket file (cleanup after a system crash, etc.) QFile pidFile(pidFileName); if (QFile::exists(socketFile) && pidFile.open(QIODevice::ReadOnly)) { qCDebug(AKONADISERVER_LOG) << "Found a mysqld pid file, checking whether the server is still running..."; QByteArray pid = pidFile.readLine().trimmed(); QFile proc(QString::fromLatin1("/proc/" + pid + "/stat")); // Check whether the process with the PID from pidfile still exists and whether // it's actually still mysqld or, whether the PID has been recycled in the meanwhile. bool serverIsRunning = false; if (proc.open(QIODevice::ReadOnly)) { const QByteArray stat = proc.readAll(); const QList stats = stat.split(' '); if (stats.count() > 1) { // Make sure the PID actually belongs to mysql process // Linux trims executable name in /proc filesystem to 15 characters const QString expectedProcName = QFileInfo(mMysqldPath).fileName().left(15); - if (QString::fromLatin1(stats[1]) == QString::fromLatin1("(%1)").arg(expectedProcName)) { + if (QString::fromLatin1(stats[1]) == QStringLiteral("(%1)").arg(expectedProcName)) { // Yup, our mysqld is actually running, so pretend we started the server // and try to connect to it qCWarning(AKONADISERVER_LOG) << "mysqld for Akonadi is already running, trying to connect to it."; serverIsRunning = true; } } proc.close(); } if (!serverIsRunning) { qCDebug(AKONADISERVER_LOG) << "No mysqld process with specified PID is running. Removing the pidfile and starting a new instance..."; pidFile.close(); pidFile.remove(); QFile::remove(socketFile); } } #endif // synthesize the mysqld command QStringList arguments; arguments << QStringLiteral("--defaults-file=%1/mysql.conf").arg(akDir); arguments << QStringLiteral("--datadir=%1/").arg(dataDir); #ifndef Q_OS_WIN arguments << QStringLiteral("--socket=%1").arg(socketFile); arguments << QStringLiteral("--pid-file=%1").arg(pidFileName); #else arguments << QString::fromLatin1("--shared-memory"); #endif #ifndef Q_OS_WIN // If mysql socket file does not exists, then we must start the server, // otherwise we reconnect to it if (!QFile::exists(socketFile)) { // move mysql error log file out of the way const QFileInfo errorLog(dataDir + QDir::separator() + QLatin1String("mysql.err")); if (errorLog.exists()) { QFile logFile(errorLog.absoluteFilePath()); QFile oldLogFile(dataDir + QDir::separator() + QLatin1String("mysql.err.old")); if (logFile.open(QFile::ReadOnly) && oldLogFile.open(QFile::Append)) { oldLogFile.write(logFile.readAll()); oldLogFile.close(); logFile.close(); logFile.remove(); } else { qCCritical(AKONADISERVER_LOG) << "Failed to open MySQL error log."; } } // first run, some MySQL versions need a mysql_install_db run for that const QString confFile = StandardDirs::locateResourceFile("config", QStringLiteral("mysql-global.conf")); if (QDir(dataDir).entryList(QDir::NoDotAndDotDot | QDir::AllEntries).isEmpty()) { if (isMariaDB) { initializeMariaDBDatabase(confFile, dataDir); } else if (localVersion >= MYSQL_VERSION_CHECK(5, 7, 6)) { initializeMySQL5_7_6Database(confFile, dataDir); } else { initializeMySQLDatabase(confFile, dataDir); } } // clear mysql ib_logfile's in case innodb_log_file_size option changed in last confUpdate if (confUpdate) { QFile(dataDir + QDir::separator() + QLatin1String("ib_logfile0")).remove(); QFile(dataDir + QDir::separator() + QLatin1String("ib_logfile1")).remove(); } qCDebug(AKONADISERVER_LOG) << "Executing:" << mMysqldPath << arguments.join(QLatin1Char(' ')); mDatabaseProcess = new QProcess; mDatabaseProcess->start(mMysqldPath, arguments); if (!mDatabaseProcess->waitForStarted()) { qCCritical(AKONADISERVER_LOG) << "Could not start database server!"; qCCritical(AKONADISERVER_LOG) << "executable:" << mMysqldPath; qCCritical(AKONADISERVER_LOG) << "arguments:" << arguments; qCCritical(AKONADISERVER_LOG) << "process error:" << mDatabaseProcess->errorString(); return false; } connect(mDatabaseProcess, QOverload::of(&QProcess::finished), this, &DbConfigMysql::processFinished); // wait until mysqld has created the socket file (workaround for QTBUG-47475 in Qt5.5.0) int counter = 50; // avoid an endless loop in case mysqld terminated while ((counter-- > 0) && !QFileInfo::exists(socketFile)) { QThread::msleep(100); } } else { qCDebug(AKONADISERVER_LOG) << "Found " << qPrintable(s_mysqlSocketFileName) << " file, reconnecting to the database"; } #endif const QLatin1String initCon("initConnection"); { QSqlDatabase db = QSqlDatabase::addDatabase(QStringLiteral("QMYSQL"), initCon); apply(db); db.setDatabaseName(QString()); // might not exist yet, then connecting to the actual db will fail if (!db.isValid()) { qCCritical(AKONADISERVER_LOG) << "Invalid database object during database server startup"; return false; } bool opened = false; for (int i = 0; i < 120; ++i) { opened = db.open(); if (opened) { break; } if (mDatabaseProcess && mDatabaseProcess->waitForFinished(500)) { qCCritical(AKONADISERVER_LOG) << "Database process exited unexpectedly during initial connection!"; qCCritical(AKONADISERVER_LOG) << "executable:" << mMysqldPath; qCCritical(AKONADISERVER_LOG) << "arguments:" << arguments; qCCritical(AKONADISERVER_LOG) << "stdout:" << mDatabaseProcess->readAllStandardOutput(); qCCritical(AKONADISERVER_LOG) << "stderr:" << mDatabaseProcess->readAllStandardError(); qCCritical(AKONADISERVER_LOG) << "exit code:" << mDatabaseProcess->exitCode(); qCCritical(AKONADISERVER_LOG) << "process error:" << mDatabaseProcess->errorString(); return false; } } if (opened) { if (!mMysqlCheckPath.isEmpty()) { execute(mMysqlCheckPath, { QStringLiteral("--defaults-file=%1/mysql.conf").arg(akDir), QStringLiteral("--check-upgrade"), QStringLiteral("--auto-repair"), #ifndef Q_OS_WIN QStringLiteral("--socket=%1/%2").arg(socketDirectory, s_mysqlSocketFileName), #endif mDatabaseName }); } // Verify MySQL version { QSqlQuery query(db); if (!query.exec(QStringLiteral("SELECT VERSION()")) || !query.first()) { qCCritical(AKONADISERVER_LOG) << "Failed to verify database server version"; qCCritical(AKONADISERVER_LOG) << "Query error:" << query.lastError().text(); qCCritical(AKONADISERVER_LOG) << "Database error:" << db.lastError().text(); return false; } const QString version = query.value(0).toString(); const QStringList versions = version.split(QLatin1Char('.'), QString::SkipEmptyParts); if (versions.count() < 3) { qCCritical(AKONADISERVER_LOG) << "Invalid database server version: " << version; return false; } if (versions[0].toInt() < MYSQL_MIN_MAJOR || (versions[0].toInt() == MYSQL_MIN_MAJOR && versions[1].toInt() < MYSQL_MIN_MINOR)) { qCCritical(AKONADISERVER_LOG) << "Unsupported MySQL version:"; qCCritical(AKONADISERVER_LOG) << "Current version:" << QStringLiteral("%1.%2").arg(versions[0], versions[1]); qCCritical(AKONADISERVER_LOG) << "Minimum required version:" << QStringLiteral("%1.%2").arg(MYSQL_MIN_MAJOR).arg(MYSQL_MIN_MINOR); qCCritical(AKONADISERVER_LOG) << "Please update your MySQL database server"; return false; } else { qCDebug(AKONADISERVER_LOG) << "MySQL version OK" << "(required" << QStringLiteral("%1.%2").arg(MYSQL_MIN_MAJOR).arg(MYSQL_MIN_MINOR) << ", available" << QStringLiteral("%1.%2").arg(versions[0], versions[1]) << ")"; } } { QSqlQuery query(db); if (!query.exec(QStringLiteral("USE %1").arg(mDatabaseName))) { qCDebug(AKONADISERVER_LOG) << "Failed to use database" << mDatabaseName; qCDebug(AKONADISERVER_LOG) << "Query error:" << query.lastError().text(); qCDebug(AKONADISERVER_LOG) << "Database error:" << db.lastError().text(); qCDebug(AKONADISERVER_LOG) << "Trying to create database now..."; if (!query.exec(QStringLiteral("CREATE DATABASE akonadi"))) { qCCritical(AKONADISERVER_LOG) << "Failed to create database"; qCCritical(AKONADISERVER_LOG) << "Query error:" << query.lastError().text(); qCCritical(AKONADISERVER_LOG) << "Database error:" << db.lastError().text(); success = false; } } } // make sure query is destroyed before we close the db db.close(); } else { qCCritical(AKONADISERVER_LOG) << "Failed to connect to database!"; qCCritical(AKONADISERVER_LOG) << "Database error:" << db.lastError().text(); success = false; } } return success; } void DbConfigMysql::processFinished(int exitCode, QProcess::ExitStatus exitStatus) { Q_UNUSED(exitCode); Q_UNUSED(exitStatus); qCCritical(AKONADISERVER_LOG) << "database server stopped unexpectedly"; #ifndef Q_OS_WIN // when the server stopped unexpectedly, make sure to remove the stale socket file since otherwise // it can not be started again const QString socketDirectory = Utils::preferredSocketDirectory(StandardDirs::saveDir("data", QStringLiteral("db_misc")), s_mysqlSocketFileName.length()); const QString socketFile = QStringLiteral("%1/%2").arg(socketDirectory, s_mysqlSocketFileName); QFile::remove(socketFile); #endif QCoreApplication::quit(); } void DbConfigMysql::stopInternalServer() { if (!mDatabaseProcess) { return; } // closing initConnection this late to work around QTBUG-63108 QSqlDatabase::removeDatabase(QStringLiteral("initConnection")); disconnect(mDatabaseProcess, static_cast(&QProcess::finished), this, &DbConfigMysql::processFinished); // first, try the nicest approach if (!mCleanServerShutdownCommand.isEmpty()) { QProcess::execute(mCleanServerShutdownCommand, QStringList()); if (mDatabaseProcess->waitForFinished(3000)) { return; } } mDatabaseProcess->terminate(); const bool result = mDatabaseProcess->waitForFinished(3000); // We've waited nicely for 3 seconds, to no avail, let's be rude. if (!result) { mDatabaseProcess->kill(); } } void DbConfigMysql::initSession(const QSqlDatabase &database) { QSqlQuery query(database); query.exec(QStringLiteral("SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED")); } int DbConfigMysql::parseCommandLineToolsVersion() const { QProcess mysqldProcess; mysqldProcess.start(mMysqldPath, { QStringLiteral("--version") }); mysqldProcess.waitForFinished(10000 /* 10 secs */); const QString out = QString::fromLocal8Bit(mysqldProcess.readAllStandardOutput()); QRegularExpression regexp(QStringLiteral("Ver ([0-9]+)\\.([0-9]+)\\.([0-9]+)")); auto match = regexp.match(out); if (!match.hasMatch()) { return 0; } return (match.capturedRef(1).toInt() << 16) | (match.capturedRef(2).toInt() << 8) | match.capturedRef(3).toInt(); } bool DbConfigMysql::initializeMariaDBDatabase(const QString &confFile, const QString &dataDir) const { // KDE Neon (and possible others) don't ship mysql_install_db, but it seems // that MariaDB can initialize itself automatically on first start, it only // needs that the datadir directory exists if (mMysqlInstallDbPath.isEmpty()) { return QDir().mkpath(dataDir); } QFileInfo fi(mMysqlInstallDbPath); QDir dir = fi.dir(); dir.cdUp(); const QString baseDir = dir.absolutePath(); return 0 == execute(mMysqlInstallDbPath, { QStringLiteral("--defaults-file=%1").arg(confFile), QStringLiteral("--force"), QStringLiteral("--basedir=%1").arg(baseDir), QStringLiteral("--datadir=%1/").arg(dataDir) }); } /** * As of MySQL 5.7.6 mysql_install_db is deprecated and mysqld --initailize should be used instead * See MySQL Reference Manual section 2.10.1.1 (Initializing the Data Directory Manually Using mysqld) */ bool DbConfigMysql::initializeMySQL5_7_6Database(const QString &confFile, const QString &dataDir) const { return 0 == execute(mMysqldPath, { QStringLiteral("--defaults-file=%1").arg(confFile), QStringLiteral("--initialize"), QStringLiteral("--datadir=%1/").arg(dataDir) }); } bool DbConfigMysql::initializeMySQLDatabase(const QString &confFile, const QString &dataDir) const { // On FreeBSD MySQL 5.6 is also installed without mysql_install_db, so this // might do the trick there as well. if (mMysqlInstallDbPath.isEmpty()) { return QDir().mkpath(dataDir); } QFileInfo fi(mMysqlInstallDbPath); QDir dir = fi.dir(); dir.cdUp(); const QString baseDir = dir.absolutePath(); // Don't use --force, it has been removed in MySQL 5.7.5 return 0 == execute(mMysqlInstallDbPath, { QStringLiteral("--defaults-file=%1").arg(confFile), QStringLiteral("--basedir=%1").arg(baseDir), QStringLiteral("--datadir=%1/").arg(dataDir) }); } diff --git a/src/server/storage/dbinitializer.h b/src/server/storage/dbinitializer.h index 387ea97c0..4566e0d56 100644 --- a/src/server/storage/dbinitializer.h +++ b/src/server/storage/dbinitializer.h @@ -1,193 +1,198 @@ /*************************************************************************** * Copyright (C) 2006 by Tobias Koenig * * * * 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. * ***************************************************************************/ #ifndef DBINITIALIZER_H #define DBINITIALIZER_H #include "dbintrospector.h" #include "schematypes.h" #include #include #include #include class DbInitializerTest; namespace Akonadi { namespace Server { class Schema; class DbUpdater; class TestInterface { public: - virtual ~TestInterface() - { - } + virtual ~TestInterface() = default; virtual void execStatement(const QString &statement) = 0; + +protected: + explicit TestInterface() = default; + +private: + Q_DISABLE_COPY_MOVE(TestInterface) }; /** * A helper class which takes a reference to a database object and * the file name of a template file and initializes the database * according to the rules in the template file. * * TODO: Refactor this to be easily reusable for updater too */ class DbInitializer { friend class DbUpdater; public: typedef QSharedPointer Ptr; /** Returns an initializer instance for a given backend. */ static DbInitializer::Ptr createInstance(const QSqlDatabase &database, Schema *schema = nullptr); /** * Destroys the database initializer. */ virtual ~DbInitializer(); /** * Starts the initialization process. * On success true is returned, false otherwise. * * If something went wrong @see errorMsg() can be used to retrieve more * information. */ bool run(); /** * Returns the textual description of an occurred error. */ QString errorMsg() const; /** * Returns whether the database has working and complete foreign keys. * This information can be used for query optimizations. * @note Result is invalid before run() has been called. */ virtual bool hasForeignKeyConstraints() const = 0; /** * Checks and creates missing indexes. * * This method is run after DbUpdater to ensure that data in new columns * are populated and creation of indexes and foreign keys does not fail. */ bool updateIndexesAndConstraints(); /** * Returns a backend-specific CREATE TABLE SQL query describing given table */ virtual QString buildCreateTableStatement(const TableDescription &tableDescription) const = 0; protected: /** * Creates a new database initializer. * * @param database The reference to the database. */ DbInitializer(const QSqlDatabase &database); /** * Overwrite in backend-specific sub-classes to return the SQL type for a given C++ type. * @param type Name of the C++ type. * @param size Optional size hint for the column, if -1 use the default SQL type for @p type. */ virtual QString sqlType(const ColumnDescription &col, int size) const; /** Overwrite in backend-specific sub-classes to return the SQL value for a given C++ value. */ virtual QString sqlValue(const ColumnDescription &col, const QString &value) const; virtual QString buildColumnStatement(const ColumnDescription &columnDescription, const TableDescription &tableDescription) const = 0; virtual QString buildAddColumnStatement(const TableDescription &tableDescription, const ColumnDescription &columnDescription) const; virtual QString buildCreateIndexStatement(const TableDescription &tableDescription, const IndexDescription &indexDescription) const; virtual QString buildInsertValuesStatement(const TableDescription &tableDescription, const DataDescription &dataDescription) const = 0; /** * Returns an SQL statements to add a foreign key constraint to an existing column @p column. * The default implementation returns an empty string, so any backend supporting foreign key constraints * must reimplement this. */ virtual QStringList buildAddForeignKeyConstraintStatements(const TableDescription &table, const ColumnDescription &column) const; /** * Returns an SQL statements to remove the foreign key constraint @p fk from table @p table. * The default implementation returns an empty string, so any backend supporting foreign key constraints * must reimplement this. */ virtual QStringList buildRemoveForeignKeyConstraintStatements(const DbIntrospector::ForeignKey &fk, const TableDescription &table) const; static QString buildReferentialAction(ColumnDescription::ReferentialAction onUpdate, ColumnDescription::ReferentialAction onDelete); /// Use for multi-column primary keys during table creation static QString buildPrimaryKeyStatement(const TableDescription &table); private: friend class ::DbInitializerTest; + Q_DISABLE_COPY_MOVE(DbInitializer) /** * Sets the debug @p interface that shall be used on unit test run. */ void setTestInterface(TestInterface *interface); /** * Sets a different DbIntrospector. This allows unit tests to simulate certain * states of the database. */ void setIntrospector(const DbIntrospector::Ptr &introspector); /** Helper method for executing a query. * If a debug interface is set for testing, that gets the queries instead. * @throws DbException if something went wrong. */ void execQuery(const QString &queryString); bool checkTable(const TableDescription &tableDescription); /** * Checks foreign key constraints on table @p tableDescription and fixes them if necessary. */ void checkForeignKeys(const TableDescription &tableDescription); void checkIndexes(const TableDescription &tableDescription); bool checkRelation(const RelationDescription &relationDescription); static QString referentialActionToString(ColumnDescription::ReferentialAction action); void execPendingQueries(const QStringList &queries); QSqlDatabase mDatabase; Schema *mSchema = nullptr; QString mErrorMsg; TestInterface *mTestInterface = nullptr; DbIntrospector::Ptr m_introspector; QStringList m_pendingIndexes; QStringList m_pendingForeignKeys; QStringList m_removedForeignKeys; }; } // namespace Server } // namespace Akonadi #endif diff --git a/src/server/storage/dbinitializer_p.cpp b/src/server/storage/dbinitializer_p.cpp index df1b25eac..ac2aada70 100644 --- a/src/server/storage/dbinitializer_p.cpp +++ b/src/server/storage/dbinitializer_p.cpp @@ -1,375 +1,375 @@ /*************************************************************************** * Copyright (C) 2006 by Tobias Koenig * * Copyright (C) 2010 by Volker Krause * * * * 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 "storage/dbinitializer_p.h" #include using namespace Akonadi; using namespace Akonadi::Server; //BEGIN MySQL DbInitializerMySql::DbInitializerMySql(const QSqlDatabase &database) : DbInitializer(database) { } bool DbInitializerMySql::hasForeignKeyConstraints() const { return true; } QString DbInitializerMySql::sqlType(const ColumnDescription &col, int size) const { if (col.type == QLatin1String("QString")) { return QLatin1String("VARBINARY(") + QString::number(size <= 0 ? 255 : size) + QLatin1String(")"); } else { return DbInitializer::sqlType(col, size); } } QString DbInitializerMySql::buildCreateTableStatement(const TableDescription &tableDescription) const { QStringList columns; QStringList references; Q_FOREACH (const ColumnDescription &columnDescription, tableDescription.columns) { columns.append(buildColumnStatement(columnDescription, tableDescription)); if (!columnDescription.refTable.isEmpty() && !columnDescription.refColumn.isEmpty()) { references << QStringLiteral("FOREIGN KEY (%1) REFERENCES %2Table(%3) ") .arg(columnDescription.name, columnDescription.refTable, columnDescription.refColumn) + buildReferentialAction(columnDescription.onUpdate, columnDescription.onDelete); } } if (tableDescription.primaryKeyColumnCount() > 1) { columns.push_back(buildPrimaryKeyStatement(tableDescription)); } columns << references; QString tableProperties = QStringLiteral(" COLLATE=utf8_general_ci DEFAULT CHARSET=utf8"); if (tableDescription.columns | AkRanges::Actions::any([](const auto &col) { return col.type == QLatin1String("QString") && col.size > 255; })) { tableProperties += QStringLiteral(" ROW_FORMAT=DYNAMIC"); } return QStringLiteral("CREATE TABLE %1 (%2) %3").arg(tableDescription.name, columns.join(QStringLiteral(", ")), tableProperties); } QString DbInitializerMySql::buildColumnStatement(const ColumnDescription &columnDescription, const TableDescription &tableDescription) const { QString column = columnDescription.name; column += QLatin1Char(' ') + sqlType(columnDescription, columnDescription.size); if (!columnDescription.allowNull) { column += QLatin1String(" NOT NULL"); } if (columnDescription.isAutoIncrement) { column += QLatin1String(" AUTO_INCREMENT"); } if (columnDescription.isPrimaryKey && tableDescription.primaryKeyColumnCount() == 1) { column += QLatin1String(" PRIMARY KEY"); } if (columnDescription.isUnique) { column += QLatin1String(" UNIQUE"); } if (!columnDescription.defaultValue.isEmpty()) { const QString defaultValue = sqlValue(columnDescription, columnDescription.defaultValue); if (!defaultValue.isEmpty()) { column += QStringLiteral(" DEFAULT %1").arg(defaultValue); } } return column; } QString DbInitializerMySql::buildInsertValuesStatement(const TableDescription &tableDescription, const DataDescription &dataDescription) const { QMap data = dataDescription.data; - QMutableMapIterator it(data); - while (it.hasNext()) { - it.next(); + QStringList keys, values; + for (auto it = data.begin(), end = data.end(); it != end; ++it) { it.value().replace(QLatin1String("\\"), QLatin1String("\\\\")); + keys.push_back(it.key()); + values.push_back(it.value()); } return QStringLiteral("INSERT INTO %1 (%2) VALUES (%3)") - .arg(tableDescription.name, - QStringList(data.keys()).join(QLatin1Char(',')), - QStringList(data.values()).join(QLatin1Char(','))); + .arg(tableDescription.name, keys.join(QLatin1Char(',')), values.join(QLatin1Char(','))); } QStringList DbInitializerMySql::buildAddForeignKeyConstraintStatements(const TableDescription &table, const ColumnDescription &column) const { return { QStringLiteral("ALTER TABLE %1 ADD FOREIGN KEY (%2) REFERENCES %4Table(%5) %6") .arg(table.name, column.name, column.refTable, column.refColumn, buildReferentialAction(column.onUpdate, column.onDelete)) }; } QStringList DbInitializerMySql::buildRemoveForeignKeyConstraintStatements(const DbIntrospector::ForeignKey &fk, const TableDescription &table) const { return { QStringLiteral("ALTER TABLE %1 DROP FOREIGN KEY %2").arg(table.name, fk.name) }; } //END MySQL //BEGIN Sqlite DbInitializerSqlite::DbInitializerSqlite(const QSqlDatabase &database) : DbInitializer(database) { } bool DbInitializerSqlite::hasForeignKeyConstraints() const { return true; } QString DbInitializerSqlite::buildCreateTableStatement(const TableDescription &tableDescription) const { QStringList columns; columns.reserve(tableDescription.columns.count() + 1); for (const ColumnDescription &columnDescription : qAsConst(tableDescription.columns)) { columns.append(buildColumnStatement(columnDescription, tableDescription)); } if (tableDescription.primaryKeyColumnCount() > 1) { columns.push_back(buildPrimaryKeyStatement(tableDescription)); } QStringList references; for (const ColumnDescription &columnDescription : qAsConst(tableDescription.columns)) { if (!columnDescription.refTable.isEmpty() && !columnDescription.refColumn.isEmpty()) { const auto constraintName = QStringLiteral("%1%2_%3%4_fk").arg(tableDescription.name, columnDescription.name, columnDescription.refTable, columnDescription.refColumn); references << QStringLiteral("CONSTRAINT %1 FOREIGN KEY (%2) REFERENCES %3Table(%4) %5 DEFERRABLE INITIALLY DEFERRED") .arg(constraintName, columnDescription.name, columnDescription.refTable, columnDescription.refColumn, buildReferentialAction(columnDescription.onUpdate, columnDescription.onDelete)); } } columns << references; return QStringLiteral("CREATE TABLE %1 (%2)").arg(tableDescription.name, columns.join(QStringLiteral(", "))); } QString DbInitializerSqlite::buildColumnStatement(const ColumnDescription &columnDescription, const TableDescription &tableDescription) const { QString column = columnDescription.name + QLatin1Char(' '); if (columnDescription.isAutoIncrement) { column += QLatin1String("INTEGER"); } else { column += sqlType(columnDescription, columnDescription.size); } if (columnDescription.isPrimaryKey && tableDescription.primaryKeyColumnCount() == 1) { column += QLatin1String(" PRIMARY KEY"); } else if (columnDescription.isUnique) { column += QLatin1String(" UNIQUE"); } if (columnDescription.isAutoIncrement) { column += QLatin1String(" AUTOINCREMENT"); } if (!columnDescription.allowNull) { column += QLatin1String(" NOT NULL"); } if (!columnDescription.defaultValue.isEmpty()) { const QString defaultValue = sqlValue(columnDescription, columnDescription.defaultValue); if (!defaultValue.isEmpty()) { column += QStringLiteral(" DEFAULT %1").arg(defaultValue); } } return column; } QString DbInitializerSqlite::buildInsertValuesStatement(const TableDescription &tableDescription, const DataDescription &dataDescription) const { QMap data = dataDescription.data; - QMutableMapIterator it(data); - while (it.hasNext()) { - it.next(); + QStringList keys, values; + for (auto it = data.begin(), end = data.end(); it != end; ++it) { it.value().replace(QLatin1String("true"), QLatin1String("1")); it.value().replace(QLatin1String("false"), QLatin1String("0")); + keys.push_back(it.key()); + values.push_back(it.value()); } return QStringLiteral("INSERT INTO %1 (%2) VALUES (%3)") - .arg(tableDescription.name, - QStringList(data.keys()).join(QLatin1Char(',')), - QStringList(data.values()).join(QLatin1Char(','))); + .arg(tableDescription.name, keys.join(QLatin1Char(',')), values.join(QLatin1Char(','))); } QString DbInitializerSqlite::sqlValue(const ColumnDescription &col, const QString &value) const { if (col.type == QLatin1String("bool")) { if (value == QLatin1String("false")) { return QStringLiteral("0"); } else if (value == QLatin1String("true")) { return QStringLiteral("1"); } return value; } return Akonadi::Server::DbInitializer::sqlValue(col, value); } QStringList DbInitializerSqlite::buildAddForeignKeyConstraintStatements(const TableDescription &table, const ColumnDescription &) const { return buildUpdateForeignKeyConstraintsStatements(table); } QStringList DbInitializerSqlite::buildRemoveForeignKeyConstraintStatements(const DbIntrospector::ForeignKey &, const TableDescription &table) const { return buildUpdateForeignKeyConstraintsStatements(table); } QStringList DbInitializerSqlite::buildUpdateForeignKeyConstraintsStatements(const TableDescription &table) const { // Unforunately, SQLite does not support add or removing foreign keys through ALTER TABLE, // this is the only way how to do it. return { QStringLiteral("PRAGMA defer_foreign_keys=ON"), QStringLiteral("BEGIN TRANSACTION"), QStringLiteral("ALTER TABLE %1 RENAME TO %1_old").arg(table.name), buildCreateTableStatement(table), QStringLiteral("INSERT INTO %1 SELECT * FROM %1_old").arg(table.name), QStringLiteral("DROP TABLE %1_old").arg(table.name), QStringLiteral("COMMIT"), QStringLiteral("PRAGMA defer_foreign_keys=OFF") }; } //END Sqlite //BEGIN PostgreSQL DbInitializerPostgreSql::DbInitializerPostgreSql(const QSqlDatabase &database) : DbInitializer(database) { } bool DbInitializerPostgreSql::hasForeignKeyConstraints() const { return true; } QString DbInitializerPostgreSql::sqlType(const ColumnDescription &col, int size) const { if (col.type == QLatin1String("qint64")) { return QStringLiteral("int8"); } else if (col.type == QLatin1String("QByteArray")) { return QStringLiteral("BYTEA"); } else if (col.isEnum) { return QStringLiteral("SMALLINT"); } return DbInitializer::sqlType(col, size); } QString DbInitializerPostgreSql::buildCreateTableStatement(const TableDescription &tableDescription) const { QStringList columns; columns.reserve(tableDescription.columns.size() + 1); Q_FOREACH (const ColumnDescription &columnDescription, tableDescription.columns) { columns.append(buildColumnStatement(columnDescription, tableDescription)); } if (tableDescription.primaryKeyColumnCount() > 1) { columns.push_back(buildPrimaryKeyStatement(tableDescription)); } return QStringLiteral("CREATE TABLE %1 (%2)").arg(tableDescription.name, columns.join(QStringLiteral(", "))); } QString DbInitializerPostgreSql::buildColumnStatement(const ColumnDescription &columnDescription, const TableDescription &tableDescription) const { QString column = columnDescription.name + QLatin1Char(' '); if (columnDescription.isAutoIncrement) { column += QLatin1String("SERIAL"); } else { column += sqlType(columnDescription, columnDescription.size); } if (columnDescription.isPrimaryKey && tableDescription.primaryKeyColumnCount() == 1) { column += QLatin1String(" PRIMARY KEY"); } else if (columnDescription.isUnique) { column += QLatin1String(" UNIQUE"); } if (!columnDescription.allowNull && !(columnDescription.isPrimaryKey && tableDescription.primaryKeyColumnCount() == 1)) { column += QLatin1String(" NOT NULL"); } if (!columnDescription.defaultValue.isEmpty()) { const QString defaultValue = sqlValue(columnDescription, columnDescription.defaultValue); if (!defaultValue.isEmpty()) { column += QStringLiteral(" DEFAULT %1").arg(defaultValue); } } return column; } QString DbInitializerPostgreSql::buildInsertValuesStatement(const TableDescription &tableDescription, const DataDescription &dataDescription) const { - QMap data = dataDescription.data; + QStringList keys, values; + for (auto it = dataDescription.data.cbegin(), end = dataDescription.data.cend(); it != end; ++it) { + keys.push_back(it.key()); + values.push_back(it.value()); + } return QStringLiteral("INSERT INTO %1 (%2) VALUES (%3)") - .arg(tableDescription.name, - QStringList(data.keys()).join(QLatin1Char(',')), - QStringList(data.values()).join(QLatin1Char(','))); + .arg(tableDescription.name, keys.join(QLatin1Char(',')), values.join(QLatin1Char(','))); } QStringList DbInitializerPostgreSql::buildAddForeignKeyConstraintStatements(const TableDescription &table, const ColumnDescription &column) const { // constraints must have name in PostgreSQL const QString constraintName = table.name + column.name + QLatin1String("_") + column.refTable + column.refColumn + QLatin1String("_fk"); return { QStringLiteral("ALTER TABLE %1 ADD CONSTRAINT %2 FOREIGN KEY (%3) REFERENCES %4Table(%5) %6 DEFERRABLE INITIALLY DEFERRED") .arg(table.name, constraintName, column.name, column.refTable, column.refColumn, buildReferentialAction(column.onUpdate, column.onDelete)) }; } QStringList DbInitializerPostgreSql::buildRemoveForeignKeyConstraintStatements(const DbIntrospector::ForeignKey &fk, const TableDescription &table) const { return { QStringLiteral("ALTER TABLE %1 DROP CONSTRAINT %2").arg(table.name, fk.name) }; } //END PostgreSQL diff --git a/src/server/storage/dbintrospector.h b/src/server/storage/dbintrospector.h index cb4098d9f..7a519321a 100644 --- a/src/server/storage/dbintrospector.h +++ b/src/server/storage/dbintrospector.h @@ -1,126 +1,128 @@ /* Copyright (C) 2006 by Tobias Koenig Copyright (c) 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. */ #ifndef DBINTROSPECTOR_H #define DBINTROSPECTOR_H #include #include #include #include class DbIntrospectorTest; namespace Akonadi { namespace Server { /** * Methods for introspecting the current state of a database schema. * I.e. this is about the structure of a database, not its content. */ class DbIntrospector { public: typedef QSharedPointer Ptr; /** A structure describing an existing foreign key. */ class ForeignKey { public: QString name; QString column; QString refTable; QString refColumn; QString onUpdate; // TODO use same enum as DbInitializer QString onDelete; // dito }; /** * Returns an introspector instance for a given database. */ static DbIntrospector::Ptr createInstance(const QSqlDatabase &database); virtual ~DbIntrospector(); /** * Returns @c true if table @p tableName exists. * The default implementation relies on QSqlDatabase::tables(). Usually this * does not need to be reimplemented. */ virtual bool hasTable(const QString &tableName); /** * Returns @c true of the given table has an index with the given name. * The default implementation performs the query returned by hasIndexQuery(). * @see hasIndexQuery() * @throws DbException on database errors. */ virtual bool hasIndex(const QString &tableName, const QString &indexName); /** * Check whether table @p tableName has a column named @p columnName. * The default implementation should work with all backends. */ virtual bool hasColumn(const QString &tableName, const QString &columnName); /** * Check whether table @p tableName is empty, ie. does not contain any rows. * The default implementation should work for all backends. * @throws DbException on database errors. */ virtual bool isTableEmpty(const QString &tableName); /** * Returns the foreign key constraints on table @p tableName. * The default implementation returns an empty list, so any backend supporting * referential integrity should reimplement this. */ virtual QVector foreignKeyConstraints(const QString &tableName); protected: /** * Creates a new database introspector, call from subclass. * * @param database The database to introspect. */ DbIntrospector(const QSqlDatabase &database); /** * Returns a query string to determine if @p tableName has an index @p indexName. * The query is expected to have one boolean result row/column. * This is used by the default implementation of hasIndex() only, thus reimplmentation * is not necessary if you reimplement hasIndex() * The default implementation asserts. */ virtual QString hasIndexQuery(const QString &tableName, const QString &indexName); /** The database connection we are introspecting. */ QSqlDatabase m_database; private: + Q_DISABLE_COPY_MOVE(DbIntrospector) + friend class ::DbIntrospectorTest; QHash m_columnCache; // avoids extra db roundtrips }; } // namespace Server } // namespace Akonadi #endif // DBINTROSPECTOR_H diff --git a/src/server/storage/dbupdater.cpp b/src/server/storage/dbupdater.cpp index 7ddc13b4d..d2ed54c5d 100644 --- a/src/server/storage/dbupdater.cpp +++ b/src/server/storage/dbupdater.cpp @@ -1,578 +1,578 @@ /* 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 "dbupdater.h" #include "dbtype.h" #include "entities.h" #include "akonadischema.h" #include "querybuilder.h" #include "selectquerybuilder.h" #include "datastore.h" #include "dbconfig.h" #include "dbintrospector.h" #include "dbinitializer_p.h" #include "akonadiserver_debug.h" #include #include #include #include #include #include #include #include #include #include #include #include using namespace Akonadi; using namespace Akonadi::Server; DbUpdater::DbUpdater(const QSqlDatabase &database, const QString &filename) : m_database(database) , m_filename(filename) { } bool DbUpdater::run() { Q_ASSERT(QThread::currentThread() == QCoreApplication::instance()->thread()); // TODO error handling - SchemaVersion currentVersion = SchemaVersion::retrieveAll().first(); + auto currentVersion = SchemaVersion::retrieveAll().at(0); UpdateSet::Map updates; if (!parseUpdateSets(currentVersion.version(), updates)) { return false; } if (updates.isEmpty()) { return true; } // indicate clients this might take a while // we can ignore unregistration in error cases, that'll kill the server anyway if (!QDBusConnection::sessionBus().registerService(DBus::serviceName(DBus::UpgradeIndicator))) { qCCritical(AKONADISERVER_LOG) << "Unable to connect to dbus service: " << QDBusConnection::sessionBus().lastError().message(); } // QMap is sorted, so we should be replaying the changes in correct order for (QMap::ConstIterator it = updates.constBegin(); it != updates.constEnd(); ++it) { Q_ASSERT(it.key() > currentVersion.version()); qCDebug(AKONADISERVER_LOG) << "DbUpdater: update to version:" << it.key() << " mandatory:" << it.value().abortOnFailure; bool success = false; bool hasTransaction = false; if (it.value().complex) { // complex update const QString methodName = QStringLiteral("complexUpdate_%1()").arg(it.value().version); const int index = metaObject()->indexOfMethod(methodName.toLatin1().constData()); if (index == -1) { success = false; qCCritical(AKONADISERVER_LOG) << "Update to version" << it.value().version << "marked as complex, but no implementation is available"; } else { const QMetaMethod method = metaObject()->method(index); method.invoke(this, Q_RETURN_ARG(bool, success)); if (!success) { qCCritical(AKONADISERVER_LOG) << "Update failed"; } } } else { // regular update success = m_database.transaction(); if (success) { hasTransaction = true; const QStringList statements = it.value().statements; for (const QString &statement : statements) { QSqlQuery query(m_database); success = query.exec(statement); if (!success) { qCCritical(AKONADISERVER_LOG) << "DBUpdater: query error:" << query.lastError().text() << m_database.lastError().text(); qCCritical(AKONADISERVER_LOG) << "Query was: " << statement; qCCritical(AKONADISERVER_LOG) << "Target version was: " << it.key(); qCCritical(AKONADISERVER_LOG) << "Mandatory: " << it.value().abortOnFailure; } } } } if (success) { currentVersion.setVersion(it.key()); success = currentVersion.update(); } if (!success || (hasTransaction && !m_database.commit())) { qCCritical(AKONADISERVER_LOG) << "Failed to commit transaction for database update"; if (hasTransaction) { m_database.rollback(); } if (it.value().abortOnFailure) { return false; } } } QDBusConnection::sessionBus().unregisterService(DBus::serviceName(DBus::UpgradeIndicator)); return true; } bool DbUpdater::parseUpdateSets(int currentVersion, UpdateSet::Map &updates) const { QFile file(m_filename); if (!file.open(QIODevice::ReadOnly)) { qCCritical(AKONADISERVER_LOG) << "Unable to open update description file" << m_filename; return false; } QDomDocument document; QString errorMsg; int line, column; if (!document.setContent(&file, &errorMsg, &line, &column)) { qCCritical(AKONADISERVER_LOG) << "Unable to parse update description file" << m_filename << ":" << errorMsg << "at line" << line << "column" << column; return false; } const QDomElement documentElement = document.documentElement(); if (documentElement.tagName() != QLatin1String("updates")) { qCCritical(AKONADISERVER_LOG) << "Invalid update description file format"; return false; } // iterate over the xml document and extract update information into an UpdateSet QDomElement updateElement = documentElement.firstChildElement(); while (!updateElement.isNull()) { if (updateElement.tagName() == QLatin1String("update")) { const int version = updateElement.attribute(QStringLiteral("version"), QStringLiteral("-1")).toInt(); if (version <= 0) { qCCritical(AKONADISERVER_LOG) << "Invalid version attribute in database update description"; return false; } if (updates.contains(version)) { qCCritical(AKONADISERVER_LOG) << "Duplicate version attribute in database update description"; return false; } if (version <= currentVersion) { qCDebug(AKONADISERVER_LOG) << "skipping update" << version; } else { UpdateSet updateSet; updateSet.version = version; updateSet.abortOnFailure = (updateElement.attribute(QStringLiteral("abortOnFailure")) == QLatin1String("true")); QDomElement childElement = updateElement.firstChildElement(); while (!childElement.isNull()) { if (childElement.tagName() == QLatin1String("raw-sql")) { if (updateApplicable(childElement.attribute(QStringLiteral("backends")))) { updateSet.statements << buildRawSqlStatement(childElement); } } else if (childElement.tagName() == QLatin1String("complex-update")) { if (updateApplicable(childElement.attribute(QStringLiteral("backends")))) { updateSet.complex = true; } } //TODO: check for generic tags here in the future childElement = childElement.nextSiblingElement(); } if (!updateSet.statements.isEmpty() || updateSet.complex) { updates.insert(version, updateSet); } } } updateElement = updateElement.nextSiblingElement(); } return true; } bool DbUpdater::updateApplicable(const QString &backends) const { const QStringList matchingBackends = backends.split(QLatin1Char(',')); QString currentBackend; switch (DbType::type(m_database)) { case DbType::MySQL: currentBackend = QStringLiteral("mysql"); break; case DbType::PostgreSQL: currentBackend = QStringLiteral("psql"); break; case DbType::Sqlite: currentBackend = QStringLiteral("sqlite"); break; case DbType::Unknown: return false; } return matchingBackends.contains(currentBackend); } QString DbUpdater::buildRawSqlStatement(const QDomElement &element) const { return element.text().trimmed(); } bool DbUpdater::complexUpdate_25() { qCDebug(AKONADISERVER_LOG) << "Starting database update to version 25"; DbType::Type dbType = DbType::type(DataStore::self()->database()); QElapsedTimer ttotal; ttotal.start(); // Recover from possibly failed or interrupted update { // We don't care if this fails, it just means that there was no failed update QSqlQuery query(DataStore::self()->database()); query.exec(QStringLiteral("ALTER TABLE PartTable_old RENAME TO PartTable")); } { QSqlQuery query(DataStore::self()->database()); query.exec(QStringLiteral("DROP TABLE IF EXISTS PartTable_new")); } { // Make sure the table is empty, otherwise we get duplicate key error QSqlQuery query(DataStore::self()->database()); if (dbType == DbType::Sqlite) { query.exec(QStringLiteral("DELETE FROM PartTypeTable")); } else { // MySQL, PostgreSQL query.exec(QStringLiteral("TRUNCATE TABLE PartTypeTable")); } } { // It appears that more users than expected have the invalid "GID" part in their // PartTable, which breaks the migration below (see BKO#331867), so we apply this // wanna-be fix to remove the invalid part before we start the actual migration. QueryBuilder qb(QStringLiteral("PartTable"), QueryBuilder::Delete); qb.addValueCondition(QStringLiteral("PartTable.name"), Query::Equals, QLatin1String("GID")); qb.exec(); } qCDebug(AKONADISERVER_LOG) << "Creating a PartTable_new"; { TableDescription description; description.name = QStringLiteral("PartTable_new"); ColumnDescription idColumn; idColumn.name = QStringLiteral("id"); idColumn.type = QStringLiteral("qint64"); idColumn.isAutoIncrement = true; idColumn.isPrimaryKey = true; description.columns << idColumn; ColumnDescription pimItemIdColumn; pimItemIdColumn.name = QStringLiteral("pimItemId"); pimItemIdColumn.type = QStringLiteral("qint64"); pimItemIdColumn.allowNull = false; description.columns << pimItemIdColumn; ColumnDescription partTypeIdColumn; partTypeIdColumn.name = QStringLiteral("partTypeId"); partTypeIdColumn.type = QStringLiteral("qint64"); partTypeIdColumn.allowNull = false; description.columns << partTypeIdColumn; ColumnDescription dataColumn; dataColumn.name = QStringLiteral("data"); dataColumn.type = QStringLiteral("QByteArray"); description.columns << dataColumn; ColumnDescription dataSizeColumn; dataSizeColumn.name = QStringLiteral("datasize"); dataSizeColumn.type = QStringLiteral("qint64"); dataSizeColumn.allowNull = false; description.columns << dataSizeColumn; ColumnDescription versionColumn; versionColumn.name = QStringLiteral("version"); versionColumn.type = QStringLiteral("int"); versionColumn.defaultValue = QStringLiteral("0"); description.columns << versionColumn; ColumnDescription externalColumn; externalColumn.name = QStringLiteral("external"); externalColumn.type = QStringLiteral("bool"); externalColumn.defaultValue = QStringLiteral("false"); description.columns << externalColumn; DbInitializer::Ptr initializer = DbInitializer::createInstance(DataStore::self()->database()); const QString queryString = initializer->buildCreateTableStatement(description); QSqlQuery query(DataStore::self()->database()); if (!query.exec(queryString)) { qCCritical(AKONADISERVER_LOG) << query.lastError().text(); return false; } } qCDebug(AKONADISERVER_LOG) << "Migrating part types"; { // Get list of all part names QueryBuilder qb(QStringLiteral("PartTable"), QueryBuilder::Select); qb.setDistinct(true); qb.addColumn(QStringLiteral("PartTable.name")); if (!qb.exec()) { qCCritical(AKONADISERVER_LOG) << qb.query().lastError().text(); return false; } // Process them one by one QSqlQuery query = qb.query(); while (query.next()) { // Split the part name to namespace and name and insert it to PartTypeTable const QString partName = query.value(0).toString(); const QString ns = partName.left(3); const QString name = partName.mid(4); { QueryBuilder qb(QStringLiteral("PartTypeTable"), QueryBuilder::Insert); qb.setColumnValue(QStringLiteral("ns"), ns); qb.setColumnValue(QStringLiteral("name"), name); if (!qb.exec()) { qCCritical(AKONADISERVER_LOG) << qb.query().lastError().text(); return false; } } qCDebug(AKONADISERVER_LOG) << "\t Moved part type" << partName << "to PartTypeTable"; } query.finish(); } qCDebug(AKONADISERVER_LOG) << "Migrating data from PartTable to PartTable_new"; { QSqlQuery query(DataStore::self()->database()); QString queryString; if (dbType == DbType::PostgreSQL) { queryString = QStringLiteral("INSERT INTO PartTable_new (id, pimItemId, partTypeId, data, datasize, version, external) " "SELECT PartTable.id, PartTable.pimItemId, PartTypeTable.id, PartTable.data, " " PartTable.datasize, PartTable.version, PartTable.external " "FROM PartTable " "LEFT JOIN PartTypeTable ON " " PartTable.name = CONCAT(PartTypeTable.ns, ':', PartTypeTable.name)"); } else if (dbType == DbType::MySQL) { queryString = QStringLiteral("INSERT INTO PartTable_new (id, pimItemId, partTypeId, data, datasize, version, external) " "SELECT PartTable.id, PartTable.pimItemId, PartTypeTable.id, PartTable.data, " "PartTable.datasize, PartTable.version, PartTable.external " "FROM PartTable " "LEFT JOIN PartTypeTable ON PartTable.name = CONCAT(PartTypeTable.ns, ':', PartTypeTable.name)"); } else if (dbType == DbType::Sqlite) { queryString = QStringLiteral("INSERT INTO PartTable_new (id, pimItemId, partTypeId, data, datasize, version, external) " "SELECT PartTable.id, PartTable.pimItemId, PartTypeTable.id, PartTable.data, " "PartTable.datasize, PartTable.version, PartTable.external " "FROM PartTable " "LEFT JOIN PartTypeTable ON PartTable.name = PartTypeTable.ns || ':' || PartTypeTable.name"); } if (!query.exec(queryString)) { qCCritical(AKONADISERVER_LOG) << query.lastError().text(); return false; } } qCDebug(AKONADISERVER_LOG) << "Swapping PartTable_new for PartTable"; { // Does an atomic swap QSqlQuery query(DataStore::self()->database()); if (dbType == DbType::PostgreSQL || dbType == DbType::Sqlite) { if (dbType == DbType::PostgreSQL) { DataStore::self()->beginTransaction(QStringLiteral("DBUPDATER (r25)")); } if (!query.exec(QStringLiteral("ALTER TABLE PartTable RENAME TO PartTable_old"))) { qCCritical(AKONADISERVER_LOG) << query.lastError().text(); DataStore::self()->rollbackTransaction(); return false; } // If this fails in SQLite (i.e. without transaction), we can still recover on next start) if (!query.exec(QStringLiteral("ALTER TABLE PartTable_new RENAME TO PartTable"))) { qCCritical(AKONADISERVER_LOG) << query.lastError().text(); if (DataStore::self()->inTransaction()) { DataStore::self()->rollbackTransaction(); } return false; } if (dbType == DbType::PostgreSQL) { DataStore::self()->commitTransaction(); } } else { // MySQL cannot do rename in transaction, but supports atomic renames if (!query.exec(QStringLiteral("RENAME TABLE PartTable TO PartTable_old," " PartTable_new TO PartTable"))) { qCCritical(AKONADISERVER_LOG) << query.lastError().text(); return false; } } } qCDebug(AKONADISERVER_LOG) << "Removing PartTable_old"; { QSqlQuery query(DataStore::self()->database()); if (!query.exec(QStringLiteral("DROP TABLE PartTable_old;"))) { // It does not matter when this fails, we are successfully migrated qCDebug(AKONADISERVER_LOG) << query.lastError().text(); qCDebug(AKONADISERVER_LOG) << "Not a fatal problem, continuing..."; } } // Fine tuning for PostgreSQL qCDebug(AKONADISERVER_LOG) << "Final tuning of new PartTable"; { QSqlQuery query(DataStore::self()->database()); if (dbType == DbType::PostgreSQL) { query.exec(QStringLiteral("ALTER TABLE PartTable RENAME CONSTRAINT parttable_new_pkey TO parttable_pkey")); query.exec(QStringLiteral("ALTER SEQUENCE parttable_new_id_seq RENAME TO parttable_id_seq")); query.exec(QStringLiteral("SELECT setval('parttable_id_seq', MAX(id) + 1) FROM PartTable")); } else if (dbType == DbType::MySQL) { // 0 will automatically reset AUTO_INCREMENT to SELECT MAX(id) + 1 FROM PartTable query.exec(QStringLiteral("ALTER TABLE PartTable AUTO_INCREMENT = 0")); } } qCDebug(AKONADISERVER_LOG) << "Update done in" << ttotal.elapsed() << "ms"; // Foreign keys and constraints will be reconstructed automatically once // all updates are done return true; } bool DbUpdater::complexUpdate_36() { qCDebug(AKONADISERVER_LOG, "Starting database update to version 36"); Q_ASSERT(DbType::type(DataStore::self()->database()) == DbType::Sqlite); QSqlQuery query(DataStore::self()->database()); if (!query.exec(QStringLiteral("PRAGMA foreign_key_checks=OFF"))) { qCCritical(AKONADISERVER_LOG, "Failed to disable foreign key checks!"); return false; } const auto hasForeignKeys = [](const TableDescription &desc) { return std::any_of(desc.columns.cbegin(), desc.columns.cend(), [](const ColumnDescription &col) { return !col.refTable.isEmpty() && !col.refColumn.isEmpty(); }); }; const auto recreateTableWithForeignKeys = [](const TableDescription &table) -> QPair { qCDebug(AKONADISERVER_LOG) << "Updating foreign keys in table" << table.name; QSqlQuery query(DataStore::self()->database()); // Recover from possibly failed or interrupted update // We don't care if this fails, it just means that there was no failed update query.exec(QStringLiteral("ALTER TABLE %1_old RENAME TO %1").arg(table.name)); query.exec(QStringLiteral("DROP TABLE %1_new").arg(table.name)); qCDebug(AKONADISERVER_LOG, "\tCreating table %s_new with foreign keys", qUtf8Printable(table.name)); { const auto initializer = DbInitializer::createInstance(DataStore::self()->database()); TableDescription copy = table; copy.name += QStringLiteral("_new"); if (!query.exec(initializer->buildCreateTableStatement(copy))) { // If this fails we will recover on next start return {false, query}; } } qCDebug(AKONADISERVER_LOG, "\tCopying values from %s to %s_new (this may take a very long of time...)", qUtf8Printable(table.name), qUtf8Printable(table.name)); if (!query.exec(QStringLiteral("INSERT INTO %1_new SELECT * FROM %1").arg(table.name))) { // If this fails, we will recover on next start return {false, query}; } qCDebug(AKONADISERVER_LOG, "\tSwapping %s_new for %s", qUtf8Printable(table.name), qUtf8Printable(table.name)); if (!query.exec(QStringLiteral("ALTER TABLE %1 RENAME TO %1_old").arg(table.name))) { // If this fails we will recover on next start return {false, query}; } if (!query.exec(QStringLiteral("ALTER TABLE %1_new RENAME TO %1").arg(table.name))) { // If this fails we will recover on next start return {false, query}; } qCDebug(AKONADISERVER_LOG, "\tRemoving table %s_old", qUtf8Printable(table.name)); if (!query.exec(QStringLiteral("DROP TABLE %1_old").arg(table.name))) { // We don't care if this fails qCWarning(AKONADISERVER_LOG, "Failed to DROP TABLE %s (not fatal, update will continue)", qUtf8Printable(table.name)); qCWarning(AKONADISERVER_LOG, "Error: %s", qUtf8Printable(query.lastError().text())); } qCDebug(AKONADISERVER_LOG) << "\tOptimizing table %s", qUtf8Printable(table.name); if (!query.exec(QStringLiteral("ANALYZE %1").arg(table.name))) { // We don't care if this fails qCWarning(AKONADISERVER_LOG, "Failed to ANALYZE %s (not fatal, update will continue)", qUtf8Printable(table.name)); qCWarning(AKONADISERVER_LOG, "Error: %s", qUtf8Printable(query.lastError().text())); } qCDebug(AKONADISERVER_LOG) << "\tDone"; return {true, QSqlQuery()}; }; AkonadiSchema schema; const auto tables = schema.tables(); for (const auto &table : tables) { if (!hasForeignKeys(table)) { continue; } const auto &[ok, query] = recreateTableWithForeignKeys(table); if (!ok) { qCCritical(AKONADISERVER_LOG, "SQL error when updating table %s", qUtf8Printable(table.name)); qCCritical(AKONADISERVER_LOG, "Query: %s", qUtf8Printable(query.executedQuery())); qCCritical(AKONADISERVER_LOG, "Error: %s", qUtf8Printable(query.lastError().text())); return false; } } const auto relations = schema.relations(); for (const auto &relation : relations) { const RelationTableDescription table(relation); const auto &[ok, query] = recreateTableWithForeignKeys(table); if (!ok) { qCCritical(AKONADISERVER_LOG, "SQL error when updating relation table %s", qUtf8Printable(table.name)); qCCritical(AKONADISERVER_LOG, "Query: %s", qUtf8Printable(query.executedQuery())); qCCritical(AKONADISERVER_LOG, "Error: %s", qUtf8Printable(query.lastError().text())); return false; } } qCDebug(AKONADISERVER_LOG) << "Running VACUUM to reduce DB size"; if (!query.exec(QStringLiteral("VACUUM"))) { qCWarning(AKONADISERVER_LOG) << "Vacuum failed (not fatal, update will continue)"; qCWarning(AKONADISERVER_LOG) << "Error:" << query.lastError().text(); } return true; } diff --git a/src/server/storage/itemretrievalmanager.h b/src/server/storage/itemretrievalmanager.h index 7e3a021d0..a5b2fc93a 100644 --- a/src/server/storage/itemretrievalmanager.h +++ b/src/server/storage/itemretrievalmanager.h @@ -1,110 +1,113 @@ /* Copyright (c) 2009 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. */ #ifndef AKONADI_ITEMRETRIEVALMANAGER_H #define AKONADI_ITEMRETRIEVALMANAGER_H #include "itemretriever.h" #include "itemretrievalrequest.h" #include "akthread.h" #include #include #include #include #include #include class OrgFreedesktopAkonadiResourceInterface; namespace Akonadi { namespace Server { class Collection; class ItemRetrievalJob; class AbstractItemRetrievalJob; class AbstractItemRetrievalJobFactory { public: virtual ~AbstractItemRetrievalJobFactory() = default; virtual AbstractItemRetrievalJob *retrievalJob(ItemRetrievalRequest request, QObject *parent) = 0; +protected: + explicit AbstractItemRetrievalJobFactory() = default; + private: Q_DISABLE_COPY_MOVE(AbstractItemRetrievalJobFactory) }; /** Manages and processes item retrieval requests. */ class ItemRetrievalManager : public AkThread { Q_OBJECT public: explicit ItemRetrievalManager(QObject *parent = nullptr); explicit ItemRetrievalManager(std::unique_ptr factory, QObject *parent = nullptr); ~ItemRetrievalManager() override; /** * Added for convenience. ItemRetrievalManager takes ownership over the * pointer and deletes it when the request is processed. */ virtual void requestItemDelivery(ItemRetrievalRequest request); void triggerCollectionSync(const QString &resource, qint64 colId); void triggerCollectionTreeSync(const QString &resource); Q_SIGNALS: void requestFinished(const Akonadi::Server::ItemRetrievalResult &result); void requestAdded(); private: OrgFreedesktopAkonadiResourceInterface *resourceInterface(const QString &id); QVector scheduleJobsForIdleResourcesLocked(); private Q_SLOTS: void init() override; void serviceOwnerChanged(const QString &serviceName, const QString &oldOwner, const QString &newOwner); void processRequest(); void retrievalJobFinished(Akonadi::Server::AbstractItemRetrievalJob *job); protected: std::unique_ptr mJobFactory; /// Protects mPendingRequests and every Request object posted to it QReadWriteLock mLock; /// Used to let requesting threads wait until the request has been processed QWaitCondition mWaitCondition; /// Pending requests queues, one per resource std::unordered_map> mPendingRequests; /// Currently running jobs, one per resource QHash mCurrentJobs; // resource dbus interface cache std::unordered_map> mResourceInterfaces; }; } // namespace Server } // namespace Akonadi #endif diff --git a/src/server/storage/itemretriever.cpp b/src/server/storage/itemretriever.cpp index 3834df234..ed8cf0ab9 100644 --- a/src/server/storage/itemretriever.cpp +++ b/src/server/storage/itemretriever.cpp @@ -1,450 +1,450 @@ /* 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 "akonadi.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; using namespace AkRanges; Q_DECLARE_METATYPE(ItemRetrievalResult) ItemRetriever::ItemRetriever(ItemRetrievalManager &manager, Connection *connection, const CommandContext &context) : mScope() , mItemRetrievalManager(manager) , mConnection(connection) , mContext(context) , mFullPayload(false) , mRecursive(false) , mCanceled(false) { qRegisterMetaType("Akonadi::Server::ItemRetrievalResult"); if (mConnection) { 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) { if (!isUid && mContext.collectionId() >= 0) { setItemSet(set, mContext.collection()); } else { setItemSet(set); } } -void ItemRetriever::setItem(const Entity::Id &id) +void ItemRetriever::setItem(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, mContext, 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(const ItemRetrievalRequest &req, const QSet &availableParts) { return std::all_of(req.parts.begin(), req.parts.end(), [&availableParts](const auto &part) { return availableParts.contains(part); }); } } -bool ItemRetriever::runItemRetrievalRequests(std::list requests) +bool ItemRetriever::runItemRetrievalRequests(std::list requests) // clazy:exclude=function-args-by-ref { QEventLoop eventLoop; std::vector pendingRequests; connect(&mItemRetrievalManager, &ItemRetrievalManager::requestFinished, - this, [this, &eventLoop, &pendingRequests](const ItemRetrievalResult &result) { + this, [this, &eventLoop, &pendingRequests](const ItemRetrievalResult &result) { // clazy:exclude=lambda-in-connect const auto requestId = std::find(pendingRequests.begin(), pendingRequests.end(), result.request.id); if (requestId != pendingRequests.end()) { if (mCanceled) { eventLoop.exit(1); } else if (result.errorMsg.has_value()) { mLastError = result.errorMsg->toUtf8(); eventLoop.exit(1); } else { Q_EMIT itemsRetrieved(result.request.ids); pendingRequests.erase(requestId); if (pendingRequests.empty()) { eventLoop.quit(); } } } - }, Qt::UniqueConnection); + }); if (mConnection) { connect(mConnection, &Connection::connectionClosing, &eventLoop, [&eventLoop]() { eventLoop.exit(1); }); } for (auto &&request : requests) { if ((!mFullPayload && request.parts.isEmpty()) || request.ids.isEmpty()) { 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; pendingRequests.push_back(request.id); mItemRetrievalManager.requestItemDelivery(std::move(request)); } catch (const ItemRetrieverException &e) { qCCritical(AKONADISERVER_LOG) << e.type() << ": " << e.what(); mLastError = e.what(); return false; } } if (!pendingRequests.empty()) { if (eventLoop.exec()) { return false; } } return true; } std::optional ItemRetriever::prepareRequests(QSqlQuery &query, const QByteArrayList &parts) { QHash resourceIdNameCache; std::list requests; QHash colRequests; QHash itemRequests; QVector readyItems; qint64 prevPimItemId = -1; QSet availableParts; decltype(requests)::iterator lastRequest = requests.end(); 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 std::nullopt; } 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 != requests.end()) { 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.value(); } else { const auto colIt = colRequests.find(collectionId); lastRequest = (colIt == colRequests.end()) ? requests.end() : colIt.value(); if (lastRequest == requests.end() || lastRequest->ids.size() > 100) { requests.emplace_front(ItemRetrievalRequest{}); lastRequest = requests.begin(); 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); } else { lastRequest->ids.push_back(pimItemId); itemRequests.insert(pimItemId, lastRequest); colRequests.insert(collectionId, lastRequest); } } Q_ASSERT(lastRequest != requests.end()); 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.push_back(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 != requests.end() && hasAllParts(*lastRequest, availableParts)) { lastRequest->ids.removeOne(prevPimItemId); readyItems.push_back(prevPimItemId); // No need to update the hashtable at this point } return PreparedRequests{std::move(requests), std::move(readyItems)}; } bool ItemRetriever::exec() { if (mParts.isEmpty() && !mFullPayload) { return true; } verifyCache(); QSqlQuery query = buildQuery(); const auto parts = mParts | Views::filter([](const auto &part) { return part.startsWith(AKONADI_PARAM_PLD); }) | Views::transform([](const auto &part) { return part.mid(4); }) | Actions::toQList; const auto requests = prepareRequests(query, parts); if (!requests.has_value()) { return false; } if (!requests->readyItems.isEmpty()) { Q_EMIT itemsRetrieved(requests->readyItems); } if (!runItemRetrievalRequests(std::move(requests->requests))) { 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(mItemRetrievalManager, mConnection, mContext); 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, mContext, 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/itemretriever.h b/src/server/storage/itemretriever.h index c39a381d5..7e1308516 100644 --- a/src/server/storage/itemretriever.h +++ b/src/server/storage/itemretriever.h @@ -1,117 +1,117 @@ /* Copyright (c) 2009 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. */ #ifndef ITEMRETRIEVER_H #define ITEMRETRIEVER_H #include #include #include "../exception.h" #include "entities.h" #include #include #include AKONADI_EXCEPTION_MAKE_INSTANCE(ItemRetrieverException); namespace Akonadi { namespace Server { class Connection; class CommandContext; class ItemRetrievalManager; class ItemRetrievalRequest; /** Helper class for retrieving missing items parts from remote resources. Stuff in here happens in the calling thread and does not access shared data. @todo make usable for Fetch by allowing to share queries */ class ItemRetriever : public QObject { Q_OBJECT public: explicit ItemRetriever(ItemRetrievalManager &manager, Connection *connection, const CommandContext &context); Connection *connection() const; void setRetrieveParts(const QVector &parts); QVector retrieveParts() const; void setRetrieveFullPayload(bool fullPayload); void setChangedSince(const QDateTime &changedSince); void setItemSet(const ImapSet &set, const Collection &collection = Collection()); void setItemSet(const ImapSet &set, bool isUid); - void setItem(const Entity::Id &id); + void setItem(Entity::Id id); /** Retrieve all items in the given collection. */ void setCollection(const Collection &collection, bool recursive = true); /** Retrieve all items matching the given item scope. */ void setScope(const Scope &scope); Scope scope() const; bool exec(); QByteArray lastError() const; Q_SIGNALS: void itemsRetrieved(const QVector &ids); private: QSqlQuery buildQuery() const; /** * Checks if external files are still present * This costs extra, but allows us to automatically recover from something changing the external file storage. */ void verifyCache(); /// Execute the retrieval bool runItemRetrievalRequests(std::list requests); struct PreparedRequests { std::list requests; QVector readyItems; }; std::optional prepareRequests(QSqlQuery &query, const QByteArrayList &parts); Akonadi::ImapSet mItemSet; Collection mCollection; Scope mScope; ItemRetrievalManager &mItemRetrievalManager; Connection *mConnection = nullptr; const CommandContext &mContext; QVector mParts; bool mFullPayload; bool mRecursive; QDateTime mChangedSince; mutable QByteArray mLastError; bool mCanceled; }; } // namespace Server } // namespace Akonadi #endif diff --git a/src/server/storage/notificationcollector.cpp b/src/server/storage/notificationcollector.cpp index a98c0846f..75e640d31 100644 --- a/src/server/storage/notificationcollector.cpp +++ b/src/server/storage/notificationcollector.cpp @@ -1,618 +1,618 @@ /* 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(AkonadiServer &akonadi, DataStore *db) : mDb(db) , mAkonadi(akonadi) { 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) { mAkonadi.searchManager().scheduleSearchUpdate(); mAkonadi.collectionStatistics().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) { mAkonadi.searchManager().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); mAkonadi.collectionStatistics().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) { mAkonadi.searchManager().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 = mAkonadi.cacheCleaner()) { cleaner->collectionAdded(collection.id()); } mAkonadi.intervalChecker().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 = mAkonadi.cacheCleaner()) { cleaner->collectionChanged(collection.id()); } mAkonadi.intervalChecker().collectionChanged(collection.id()); if (changes.contains(AKONADI_PARAM_ENABLED)) { mAkonadi.collectionStatistics().invalidateCollection(collection); } collectionNotification(Protocol::CollectionChangeNotification::Modify, collection, collection.parentId(), -1, resource, changes | AkRanges::Actions::toQSet); } void NotificationCollector::collectionMoved(const Collection &collection, const Collection &source, const QByteArray &resource, const QByteArray &destResource) { if (auto cleaner = mAkonadi.cacheCleaner()) { cleaner->collectionChanged(collection.id()); } mAkonadi.intervalChecker().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 = mAkonadi.cacheCleaner()) { cleaner->collectionRemoved(collection.id()); } mAkonadi.intervalChecker().collectionRemoved(collection.id()); mAkonadi.collectionStatistics().invalidateCollection(collection); collectionNotification(Protocol::CollectionChangeNotification::Remove, collection, collection.parentId(), -1, resource); } void NotificationCollector::collectionSubscribed(const Collection &collection, const QByteArray &resource) { if (auto cleaner = mAkonadi.cacheCleaner()) { cleaner->collectionAdded(collection.id()); } mAkonadi.intervalChecker().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 = mAkonadi.cacheCleaner()) { cleaner->collectionRemoved(collection.id()); } mAkonadi.intervalChecker().collectionRemoved(collection.id()); mAkonadi.collectionStatistics().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); mAkonadi.collectionStatistics().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) { mAkonadi.collectionStatistics().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(mAkonadi, collection); if (auto mgr = mAkonadi.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(mAkonadi, col, {}, false, 0, {}, {}, mimeTypes); } // Get up-to-date statistics if (fetchScope->fetchStatistics()) { Collection col; col.setId(msgCollection.id()); const auto stats = mAkonadi.collectionStatistics().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()); msgTag.setParentId(tag.parentId()); if (auto mgr = mAkonadi.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 = mAkonadi.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 // don't 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, mAkonadi); // 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}); } } bool NotificationCollector::dispatchNotifications() { if (!mNotifications.isEmpty()) { for (auto &ntf : mNotifications) { completeNotification(ntf); } notify(std::move(mNotifications)); clear(); return true; } return false; } -void NotificationCollector::notify(Protocol::ChangeNotificationList msgs) +void NotificationCollector::notify(Protocol::ChangeNotificationList &&msgs) { if (auto mgr = mAkonadi.notificationManager()) { QMetaObject::invokeMethod(mgr, "slotNotify", Qt::QueuedConnection, Q_ARG(Akonadi::Protocol::ChangeNotificationList, msgs)); } } diff --git a/src/server/storage/notificationcollector.h b/src/server/storage/notificationcollector.h index 0a367764c..1f1d51051 100644 --- a/src/server/storage/notificationcollector.h +++ b/src/server/storage/notificationcollector.h @@ -1,271 +1,271 @@ /* 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. */ #ifndef AKONADI_NOTIFICATIONCOLLECTOR_H #define AKONADI_NOTIFICATIONCOLLECTOR_H #include "entities.h" #include #include #include #include namespace Akonadi { namespace Server { class DataStore; class Connection; class AkonadiServer; /** Part of the DataStore, collects change notifications and emits them after the current transaction has been successfully committed. Where possible, notifications are compressed. */ class NotificationCollector { public: /** Create a new notification collector for the given DataStore @p db. @param db The datastore using this notification collector. */ explicit NotificationCollector(AkonadiServer &akonadi, DataStore *db); /** Destroys this notification collector. */ virtual ~NotificationCollector() = default; /** * Sets the connection that is causing the changes. */ void setConnection(Connection *connection); /** Notify about an added item. Provide as many parameters as you have at hand currently, everything that is missing will be looked up in the database later. */ void itemAdded(const PimItem &item, bool seen, const Collection &collection = Collection(), const QByteArray &resource = QByteArray()); /** Notify about a changed item. Provide as many parameters as you have at hand currently, everything that is missing will be looked up in the database later. */ void itemChanged(const PimItem &item, const QSet &changedParts, const Collection &collection = Collection(), const QByteArray &resource = QByteArray()); /** Notify about changed items flags Provide as many parameters as you have at hand currently, everything that is missing will be looked up in the database later. */ void itemsFlagsChanged(const PimItem::List &items, const QSet &addedFlags, const QSet &removedFlags, const Collection &collection = Collection(), const QByteArray &resource = QByteArray()); /** Notify about changed items tags **/ void itemsTagsChanged(const PimItem::List &items, const QSet &addedTags, const QSet &removedTags, const Collection &collection = Collection(), const QByteArray &resource = QByteArray()); /** Notify about changed items relations **/ void itemsRelationsChanged(const PimItem::List &items, const Relation::List &addedRelations, const Relation::List &removedRelations, const Collection &collection = Collection(), const QByteArray &resource = QByteArray()); /** Notify about moved items Provide as many parameters as you have at hand currently, everything that is missing will be looked up in the database later. */ void itemsMoved(const PimItem::List &items, const Collection &collectionSrc = Collection(), const Collection &collectionDest = Collection(), const QByteArray &sourceResource = QByteArray()); /** Notify about removed items. Make sure you either provide all parameters or call this function before actually removing the item from database. */ void itemsRemoved(const PimItem::List &items, const Collection &collection = Collection(), const QByteArray &resource = QByteArray()); /** * Notify about linked items */ void itemsLinked(const PimItem::List &items, const Collection &collection); /** * Notify about unlinked items. */ void itemsUnlinked(const PimItem::List &items, const Collection &collection); /** Notify about a added collection. Provide as many parameters as you have at hand currently, everything that is missing will be looked up in the database later. */ void collectionAdded(const Collection &collection, const QByteArray &resource = QByteArray()); /** Notify about a changed collection. Provide as many parameters as you have at hand currently, everything that is missing will be looked up in the database later. */ void collectionChanged(const Collection &collection, const QList &changes, const QByteArray &resource = QByteArray()); /** Notify about a moved collection. Provide as many parameters as you have at hand currently, everything missing will be looked up on demand in the database later. */ void collectionMoved(const Collection &collection, const Collection &source, const QByteArray &resource = QByteArray(), const QByteArray &destResource = QByteArray()); /** Notify about a removed collection. Make sure you either provide all parameters or call this function before actually removing the item from database. */ void collectionRemoved(const Collection &collection, const QByteArray &resource = QByteArray()); /** * Notify about a collection subscription. */ void collectionSubscribed(const Collection &collection, const QByteArray &resource = QByteArray()); /** * Notify about a collection unsubscription */ void collectionUnsubscribed(const Collection &collection, const QByteArray &resource = QByteArray()); /** Notify about an added tag. */ void tagAdded(const Tag &tag); /** Notify about a changed tag. */ void tagChanged(const Tag &tag); /** Notify about a removed tag. */ void tagRemoved(const Tag &tag, const QByteArray &resource, const QString &remoteId); /** Notify about an added relation. */ void relationAdded(const Relation &relation); /** Notify about a removed relation. */ void relationRemoved(const Relation &relation); /** Trigger sending of collected notifications. @returns Returns true when any notifications were dispatched, false if there were no pending notifications. */ bool dispatchNotifications(); private: void itemNotification(Protocol::ItemChangeNotification::Operation op, const PimItem::List &items, const Collection &collection, const Collection &collectionDest, const QByteArray &resource, const QSet &parts = QSet(), const QSet &addedFlags = QSet(), const QSet &removedFlags = QSet(), const QSet &addedTags = QSet(), const QSet &removedTags = QSet(), const Relation::List &addedRelations = Relation::List(), const Relation::List &removedRelations = Relation::List()); void itemNotification(Protocol::ItemChangeNotification::Operation op, const PimItem &item, const Collection &collection, const Collection &collectionDest, const QByteArray &resource, const QSet &parts = QSet()); void collectionNotification(Protocol::CollectionChangeNotification::Operation op, const Collection &collection, Collection::Id source, Collection::Id destination, const QByteArray &resource, const QSet &changes = QSet(), const QByteArray &destResource = QByteArray()); void tagNotification(Protocol::TagChangeNotification::Operation op, const Tag &tag, const QByteArray &resource = QByteArray(), const QString &remoteId = QString()); void relationNotification(Protocol::RelationChangeNotification::Operation op, const Relation &relation); void dispatchNotification(const Protocol::ChangeNotificationPtr &msg); void clear(); void completeNotification(const Protocol::ChangeNotificationPtr &msg); protected: - virtual void notify(Protocol::ChangeNotificationList ntfs); + virtual void notify(Protocol::ChangeNotificationList &&ntfs); private: Q_DISABLE_COPY_MOVE(NotificationCollector); DataStore *mDb; Connection *mConnection = nullptr; AkonadiServer &mAkonadi; bool mIgnoreTransactions = false; Protocol::ChangeNotificationList mNotifications; }; } // namespace Server } // namespace Akonadi #endif diff --git a/src/server/storage/querybuilder.cpp b/src/server/storage/querybuilder.cpp index 7a590bfba..df85e755e 100644 --- a/src/server/storage/querybuilder.cpp +++ b/src/server/storage/querybuilder.cpp @@ -1,635 +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" #include "dbexception.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(", ")) +static void appendJoined(QString *statement, const QStringList &strings, 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 auto &[type, joinCondition] = mJoins.value(table); QueryBuilder qb(table, Select); qb.addColumn(condition.mColumn); qb.addCondition(joinCondition); // 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 auto &[joinType, joinCond] = mJoins.value(joinedTable); switch (joinType) { case LeftJoin: *statement += QLatin1String(" LEFT JOIN "); break; case InnerJoin: *statement += QLatin1String(" INNER JOIN "); break; } *statement += joinedTable; *statement += QLatin1String(" ON "); buildWhereCondition(statement, joinCond); } 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 auto &[joinType, joinCond] = mJoins.value(table); Q_ASSERT(joinType == InnerJoin); whereCondition.addCondition(joinCond); } } 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 auto &[column, value] = mColumnValues.at(i); *statement += column; *statement += QLatin1String(" = "); bindValue(statement, value); 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 auto &[column, order] = mSortColumns.at(i); *statement += column; *statement += sortOrderToString(order); if (i + 1 < c) { *statement += QLatin1String(", "); } } } if (mLimit > 0) { *statement += QLatin1String(" 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 += QLatin1String(" FOR UPDATE"); } } } 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(); if (!mQuery.prepare(statement)) { qCCritical(AKONADISERVER_LOG) << "DATABASE ERROR while PREPARING QUERY:"; 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) << " Query:" << statement; return false; } 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()) { QElapsedTimer 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(); } } if (!ret) { bool needsRetry = false; // 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(); needsRetry = true; } } 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(); needsRetry = true; } 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(); // Not sure retrying helps, maybe error is good enough.... but doesn't hurt to retry a few times before giving up. needsRetry = true; } } 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(); DataStore::self()->doRollback(); needsRetry = true; } else if (error == 5 /* SQLITE_BUSY */) { qCWarning(AKONADISERVER_LOG) << "QueryBuilder::exec(): database reported transaction timeout, retrying transaction"; qCWarning(AKONADISERVER_LOG) << mQuery.lastError().text(); DataStore::self()->doRollback(); needsRetry = 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. } if (needsRetry) { DataStore::self()->transactionKilledByDB(); throw DbDeadlockException(mQuery); } 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/storage/schema.h b/src/server/storage/schema.h index a3221aa71..6241f7a58 100644 --- a/src/server/storage/schema.h +++ b/src/server/storage/schema.h @@ -1,50 +1,54 @@ /*************************************************************************** * Copyright (C) 2013 by Volker Krause * * * * 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. * ***************************************************************************/ #ifndef SCHEMA_H #define SCHEMA_H #include "schematypes.h" namespace Akonadi { namespace Server { /** Methods to access the desired database schema (@see DbInspector for accessing the actual database schema). */ class Schema { public: - inline virtual ~Schema() - { - } + inline virtual ~Schema() = default; /** List of tables in the schema. */ virtual QVector tables() = 0; /** List of relations (N:M helper tables) in the schema. */ virtual QVector relations() = 0; + +protected: + explicit Schema() = default; + +private: + Q_DISABLE_COPY_MOVE(Schema) }; } // namespace Server } // namespace Akonadi #endif diff --git a/src/server/storage/schema.xsl b/src/server/storage/schema.xsl index 773c404b8..bfd4f3641 100644 --- a/src/server/storage/schema.xsl +++ b/src/server/storage/schema.xsl @@ -1,78 +1,78 @@ header MySchema schema .indexes.reserve(); { IndexDescription idx; - idx.name = QLatin1String(""); + idx.name = QStringLiteral(""); #if QT_VERSION < QT_VERSION_CHECK(5, 15, 0) - idx.columns = QString::fromLatin1("").split( QLatin1Char( ',' ), QString::SkipEmptyParts ); + idx.columns = QStringLiteral("").split(QLatin1Char( ',' ), QString::SkipEmptyParts); #else - idx.columns = QString::fromLatin1("").split( QLatin1Char( ',' ), Qt::SkipEmptyParts ); + idx.columns = QStringLiteral("").split(QLatin1Char( ',' ), Qt::SkipEmptyParts); #endif idx.isUnique = ; idx.sort = QStringLiteral(""); .indexes.push_back(idx); } /* * This is an auto-generated file. * Do not edit! All changes made to it will be lost. */ diff --git a/src/server/tracerinterface.h b/src/server/tracerinterface.h index 2f9a26229..40344a0af 100644 --- a/src/server/tracerinterface.h +++ b/src/server/tracerinterface.h @@ -1,116 +1,121 @@ /*************************************************************************** * Copyright (C) 2006 by Tobias Koenig * * * * 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. * ***************************************************************************/ #ifndef AKONADI_TRACERINTERFACE_H #define AKONADI_TRACERINTERFACE_H +#include + class QByteArray; class QString; namespace Akonadi { namespace Server { /** * This interface can be reimplemented to deliver tracing information * of the akonadi server to the outside. * * Possible implementations: * - log file * - dbus signals * - live gui */ class TracerInterface { public: enum ConnectionFormat { DebugString, Json }; virtual ~TracerInterface() = default; /** * This method is called whenever a new data (imap) connection to the akonadi server * is established. * * @param identifier The unique identifier for this connection. All input and output * messages for this connection will have the same identifier. * * @param msg A message specific string. */ virtual void beginConnection(const QString &identifier, const QString &msg) = 0; /** * This method is called whenever a data (imap) connection to akonadi server is * closed. * * @param identifier The unique identifier of this connection. * @param msg A message specific string. */ virtual void endConnection(const QString &identifier, const QString &msg) = 0; /** * This method is called whenever the akonadi server retrieves some data from the * outside. * * @param identifier The unique identifier of the connection on which the data * is retrieved. * @param msg A message specific string. */ virtual void connectionInput(const QString &identifier, const QByteArray &msg) = 0; /** * This method is called whenever the akonadi server sends some data out to a client. * * @param identifier The unique identifier of the connection on which the * data is send. * @param msg A message specific string. */ virtual void connectionOutput(const QString &identifier, const QByteArray &msg) = 0; /** * This method is called whenever a dbus signal is emitted on the bus. * * @param signalName The name of the signal being sent. * @param msg A message specific string. */ virtual void signal(const QString &signalName, const QString &msg) = 0; /** * This method is called whenever a component wants to output a warning. */ virtual void warning(const QString &componentName, const QString &msg) = 0; /** * This method is called whenever a component wants to output an error. */ virtual void error(const QString &componentName, const QString &msg) = 0; virtual ConnectionFormat connectionFormat() const {return DebugString;} +protected: + explicit TracerInterface() = default; + private: Q_DISABLE_COPY_MOVE(TracerInterface); }; } // namespace Server } // namespace Akonadi #endif diff --git a/src/server/utils.cpp b/src/server/utils.cpp index 7ded74687..b93937666 100644 --- a/src/server/utils.cpp +++ b/src/server/utils.cpp @@ -1,244 +1,244 @@ /* * Copyright (C) 2010 Tobias Koenig * 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 "utils.h" #include "akonadiserver_debug.h" #include "instance_p.h" #include #include #include #include #include #if !defined(Q_OS_WIN) #include #include #include #include #include static QString akonadiSocketDirectory(); static bool checkSocketDirectory(const QString &path); static bool createSocketDirectory(const QString &link); #endif #ifdef Q_OS_LINUX #include #include #include #include #endif using namespace Akonadi; using namespace Akonadi::Server; QString Utils::preferredSocketDirectory(const QString &defaultDirectory, int fnLengthHint) { const QString serverConfigFile = StandardDirs::serverConfigFile(StandardDirs::ReadWrite); const QSettings serverSettings(serverConfigFile, QSettings::IniFormat); #if defined(Q_OS_WIN) const QString socketDir = serverSettings.value(QLatin1String("Connection/SocketDirectory"), defaultDirectory).toString(); #else QString socketDir = defaultDirectory; - if (!serverSettings.contains(QLatin1String("Connection/SocketDirectory"))) { + if (!serverSettings.contains(QStringLiteral("Connection/SocketDirectory"))) { // if no socket directory is defined, use the symlinked from /tmp socketDir = akonadiSocketDirectory(); if (socketDir.isEmpty()) { // if that does not work, fall back on default socketDir = defaultDirectory; } } else { socketDir = serverSettings.value(QStringLiteral("Connection/SocketDirectory"), defaultDirectory).toString(); } if (socketDir.contains(QLatin1String("$USER"))) { const QString userName = QString::fromLocal8Bit(qgetenv("USER")); if (!userName.isEmpty()) { socketDir.replace(QLatin1String("$USER"), userName); } } if (socketDir[0] != QLatin1Char('/')) { QDir::home().mkdir(socketDir); socketDir = QDir::homePath() + QLatin1Char('/') + socketDir; } QFileInfo dirInfo(socketDir); if (!dirInfo.exists()) { QDir::home().mkpath(dirInfo.absoluteFilePath()); } const std::size_t totalLength = socketDir.length() + 1 + fnLengthHint; const std::size_t maxLen = sizeof(sockaddr_un::sun_path); if (totalLength >= maxLen) { qCCritical(AKONADISERVER_LOG) << "akonadiSocketDirectory() length of" << totalLength << "is longer than the system limit" << maxLen; } #endif return socketDir; } #if !defined(Q_OS_WIN) QString akonadiSocketDirectory() { const QString hostname = QHostInfo::localHostName(); if (hostname.isEmpty()) { qCCritical(AKONADISERVER_LOG) << "QHostInfo::localHostName() failed"; return QString(); } - const QString identifier = Instance::hasIdentifier() ? Instance::identifier() : QLatin1String("default"); + const QString identifier = Instance::hasIdentifier() ? Instance::identifier() : QStringLiteral("default"); const QString link = StandardDirs::saveDir("data") + QStringLiteral("/socket-%1-%2").arg(hostname, identifier); if (checkSocketDirectory(link)) { return QFileInfo(link).symLinkTarget(); } if (createSocketDirectory(link)) { return QFileInfo(link).symLinkTarget(); } qCCritical(AKONADISERVER_LOG) << "Could not create socket directory for Akonadi."; return QString(); } static bool checkSocketDirectory(const QString &path) { QFileInfo info(path); if (!info.exists()) { return false; } if (info.isSymLink()) { info = QFileInfo(info.symLinkTarget()); } if (!info.isDir()) { return false; } if (info.ownerId() != getuid()) { return false; } return true; } static bool createSocketDirectory(const QString &link) { const QString directory = StandardDirs::saveDir("runtime"); if (!QDir().mkpath(directory)) { qCCritical(AKONADISERVER_LOG) << "Creating socket directory with name" << directory << "failed:" << strerror(errno); return false; } QFile::remove(link); if (!QFile::link(directory, link)) { qCCritical(AKONADISERVER_LOG) << "Creating symlink from" << directory << "to" << link << "failed"; return false; } return true; } #endif QString Utils::getDirectoryFileSystem(const QString &directory) { #ifndef Q_OS_LINUX Q_UNUSED(directory); return QString(); #else QString bestMatchPath; QString bestMatchFS; FILE *mtab = setmntent("/etc/mtab", "r"); if (!mtab) { return QString(); } while (mntent *mnt = getmntent(mtab)) { if (qstrcmp(mnt->mnt_type, MNTTYPE_IGNORE) == 0) { continue; } const QString dir = QString::fromLocal8Bit(mnt->mnt_dir); if (!directory.startsWith(dir) || dir.length() < bestMatchPath.length()) { continue; } bestMatchPath = dir; bestMatchFS = QString::fromLocal8Bit(mnt->mnt_type); } endmntent(mtab); return bestMatchFS; #endif } void Utils::disableCoW(const QString &path) { #ifndef Q_OS_LINUX Q_UNUSED(path); #else qCDebug(AKONADISERVER_LOG) << "Detected Btrfs, disabling copy-on-write on database files"; // from linux/fs.h, so that Akonadi does not depend on Linux header files #ifndef FS_IOC_GETFLAGS #define FS_IOC_GETFLAGS _IOR('f', 1, long) #endif #ifndef FS_IOC_SETFLAGS #define FS_IOC_SETFLAGS _IOW('f', 2, long) #endif // Disable COW on file #ifndef FS_NOCOW_FL #define FS_NOCOW_FL 0x00800000 #endif ulong flags = 0; const int fd = open(qPrintable(path), O_RDONLY); if (fd == -1) { qCWarning(AKONADISERVER_LOG) << "Failed to open" << path << "to modify flags (" << errno << ")"; return; } if (ioctl(fd, FS_IOC_GETFLAGS, &flags) == -1) { qCWarning(AKONADISERVER_LOG) << "ioctl error: failed to get file flags (" << errno << ")"; close(fd); return; } if (!(flags & FS_NOCOW_FL)) { flags |= FS_NOCOW_FL; if (ioctl(fd, FS_IOC_SETFLAGS, &flags) == -1) { qCWarning(AKONADISERVER_LOG) << "ioctl error: failed to set file flags (" << errno << ")"; close(fd); return; } } close(fd); #endif } diff --git a/src/shared/CMakeLists.txt b/src/shared/CMakeLists.txt index c7980c68e..f94925f3e 100644 --- a/src/shared/CMakeLists.txt +++ b/src/shared/CMakeLists.txt @@ -1,28 +1,28 @@ set(akonadi_shared_srcs akapplication.cpp akdebug.cpp akremotelog.cpp ) add_library(akonadi_shared STATIC ${akonadi_shared_srcs}) target_include_directories(akonadi_shared INTERFACE $) target_link_libraries(akonadi_shared PUBLIC - Qt5::Core KF5AkonadiPrivate + Qt5::Core KF5::Crash ) ecm_generate_headers(shared_HEADERS HEADER_NAMES VectorHelper REQUIRED_HEADERS shared_HEADERS ) # shared is not generally a public library, so install only the useful # public stuff to core install(FILES ${shared_HEADERS} DESTINATION ${KDE_INSTALL_INCLUDEDIR_KF5}/AkonadiCore COMPONENT Devel )