diff --git a/autotests/libs/itemstoretest.cpp b/autotests/libs/itemstoretest.cpp index 6547ae125..0c40cb8e9 100644 --- a/autotests/libs/itemstoretest.cpp +++ b/autotests/libs/itemstoretest.cpp @@ -1,384 +1,422 @@ /* Copyright (c) 2006 Volker Krause Copyright (c) 2007 Robert Zwerus 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 "itemstoretest.h" #include "control.h" #include "testattribute.h" #include #include #include #include #include #include #include #include #include #include #include #include #include "test_utils.h" using namespace Akonadi; QTEST_AKONADIMAIN(ItemStoreTest) static Collection res1_foo; static Collection res2; static Collection res3; void ItemStoreTest::initTestCase() { AkonadiTest::checkTestIsIsolated(); Control::start(); AttributeFactory::registerAttribute(); // get the collections we run the tests on res1_foo = Collection(collectionIdFromPath(QStringLiteral("res1/foo"))); QVERIFY(res1_foo.isValid()); res2 = Collection(collectionIdFromPath(QStringLiteral("res2"))); QVERIFY(res2.isValid()); res3 = Collection(collectionIdFromPath(QStringLiteral("res3"))); QVERIFY(res3.isValid()); AkonadiTest::setAllResourcesOffline(); } void ItemStoreTest::testFlagChange() { ItemFetchJob *fjob = new ItemFetchJob(Item(1)); AKVERIFYEXEC(fjob); QCOMPARE(fjob->items().count(), 1); Item item = fjob->items()[0]; // add a flag Item::Flags origFlags = item.flags(); Item::Flags expectedFlags = origFlags; expectedFlags.insert("added_test_flag_1"); item.setFlag("added_test_flag_1"); ItemModifyJob *sjob = new ItemModifyJob(item, this); AKVERIFYEXEC(sjob); fjob = new ItemFetchJob(Item(1)); AKVERIFYEXEC(fjob); QCOMPARE(fjob->items().count(), 1); item = fjob->items()[0]; QCOMPARE(item.flags().count(), expectedFlags.count()); Item::Flags diff = expectedFlags - item.flags(); QVERIFY(diff.isEmpty()); // set flags expectedFlags.insert("added_test_flag_2"); item.setFlags(expectedFlags); sjob = new ItemModifyJob(item, this); AKVERIFYEXEC(sjob); fjob = new ItemFetchJob(Item(1)); AKVERIFYEXEC(fjob); QCOMPARE(fjob->items().count(), 1); item = fjob->items()[0]; QCOMPARE(item.flags().count(), expectedFlags.count()); diff = expectedFlags - item.flags(); QVERIFY(diff.isEmpty()); // remove a flag item.clearFlag("added_test_flag_1"); item.clearFlag("added_test_flag_2"); sjob = new ItemModifyJob(item, this); AKVERIFYEXEC(sjob); fjob = new ItemFetchJob(Item(1)); AKVERIFYEXEC(fjob); QCOMPARE(fjob->items().count(), 1); item = fjob->items()[0]; QCOMPARE(item.flags().count(), origFlags.count()); diff = origFlags - item.flags(); QVERIFY(diff.isEmpty()); } void ItemStoreTest::testDataChange_data() { QTest::addColumn("data"); QTest::newRow("simple") << QByteArray("testbody"); QTest::newRow("null") << QByteArray(); QTest::newRow("empty") << QByteArray(""); QTest::newRow("nullbyte") << QByteArray("\0", 1); QTest::newRow("nullbyte2") << QByteArray("\0X", 2); QTest::newRow("linebreaks") << QByteArray("line1\nline2\n\rline3\rline4\r\n"); QTest::newRow("linebreaks2") << QByteArray("line1\r\nline2\r\n\r\n"); QTest::newRow("linebreaks3") << QByteArray("line1\nline2"); QByteArray b; QTest::newRow("big") << b.fill('a', 1 << 20); QTest::newRow("bignull") << b.fill('\0', 1 << 20); QTest::newRow("bigcr") << b.fill('\r', 1 << 20); QTest::newRow("biglf") << b.fill('\n', 1 << 20); } void ItemStoreTest::testDataChange() { QFETCH(QByteArray, data); Item item; ItemFetchJob *prefetchjob = new ItemFetchJob(Item(1)); AKVERIFYEXEC(prefetchjob); item = prefetchjob->items()[0]; item.setMimeType(QStringLiteral("application/octet-stream")); item.setPayload(data); QCOMPARE(item.payload(), data); // modify data ItemModifyJob *sjob = new ItemModifyJob(item); AKVERIFYEXEC(sjob); ItemFetchJob *fjob = new ItemFetchJob(Item(1)); fjob->fetchScope().fetchFullPayload(); fjob->fetchScope().setCacheOnly(true); AKVERIFYEXEC(fjob); QCOMPARE(fjob->items().count(), 1); item = fjob->items()[0]; QVERIFY(item.hasPayload()); QCOMPARE(item.payload(), data); QEXPECT_FAIL("null", "STORE will not update item size on 0 sizes", Continue); QEXPECT_FAIL("empty", "STORE will not update item size on 0 sizes", Continue); QCOMPARE(item.size(), static_cast(data.size())); } void ItemStoreTest::testRemoteId_data() { QTest::addColumn("rid"); QTest::addColumn("exprid"); QTest::newRow("set") << QStringLiteral("A") << QStringLiteral("A"); QTest::newRow("no-change") << QString() << QStringLiteral("A"); QTest::newRow("clear") << QStringLiteral("") << QStringLiteral(""); QTest::newRow("reset") << QStringLiteral("A") << QStringLiteral("A"); QTest::newRow("utf8") << QStringLiteral("ä ö ü @") << QStringLiteral("ä ö ü @"); } void ItemStoreTest::testRemoteId() { QFETCH(QString, rid); QFETCH(QString, exprid); // pretend to be a resource, we cannot change remote identifiers otherwise ResourceSelectJob *rsel = new ResourceSelectJob(QStringLiteral("akonadi_knut_resource_0"), this); AKVERIFYEXEC(rsel); ItemFetchJob *prefetchjob = new ItemFetchJob(Item(1)); AKVERIFYEXEC(prefetchjob); Item item = prefetchjob->items()[0]; item.setRemoteId(rid); ItemModifyJob *store = new ItemModifyJob(item, this); store->disableRevisionCheck(); store->setIgnorePayload(true); // we only want to update the remote id AKVERIFYEXEC(store); ItemFetchJob *fetch = new ItemFetchJob(item, this); AKVERIFYEXEC(fetch); QCOMPARE(fetch->items().count(), 1); item = fetch->items().at(0); QEXPECT_FAIL("clear", "Clearing RID by clients is currently forbidden to avoid conflicts.", Continue); QCOMPARE(item.remoteId().toUtf8(), exprid.toUtf8()); // no longer pretend to be a resource rsel = new ResourceSelectJob(QString(), this); AKVERIFYEXEC(rsel); } void ItemStoreTest::testMultiPart() { ItemFetchJob *prefetchjob = new ItemFetchJob(Item(1)); AKVERIFYEXEC(prefetchjob); QCOMPARE(prefetchjob->items().count(), 1); Item item = prefetchjob->items()[0]; item.setMimeType(QStringLiteral("application/octet-stream")); item.setPayload("testmailbody"); item.attribute(Item::AddIfMissing)->data = "extra"; // store item ItemModifyJob *sjob = new ItemModifyJob(item); AKVERIFYEXEC(sjob); ItemFetchJob *fjob = new ItemFetchJob(Item(1)); fjob->fetchScope().fetchAttribute(); fjob->fetchScope().fetchFullPayload(); AKVERIFYEXEC(fjob); QCOMPARE(fjob->items().count(), 1); item = fjob->items()[0]; QVERIFY(item.hasPayload()); QCOMPARE(item.payload(), QByteArray("testmailbody")); QVERIFY(item.hasAttribute()); QCOMPARE(item.attribute()->data, QByteArray("extra")); // clean up item.removeAttribute("EXTRA"); sjob = new ItemModifyJob(item); AKVERIFYEXEC(sjob); } void ItemStoreTest::testPartRemove() { ItemFetchJob *prefetchjob = new ItemFetchJob(Item(2)); AKVERIFYEXEC(prefetchjob); Item item = prefetchjob->items()[0]; item.setMimeType(QStringLiteral("application/octet-stream")); item.attribute(Item::AddIfMissing)->data = "extra"; // store item ItemModifyJob *sjob = new ItemModifyJob(item); AKVERIFYEXEC(sjob); // fetch item and its parts (should be RFC822, HEAD and EXTRA) ItemFetchJob *fjob = new ItemFetchJob(Item(2)); fjob->fetchScope().fetchFullPayload(); fjob->fetchScope().fetchAllAttributes(); fjob->fetchScope().setCacheOnly(true); AKVERIFYEXEC(fjob); QCOMPARE(fjob->items().count(), 1); item = fjob->items()[0]; QCOMPARE(item.attributes().count(), 2); QVERIFY(item.hasAttribute()); // remove a part item.removeAttribute(); sjob = new ItemModifyJob(item); AKVERIFYEXEC(sjob); // fetch item again (should only have RFC822 and HEAD left) ItemFetchJob *fjob2 = new ItemFetchJob(Item(2)); fjob2->fetchScope().fetchFullPayload(); fjob2->fetchScope().fetchAllAttributes(); fjob2->fetchScope().setCacheOnly(true); AKVERIFYEXEC(fjob2); QCOMPARE(fjob2->items().count(), 1); item = fjob2->items()[0]; QCOMPARE(item.attributes().count(), 1); QVERIFY(!item.hasAttribute()); } void ItemStoreTest::testRevisionCheck() { // fetch same item twice Item ref(2); ItemFetchJob *prefetchjob = new ItemFetchJob(ref); AKVERIFYEXEC(prefetchjob); QCOMPARE(prefetchjob->items().count(), 1); Item item1 = prefetchjob->items()[0]; Item item2 = prefetchjob->items()[0]; // store first item unmodified ItemModifyJob *sjob = new ItemModifyJob(item1); AKVERIFYEXEC(sjob); // store the first item with modifications (should work) item1.attribute(Item::AddIfMissing)->data = "random stuff 1"; sjob = new ItemModifyJob(item1, this); AKVERIFYEXEC(sjob); // try to store second item with modifications (should be detected as a conflict) item2.attribute(Item::AddIfMissing)->data = "random stuff 2"; ItemModifyJob *sjob2 = new ItemModifyJob(item2); sjob2->disableAutomaticConflictHandling(); QVERIFY(!sjob2->exec()); // fetch same again prefetchjob = new ItemFetchJob(ref); AKVERIFYEXEC(prefetchjob); item1 = prefetchjob->items()[0]; // delete item ItemDeleteJob *djob = new ItemDeleteJob(ref, this); AKVERIFYEXEC(djob); // try to store it sjob = new ItemModifyJob(item1); QVERIFY(!sjob->exec()); } void ItemStoreTest::testModificationTime() { Item item; item.setMimeType(QStringLiteral("text/directory")); QVERIFY(item.modificationTime().isNull()); ItemCreateJob *job = new ItemCreateJob(item, res1_foo); AKVERIFYEXEC(job); // The item should have a datetime set now. item = job->item(); QVERIFY(!item.modificationTime().isNull()); QDateTime initialDateTime = item.modificationTime(); // Fetch the same item again. Item item2(item.id()); ItemFetchJob *fjob = new ItemFetchJob(item2, this); AKVERIFYEXEC(fjob); item2 = fjob->items().first(); QCOMPARE(initialDateTime, item2.modificationTime()); // Lets wait at least a second, which is the resolution of mtime QTest::qWait(1000); // Modify the item item.attribute(Item::AddIfMissing)->data = "extra"; ItemModifyJob *mjob = new ItemModifyJob(item); AKVERIFYEXEC(mjob); // The item should still have a datetime set and that date should be somewhere // after the initialDateTime. item = mjob->item(); QVERIFY(!item.modificationTime().isNull()); QVERIFY(initialDateTime < item.modificationTime()); // Fetch the item after modification. Item item3(item.id()); ItemFetchJob *fjob2 = new ItemFetchJob(item3, this); AKVERIFYEXEC(fjob2); // item3 should have the same modification time as item. item3 = fjob2->items().first(); QCOMPARE(item3.modificationTime(), item.modificationTime()); // Clean up ItemDeleteJob *idjob = new ItemDeleteJob(item, this); AKVERIFYEXEC(idjob); } void ItemStoreTest::testRemoteIdRace() { // Create an item and store it Item item; item.setMimeType(QStringLiteral("text/directory")); ItemCreateJob *job = new ItemCreateJob(item, res1_foo); AKVERIFYEXEC(job); // Fetch the same item again. It should not have a remote Id yet, as the resource // is offline. // The remote id should be null, not only empty, so that item modify jobs with this // item don't overwrite the remote id. Item item2(job->item().id()); ItemFetchJob *fetchJob = new ItemFetchJob(item2); AKVERIFYEXEC(fetchJob); QCOMPARE(fetchJob->items().size(), 1); QVERIFY(fetchJob->items().first().remoteId().isEmpty()); } +void ItemStoreTest::itemModifyJobShouldOnlySendModifiedAttributes() +{ + // Given an item with an attribute (created on the server) + Item item; + item.setMimeType(QStringLiteral("text/directory")); + item.attribute(Item::AddIfMissing)->data = "initial"; + ItemCreateJob *job = new ItemCreateJob(item, res1_foo); + AKVERIFYEXEC(job); + item = job->item(); + QCOMPARE(item.attributes().count(), 1); + + // When one job modifies this attribute, and another one does an unrelated change + Item item1(item.id()); + item1.attribute(Item::AddIfMissing)->data = "modified"; + ItemModifyJob *mjob = new ItemModifyJob(item1); + mjob->disableRevisionCheck(); + AKVERIFYEXEC(mjob); + + item.setFlag("added_test_flag_1"); + // this job shouldn't send the old attribute again + ItemModifyJob *mjob2 = new ItemModifyJob(item); + mjob2->disableRevisionCheck(); + AKVERIFYEXEC(mjob2); + + // Then the item has the new value for the attribute (the other one didn't send the old attribute value) + { + auto *fetchJob = new ItemFetchJob(Item(item.id())); + ItemFetchScope fetchScope; + fetchScope.fetchAllAttributes(true); + fetchJob->setFetchScope(fetchScope); + AKVERIFYEXEC(fetchJob); + QCOMPARE(fetchJob->items().size(), 1); + const Item fetchedItem = fetchJob->items().first(); + QCOMPARE(fetchedItem.flags().count(), 1); + QCOMPARE(fetchedItem.attributes().count(), 1); + QCOMPARE(fetchedItem.attribute()->data, "modified"); + } +} diff --git a/autotests/libs/itemstoretest.h b/autotests/libs/itemstoretest.h index 1121408f4..a23642780 100644 --- a/autotests/libs/itemstoretest.h +++ b/autotests/libs/itemstoretest.h @@ -1,43 +1,44 @@ /* Copyright (c) 2006 Volker Krause Copyright (c) 2007 Robert Zwerus 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 ITEMSTORETEST_H #define ITEMSTORETEST_H #include class ItemStoreTest : public QObject { Q_OBJECT private Q_SLOTS: void initTestCase(); void testFlagChange(); void testDataChange_data(); void testDataChange(); void testRemoteId_data(); void testRemoteId(); void testMultiPart(); void testPartRemove(); void testRevisionCheck(); void testModificationTime(); void testRemoteIdRace(); + void itemModifyJobShouldOnlySendModifiedAttributes(); }; #endif diff --git a/src/core/item.cpp b/src/core/item.cpp index 2778a59a7..df51e4eaa 100644 --- a/src/core/item.cpp +++ b/src/core/item.cpp @@ -1,772 +1,741 @@ /* Copyright (c) 2006 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 "item.h" #include "item_p.h" #include "akonadicore_debug.h" #include "itemserializer_p.h" #include "private/protocol_p.h" #include #include #include #include #include #include #include using namespace Akonadi; Q_GLOBAL_STATIC(Akonadi::Collection, s_defaultParentCollection) uint Akonadi::qHash(const Akonadi::Item &item) { return ::qHash(item.id()); } namespace { struct ByTypeId { typedef bool result_type; bool operator()(const std::shared_ptr &lhs, const std::shared_ptr &rhs) const { return strcmp(lhs->typeName(), rhs->typeName()) < 0; } }; } // anon namespace typedef QHash, std::pair, ByTypeId>> LegacyMap; Q_GLOBAL_STATIC(LegacyMap, typeInfoToMetaTypeIdMap) Q_GLOBAL_STATIC_WITH_ARGS(QReadWriteLock, legacyMapLock, (QReadWriteLock::Recursive)) void Item::addToLegacyMappingImpl(const QString &mimeType, int spid, int mtid, std::unique_ptr &p) { if (!p.get()) { return; } const std::shared_ptr sp(p.release()); const QWriteLocker locker(legacyMapLock()); std::pair &item = (*typeInfoToMetaTypeIdMap())[mimeType][sp]; item.first = spid; item.second = mtid; } namespace { class MyReadLocker { public: explicit MyReadLocker(QReadWriteLock *rwl) : rwl(rwl) , locked(false) { if (rwl) { rwl->lockForRead(); } locked = true; } ~MyReadLocker() { if (rwl && locked) { rwl->unlock(); } } template std::shared_ptr makeUnlockingPointer(T *t) { if (t) { // the bind() doesn't throw, so if shared_ptr // construction line below, or anything else after it, // throws, we're unlocked. Mark us as such: locked = false; const std::shared_ptr result(t, [&](const void *) { rwl->unlock(); }); // from now on, the shared_ptr is responsible for unlocking return result; } else { return std::shared_ptr(); } } private: Q_DISABLE_COPY(MyReadLocker) QReadWriteLock *const rwl; bool locked; }; } static std::shared_ptr> lookupLegacyMapping(const QString &mimeType, Internal::PayloadBase *p) { MyReadLocker locker(legacyMapLock()); const LegacyMap::const_iterator hit = typeInfoToMetaTypeIdMap()->constFind(mimeType); if (hit == typeInfoToMetaTypeIdMap()->constEnd()) { return std::shared_ptr>(); } const std::shared_ptr sp(p, [ = ](Internal::PayloadBase *) { /*noop*/ }); const LegacyMap::mapped_type::const_iterator it = hit->find(sp); if (it == hit->end()) { return std::shared_ptr>(); } return locker.makeUnlockingPointer(&it->second); } // Change to something != RFC822 as soon as the server supports it const char Item::FullPayload[] = "RFC822"; Item::Item() : d_ptr(new ItemPrivate) { } Item::Item(Id id) : d_ptr(new ItemPrivate(id)) { } Item::Item(const QString &mimeType) : d_ptr(new ItemPrivate) { d_ptr->mMimeType = mimeType; } Item::Item(const Item &other) : d_ptr(other.d_ptr) { } Item::~Item() { } void Item::setId(Item::Id identifier) { d_ptr->mId = identifier; } Item::Id Item::id() const { return d_ptr->mId; } void Item::setRemoteId(const QString &id) { d_ptr->mRemoteId = id; } QString Item::remoteId() const { return d_ptr->mRemoteId; } void Item::setRemoteRevision(const QString &revision) { d_ptr->mRemoteRevision = revision; } QString Item::remoteRevision() const { return d_ptr->mRemoteRevision; } bool Item::isValid() const { return (d_ptr->mId >= 0); } bool Item::operator==(const Item &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::Item::operator!=(const Item &other) const { return (isValid() || other.isValid()) && (d_ptr->mId != other.d_ptr->mId); } Item &Item ::operator=(const Item &other) { if (this != &other) { d_ptr = other.d_ptr; } return *this; } bool Akonadi::Item::operator<(const Item &other) const { return d_ptr->mId < other.d_ptr->mId; } void Item::addAttribute(Attribute *attr) { - Q_ASSERT(attr); - Attribute *existing = d_ptr->mAttributes.value(attr->type()); - if (existing) { - if (attr == existing) { - return; - } - d_ptr->mAttributes.remove(attr->type()); - delete existing; - } - d_ptr->mAttributes.insert(attr->type(), attr); - ItemChangeLog::instance()->deletedAttributes(d_ptr).remove(attr->type()); + ItemChangeLog::instance()->attributeStorage(d_ptr).addAttribute(attr); } void Item::removeAttribute(const QByteArray &type) { - ItemChangeLog::instance()->deletedAttributes(d_ptr).insert(type); - delete d_ptr->mAttributes.take(type); + ItemChangeLog::instance()->attributeStorage(d_ptr).removeAttribute(type); } bool Item::hasAttribute(const QByteArray &type) const { - return d_ptr->mAttributes.contains(type); + return ItemChangeLog::instance()->attributeStorage(d_ptr).hasAttribute(type); } Attribute::List Item::attributes() const { - return d_ptr->mAttributes.values(); + return ItemChangeLog::instance()->attributeStorage(d_ptr).attributes(); } void Akonadi::Item::clearAttributes() { - ItemChangeLog *changelog = ItemChangeLog::instance(); - QSet &deletedAttributes = changelog->deletedAttributes(d_ptr); - for (Attribute *attr : qAsConst(d_ptr->mAttributes)) { - deletedAttributes.insert(attr->type()); - delete attr; - } - d_ptr->mAttributes.clear(); + ItemChangeLog::instance()->attributeStorage(d_ptr).clearAttributes(); } Attribute *Item::attribute(const QByteArray &type) const { - return d_ptr->mAttributes.value(type); + return ItemChangeLog::instance()->attributeStorage(d_ptr).attribute(type); } Collection &Item::parentCollection() { if (!d_ptr->mParent) { d_ptr->mParent = new Collection(); } return *(d_ptr->mParent); } Collection Item::parentCollection() const { if (!d_ptr->mParent) { return *(s_defaultParentCollection); } else { return *(d_ptr->mParent); } } void Item::setParentCollection(const Collection &parent) { delete d_ptr->mParent; d_ptr->mParent = new Collection(parent); } Item::Flags Item::flags() const { return d_ptr->mFlags; } void Item::setFlag(const QByteArray &name) { d_ptr->mFlags.insert(name); if (!d_ptr->mFlagsOverwritten) { Item::Flags &deletedFlags = ItemChangeLog::instance()->deletedFlags(d_ptr); auto iter = deletedFlags.find(name); if (iter != deletedFlags.end()) { deletedFlags.erase(iter); } else { ItemChangeLog::instance()->addedFlags(d_ptr).insert(name); } } } void Item::clearFlag(const QByteArray &name) { d_ptr->mFlags.remove(name); if (!d_ptr->mFlagsOverwritten) { Item::Flags &addedFlags = ItemChangeLog::instance()->addedFlags(d_ptr); auto iter = addedFlags.find(name); if (iter != addedFlags.end()) { addedFlags.erase(iter); } else { ItemChangeLog::instance()->deletedFlags(d_ptr).insert(name); } } } void Item::setFlags(const Flags &flags) { d_ptr->mFlags = flags; d_ptr->mFlagsOverwritten = true; } void Item::clearFlags() { d_ptr->mFlags.clear(); d_ptr->mFlagsOverwritten = true; } QDateTime Item::modificationTime() const { return d_ptr->mModificationTime; } void Item::setModificationTime(const QDateTime &datetime) { d_ptr->mModificationTime = datetime; } bool Item::hasFlag(const QByteArray &name) const { return d_ptr->mFlags.contains(name); } void Item::setTags(const Tag::List &list) { d_ptr->mTags = list; d_ptr->mTagsOverwritten = true; } void Item::setTag(const Tag &tag) { d_ptr->mTags << tag; if (!d_ptr->mTagsOverwritten) { Tag::List &deletedTags = ItemChangeLog::instance()->deletedTags(d_ptr); if (deletedTags.contains(tag)) { deletedTags.removeOne(tag); } else { ItemChangeLog::instance()->addedTags(d_ptr).push_back(tag); } } } void Item::clearTags() { d_ptr->mTags.clear(); d_ptr->mTagsOverwritten = true; } void Item::clearTag(const Tag &tag) { d_ptr->mTags.removeOne(tag); if (!d_ptr->mTagsOverwritten) { Tag::List &addedTags = ItemChangeLog::instance()->addedTags(d_ptr); if (addedTags.contains(tag)) { addedTags.removeOne(tag); } else { ItemChangeLog::instance()->deletedTags(d_ptr).push_back(tag); } } } bool Item::hasTag(const Tag &tag) const { return d_ptr->mTags.contains(tag); } Tag::List Item::tags() const { return d_ptr->mTags; } Relation::List Item::relations() const { return d_ptr->mRelations; } QSet Item::loadedPayloadParts() const { return ItemSerializer::parts(*this); } QByteArray Item::payloadData() const { int version = 0; QByteArray data; ItemSerializer::serialize(*this, FullPayload, data, version); return data; } void Item::setPayloadFromData(const QByteArray &data) { ItemSerializer::deserialize(*this, FullPayload, data, 0, ItemSerializer::Internal); } void Item::clearPayload() { d_ptr->mClearPayload = true; } int Item::revision() const { return d_ptr->mRevision; } void Item::setRevision(int rev) { d_ptr->mRevision = rev; } Collection::Id Item::storageCollectionId() const { return d_ptr->mCollectionId; } void Item::setStorageCollectionId(Collection::Id collectionId) { d_ptr->mCollectionId = collectionId; } QString Item::mimeType() const { return d_ptr->mMimeType; } void Item::setSize(qint64 size) { d_ptr->mSize = size; d_ptr->mSizeChanged = true; } qint64 Item::size() const { return d_ptr->mSize; } void Item::setMimeType(const QString &mimeType) { d_ptr->mMimeType = mimeType; } void Item::setGid(const QString &id) { d_ptr->mGid = id; } QString Item::gid() const { return d_ptr->mGid; } void Item::setVirtualReferences(const Collection::List &collections) { d_ptr->mVirtualReferences = collections; } Collection::List Item::virtualReferences() const { return d_ptr->mVirtualReferences; } bool Item::hasPayload() const { return d_ptr->hasMetaTypeId(-1); } QUrl Item::url(UrlType type) const { QUrlQuery query; query.addQueryItem(QStringLiteral("item"), QString::number(id())); if (type == UrlWithMimeType) { query.addQueryItem(QStringLiteral("type"), mimeType()); } QUrl url; url.setScheme(QStringLiteral("akonadi")); url.setQuery(query); return url; } Item Item::fromUrl(const QUrl &url) { if (url.scheme() != QLatin1String("akonadi")) { return Item(); } const QString itemStr = QUrlQuery(url).queryItemValue(QStringLiteral("item")); bool ok = false; Item::Id itemId = itemStr.toLongLong(&ok); if (!ok) { return Item(); } return Item(itemId); } namespace { class Dummy { }; } Q_GLOBAL_STATIC(Internal::Payload, dummyPayload) Internal::PayloadBase *Item::payloadBase() const { d_ptr->tryEnsureLegacyPayload(); if (d_ptr->mLegacyPayload) { return d_ptr->mLegacyPayload.get(); } else { return dummyPayload(); } } void ItemPrivate::tryEnsureLegacyPayload() const { if (!mLegacyPayload) { for (PayloadContainer::const_iterator it = mPayloads.begin(), end = mPayloads.end(); it != end; ++it) { if (lookupLegacyMapping(mMimeType, it->payload.get())) { mLegacyPayload = it->payload; // clones } } } } Internal::PayloadBase *Item::payloadBaseV2(int spid, int mtid) const { return d_ptr->payloadBaseImpl(spid, mtid); } namespace { class ConversionGuard { const bool old; bool &b; public: explicit ConversionGuard(bool &b) : old(b) , b(b) { b = true; } ~ConversionGuard() { b = old; } private: Q_DISABLE_COPY(ConversionGuard) }; } bool Item::ensureMetaTypeId(int mtid) const { // 0. Nothing there - nothing to convert from, either if (d_ptr->mPayloads.empty()) { return false; } // 1. Look whether we already have one: if (d_ptr->hasMetaTypeId(mtid)) { return true; } // recursion detection (shouldn't trigger, but does if the // serialiser plugins are acting funky): if (d_ptr->mConversionInProgress) { return false; } // 2. Try to create one by conversion from a different representation: try { const ConversionGuard guard(d_ptr->mConversionInProgress); Item converted = ItemSerializer::convert(*this, mtid); return d_ptr->movePayloadFrom(converted.d_ptr, mtid); } catch (const std::exception &e) { qCDebug(AKONADICORE_LOG) << "conversion threw:" << e.what(); return false; } catch (...) { qCDebug(AKONADICORE_LOG) << "conversion threw something not derived from std::exception: fix the program!"; return false; } } static QString format_type(int spid, int mtid) { return QStringLiteral("sp(%1)<%2>") .arg(spid).arg(QLatin1String(QMetaType::typeName(mtid))); } static QString format_types(const PayloadContainer &c) { QStringList result; result.reserve(c.size()); for (PayloadContainer::const_iterator it = c.begin(), end = c.end(); it != end; ++it) { result.push_back(format_type(it->sharedPointerId, it->metaTypeId)); } return result.join(QStringLiteral(", ")); } #if 0 QString Item::payloadExceptionText(int spid, int mtid) const { if (d_ptr->mPayloads.empty()) { return QStringLiteral("No payload set"); } else { return QStringLiteral("Wrong payload type (requested: %1; present: %2") .arg(format_type(spid, mtid), format_types(d_ptr->mPayloads)); } } #else void Item::throwPayloadException(int spid, int mtid) const { if (d_ptr->mPayloads.empty()) { qCDebug(AKONADICORE_LOG) << "Throwing PayloadException: No payload set"; throw PayloadException("No payload set"); } else { qCDebug(AKONADICORE_LOG) << "Throwing PayloadException: Wrong payload type (requested:" << format_type(spid, mtid) << "; present: " << format_types(d_ptr->mPayloads) << "), item mime type is" << mimeType(); throw PayloadException(QStringLiteral("Wrong payload type (requested: %1; present: %2)") .arg(format_type(spid, mtid), format_types(d_ptr->mPayloads))); } } #endif void Item::setPayloadBase(Internal::PayloadBase *p) { d_ptr->setLegacyPayloadBaseImpl(std::unique_ptr(p)); } void ItemPrivate::setLegacyPayloadBaseImpl(std::unique_ptr p) { if (const std::shared_ptr> pair = lookupLegacyMapping(mMimeType, p.get())) { std::unique_ptr clone; if (p.get()) { clone.reset(p->clone()); } setPayloadBaseImpl(pair->first, pair->second, p, false); mLegacyPayload.reset(clone.release()); } else { mPayloads.clear(); mLegacyPayload.reset(p.release()); } } void Item::setPayloadBaseV2(int spid, int mtid, std::unique_ptr &p) { d_ptr->setPayloadBaseImpl(spid, mtid, p, false); } void Item::addPayloadBaseVariant(int spid, int mtid, std::unique_ptr &p) const { d_ptr->setPayloadBaseImpl(spid, mtid, p, true); } QSet Item::cachedPayloadParts() const { return d_ptr->mCachedPayloadParts; } void Item::setCachedPayloadParts(const QSet &cachedParts) { d_ptr->mCachedPayloadParts = cachedParts; } QSet Item::availablePayloadParts() const { return ItemSerializer::availableParts(*this); } QVector Item::availablePayloadMetaTypeIds() const { QVector result; result.reserve(d_ptr->mPayloads.size()); // Stable Insertion Sort - N is typically _very_ low (1 or 2). for (PayloadContainer::const_iterator it = d_ptr->mPayloads.begin(), end = d_ptr->mPayloads.end(); it != end; ++it) { result.insert(std::upper_bound(result.begin(), result.end(), it->metaTypeId), it->metaTypeId); } return result; } void Item::setPayloadPath(const QString &filePath) { // Load payload from the external file, so that it's accessible via // Item::payload(). It internally calls setPayload(), which will clear // mPayloadPath, so we call it afterwards ItemSerializer::deserialize(*this, "RFC822", filePath.toUtf8(), 0, ItemSerializer::Foreign); d_ptr->mPayloadPath = filePath; } QString Item::payloadPath() const { return d_ptr->mPayloadPath; } void Item::apply(const Item &other) { if (mimeType() != other.mimeType() || id() != other.id()) { qCDebug(AKONADICORE_LOG) << "mimeType() = " << mimeType() << "; other.mimeType() = " << other.mimeType(); qCDebug(AKONADICORE_LOG) << "id() = " << id() << "; other.id() = " << other.id(); Q_ASSERT_X(false, "Item::apply", "mimetype or id missmatch"); } setRemoteId(other.remoteId()); setRevision(other.revision()); setRemoteRevision(other.remoteRevision()); setFlags(other.flags()); setTags(other.tags()); setModificationTime(other.modificationTime()); setSize(other.size()); setParentCollection(other.parentCollection()); setStorageCollectionId(other.storageCollectionId()); - QList attrs; - attrs.reserve(other.attributes().count()); - const Akonadi::Attribute::List lstAttrs = other.attributes(); - for (Attribute *attribute : lstAttrs) { - addAttribute(attribute->clone()); - attrs.append(attribute->type()); - } - - QMutableHashIterator it(d_ptr->mAttributes); - while (it.hasNext()) { - it.next(); - if (!attrs.contains(it.key())) { - delete it.value(); - it.remove(); - } - } + ItemChangeLog *changelog = ItemChangeLog::instance(); + changelog->attributeStorage(d_ptr) = changelog->attributeStorage(other.d_ptr); ItemSerializer::apply(*this, other); d_ptr->resetChangeLog(); // Must happen after payload update d_ptr->mPayloadPath = other.payloadPath(); } diff --git a/src/core/item_p.h b/src/core/item_p.h index 79f74aa72..6d1d580f9 100644 --- a/src/core/item_p.h +++ b/src/core/item_p.h @@ -1,468 +1,463 @@ /* Copyright (c) 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_ITEM_P_H #define AKONADI_ITEM_P_H #include #include #include "itempayloadinternals_p.h" #include "itemchangelog_p.h" #include "tag.h" #include #include #include #include namespace Akonadi { namespace _detail { template class clone_ptr { T *t; public: clone_ptr() : t(nullptr) { } explicit clone_ptr(T *t) : t(t) { } clone_ptr(const clone_ptr &other) : t(other.t ? other.t->clone() : nullptr) { } ~clone_ptr() { delete t; } clone_ptr &operator=(const clone_ptr &other) { if (this != &other) { clone_ptr copy(other); swap(copy); } return *this; } void swap(clone_ptr &other) { using std::swap; swap(t, other.t); } T *operator->() const { return get(); } T &operator*() const { assert(get() != nullptr); return *get(); } T *get() const { return t; } T *release() { T *const r = t; t = nullptr; return r; } void reset(T *other = nullptr) { delete t; t = other; } private: struct _save_bool { void f() { } }; typedef void (_save_bool::*save_bool)(); public: operator save_bool() const { return get() ? &_save_bool::f : nullptr; } }; template inline void swap(clone_ptr &lhs, clone_ptr &rhs) { lhs.swap(rhs); } template class VarLengthArray { QVarLengthArray impl; // ###should be replaced by self-written container that doesn't waste so much space public: typedef T value_type; typedef T *iterator; typedef const T *const_iterator; typedef T *pointer; typedef const T *const_pointer; typedef T &reference; typedef const T &const_reference; explicit VarLengthArray(int size = 0) : impl(size) { } // compiler-generated dtor, copy ctor, copy assignment are ok // swap() makes little sense void push_back(const T &t) { impl.append(t); } int capacity() const { return impl.capacity(); } void clear() { impl.clear(); } size_t size() const { return impl.count(); } bool empty() const { return impl.isEmpty(); } void pop_back() { return impl.removeLast(); } void reserve(size_t n) { impl.reserve(n); } void resize(size_t n) { impl.resize(n); } iterator begin() { return impl.data(); } iterator end() { return impl.data() + impl.size(); } const_iterator begin() const { return impl.data(); } const_iterator end() const { return impl.data() + impl.size(); } const_iterator cbegin() const { return begin(); } const_iterator cend() const { return end(); } reference front() { return *impl.data(); } reference back() { return *(impl.data() + impl.size()); } const_reference front() const { return *impl.data(); } const_reference back() const { return *(impl.data() + impl.size()); } reference operator[](size_t n) { return impl[n]; } const_reference operator[](size_t n) const { return impl[n]; } }; struct TypedPayload { clone_ptr payload; int sharedPointerId; int metaTypeId; }; struct BySharedPointerAndMetaTypeID { const int spid; const int mtid; BySharedPointerAndMetaTypeID(int spid, int mtid) : spid(spid) , mtid(mtid) { } bool operator()(const TypedPayload &tp) const { return (mtid == -1 || mtid == tp.metaTypeId) && (spid == -1 || spid == tp.sharedPointerId); } }; } } // namespace Akonadi namespace std { template <> inline void swap(Akonadi::_detail::TypedPayload &lhs, Akonadi::_detail::TypedPayload &rhs) { lhs.payload.swap(rhs.payload); swap(lhs.sharedPointerId, rhs.sharedPointerId); swap(lhs.metaTypeId, rhs.metaTypeId); } } namespace Akonadi { //typedef _detail::VarLengthArray<_detail::TypedPayload,2> PayloadContainer; typedef std::vector<_detail::TypedPayload> PayloadContainer; } namespace QtPrivate { // disable Q_FOREACH on PayloadContainer (b/c it likes to take copies and clone_ptr doesn't like that) template <> class QForeachContainer { }; } namespace Akonadi { /** * @internal */ class ItemPrivate : public QSharedData { public: explicit ItemPrivate(Item::Id id = -1) : QSharedData() , mRevision(-1) , mId(id) , mParent(nullptr) , mLegacyPayload() , mPayloads() , mCollectionId(-1) , mSize(0) , mModificationTime() , mFlagsOverwritten(false) , mTagsOverwritten(false) , mSizeChanged(false) , mClearPayload(false) , mConversionInProgress(false) { } ItemPrivate(const ItemPrivate &other) : QSharedData(other) , mParent(nullptr) { mId = other.mId; mRemoteId = other.mRemoteId; mRemoteRevision = other.mRemoteRevision; mPayloadPath = other.mPayloadPath; - Q_FOREACH (Attribute *attr, other.mAttributes) { - mAttributes.insert(attr->type(), attr->clone()); - } if (other.mParent) { mParent = new Collection(*(other.mParent)); } mFlags = other.mFlags; mRevision = other.mRevision; mTags = other.mTags; mRelations = other.mRelations; mSize = other.mSize; mModificationTime = other.mModificationTime; mMimeType = other.mMimeType; mLegacyPayload = other.mLegacyPayload; mPayloads = other.mPayloads; mFlagsOverwritten = other.mFlagsOverwritten; mSizeChanged = other.mSizeChanged; mCollectionId = other.mCollectionId; mClearPayload = other.mClearPayload; mVirtualReferences = other.mVirtualReferences; mGid = other.mGid; mCachedPayloadParts = other.mCachedPayloadParts; mTagsOverwritten = other.mTagsOverwritten; mConversionInProgress = false; ItemChangeLog *changelog = ItemChangeLog::instance(); changelog->addedFlags(this) = changelog->addedFlags(&other); changelog->deletedFlags(this) = changelog->deletedFlags(&other); changelog->addedTags(this) = changelog->addedTags(&other); changelog->deletedTags(this) = changelog->deletedTags(&other); - changelog->deletedAttributes(this) = changelog->deletedAttributes(&other); + changelog->attributeStorage(this) = changelog->attributeStorage(&other); } ~ItemPrivate() { - qDeleteAll(mAttributes); delete mParent; - ItemChangeLog::instance()->clearItemChangelog(this); + ItemChangeLog::instance()->removeItem(this); } void resetChangeLog() { mFlagsOverwritten = false; mSizeChanged = false; mTagsOverwritten = false; ItemChangeLog::instance()->clearItemChangelog(this); } bool hasMetaTypeId(int mtid) const { return std::find_if(mPayloads.cbegin(), mPayloads.cend(), _detail::BySharedPointerAndMetaTypeID(-1, mtid)) != mPayloads.cend(); } Internal::PayloadBase *payloadBaseImpl(int spid, int mtid) const { auto it = std::find_if(mPayloads.cbegin(), mPayloads.cend(), _detail::BySharedPointerAndMetaTypeID(spid, mtid)); return it == mPayloads.cend() ? nullptr : it->payload.get(); } bool movePayloadFrom(ItemPrivate *other, int mtid) const /*sic!*/ { assert(other); const size_t oldSize = mPayloads.size(); PayloadContainer &oPayloads = other->mPayloads; const _detail::BySharedPointerAndMetaTypeID matcher(-1, mtid); const size_t numMatching = std::count_if(oPayloads.begin(), oPayloads.end(), matcher); mPayloads.resize(oldSize + numMatching); using namespace std; // for swap() for (PayloadContainer::iterator dst = mPayloads.begin() + oldSize, src = oPayloads.begin(), end = oPayloads.end(); src != end; ++src) { if (matcher(*src)) { swap(*dst, *src); ++dst; } } return numMatching > 0; } #if 0 std::auto_ptr takePayloadBaseImpl(int spid, int mtid) { PayloadContainer::iterator it = std::find_if(mPayloads.begin(), mPayloads.end(), _detail::BySharedPointerAndMetaTypeID(spid, mtid)); if (it == mPayloads.end()) { return std::auto_ptr(); } std::rotate(it, it + 1, mPayloads.end()); std::auto_ptr result(it->payload.release()); mPayloads.pop_back(); return result; } #endif void setPayloadBaseImpl(int spid, int mtid, std::unique_ptr &p, bool add) const /*sic!*/ { if (!add) { mLegacyPayload.reset(); } if (!p.get()) { if (!add) { mPayloads.clear(); } return; } // if !add, delete all payload variants // (they're conversions of each other) mPayloadPath.clear(); mPayloads.resize(add ? mPayloads.size() + 1 : 1); _detail::TypedPayload &tp = mPayloads.back(); tp.payload.reset(p.release()); tp.sharedPointerId = spid; tp.metaTypeId = mtid; } void setLegacyPayloadBaseImpl(std::unique_ptr p); void tryEnsureLegacyPayload() const; // Utilise the 4-bytes padding from QSharedData int mRevision; Item::Id mId; QString mRemoteId; QString mRemoteRevision; mutable QString mPayloadPath; - QHash mAttributes; mutable Collection *mParent; mutable _detail::clone_ptr mLegacyPayload; mutable PayloadContainer mPayloads; Item::Flags mFlags; Tag::List mTags; Relation::List mRelations; Item::Id mCollectionId; Collection::List mVirtualReferences; // TODO: Maybe just use uint? Would save us another 8 bytes after reordering qint64 mSize; QDateTime mModificationTime; QString mMimeType; QString mGid; QSet mCachedPayloadParts; bool mFlagsOverwritten : 1; bool mTagsOverwritten : 1; bool mSizeChanged : 1; bool mClearPayload : 1; mutable bool mConversionInProgress; // 6 bytes padding here }; } #endif diff --git a/src/core/itemchangelog.cpp b/src/core/itemchangelog.cpp index e41ef4bc6..a3e300287 100644 --- a/src/core/itemchangelog.cpp +++ b/src/core/itemchangelog.cpp @@ -1,73 +1,83 @@ /* * Copyright 2015 Daniel Vrátil * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ #include "itemchangelog_p.h" using namespace Akonadi; ItemChangeLog *ItemChangeLog::sInstance = nullptr; ItemChangeLog *ItemChangeLog::instance() { if (!sInstance) { sInstance = new ItemChangeLog; } return sInstance; } ItemChangeLog::ItemChangeLog() { } Item::Flags &ItemChangeLog::addedFlags(const ItemPrivate *priv) { return m_addedFlags[const_cast(priv)]; } Item::Flags &ItemChangeLog::deletedFlags(const ItemPrivate *priv) { return m_deletedFlags[const_cast(priv)]; } Tag::List &ItemChangeLog::addedTags(const ItemPrivate *priv) { return m_addedTags[const_cast(priv)]; } Tag::List &ItemChangeLog::deletedTags(const ItemPrivate *priv) { return m_deletedTags[const_cast(priv)]; } -QSet &ItemChangeLog::deletedAttributes(const ItemPrivate *priv) +AttributeStorage &ItemChangeLog::attributeStorage(const ItemPrivate *priv) { - return m_deletedAttributes[const_cast(priv)]; + return m_attributeStorage[const_cast(priv)]; +} + +void ItemChangeLog::removeItem(const ItemPrivate *priv) +{ + ItemPrivate *p = const_cast(priv); + m_addedFlags.remove(p); + m_deletedFlags.remove(p); + m_addedTags.remove(p); + m_deletedTags.remove(p); + m_attributeStorage.remove(p); } void ItemChangeLog::clearItemChangelog(const ItemPrivate *priv) { ItemPrivate *p = const_cast(priv); m_addedFlags.remove(p); m_deletedFlags.remove(p); m_addedTags.remove(p); m_deletedTags.remove(p); - m_deletedAttributes.remove(p); + m_attributeStorage[p].resetChangeLog(); // keep the attributes } diff --git a/src/core/itemchangelog_p.h b/src/core/itemchangelog_p.h index b5884b240..78c6c2a3a 100644 --- a/src/core/itemchangelog_p.h +++ b/src/core/itemchangelog_p.h @@ -1,61 +1,63 @@ /* * Copyright 2015 Daniel Vrátil * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ #ifndef ITEMCHANGELOG_H #define ITEMCHANGELOG_H #include "item.h" #include "akonaditests_export.h" +#include "attributestorage_p.h" namespace Akonadi { class AKONADI_TESTS_EXPORT ItemChangeLog { public: static ItemChangeLog *instance(); Item::Flags &addedFlags(const ItemPrivate *priv); Item::Flags &deletedFlags(const ItemPrivate *priv); Tag::List &addedTags(const ItemPrivate *priv); Tag::List &deletedTags(const ItemPrivate *priv); - QSet &deletedAttributes(const ItemPrivate *priv); + AttributeStorage &attributeStorage(const ItemPrivate *priv); + void removeItem(const ItemPrivate *priv); void clearItemChangelog(const ItemPrivate *priv); private: explicit ItemChangeLog(); static ItemChangeLog *sInstance; QHash m_addedFlags; QHash m_deletedFlags; QHash m_addedTags; QHash m_deletedTags; - QHash> m_deletedAttributes; + QHash m_attributeStorage; }; } // namespace Akonadi #endif // ITEMCHANGELOG_H diff --git a/src/core/jobs/itemmodifyjob.cpp b/src/core/jobs/itemmodifyjob.cpp index 4313fd35b..28f9b7ee3 100644 --- a/src/core/jobs/itemmodifyjob.cpp +++ b/src/core/jobs/itemmodifyjob.cpp @@ -1,427 +1,428 @@ /* 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 "itemmodifyjob.h" #include "itemmodifyjob_p.h" #include "akonadicore_debug.h" #include "changemediator_p.h" #include "collection.h" #include "conflicthandler_p.h" #include "item_p.h" #include "itemserializer_p.h" #include "job_p.h" #include "protocolhelper_p.h" #include "gidextractor_p.h" #include #include using namespace Akonadi; ItemModifyJobPrivate::ItemModifyJobPrivate(ItemModifyJob *parent) : JobPrivate(parent) , mRevCheck(true) , mIgnorePayload(false) , mAutomaticConflictHandlingEnabled(true) , mSilent(false) { } void ItemModifyJobPrivate::setClean() { mOperations.insert(Dirty); } Protocol::PartMetaData ItemModifyJobPrivate::preparePart(const QByteArray &partName) { ProtocolHelper::PartNamespace ns; // dummy const QByteArray partLabel = ProtocolHelper::decodePartIdentifier(partName, ns); if (!mParts.remove(partLabel)) { // Error? return Protocol::PartMetaData(); } mPendingData.clear(); int version = 0; const auto item = mItems.first(); if (mForeignParts.contains(partLabel)) { mPendingData = item.d_ptr->mPayloadPath.toUtf8(); const auto size = QFile(item.d_ptr->mPayloadPath).size(); return Protocol::PartMetaData(partName, size, version, Protocol::PartMetaData::Foreign); } else { ItemSerializer::serialize(mItems.first(), partLabel, mPendingData, version); return Protocol::PartMetaData(partName, mPendingData.size(), version); } } void ItemModifyJobPrivate::conflictResolved() { Q_Q(ItemModifyJob); q->setError(KJob::NoError); q->setErrorText(QString()); q->emitResult(); } void ItemModifyJobPrivate::conflictResolveError(const QString &message) { Q_Q(ItemModifyJob); q->setErrorText(q->errorText() + message); q->emitResult(); } void ItemModifyJobPrivate::doUpdateItemRevision(Akonadi::Item::Id itemId, int oldRevision, int newRevision) { auto it = std::find_if(mItems.begin(), mItems.end(), [&itemId](const Item & item) -> bool { return item.id() == itemId; }); if (it != mItems.end() && (*it).revision() == oldRevision) { (*it).setRevision(newRevision); } } QString ItemModifyJobPrivate::jobDebuggingString() const { try { return Protocol::debugString(fullCommand()); } catch (const Exception &e) { return QString::fromUtf8(e.what()); } } void ItemModifyJobPrivate::setSilent(bool silent) { mSilent = silent; } ItemModifyJob::ItemModifyJob(const Item &item, QObject *parent) : Job(new ItemModifyJobPrivate(this), parent) { Q_D(ItemModifyJob); d->mItems.append(item); d->mParts = item.loadedPayloadParts(); d->mOperations.insert(ItemModifyJobPrivate::RemoteId); d->mOperations.insert(ItemModifyJobPrivate::RemoteRevision); if (!item.payloadPath().isEmpty()) { d->mForeignParts = ItemSerializer::allowedForeignParts(item); } } ItemModifyJob::ItemModifyJob(const Akonadi::Item::List &items, QObject *parent) : Job(new ItemModifyJobPrivate(this), parent) { Q_ASSERT(!items.isEmpty()); Q_D(ItemModifyJob); d->mItems = items; // same as single item ctor if (d->mItems.size() == 1) { d->mParts = items.first().loadedPayloadParts(); d->mOperations.insert(ItemModifyJobPrivate::RemoteId); d->mOperations.insert(ItemModifyJobPrivate::RemoteRevision); } else { d->mIgnorePayload = true; d->mRevCheck = false; } } ItemModifyJob::~ItemModifyJob() { } Protocol::ModifyItemsCommandPtr ItemModifyJobPrivate::fullCommand() const { auto cmd = Protocol::ModifyItemsCommandPtr::create(); const Akonadi::Item item = mItems.first(); for (int op : qAsConst(mOperations)) { switch (op) { case ItemModifyJobPrivate::RemoteId: if (!item.remoteId().isNull()) { cmd->setRemoteId(item.remoteId()); } break; case ItemModifyJobPrivate::Gid: { const QString gid = GidExtractor::getGid(item); if (!gid.isNull()) { cmd->setGid(gid); } break; } case ItemModifyJobPrivate::RemoteRevision: if (!item.remoteRevision().isNull()) { cmd->setRemoteRevision(item.remoteRevision()); } break; case ItemModifyJobPrivate::Dirty: cmd->setDirty(false); break; } } if (item.d_ptr->mClearPayload) { cmd->setInvalidateCache(true); } if (mSilent) { cmd->setNotify(true); } if (item.d_ptr->mFlagsOverwritten) { cmd->setFlags(item.flags()); } else { const auto addedFlags = ItemChangeLog::instance()->addedFlags(item.d_ptr); if (!addedFlags.isEmpty()) { cmd->setAddedFlags(addedFlags); } const auto deletedFlags = ItemChangeLog::instance()->deletedFlags(item.d_ptr); if (!deletedFlags.isEmpty()) { cmd->setRemovedFlags(deletedFlags); } } if (item.d_ptr->mTagsOverwritten) { cmd->setTags(ProtocolHelper::entitySetToScope(item.tags())); } else { const auto addedTags = ItemChangeLog::instance()->addedTags(item.d_ptr); if (!addedTags.isEmpty()) { cmd->setAddedTags(ProtocolHelper::entitySetToScope(addedTags)); } const auto deletedTags = ItemChangeLog::instance()->deletedTags(item.d_ptr); if (!deletedTags.isEmpty()) { cmd->setRemovedTags(ProtocolHelper::entitySetToScope(deletedTags)); } } if (!mParts.isEmpty()) { QSet parts; parts.reserve(mParts.size()); for (const QByteArray &part : qAsConst(mParts)) { parts.insert(ProtocolHelper::encodePartIdentifier(ProtocolHelper::PartPayload, part)); } cmd->setParts(parts); } - const auto deletedAttributes = ItemChangeLog::instance()->deletedAttributes(item.d_ptr); + const AttributeStorage &attributeStorage = ItemChangeLog::instance()->attributeStorage(item.d_ptr); + const QSet deletedAttributes = attributeStorage.deletedAttributes(); if (!deletedAttributes.isEmpty()) { QSet removedParts; removedParts.reserve(deletedAttributes.size()); for (const QByteArray &part : deletedAttributes) { removedParts.insert("ATR:" + part); } cmd->setRemovedParts(removedParts); } + if (attributeStorage.hasModifiedAttributes()) { + cmd->setAttributes(ProtocolHelper::attributesToProtocol(attributeStorage.modifiedAttributes())); + } // nothing to do if (cmd->modifiedParts() == Protocol::ModifyItemsCommand::None && mParts.isEmpty() - && item.attributes().isEmpty() && !cmd->invalidateCache()) { return cmd; } cmd->setItems(ProtocolHelper::entitySetToScope(mItems)); if (mRevCheck && item.revision() >= 0) { cmd->setOldRevision(item.revision()); } if (item.d_ptr->mSizeChanged) { cmd->setItemSize(item.size()); } - cmd->setAttributes(ProtocolHelper::attributesToProtocol(item)); - return cmd; } void ItemModifyJob::doStart() { Q_D(ItemModifyJob); Protocol::ModifyItemsCommandPtr command; try { command = d->fullCommand(); } catch (const Exception &e) { setError(Job::Unknown); setErrorText(QString::fromUtf8(e.what())); emitResult(); return; } if (command->modifiedParts() == Protocol::ModifyItemsCommand::None) { emitResult(); return; } d->sendCommand(command); } bool ItemModifyJob::doHandleResponse(qint64 tag, const Protocol::CommandPtr &response) { Q_D(ItemModifyJob); if (!response->isResponse() && response->type() == Protocol::Command::StreamPayload) { const auto &streamCmd = Protocol::cmdCast(response); auto streamResp = Protocol::StreamPayloadResponsePtr::create(); if (streamCmd.request() == Protocol::StreamPayloadCommand::MetaData) { streamResp->setMetaData(d->preparePart(streamCmd.payloadName())); } else { if (streamCmd.destination().isEmpty()) { streamResp->setData(d->mPendingData); } else { QByteArray error; if (!ProtocolHelper::streamPayloadToFile(streamCmd.destination(), d->mPendingData, error)) { // TODO: Error? } } } d->sendCommand(tag, streamResp); return false; } if (response->isResponse() && response->type() == Protocol::Command::ModifyItems) { const auto &resp = Protocol::cmdCast(response); if (resp.errorCode()) { setError(Unknown); setErrorText(resp.errorMessage()); return true; } if (resp.errorMessage().contains(QLatin1String("[LLCONFLICT]"))) { if (d->mAutomaticConflictHandlingEnabled) { ConflictHandler *handler = new ConflictHandler(ConflictHandler::LocalLocalConflict, this); handler->setConflictingItems(d->mItems.first(), d->mItems.first()); connect(handler, &ConflictHandler::conflictResolved, this, [d]() { d->conflictResolved(); }); connect(handler, &ConflictHandler::error, this, [d](const QString &str) { d->conflictResolveError(str); }); QMetaObject::invokeMethod(handler, &ConflictHandler::start, Qt::QueuedConnection); return true; } } if (resp.modificationDateTime().isValid()) { Item &item = d->mItems.first(); item.setModificationTime(resp.modificationDateTime()); item.d_ptr->resetChangeLog(); } else if (resp.id() > -1) { auto it = std::find_if(d->mItems.begin(), d->mItems.end(), [&resp](const Item & item) -> bool { return item.id() == resp.id(); }); if (it == d->mItems.end()) { qCDebug(AKONADICORE_LOG) << "Received STORE response for an item we did not modify: " << tag << Protocol::debugString(response); return true; } const int newRev = resp.newRevision(); const int oldRev = (*it).revision(); if (newRev >= oldRev && newRev >= 0) { d->itemRevisionChanged((*it).id(), oldRev, newRev); (*it).setRevision(newRev); } // There will be more responses, either for other modified items, // or the final response with invalid ID, but with modification datetime return false; } for (const Item &item : qAsConst(d->mItems)) { ChangeMediator::invalidateItem(item); } return true; } return Job::doHandleResponse(tag, response); } void ItemModifyJob::setIgnorePayload(bool ignore) { Q_D(ItemModifyJob); if (d->mIgnorePayload == ignore) { return; } d->mIgnorePayload = ignore; if (d->mIgnorePayload) { d->mParts = QSet(); } else { Q_ASSERT(!d->mItems.first().mimeType().isEmpty()); d->mParts = d->mItems.first().loadedPayloadParts(); } } bool ItemModifyJob::ignorePayload() const { Q_D(const ItemModifyJob); return d->mIgnorePayload; } void ItemModifyJob::setUpdateGid(bool update) { Q_D(ItemModifyJob); if (update && !updateGid()) { d->mOperations.insert(ItemModifyJobPrivate::Gid); } else { d->mOperations.remove(ItemModifyJobPrivate::Gid); } } bool ItemModifyJob::updateGid() const { Q_D(const ItemModifyJob); return d->mOperations.contains(ItemModifyJobPrivate::Gid); } void ItemModifyJob::disableRevisionCheck() { Q_D(ItemModifyJob); d->mRevCheck = false; } void ItemModifyJob::disableAutomaticConflictHandling() { Q_D(ItemModifyJob); d->mAutomaticConflictHandlingEnabled = false; } Item ItemModifyJob::item() const { Q_D(const ItemModifyJob); Q_ASSERT(d->mItems.size() == 1); return d->mItems.first(); } Item::List ItemModifyJob::items() const { Q_D(const ItemModifyJob); return d->mItems; } #include "moc_itemmodifyjob.cpp"