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 }