File indexing completed on 2024-06-23 05:07:00

0001 /*
0002     SPDX-FileCopyrightText: 2007 Volker Krause <vkrause@kde.org>
0003 
0004     SPDX-License-Identifier: LGPL-2.0-or-later
0005 */
0006 
0007 #include "collectionfetchhandler.h"
0008 #include "akonadiserver_debug.h"
0009 
0010 #include "connection.h"
0011 #include "handlerhelper.h"
0012 #include "storage/collectionqueryhelper.h"
0013 #include "storage/datastore.h"
0014 #include "storage/selectquerybuilder.h"
0015 
0016 #include "private/scope_p.h"
0017 
0018 using namespace Akonadi;
0019 using namespace Akonadi::Server;
0020 
0021 template<typename T>
0022 static bool intersect(const QList<typename T::Id> &l1, const QList<T> &l2)
0023 {
0024     for (const T &e2 : l2) {
0025         if (l1.contains(e2.id())) {
0026             return true;
0027         }
0028     }
0029     return false;
0030 }
0031 
0032 CollectionFetchHandler::CollectionFetchHandler(AkonadiServer &akonadi)
0033     : Handler(akonadi)
0034 {
0035 }
0036 
0037 QStack<Collection> CollectionFetchHandler::ancestorsForCollection(const Collection &col)
0038 {
0039     if (mAncestorDepth <= 0) {
0040         return QStack<Collection>();
0041     }
0042     QStack<Collection> ancestors;
0043     Collection parent = col;
0044     for (int i = 0; i < mAncestorDepth; ++i) {
0045         if (parent.parentId() == 0) {
0046             break;
0047         }
0048         if (mAncestors.contains(parent.parentId())) {
0049             parent = mAncestors.value(parent.parentId());
0050         } else {
0051             parent = mCollections.value(parent.parentId());
0052         }
0053         if (!parent.isValid()) {
0054             qCWarning(AKONADISERVER_LOG) << "Found an invalid parent in ancestors of Collection" << col.name() << "(ID:" << col.id() << ")";
0055             throw HandlerException("Found invalid parent in ancestors");
0056         }
0057         ancestors.prepend(parent);
0058     }
0059     return ancestors;
0060 }
0061 
0062 CollectionAttribute::List CollectionFetchHandler::getAttributes(const Collection &col, const QSet<QByteArray> &filter)
0063 {
0064     CollectionAttribute::List attributes;
0065     auto it = mCollectionAttributes.find(col.id());
0066     while (it != mCollectionAttributes.end() && it.key() == col.id()) {
0067         if (filter.isEmpty() || filter.contains(it.value().type())) {
0068             attributes << it.value();
0069         }
0070         ++it;
0071     }
0072 
0073     {
0074         CollectionAttribute attr;
0075         attr.setType(AKONADI_PARAM_ENABLED);
0076         attr.setValue(col.enabled() ? "TRUE" : "FALSE");
0077         attributes << attr;
0078     }
0079 
0080     return attributes;
0081 }
0082 
0083 void CollectionFetchHandler::listCollection(const Collection &root,
0084                                             const QStack<Collection> &ancestors,
0085                                             const QStringList &mimeTypes,
0086                                             const CollectionAttribute::List &attributes)
0087 {
0088     QStack<CollectionAttribute::List> ancestorAttributes;
0089     // backwards compatibility, collectionToByteArray will automatically fall-back to id + remoteid
0090     if (!mAncestorAttributes.isEmpty()) {
0091         ancestorAttributes.reserve(ancestors.size());
0092         for (const Collection &col : ancestors) {
0093             ancestorAttributes.push(getAttributes(col, mAncestorAttributes));
0094         }
0095     }
0096 
0097     // write out collection details
0098     Collection dummy = root;
0099     storageBackend()->activeCachePolicy(dummy);
0100 
0101     sendResponse(
0102         HandlerHelper::fetchCollectionsResponse(akonadi(), dummy, attributes, mIncludeStatistics, mAncestorDepth, ancestors, ancestorAttributes, mimeTypes));
0103 }
0104 
0105 static Query::Condition filterCondition(const QString &column)
0106 {
0107     Query::Condition orCondition(Query::Or);
0108     orCondition.addValueCondition(column, Query::Equals, static_cast<int>(Collection::True));
0109     Query::Condition andCondition(Query::And);
0110     andCondition.addValueCondition(column, Query::Equals, static_cast<int>(Collection::Undefined));
0111     andCondition.addValueCondition(Collection::enabledFullColumnName(), Query::Equals, true);
0112     orCondition.addCondition(andCondition);
0113     return orCondition;
0114 }
0115 
0116 bool CollectionFetchHandler::checkFilterCondition(const Collection &col) const
0117 {
0118     // Don't include the collection when only looking for enabled collections
0119     if (mEnabledCollections && !col.enabled()) {
0120         return false;
0121     }
0122     // Don't include the collection when only looking for collections to display/index/sync
0123     if (mCollectionsToDisplay && (((col.displayPref() == Collection::Undefined) && !col.enabled()) || (col.displayPref() == Collection::False))) {
0124         return false;
0125     }
0126     if (mCollectionsToIndex && (((col.indexPref() == Collection::Undefined) && !col.enabled()) || (col.indexPref() == Collection::False))) {
0127         return false;
0128     }
0129     // Single collection sync will still work since that is using a base fetch
0130     if (mCollectionsToSynchronize && (((col.syncPref() == Collection::Undefined) && !col.enabled()) || (col.syncPref() == Collection::False))) {
0131         return false;
0132     }
0133     return true;
0134 }
0135 
0136 static QSqlQuery getAttributeQuery(const QVariantList &ids, const QSet<QByteArray> &requestedAttributes)
0137 {
0138     QueryBuilder qb(CollectionAttribute::tableName());
0139 
0140     qb.addValueCondition(CollectionAttribute::collectionIdFullColumnName(), Query::In, ids);
0141 
0142     qb.addColumn(CollectionAttribute::collectionIdFullColumnName());
0143     qb.addColumn(CollectionAttribute::typeFullColumnName());
0144     qb.addColumn(CollectionAttribute::valueFullColumnName());
0145 
0146     if (!requestedAttributes.isEmpty()) {
0147         QVariantList attributes;
0148         attributes.reserve(requestedAttributes.size());
0149         for (const QByteArray &type : requestedAttributes) {
0150             attributes << type;
0151         }
0152         qb.addValueCondition(CollectionAttribute::typeFullColumnName(), Query::In, attributes);
0153     }
0154 
0155     qb.addSortColumn(CollectionAttribute::collectionIdFullColumnName(), Query::Ascending);
0156 
0157     if (!qb.exec()) {
0158         throw HandlerException("Unable to retrieve attributes for listing");
0159     }
0160     return qb.query();
0161 }
0162 
0163 void CollectionFetchHandler::retrieveAttributes(const QVariantList &collectionIds)
0164 {
0165     // We are querying for the attributes in batches because something can't handle WHERE IN queries with sets larger than 999
0166     int start = 0;
0167     const int size = 999;
0168     while (start < collectionIds.size()) {
0169         const QVariantList ids = collectionIds.mid(start, size);
0170         QSqlQuery attributeQuery = getAttributeQuery(ids, mAncestorAttributes);
0171         while (attributeQuery.next()) {
0172             CollectionAttribute attr;
0173             attr.setType(attributeQuery.value(1).toByteArray());
0174             attr.setValue(attributeQuery.value(2).toByteArray());
0175             // qCDebug(AKONADISERVER_LOG) << "found attribute " << attr.type() << attr.value();
0176             mCollectionAttributes.insert(attributeQuery.value(0).toLongLong(), attr);
0177         }
0178         attributeQuery.finish();
0179         start += size;
0180     }
0181 }
0182 
0183 static QSqlQuery getMimeTypeQuery(const QVariantList &ids)
0184 {
0185     QueryBuilder qb(CollectionMimeTypeRelation::tableName());
0186 
0187     qb.addJoin(QueryBuilder::LeftJoin, MimeType::tableName(), MimeType::idFullColumnName(), CollectionMimeTypeRelation::rightFullColumnName());
0188     qb.addValueCondition(CollectionMimeTypeRelation::leftFullColumnName(), Query::In, ids);
0189 
0190     qb.addColumn(CollectionMimeTypeRelation::leftFullColumnName());
0191     qb.addColumn(CollectionMimeTypeRelation::rightFullColumnName());
0192     qb.addColumn(MimeType::nameFullColumnName());
0193     qb.addSortColumn(CollectionMimeTypeRelation::leftFullColumnName(), Query::Ascending);
0194 
0195     if (!qb.exec()) {
0196         throw HandlerException("Unable to retrieve mimetypes for listing");
0197     }
0198     return qb.query();
0199 }
0200 
0201 void CollectionFetchHandler::retrieveCollections(const Collection &topParent, int depth)
0202 {
0203     /*
0204      * Retrieval of collections:
0205      * The aim is to reduce the amount of queries as much as possible, as this has the largest performance impact for large queries.
0206      * * First all collections that match the given criteria are queried
0207      * * We then filter the false positives:
0208      * ** all collections out that are not part of the tree we asked for are filtered
0209      * * Finally we complete the tree by adding missing collections
0210      *
0211      * Mimetypes and attributes are also retrieved in single queries to avoid spawning two queries per collection (the N+1 problem).
0212      * Note that we're not querying attributes and mimetypes for the collections that are only included to complete the tree,
0213      * this results in no items being queried for those collections.
0214      */
0215 
0216     const qint64 parentId = topParent.isValid() ? topParent.id() : 0;
0217     {
0218         SelectQueryBuilder<Collection> qb;
0219 
0220         if (depth == 0) {
0221             qb.addValueCondition(Collection::idFullColumnName(), Query::Equals, parentId);
0222         } else if (depth == 1) {
0223             if (topParent.isValid()) {
0224                 qb.addValueCondition(Collection::parentIdFullColumnName(), Query::Equals, parentId);
0225             } else {
0226                 qb.addValueCondition(Collection::parentIdFullColumnName(), Query::Is, QVariant());
0227             }
0228         } else {
0229             if (topParent.isValid()) {
0230                 qb.addValueCondition(Collection::resourceIdFullColumnName(), Query::Equals, topParent.resourceId());
0231             } else {
0232                 // Gimme gimme gimme...everything!
0233             }
0234         }
0235 
0236         // Base listings should succeed always
0237         if (depth != 0) {
0238             if (mCollectionsToSynchronize) {
0239                 qb.addCondition(filterCondition(Collection::syncPrefFullColumnName()));
0240             } else if (mCollectionsToDisplay) {
0241                 qb.addCondition(filterCondition(Collection::displayPrefFullColumnName()));
0242             } else if (mCollectionsToIndex) {
0243                 qb.addCondition(filterCondition(Collection::indexPrefFullColumnName()));
0244             } else if (mEnabledCollections) {
0245                 qb.addValueCondition(Collection::enabledFullColumnName(), Query::Equals, true);
0246             }
0247             if (mResource.isValid()) {
0248                 qb.addValueCondition(Collection::resourceIdFullColumnName(), Query::Equals, mResource.id());
0249             }
0250 
0251             if (!mMimeTypes.isEmpty()) {
0252                 qb.addJoin(QueryBuilder::LeftJoin,
0253                            CollectionMimeTypeRelation::tableName(),
0254                            CollectionMimeTypeRelation::leftColumn(),
0255                            Collection::idFullColumnName());
0256                 QVariantList mimeTypeFilter;
0257                 mimeTypeFilter.reserve(mMimeTypes.size());
0258                 for (MimeType::Id mtId : std::as_const(mMimeTypes)) {
0259                     mimeTypeFilter << mtId;
0260                 }
0261                 qb.addValueCondition(CollectionMimeTypeRelation::rightColumn(), Query::In, mimeTypeFilter);
0262                 qb.addGroupColumn(Collection::idFullColumnName());
0263             }
0264         }
0265 
0266         if (!qb.exec()) {
0267             throw HandlerException("Unable to retrieve collection for listing");
0268         }
0269         const auto result{qb.result()};
0270         for (const Collection &col : result) {
0271             mCollections.insert(col.id(), col);
0272         }
0273     }
0274 
0275     // Post filtering that we couldn't do as part of the sql query
0276     if (depth > 0) {
0277         auto it = mCollections.begin();
0278         while (it != mCollections.end()) {
0279             if (topParent.isValid()) {
0280                 // Check that each collection is linked to the root collection
0281                 bool foundParent = false;
0282                 // We iterate over parents to link it to topParent if possible
0283                 Collection::Id id = it->parentId();
0284                 while (id > 0) {
0285                     if (id == parentId) {
0286                         foundParent = true;
0287                         break;
0288                     }
0289                     Collection col = mCollections.value(id);
0290                     if (!col.isValid()) {
0291                         col = Collection::retrieveById(id);
0292                     }
0293                     id = col.parentId();
0294                 }
0295                 if (!foundParent) {
0296                     it = mCollections.erase(it);
0297                     continue;
0298                 }
0299             }
0300             ++it;
0301         }
0302     }
0303 
0304     QVariantList mimeTypeIds;
0305     QVariantList attributeIds;
0306     QVariantList ancestorIds;
0307     const auto collectionSize{mCollections.size()};
0308     mimeTypeIds.reserve(collectionSize);
0309     attributeIds.reserve(collectionSize);
0310     // We'd only require the non-leaf collections, but we don't know which those are, so we take all.
0311     ancestorIds.reserve(collectionSize);
0312     for (auto it = mCollections.cbegin(), end = mCollections.cend(); it != end; ++it) {
0313         mimeTypeIds << it.key();
0314         attributeIds << it.key();
0315         ancestorIds << it.key();
0316     }
0317 
0318     if (mAncestorDepth > 0 && topParent.isValid()) {
0319         // unless depth is 0 the base collection is not part of the listing
0320         mAncestors.insert(topParent.id(), topParent);
0321         ancestorIds << topParent.id();
0322         // We need to retrieve additional ancestors to what we already have in the tree
0323         Collection parent = topParent;
0324         for (int i = 0; i < mAncestorDepth; ++i) {
0325             if (parent.parentId() == 0) {
0326                 break;
0327             }
0328             parent = parent.parent();
0329             mAncestors.insert(parent.id(), parent);
0330             // We also require the attributes
0331             ancestorIds << parent.id();
0332         }
0333     }
0334 
0335     QSet<qint64> missingCollections;
0336     if (depth > 0) {
0337         for (const Collection &col : std::as_const(mCollections)) {
0338             if (col.parentId() != parentId && !mCollections.contains(col.parentId())) {
0339                 missingCollections.insert(col.parentId());
0340             }
0341         }
0342     }
0343 
0344     /*
0345     QSet<qint64> knownIds;
0346     for (const Collection &col : mCollections) {
0347         knownIds.insert(col.id());
0348     }
0349     qCDebug(AKONADISERVER_LOG) << "HAS:" << knownIds;
0350     qCDebug(AKONADISERVER_LOG) << "MISSING:" << missingCollections;
0351     */
0352 
0353     // Fetch missing collections that are part of the tree
0354     while (!missingCollections.isEmpty()) {
0355         SelectQueryBuilder<Collection> qb;
0356         QVariantList ids;
0357         ids.reserve(missingCollections.size());
0358         for (qint64 id : std::as_const(missingCollections)) {
0359             ids << id;
0360         }
0361         qb.addValueCondition(Collection::idFullColumnName(), Query::In, ids);
0362         if (!qb.exec()) {
0363             throw HandlerException("Unable to retrieve collections for listing");
0364         }
0365 
0366         missingCollections.clear();
0367         const auto missingCols = qb.result();
0368         for (const Collection &missingCol : missingCols) {
0369             mCollections.insert(missingCol.id(), missingCol);
0370             ancestorIds << missingCol.id();
0371             attributeIds << missingCol.id();
0372             mimeTypeIds << missingCol.id();
0373             // We have to do another round if the parents parent is missing
0374             if (missingCol.parentId() != parentId && !mCollections.contains(missingCol.parentId())) {
0375                 missingCollections.insert(missingCol.parentId());
0376             }
0377         }
0378     }
0379 
0380     // Since we don't know when we'll need the ancestor attributes, we have to fetch them all together.
0381     // The alternative would be to query for each collection which would reintroduce the N+1 query performance problem.
0382     if (!mAncestorAttributes.isEmpty()) {
0383         retrieveAttributes(ancestorIds);
0384     }
0385 
0386     // We are querying in batches because something can't handle WHERE IN queries with sets larger than 999
0387     const int querySizeLimit = 999;
0388     int mimetypeQueryStart = 0;
0389     int attributeQueryStart = 0;
0390     QSqlQuery mimeTypeQuery(storageBackend()->database());
0391     QSqlQuery attributeQuery(storageBackend()->database());
0392     auto it = mCollections.begin();
0393     while (it != mCollections.end()) {
0394         const Collection col = it.value();
0395 
0396         QStringList mimeTypes;
0397         {
0398             // Get new query if necessary
0399             if (!mimeTypeQuery.isValid() && mimetypeQueryStart < mimeTypeIds.size()) {
0400                 const QVariantList ids = mimeTypeIds.mid(mimetypeQueryStart, querySizeLimit);
0401                 mimetypeQueryStart += querySizeLimit;
0402                 mimeTypeQuery = getMimeTypeQuery(ids);
0403                 mimeTypeQuery.next(); // place at first record
0404             }
0405 
0406             while (mimeTypeQuery.isValid() && mimeTypeQuery.value(0).toLongLong() < col.id()) {
0407                 if (!mimeTypeQuery.next()) {
0408                     break;
0409                 }
0410             }
0411             // Advance query while a mimetype for this collection is returned
0412             while (mimeTypeQuery.isValid() && mimeTypeQuery.value(0).toLongLong() == col.id()) {
0413                 mimeTypes << mimeTypeQuery.value(2).toString();
0414                 if (!mimeTypeQuery.next()) {
0415                     break;
0416                 }
0417             }
0418         }
0419 
0420         CollectionAttribute::List attributes;
0421         {
0422             // Get new query if necessary
0423             if (!attributeQuery.isValid() && attributeQueryStart < attributeIds.size()) {
0424                 const QVariantList ids = attributeIds.mid(attributeQueryStart, querySizeLimit);
0425                 attributeQueryStart += querySizeLimit;
0426                 attributeQuery = getAttributeQuery(ids, QSet<QByteArray>());
0427                 attributeQuery.next(); // place at first record
0428             }
0429 
0430             while (attributeQuery.isValid() && attributeQuery.value(0).toLongLong() < col.id()) {
0431                 if (!attributeQuery.next()) {
0432                     break;
0433                 }
0434             }
0435             // Advance query while a mimetype for this collection is returned
0436             while (attributeQuery.isValid() && attributeQuery.value(0).toLongLong() == col.id()) {
0437                 CollectionAttribute attr;
0438                 attr.setType(attributeQuery.value(1).toByteArray());
0439                 attr.setValue(attributeQuery.value(2).toByteArray());
0440                 attributes << attr;
0441 
0442                 if (!attributeQuery.next()) {
0443                     break;
0444                 }
0445             }
0446         }
0447 
0448         listCollection(col, ancestorsForCollection(col), mimeTypes, attributes);
0449         it++;
0450     }
0451     attributeQuery.finish();
0452     mimeTypeQuery.finish();
0453 }
0454 
0455 bool CollectionFetchHandler::parseStream()
0456 {
0457     const auto &cmd = Protocol::cmdCast<Protocol::FetchCollectionsCommand>(m_command);
0458 
0459     if (!cmd.resource().isEmpty()) {
0460         mResource = Resource::retrieveByName(cmd.resource());
0461         if (!mResource.isValid()) {
0462             return failureResponse(QStringLiteral("Unknown resource %1").arg(cmd.resource()));
0463         }
0464     }
0465     const QStringList lstMimeTypes = cmd.mimeTypes();
0466     for (const QString &mtName : lstMimeTypes) {
0467         const MimeType mt = MimeType::retrieveByNameOrCreate(mtName);
0468         if (!mt.isValid()) {
0469             return failureResponse("Failed to create mimetype record");
0470         }
0471         mMimeTypes.append(mt.id());
0472     }
0473 
0474     mEnabledCollections = cmd.enabled();
0475     mCollectionsToSynchronize = cmd.syncPref();
0476     mCollectionsToDisplay = cmd.displayPref();
0477     mCollectionsToIndex = cmd.indexPref();
0478     mIncludeStatistics = cmd.fetchStats();
0479 
0480     int depth = 0;
0481     switch (cmd.depth()) {
0482     case Protocol::FetchCollectionsCommand::BaseCollection:
0483         depth = 0;
0484         break;
0485     case Protocol::FetchCollectionsCommand::ParentCollection:
0486         depth = 1;
0487         break;
0488     case Protocol::FetchCollectionsCommand::AllCollections:
0489         depth = INT_MAX;
0490         break;
0491     }
0492 
0493     switch (cmd.ancestorsDepth()) {
0494     case Protocol::Ancestor::NoAncestor:
0495         mAncestorDepth = 0;
0496         break;
0497     case Protocol::Ancestor::ParentAncestor:
0498         mAncestorDepth = 1;
0499         break;
0500     case Protocol::Ancestor::AllAncestors:
0501         mAncestorDepth = INT_MAX;
0502         break;
0503     }
0504     mAncestorAttributes = cmd.ancestorsAttributes();
0505 
0506     Scope scope = cmd.collections();
0507     if (!scope.isEmpty()) { // not root
0508         Collection col;
0509         if (scope.scope() == Scope::Uid) {
0510             col = Collection::retrieveById(scope.uid());
0511         } else if (scope.scope() == Scope::Rid) {
0512             SelectQueryBuilder<Collection> qb;
0513             qb.addValueCondition(Collection::remoteIdFullColumnName(), Query::Equals, scope.rid());
0514             qb.addJoin(QueryBuilder::InnerJoin, Resource::tableName(), Collection::resourceIdFullColumnName(), Resource::idFullColumnName());
0515             if (mCollectionsToSynchronize) {
0516                 qb.addCondition(filterCondition(Collection::syncPrefFullColumnName()));
0517             } else if (mCollectionsToDisplay) {
0518                 qb.addCondition(filterCondition(Collection::displayPrefFullColumnName()));
0519             } else if (mCollectionsToIndex) {
0520                 qb.addCondition(filterCondition(Collection::indexPrefFullColumnName()));
0521             }
0522             if (mResource.isValid()) {
0523                 qb.addValueCondition(Resource::idFullColumnName(), Query::Equals, mResource.id());
0524             } else if (connection()->context().resource().isValid()) {
0525                 qb.addValueCondition(Resource::idFullColumnName(), Query::Equals, connection()->context().resource().id());
0526             } else {
0527                 return failureResponse("Cannot retrieve collection based on remote identifier without a resource context");
0528             }
0529             if (!qb.exec()) {
0530                 return failureResponse("Unable to retrieve collection for listing");
0531             }
0532             Collection::List results = qb.result();
0533             if (results.count() != 1) {
0534                 return failureResponse(QString::number(results.count()) + QStringLiteral(" collections found"));
0535             }
0536             col = results.first();
0537         } else if (scope.scope() == Scope::HierarchicalRid) {
0538             if (!connection()->context().resource().isValid()) {
0539                 return failureResponse("Cannot retrieve collection based on hierarchical remote identifier without a resource context");
0540             }
0541             col = CollectionQueryHelper::resolveHierarchicalRID(scope.hridChain(), connection()->context().resource().id());
0542         } else {
0543             return failureResponse("Unexpected error");
0544         }
0545 
0546         if (!col.isValid()) {
0547             return failureResponse("Collection does not exist");
0548         }
0549 
0550         retrieveCollections(col, depth);
0551     } else { // Root folder listing
0552         if (depth != 0) {
0553             retrieveCollections(Collection(), depth);
0554         }
0555     }
0556 
0557     return successResponse<Protocol::FetchCollectionsResponse>();
0558 }