File indexing completed on 2025-01-05 04:46:55

0001 /*
0002     SPDX-FileCopyrightText: 2017 Daniel Vrátil <dvratil@kde.org>
0003 
0004     SPDX-License-Identifier: LGPL-2.0-or-later
0005 */
0006 
0007 #include "collectiontreecache.h"
0008 #include "akonadiserver_debug.h"
0009 #include "commandcontext.h"
0010 #include "selectquerybuilder.h"
0011 
0012 #include "private/scope_p.h"
0013 
0014 #include <QStack>
0015 
0016 #include <algorithm>
0017 #include <iterator>
0018 #include <map>
0019 #include <tuple>
0020 
0021 using namespace Akonadi::Server;
0022 
0023 namespace
0024 {
0025 enum Column {
0026     IdColumn,
0027     ParentIdColumn,
0028     RIDColumn,
0029     DisplayPrefColumn,
0030     SyncPrefColumn,
0031     IndexPrefColumn,
0032     EnabledColumn,
0033     ReferencedColumn,
0034     ResourceNameColumn,
0035 };
0036 }
0037 
0038 CollectionTreeCache::Node::Node()
0039     : parent(nullptr)
0040     , lruCounter(0)
0041     , id(-1)
0042 {
0043 }
0044 
0045 CollectionTreeCache::Node::Node(const Collection &col)
0046     : parent(nullptr)
0047     , id(col.id())
0048     , collection(col)
0049 
0050 {
0051 }
0052 
0053 CollectionTreeCache::Node::~Node()
0054 {
0055     qDeleteAll(children);
0056 }
0057 
0058 void CollectionTreeCache::Node::appendChild(Node *child)
0059 {
0060     child->parent = this;
0061     children.push_back(child);
0062 }
0063 
0064 void CollectionTreeCache::Node::removeChild(Node *child)
0065 {
0066     child->parent = nullptr;
0067     children.removeOne(child);
0068 }
0069 
0070 CollectionTreeCache::CollectionTreeCache()
0071     : AkThread(QStringLiteral("CollectionTreeCache"))
0072 {
0073 }
0074 
0075 CollectionTreeCache::~CollectionTreeCache()
0076 {
0077     quitThread();
0078 }
0079 
0080 void CollectionTreeCache::init()
0081 {
0082     AkThread::init();
0083 
0084     QWriteLocker locker(&mLock);
0085 
0086     mRoot = new Node;
0087     mRoot->id = 0;
0088     mRoot->parent = nullptr;
0089     mNodeLookup.insert(0, mRoot);
0090 
0091     SelectQueryBuilder<Collection> qb;
0092     qb.addSortColumn(Collection::idFullColumnName(), Query::Ascending);
0093 
0094     if (!qb.exec()) {
0095         qCCritical(AKONADISERVER_LOG) << "Failed to initialize Collection tree cache!";
0096         return;
0097     }
0098 
0099     // std's reverse iterators makes processing pendingNodes much easier.
0100     std::multimap<qint64 /* parentID */, Node *> pendingNodes;
0101     const auto collections = qb.result();
0102     for (const auto &col : collections) {
0103         auto parent = mNodeLookup.value(col.parentId(), nullptr);
0104 
0105         auto node = new Node(col);
0106 
0107         if (parent) {
0108             parent->appendChild(node);
0109             mNodeLookup.insert(node->id, node);
0110         } else {
0111             pendingNodes.insert({col.parentId(), node});
0112         }
0113     }
0114 
0115     if (!pendingNodes.empty()) {
0116         int inserts = 0;
0117 
0118         // Thanks to the SQL results being ordered by ID we already inserted most
0119         // of the nodes in the loop above and here we only handle the rare cases
0120         // when child has ID lower than its parent, i.e. moved collections.
0121         //
0122         // In theory we may need multiple passes to insert all nodes, but we can
0123         // optimize by iterating the ordered map in reverse order. This way we find
0124         // the parents with higher IDs first and their children later, thus needing
0125         // fewer passes to process all the nodes.
0126         auto it = pendingNodes.rbegin();
0127         while (!pendingNodes.empty()) {
0128             auto parent = mNodeLookup.value(it->first, nullptr);
0129             if (!parent) {
0130                 // Parent of this node is still somewhere in pendingNodes, let's skip
0131                 // this one for now and try again in the next iteration
0132                 ++it;
0133             } else {
0134                 auto node = it->second;
0135                 parent->appendChild(node);
0136                 mNodeLookup.insert(node->id, node);
0137                 pendingNodes.erase((++it).base());
0138                 ++inserts;
0139             }
0140 
0141             if (it == pendingNodes.rend()) {
0142                 if (Q_UNLIKELY(inserts == 0)) {
0143                     // This means we iterated through the entire pendingNodes but did
0144                     // not manage to insert any collection to the node tree. That
0145                     // means that there is an unreferenced collection in the database
0146                     // that points to an invalid parent (or has a parent which points
0147                     // to an invalid parent etc.). This should not happen
0148                     // anymore with DB constraints, but who knows...
0149                     qCWarning(AKONADISERVER_LOG) << "Found unreferenced Collections!";
0150                     auto unref = pendingNodes.begin();
0151                     while (unref != pendingNodes.end()) {
0152                         qCWarning(AKONADISERVER_LOG) << "\tCollection" << unref->second->id << "references an invalid parent" << it->first;
0153                         // Remove the unreferenced collection from the map
0154                         delete unref->second;
0155                         unref = pendingNodes.erase(unref);
0156                     }
0157                     qCWarning(AKONADISERVER_LOG) << "Please run \"akonadictl fsck\" to correct the inconsistencies!";
0158                     // pendingNodes should be empty now so break the loop here
0159                     break;
0160                 }
0161 
0162                 it = pendingNodes.rbegin();
0163                 inserts = 0;
0164             }
0165         }
0166     }
0167 
0168     Q_ASSERT(pendingNodes.empty());
0169     Q_ASSERT(mNodeLookup.size() == collections.count() + 1 /* root */);
0170     // Now we should have a complete tree built, yay!
0171 }
0172 
0173 void CollectionTreeCache::quit()
0174 {
0175     delete mRoot;
0176 
0177     AkThread::quit();
0178 }
0179 
0180 void CollectionTreeCache::collectionAdded(const Collection &col)
0181 {
0182     QWriteLocker locker(&mLock);
0183 
0184     auto parent = mNodeLookup.value(col.parentId(), nullptr);
0185     if (!parent) {
0186         qCWarning(AKONADISERVER_LOG) << "Received a new collection (" << col.id() << ") with unknown parent (" << col.parentId() << ")";
0187         return;
0188     }
0189 
0190     auto node = new Node(col);
0191     parent->appendChild(node);
0192     mNodeLookup.insert(node->id, node);
0193 }
0194 
0195 void CollectionTreeCache::collectionChanged(const Collection &col)
0196 {
0197     QWriteLocker locker(&mLock);
0198 
0199     auto node = mNodeLookup.value(col.id(), nullptr);
0200     if (!node) {
0201         qCWarning(AKONADISERVER_LOG) << "Received an unknown changed collection (" << col.id() << ")";
0202         return;
0203     }
0204 
0205     // Only update non-expired nodes
0206     if (node->collection.isValid()) {
0207         node->collection = col;
0208     }
0209 }
0210 
0211 void CollectionTreeCache::collectionMoved(const Collection &col)
0212 {
0213     QWriteLocker locker(&mLock);
0214 
0215     auto node = mNodeLookup.value(col.id(), nullptr);
0216     if (!node) {
0217         qCWarning(AKONADISERVER_LOG) << "Received an unknown moved collection (" << col.id() << ")";
0218         return;
0219     }
0220     auto oldParent = node->parent;
0221 
0222     auto newParent = mNodeLookup.value(col.parentId(), nullptr);
0223     if (!newParent) {
0224         qCWarning(AKONADISERVER_LOG) << "Received a moved collection (" << col.id() << ") with an unknown move destination (" << col.parentId() << ")";
0225         return;
0226     }
0227 
0228     oldParent->removeChild(node);
0229     newParent->appendChild(node);
0230     if (node->collection.isValid()) {
0231         node->collection = col;
0232     }
0233 }
0234 
0235 void CollectionTreeCache::collectionRemoved(const Collection &col)
0236 {
0237     QWriteLocker locker(&mLock);
0238 
0239     auto node = mNodeLookup.value(col.id(), nullptr);
0240     if (!node) {
0241         qCWarning(AKONADISERVER_LOG) << "Received unknown removed collection (" << col.id() << ")";
0242         return;
0243     }
0244 
0245     auto parent = node->parent;
0246     parent->removeChild(node);
0247     mNodeLookup.remove(node->id);
0248     delete node;
0249 }
0250 
0251 CollectionTreeCache::Node *CollectionTreeCache::findNode(const QString &rid, const QString &resource) const
0252 {
0253     QReadLocker locker(&mLock);
0254 
0255     // Find a subtree that belongs to the respective resource
0256     auto root = std::find_if(mRoot->children.cbegin(), mRoot->children.cend(), [resource](Node *node) {
0257         // resource().name() may seem expensive, but really
0258         // there are only few resources and they are all cached
0259         // in memory.
0260         return node->collection.resource().name() == resource;
0261     });
0262     if (root == mRoot->children.cend()) {
0263         return nullptr;
0264     }
0265 
0266     return findNode((*root), [rid](Node *node) {
0267         return node->collection.remoteId() == rid;
0268     });
0269 }
0270 
0271 QList<Collection> CollectionTreeCache::retrieveCollections(CollectionTreeCache::Node *root, int depth, int ancestorDepth) const
0272 {
0273     QReadLocker locker(&mLock);
0274 
0275     QList<Node *> nodes;
0276     // Get all ancestors for root
0277     Node *parent = root->parent;
0278     for (int i = 0; i < ancestorDepth && parent != nullptr; ++i) {
0279         nodes.push_back(parent);
0280         parent = parent->parent;
0281     }
0282 
0283     struct StackTuple {
0284         Node *node;
0285         int depth;
0286     };
0287     QStack<StackTuple> stack;
0288     stack.push({root, 0});
0289     while (!stack.isEmpty()) {
0290         auto c = stack.pop();
0291         if (c.depth > depth) {
0292             break;
0293         }
0294 
0295         if (c.node->id > 0) { // skip root
0296             nodes.push_back(c.node);
0297         }
0298 
0299         for (auto child : std::as_const(c.node->children)) {
0300             stack.push({child, c.depth + 1});
0301         }
0302     }
0303 
0304     QList<Collection> cols;
0305     QList<Node *> missing;
0306     for (auto node : nodes) {
0307         if (node->collection.isValid()) {
0308             cols.push_back(node->collection);
0309         } else {
0310             missing.push_back(node);
0311         }
0312     }
0313 
0314     if (!missing.isEmpty()) {
0315         // TODO: Check if no-one else is currently retrieving the same collections
0316         SelectQueryBuilder<Collection> qb;
0317         Query::Condition cond(Query::Or);
0318         for (auto node : std::as_const(missing)) {
0319             cond.addValueCondition(Collection::idFullColumnName(), Query::Equals, node->id);
0320         }
0321         qb.addCondition(cond);
0322         if (!qb.exec()) {
0323             qCWarning(AKONADISERVER_LOG) << "Failed to retrieve collections from the database";
0324             return {};
0325         }
0326 
0327         const auto results = qb.result();
0328         if (results.size() != missing.size()) {
0329             qCWarning(AKONADISERVER_LOG) << "Could not obtain all missing collections! Node tree refers to a non-existent collection";
0330         }
0331 
0332         cols += results;
0333 
0334         // Relock for write
0335         // TODO: Needs a better lock-upgrade mechanism
0336         locker.unlock();
0337         QWriteLocker wLocker(&mLock);
0338         for (auto node : std::as_const(missing)) {
0339             auto it = std::find_if(results.cbegin(), results.cend(), [node](const Collection &col) {
0340                 return node->id == col.id();
0341             });
0342             if (Q_UNLIKELY(it == results.cend())) {
0343                 continue;
0344             }
0345 
0346             node->collection = *it;
0347         }
0348     }
0349 
0350     return cols;
0351 }
0352 
0353 QList<Collection>
0354 CollectionTreeCache::retrieveCollections(const Scope &scope, int depth, int ancestorDepth, const QString &resource, CommandContext *context) const
0355 {
0356     if (scope.isEmpty()) {
0357         return retrieveCollections(mRoot, depth, ancestorDepth);
0358     } else if (scope.scope() == Scope::Rid) {
0359         // Caller must ensure!
0360         Q_ASSERT(!resource.isEmpty() || (context && context->resource().isValid()));
0361 
0362         Node *node = nullptr;
0363         if (!resource.isEmpty()) {
0364             node = findNode(scope.rid(), resource);
0365         } else if (context && context->resource().isValid()) {
0366             node = findNode(scope.rid(), context->resource().name());
0367         } else {
0368             return {};
0369         }
0370 
0371         if (Q_LIKELY(node)) {
0372             return retrieveCollections(node, depth, ancestorDepth);
0373         }
0374     } else if (scope.scope() == Scope::Uid) {
0375         Node *node = mNodeLookup.value(scope.uid());
0376         if (Q_LIKELY(node)) {
0377             return retrieveCollections(node, depth, ancestorDepth);
0378         }
0379     }
0380 
0381     return {};
0382 }
0383 
0384 #include "moc_collectiontreecache.cpp"