File indexing completed on 2024-11-10 04:40:28

0001 /*
0002     SPDX-FileCopyrightText: 2006-2007 Volker Krause <vkrause@kde.org>
0003 
0004     SPDX-License-Identifier: LGPL-2.0-or-later
0005 */
0006 
0007 #include "collectionfetchjob.h"
0008 
0009 #include "collection_p.h"
0010 #include "collectionfetchscope.h"
0011 #include "collectionutils.h"
0012 #include "job_p.h"
0013 #include "private/protocol_p.h"
0014 #include "protocolhelper_p.h"
0015 
0016 #include "akonadicore_debug.h"
0017 
0018 #include <KLocalizedString>
0019 
0020 #include <QHash>
0021 #include <QObject>
0022 #include <QTimer>
0023 
0024 using namespace Akonadi;
0025 
0026 class Akonadi::CollectionFetchJobPrivate : public JobPrivate
0027 {
0028 public:
0029     explicit CollectionFetchJobPrivate(CollectionFetchJob *parent)
0030         : JobPrivate(parent)
0031     {
0032         mEmitTimer.setSingleShot(true);
0033         mEmitTimer.setInterval(std::chrono::milliseconds{100});
0034     }
0035 
0036     void init()
0037     {
0038         QObject::connect(&mEmitTimer, &QTimer::timeout, q_ptr, [this]() {
0039             timeout();
0040         });
0041     }
0042 
0043     Q_DECLARE_PUBLIC(CollectionFetchJob)
0044 
0045     CollectionFetchJob::Type mType = CollectionFetchJob::Base;
0046     Collection mBase;
0047     Collection::List mBaseList;
0048     Collection::List mCollections;
0049     CollectionFetchScope mScope;
0050     Collection::List mPendingCollections;
0051     QTimer mEmitTimer;
0052     bool mBasePrefetch = false;
0053     Collection::List mPrefetchList;
0054 
0055     void aboutToFinish() override
0056     {
0057         timeout();
0058     }
0059 
0060     void timeout()
0061     {
0062         Q_Q(CollectionFetchJob);
0063 
0064         mEmitTimer.stop(); // in case we are called by result()
0065         if (!mPendingCollections.isEmpty()) {
0066             if (!q->error() || mScope.ignoreRetrievalErrors()) {
0067                 Q_EMIT q->collectionsReceived(mPendingCollections);
0068             }
0069             mPendingCollections.clear();
0070         }
0071     }
0072 
0073     void subJobCollectionReceived(const Akonadi::Collection::List &collections)
0074     {
0075         mPendingCollections += collections;
0076         if (!mEmitTimer.isActive()) {
0077             mEmitTimer.start();
0078         }
0079     }
0080 
0081     QString jobDebuggingString() const override
0082     {
0083         if (mBase.isValid()) {
0084             return QStringLiteral("Collection Id %1").arg(mBase.id());
0085         } else if (CollectionUtils::hasValidHierarchicalRID(mBase)) {
0086             // return QLatin1StringView("(") + ProtocolHelper::hierarchicalRidToScope(mBase).hridChain().join(QLatin1StringView(", ")) + QLatin1Char(')');
0087             return QStringLiteral("HRID chain");
0088         } else {
0089             return QStringLiteral("Collection RemoteId %1").arg(mBase.remoteId());
0090         }
0091     }
0092 
0093     bool jobFailed(KJob *job)
0094     {
0095         Q_Q(CollectionFetchJob);
0096         if (mScope.ignoreRetrievalErrors()) {
0097             int error = job->error();
0098             if (error && !q->error()) {
0099                 q->setError(error);
0100                 q->setErrorText(job->errorText());
0101             }
0102 
0103             return error == Job::ConnectionFailed || error == Job::ProtocolVersionMismatch || error == Job::UserCanceled;
0104         } else {
0105             return job->error();
0106         }
0107     }
0108 };
0109 
0110 CollectionFetchJob::CollectionFetchJob(const Collection &collection, Type type, QObject *parent)
0111     : Job(new CollectionFetchJobPrivate(this), parent)
0112 {
0113     Q_D(CollectionFetchJob);
0114     d->init();
0115 
0116     d->mBase = collection;
0117     d->mType = type;
0118 }
0119 
0120 CollectionFetchJob::CollectionFetchJob(const Collection::List &cols, QObject *parent)
0121     : Job(new CollectionFetchJobPrivate(this), parent)
0122 {
0123     Q_D(CollectionFetchJob);
0124     d->init();
0125 
0126     Q_ASSERT(!cols.isEmpty());
0127     if (cols.size() == 1) {
0128         d->mBase = cols.first();
0129     } else {
0130         d->mBaseList = cols;
0131     }
0132     d->mType = CollectionFetchJob::Base;
0133 }
0134 
0135 CollectionFetchJob::CollectionFetchJob(const Collection::List &cols, Type type, QObject *parent)
0136     : Job(new CollectionFetchJobPrivate(this), parent)
0137 {
0138     Q_D(CollectionFetchJob);
0139     d->init();
0140 
0141     Q_ASSERT(!cols.isEmpty());
0142     if (cols.size() == 1) {
0143         d->mBase = cols.first();
0144     } else {
0145         d->mBaseList = cols;
0146     }
0147     d->mType = type;
0148 }
0149 
0150 CollectionFetchJob::CollectionFetchJob(const QList<Collection::Id> &cols, Type type, QObject *parent)
0151     : Job(new CollectionFetchJobPrivate(this), parent)
0152 {
0153     Q_D(CollectionFetchJob);
0154     d->init();
0155 
0156     Q_ASSERT(!cols.isEmpty());
0157     if (cols.size() == 1) {
0158         d->mBase = Collection(cols.first());
0159     } else {
0160         for (Collection::Id id : cols) {
0161             d->mBaseList.append(Collection(id));
0162         }
0163     }
0164     d->mType = type;
0165 }
0166 
0167 CollectionFetchJob::~CollectionFetchJob() = default;
0168 
0169 Akonadi::Collection::List CollectionFetchJob::collections() const
0170 {
0171     Q_D(const CollectionFetchJob);
0172 
0173     return d->mCollections;
0174 }
0175 
0176 void CollectionFetchJob::doStart()
0177 {
0178     Q_D(CollectionFetchJob);
0179 
0180     if (!d->mBaseList.isEmpty()) {
0181         if (d->mType == Recursive) {
0182             // Because doStart starts several subjobs and @p cols could contain descendants of
0183             // other elements in the list, if type is Recursive, we could end up with duplicates in the result.
0184             // To fix this we require an initial fetch of @p cols with Base and RetrieveAncestors,
0185             // Iterate over that result removing intersections and then perform the Recursive fetch on
0186             // the remainder.
0187             d->mBasePrefetch = true;
0188             // No need to connect to the collectionsReceived signal here. This job is internal. The
0189             // result needs to be filtered through filterDescendants before it is useful.
0190             new CollectionFetchJob(d->mBaseList, NonOverlappingRoots, this);
0191         } else if (d->mType == NonOverlappingRoots) {
0192             for (const Collection &col : std::as_const(d->mBaseList)) {
0193                 // No need to connect to the collectionsReceived signal here. This job is internal. The (aggregated)
0194                 // result needs to be filtered through filterDescendants before it is useful.
0195                 auto subJob = new CollectionFetchJob(col, Base, this);
0196                 subJob->fetchScope().setAncestorRetrieval(Akonadi::CollectionFetchScope::All);
0197             }
0198         } else {
0199             for (const Collection &col : std::as_const(d->mBaseList)) {
0200                 auto subJob = new CollectionFetchJob(col, d->mType, this);
0201                 connect(subJob, &CollectionFetchJob::collectionsReceived, this, [d](const auto &cols) {
0202                     d->subJobCollectionReceived(cols);
0203                 });
0204                 subJob->setFetchScope(fetchScope());
0205             }
0206         }
0207         return;
0208     }
0209 
0210     if (!d->mBase.isValid() && d->mBase.remoteId().isEmpty()) {
0211         setError(Unknown);
0212         setErrorText(i18n("Invalid collection given."));
0213         emitResult();
0214         return;
0215     }
0216 
0217     const auto cmd = Protocol::FetchCollectionsCommandPtr::create(ProtocolHelper::entityToScope(d->mBase));
0218     switch (d->mType) {
0219     case Base:
0220         cmd->setDepth(Protocol::FetchCollectionsCommand::BaseCollection);
0221         break;
0222     case Akonadi::CollectionFetchJob::FirstLevel:
0223         cmd->setDepth(Protocol::FetchCollectionsCommand::ParentCollection);
0224         break;
0225     case Akonadi::CollectionFetchJob::Recursive:
0226         cmd->setDepth(Protocol::FetchCollectionsCommand::AllCollections);
0227         break;
0228     default:
0229         Q_ASSERT(false);
0230     }
0231     cmd->setResource(d->mScope.resource());
0232     cmd->setMimeTypes(d->mScope.contentMimeTypes());
0233 
0234     switch (d->mScope.listFilter()) {
0235     case CollectionFetchScope::Display:
0236         cmd->setDisplayPref(true);
0237         break;
0238     case CollectionFetchScope::Sync:
0239         cmd->setSyncPref(true);
0240         break;
0241     case CollectionFetchScope::Index:
0242         cmd->setIndexPref(true);
0243         break;
0244     case CollectionFetchScope::Enabled:
0245         cmd->setEnabled(true);
0246         break;
0247     case CollectionFetchScope::NoFilter:
0248         break;
0249     default:
0250         Q_ASSERT(false);
0251     }
0252 
0253     cmd->setFetchStats(d->mScope.includeStatistics());
0254     switch (d->mScope.ancestorRetrieval()) {
0255     case CollectionFetchScope::None:
0256         cmd->setAncestorsDepth(Protocol::Ancestor::NoAncestor);
0257         break;
0258     case CollectionFetchScope::Parent:
0259         cmd->setAncestorsDepth(Protocol::Ancestor::ParentAncestor);
0260         break;
0261     case CollectionFetchScope::All:
0262         cmd->setAncestorsDepth(Protocol::Ancestor::AllAncestors);
0263         break;
0264     }
0265     if (d->mScope.ancestorRetrieval() != CollectionFetchScope::None) {
0266         cmd->setAncestorsAttributes(d->mScope.ancestorFetchScope().attributes());
0267     }
0268 
0269     d->sendCommand(cmd);
0270 }
0271 
0272 bool CollectionFetchJob::doHandleResponse(qint64 tag, const Protocol::CommandPtr &response)
0273 {
0274     Q_D(CollectionFetchJob);
0275 
0276     if (d->mBasePrefetch || d->mType == NonOverlappingRoots) {
0277         return false;
0278     }
0279 
0280     if (!response->isResponse() || response->type() != Protocol::Command::FetchCollections) {
0281         return Job::doHandleResponse(tag, response);
0282     }
0283 
0284     const auto &resp = Protocol::cmdCast<Protocol::FetchCollectionsResponse>(response);
0285     // Invalid response (no ID) means this was the last response
0286     if (resp.id() == -1) {
0287         return true;
0288     }
0289 
0290     Collection collection = ProtocolHelper::parseCollection(resp, true);
0291     if (!collection.isValid()) {
0292         return false;
0293     }
0294 
0295     collection.d_ptr->resetChangeLog();
0296     d->mCollections.append(collection);
0297     d->mPendingCollections.append(collection);
0298     if (!d->mEmitTimer.isActive()) {
0299         d->mEmitTimer.start();
0300     }
0301 
0302     return false;
0303 }
0304 
0305 static Collection::List filterDescendants(const Collection::List &list)
0306 {
0307     Collection::List result;
0308 
0309     QList<QList<Collection::Id>> ids;
0310     ids.reserve(list.count());
0311     for (const Collection &collection : list) {
0312         QList<Collection::Id> ancestors;
0313         Collection parent = collection.parentCollection();
0314         ancestors << parent.id();
0315         if (parent != Collection::root()) {
0316             while (parent.parentCollection() != Collection::root()) {
0317                 parent = parent.parentCollection();
0318                 QList<Collection::Id>::iterator i = std::lower_bound(ancestors.begin(), ancestors.end(), parent.id());
0319                 ancestors.insert(i, parent.id());
0320             }
0321         }
0322         ids << ancestors;
0323     }
0324 
0325     QSet<Collection::Id> excludeList;
0326     for (const Collection &collection : list) {
0327         int i = 0;
0328         for (const QList<Collection::Id> &ancestors : std::as_const(ids)) {
0329             if (std::binary_search(ancestors.cbegin(), ancestors.cend(), collection.id())) {
0330                 excludeList.insert(list.at(i).id());
0331             }
0332             ++i;
0333         }
0334     }
0335 
0336     for (const Collection &collection : list) {
0337         if (!excludeList.contains(collection.id())) {
0338             result.append(collection);
0339         }
0340     }
0341 
0342     return result;
0343 }
0344 
0345 void CollectionFetchJob::slotResult(KJob *job)
0346 {
0347     Q_D(CollectionFetchJob);
0348 
0349     auto list = qobject_cast<CollectionFetchJob *>(job);
0350     Q_ASSERT(job);
0351 
0352     if (d->mType == NonOverlappingRoots) {
0353         d->mPrefetchList += list->collections();
0354     } else if (!d->mBasePrefetch) {
0355         d->mCollections += list->collections();
0356     }
0357 
0358     if (d_ptr->mCurrentSubJob == job && !d->jobFailed(job)) {
0359         if (job->error()) {
0360             qCWarning(AKONADICORE_LOG) << "Error during CollectionFetchJob: " << job->errorString();
0361         }
0362         d_ptr->mCurrentSubJob = nullptr;
0363         removeSubjob(job);
0364         QTimer::singleShot(0, this, [d]() {
0365             d->startNext();
0366         });
0367     } else {
0368         Job::slotResult(job);
0369     }
0370 
0371     if (d->mBasePrefetch) {
0372         d->mBasePrefetch = false;
0373         const Collection::List roots = list->collections();
0374         Q_ASSERT(!hasSubjobs());
0375         if (!job->error()) {
0376             for (const Collection &col : roots) {
0377                 auto subJob = new CollectionFetchJob(col, d->mType, this);
0378                 connect(subJob, &CollectionFetchJob::collectionsReceived, this, [d](const auto &cols) {
0379                     d->subJobCollectionReceived(cols);
0380                 });
0381                 subJob->setFetchScope(fetchScope());
0382             }
0383         }
0384         // No result yet.
0385     } else if (d->mType == NonOverlappingRoots) {
0386         if (!d->jobFailed(job) && !hasSubjobs()) {
0387             const Collection::List result = filterDescendants(d->mPrefetchList);
0388             d->mPendingCollections += result;
0389             d->mCollections = result;
0390             d->delayedEmitResult();
0391         }
0392     } else {
0393         if (!d->jobFailed(job) && !hasSubjobs()) {
0394             d->delayedEmitResult();
0395         }
0396     }
0397 }
0398 
0399 void CollectionFetchJob::setFetchScope(const CollectionFetchScope &scope)
0400 {
0401     Q_D(CollectionFetchJob);
0402     d->mScope = scope;
0403 }
0404 
0405 CollectionFetchScope &CollectionFetchJob::fetchScope()
0406 {
0407     Q_D(CollectionFetchJob);
0408     return d->mScope;
0409 }
0410 
0411 #include "moc_collectionfetchjob.cpp"