File indexing completed on 2024-05-19 16:38:21

0001 /*
0002     SPDX-FileCopyrightText: 2015, 2016 Ivan Cukic <ivan.cukic(at)kde.org>
0003 
0004     SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
0005 */
0006 
0007 // Self
0008 #include "resultmodel.h"
0009 
0010 // Qt
0011 #include <QCoreApplication>
0012 #include <QDateTime>
0013 #include <QDebug>
0014 #include <QFile>
0015 #include <QTimer>
0016 
0017 // STL
0018 #include <functional>
0019 #include <thread>
0020 
0021 // KDE
0022 #include <KSharedConfig>
0023 #include <KConfigGroup>
0024 
0025 // Local
0026 #include <common/database/Database.h>
0027 #include <utils/qsqlquery_iterator.h>
0028 #include <utils/slide.h>
0029 #include <utils/member_matcher.h>
0030 #include "resultset.h"
0031 #include "resultwatcher.h"
0032 #include "cleaning.h"
0033 #include "kactivities/consumer.h"
0034 #include "kactivities-stats-logsettings.h"
0035 
0036 #include <common/specialvalues.h>
0037 
0038 #define MAX_CHUNK_LOAD_SIZE 50
0039 #define MAX_RELOAD_CACHE_SIZE 50
0040 
0041 #define QDBG qCDebug(KACTIVITIES_STATS_LOG) << "KActivitiesStats(" << (void*)this << ")"
0042 
0043 namespace KActivities {
0044 namespace Stats {
0045 
0046 using Common::Database;
0047 
0048 class ResultModelPrivate {
0049 public:
0050     ResultModelPrivate(Query query, const QString &clientId, ResultModel *parent)
0051         : cache(this, clientId, query.limit())
0052         , query(query)
0053         , watcher(query)
0054         , hasMore(true)
0055         , database(Database::instance(Database::ResourcesDatabase, Database::ReadOnly))
0056         , q(parent)
0057     {
0058         s_privates << this;
0059     }
0060 
0061     ~ResultModelPrivate()
0062     {
0063         s_privates.removeAll(this);
0064     }
0065 
0066     enum Fetch {
0067         FetchReset,   // Remove old data and reload
0068         FetchReload,  // Update all data
0069         FetchMore,     // Load more data if there is any
0070     };
0071 
0072     class Cache { //_
0073     public:
0074         typedef QList<ResultSet::Result> Items;
0075 
0076         Cache(ResultModelPrivate *d, const QString &clientId, int limit)
0077             : d(d)
0078             , m_countLimit(limit)
0079             , m_clientId(clientId)
0080         {
0081             if (!m_clientId.isEmpty()) {
0082                 m_configFile = KSharedConfig::openConfig(QStringLiteral("kactivitymanagerd-statsrc"));
0083             }
0084         }
0085 
0086         ~Cache()
0087         {
0088         }
0089 
0090         inline int size() const
0091         {
0092             return m_items.size();
0093         }
0094 
0095         inline void setLinkedResultPosition(const QString &resourcePath,
0096                                             int position)
0097         {
0098             if (!m_orderingConfig.isValid()) {
0099                 qCWarning(KACTIVITIES_STATS_LOG) << "We can not reorder the results, no clientId was specified";
0100                 return;
0101             }
0102 
0103             // Preconditions:
0104             //  - cache is ordered properly, first on the user's desired order,
0105             //    then on the query specified order
0106             //  - the resource that needs to be moved is a linked resource, not
0107             //    one that comes from the stats (there are overly many
0108             //    corner-cases that need to be covered in order to support
0109             //    reordering of the statistics-based resources)
0110             //  - the new position for the resource is not outside of the cache
0111 
0112             auto resourcePosition = find(resourcePath);
0113 
0114             if (resourcePosition) {
0115                 if (resourcePosition.index == position) {
0116                     return;
0117                 }
0118                 if (resourcePosition.iterator->linkStatus() == ResultSet::Result::NotLinked) {
0119                     return;
0120                 }
0121             }
0122 
0123             // Lets make a list of linked items - we can only reorder them,
0124             // not others
0125             QStringList linkedItems;
0126 
0127             for (const ResultSet::Result &item : std::as_const(m_items)) {
0128                 if (item.linkStatus() == ResultSet::Result::NotLinked) {
0129                     break;
0130                 }
0131                 linkedItems << item.resource();
0132             }
0133 
0134             // We have two options:
0135             //  - we are planning to add an item to the desired position,
0136             //    but the item is not yet in the model
0137             //  - we want to move an existing item
0138             if (!resourcePosition
0139                     || resourcePosition.iterator->linkStatus() == ResultSet::Result::NotLinked) {
0140 
0141                 linkedItems.insert(position, resourcePath);
0142 
0143                 m_fixedOrderedItems = linkedItems;
0144 
0145             } else {
0146                 // We can not accept the new position to be outside
0147                 // of the linked items area
0148                 if (position >= linkedItems.size()) {
0149                     position = linkedItems.size() - 1;
0150                 }
0151 
0152                 Q_ASSERT(resourcePosition.index == linkedItems.indexOf(resourcePath));
0153                 auto oldPosition = linkedItems.indexOf(resourcePath);
0154 
0155                 kamd::utils::move_one(
0156                         linkedItems.begin() + oldPosition,
0157                         linkedItems.begin() + position);
0158 
0159                 // When we change this, the cache is not valid anymore,
0160                 // destinationFor will fail and we can not use it
0161                 m_fixedOrderedItems = linkedItems;
0162 
0163                 // We are prepared to reorder the cache
0164                 d->repositionResult(resourcePosition,
0165                                     d->destinationFor(*resourcePosition));
0166             }
0167 
0168             m_orderingConfig.writeEntry("kactivitiesLinkedItemsOrder", m_fixedOrderedItems);
0169             m_orderingConfig.sync();
0170 
0171             // We need to notify others to reload
0172             for (const auto &other : std::as_const(s_privates)) {
0173                 if (other != d && other->cache.m_clientId == m_clientId) {
0174                     other->fetch(FetchReset);
0175                 }
0176             }
0177         }
0178 
0179         inline void debug() const
0180         {
0181             for (const auto& item: m_items) {
0182                 qCDebug(KACTIVITIES_STATS_LOG) << "Item: " << item;
0183             }
0184         }
0185 
0186         void loadOrderingConfig(const QString &activityTag)
0187         {
0188             if (!m_configFile) {
0189                 qCDebug(KACTIVITIES_STATS_LOG) << "Nothing to load - the client id is empty";
0190                 return;
0191             }
0192 
0193             m_orderingConfig =
0194                 KConfigGroup(m_configFile,
0195                              QStringLiteral("ResultModel-OrderingFor-") + m_clientId + activityTag);
0196 
0197             if (m_orderingConfig.hasKey("kactivitiesLinkedItemsOrder")) {
0198                 // If we have the ordering defined, use it
0199                 m_fixedOrderedItems = m_orderingConfig.readEntry("kactivitiesLinkedItemsOrder",
0200                                                                  QStringList());
0201             } else {
0202                 // Otherwise, copy the order from the previous activity to this one
0203                 m_orderingConfig.writeEntry("kactivitiesLinkedItemsOrder", m_fixedOrderedItems);
0204                 m_orderingConfig.sync();
0205 
0206             }
0207         }
0208 
0209     private:
0210         ResultModelPrivate *const d;
0211 
0212         QList<ResultSet::Result> m_items;
0213         int m_countLimit;
0214 
0215         QString m_clientId;
0216         KSharedConfig::Ptr m_configFile;
0217         KConfigGroup m_orderingConfig;
0218         QStringList m_fixedOrderedItems;
0219 
0220         friend QDebug operator<< (QDebug out, const Cache &cache)
0221         {
0222             for (const auto& item: cache.m_items) {
0223                 out << "Cache item: " << item << "\n";
0224             }
0225 
0226             return out;
0227         }
0228 
0229     public:
0230         inline const QStringList &fixedOrderedItems() const
0231         {
0232             return m_fixedOrderedItems;
0233         }
0234 
0235         //_ Fancy iterator, find, lowerBound
0236         struct FindCacheResult {
0237             Cache *const cache;
0238             Items::iterator iterator;
0239             int index;
0240 
0241             FindCacheResult(Cache *cache, Items::iterator iterator)
0242                 : cache(cache)
0243                 , iterator(iterator)
0244                 , index(std::distance(cache->m_items.begin(), iterator))
0245             {
0246             }
0247 
0248             operator bool() const
0249             {
0250                 return iterator != cache->m_items.end();
0251             }
0252 
0253             ResultSet::Result &operator*() const
0254             {
0255                 return *iterator;
0256             }
0257 
0258             ResultSet::Result *operator->() const
0259             {
0260                 return &(*iterator);
0261             }
0262         };
0263 
0264         inline FindCacheResult find(const QString &resource)
0265         {
0266             using namespace kamd::utils::member_matcher;
0267 
0268             // Non-const iterator because the result is constructed from it
0269             return FindCacheResult(
0270                 this, std::find_if(m_items.begin(), m_items.end(), member(&ResultSet::Result::resource)
0271                                            == resource));
0272         }
0273 
0274         template <typename Predicate>
0275         inline FindCacheResult lowerBoundWithSkippedResource(Predicate &&lessThanPredicate)
0276         {
0277             using namespace kamd::utils::member_matcher;
0278             const int count = std::count_if(m_items.cbegin(), m_items.cend(),
0279                     [&] (const ResultSet::Result &result) {
0280                         return lessThanPredicate(result, _);
0281                     });
0282 
0283             return FindCacheResult(this, m_items.begin() + count);
0284 
0285 
0286             // using namespace kamd::utils::member_matcher;
0287             //
0288             // const auto position =
0289             //     std::lower_bound(m_items.begin(), m_items.end(),
0290             //                      _, std::forward<Predicate>(lessThanPredicate));
0291             //
0292             // // We seem to have found the position for the item.
0293             // // The problem is that we might have found the same position
0294             // // we were previously at. Since this function is usually used
0295             // // to reposition the result, we might not be in a completely
0296             // // sorted collection, so the next item(s) could be less than us.
0297             // // We could do this with count_if, but it would be slower
0298             //
0299             // if (position >= m_items.cend() - 1) {
0300             //     return FindCacheResult(this, position);
0301             //
0302             // } else if (lessThanPredicate(_, *(position + 1))) {
0303             //     return FindCacheResult(this, position);
0304             //
0305             // } else {
0306             //     return FindCacheResult(
0307             //         this, std::lower_bound(position + 1, m_items.end(),
0308             //                                _, std::forward<Predicate>(lessThanPredicate)));
0309             // }
0310         }
0311         //^
0312 
0313         inline void insertAt(const FindCacheResult &at,
0314                              const ResultSet::Result &result)
0315         {
0316             m_items.insert(at.iterator, result);
0317         }
0318 
0319         inline void removeAt(const FindCacheResult &at)
0320         {
0321             m_items.removeAt(at.index);
0322         }
0323 
0324         inline const ResultSet::Result &operator[] (int index) const
0325         {
0326             return m_items[index];
0327         }
0328 
0329         inline void clear()
0330         {
0331             if (m_items.size() == 0) {
0332                 return;
0333             }
0334 
0335             d->q->beginRemoveRows(QModelIndex(), 0, m_items.size() - 1);
0336             m_items.clear();
0337             d->q->endRemoveRows();
0338         }
0339 
0340         //  Algorithm to calculate the edit operations to allow
0341         //_ replaceing items without model reset
0342         inline void replace(const Items &newItems, int from = 0)
0343         {
0344             using namespace kamd::utils::member_matcher;
0345 
0346 #if 0
0347             QDBG << "======";
0348             QDBG << "Old items {";
0349             for (const auto& item: m_items) {
0350                 QDBG << item;
0351             }
0352             QDBG << "}";
0353 
0354             QDBG << "New items to be added at " << from << " {";
0355             for (const auto& item: newItems) {
0356                 QDBG << item;
0357             }
0358             QDBG << "}";
0359 #endif
0360 
0361 
0362             // Based on 'The string to string correction problem
0363             // with block moves' paper by Walter F. Tichy
0364             //
0365             // In essence, it goes like this:
0366             //
0367             // Take the first element from the new list, and try to find
0368             // it in the old one. If you can not find it, it is a new item
0369             // item - send the 'inserted' event.
0370             // If you did find it, test whether the following items also
0371             // match. This detects blocks of items that have moved.
0372             //
0373             // In this example, we find 'b', and then detect the rest of the
0374             // moved block 'b' 'c' 'd'
0375             //
0376             // Old items:  a[b c d]e f g
0377             //               ^
0378             //              /
0379             // New items: [b c d]a f g
0380             //
0381             // After processing one block, just repeat until the end of the
0382             // new list is reached.
0383             //
0384             // Then remove all remaining elements from the old list.
0385             //
0386             // The main addition here compared to the original papers is that
0387             // our 'strings' can not hold two instances of the same element,
0388             // and that we support updating from arbitrary position.
0389 
0390             auto newBlockStart = newItems.cbegin();
0391 
0392             // How many items should we add?
0393             // This should remove the need for post-replace-trimming
0394             // in the case where somebody called this with too much new items.
0395             const int maxToReplace = m_countLimit - from;
0396 
0397             if (maxToReplace <= 0) {
0398                 return;
0399             }
0400 
0401             const auto newItemsEnd =
0402                 newItems.size() <= maxToReplace ? newItems.cend() :
0403                                                   newItems.cbegin() + maxToReplace;
0404 
0405 
0406             // Finding the blocks until we reach the end of the newItems list
0407             //
0408             // from = 4
0409             // Old items: X Y Z U a b c d e f g
0410             //                      ^ oldBlockStart points to the first element
0411             //                        of the currently processed block in the old list
0412             //
0413             // New items: _ _ _ _ b c d a f g
0414             //                    ^ newBlockStartIndex is the index of the first
0415             //                      element of the block that is currently being
0416             //                      processed (with 'from' offset)
0417 
0418             while (newBlockStart != newItemsEnd) {
0419 
0420                 const int newBlockStartIndex
0421                     = from + std::distance(newItems.cbegin(), newBlockStart);
0422 
0423                 const auto oldBlockStart = std::find_if(
0424                     m_items.begin() + from, m_items.end(),
0425                     member(&ResultSet::Result::resource) == newBlockStart->resource());
0426 
0427                 if (oldBlockStart == m_items.end()) {
0428                     // This item was not found in the old cache, so we are
0429                     // inserting a new item at the same position it had in
0430                     // the newItems array
0431                     d->q->beginInsertRows(QModelIndex(), newBlockStartIndex,
0432                                           newBlockStartIndex);
0433 
0434                     m_items.insert(newBlockStartIndex, *newBlockStart);
0435                     d->q->endInsertRows();
0436 
0437                     // This block contained only one item, move on to find
0438                     // the next block - it starts from the next item
0439                     ++newBlockStart;
0440 
0441                 } else {
0442                     // We are searching for a block of matching items.
0443                     // This is a reimplementation of std::mismatch that
0444                     // accepts two complete ranges that is available only
0445                     // since C++14, so we can not use it.
0446                     auto newBlockEnd = newBlockStart;
0447                     auto oldBlockEnd = oldBlockStart;
0448 
0449                     while (newBlockEnd != newItemsEnd &&
0450                            oldBlockEnd != m_items.end() &&
0451                            newBlockEnd->resource() == oldBlockEnd->resource()) {
0452                         ++newBlockEnd;
0453                         ++oldBlockEnd;
0454                     }
0455 
0456                     // We have found matching blocks
0457                     // [newBlockStart, newBlockEnd) and [oldBlockStart, newBlockEnd)
0458                     const int oldBlockStartIndex
0459                         = std::distance(m_items.begin() + from, oldBlockStart);
0460 
0461                     const int blockSize
0462                         = std::distance(oldBlockStart, oldBlockEnd);
0463 
0464                     if (oldBlockStartIndex != newBlockStartIndex) {
0465                         // If these blocks do not have the same start,
0466                         // we need to send the move event.
0467 
0468                         // Note: If there is a crash here, it means we
0469                         // are getting a bad query which has duplicate
0470                         // results
0471 
0472                         d->q->beginMoveRows(QModelIndex(), oldBlockStartIndex,
0473                                             oldBlockStartIndex + blockSize - 1,
0474                                             QModelIndex(), newBlockStartIndex);
0475 
0476                         // Moving the items from the old location to the new one
0477                         kamd::utils::slide(
0478                                 oldBlockStart, oldBlockEnd,
0479                                 m_items.begin() + newBlockStartIndex);
0480 
0481                         d->q->endMoveRows();
0482                     }
0483 
0484                     // Skip all the items in this block, and continue with
0485                     // the search
0486                     newBlockStart = newBlockEnd;
0487                 }
0488             }
0489 
0490             // We have avoided the need for trimming for the most part,
0491             // but if the newItems list was shorter than needed, we still
0492             // need to trim the rest.
0493             trim(from + newItems.size());
0494 
0495             // Check whether we got an item representing a non-existent file,
0496             // if so, schedule its removal from the database
0497             // we want to do this async so that we don't block
0498             std::thread([=] {
0499                 QList<QString> missingResources;
0500                 for (const auto &item: newItems) {
0501                     // QFile.exists() can be incredibly slow (eg. if resource is on remote filesystem)
0502                     if (item.resource().startsWith(QLatin1Char('/')) && !QFile(item.resource()).exists()) {
0503                         missingResources << item.resource();
0504                     }
0505                 }
0506 
0507                 if (missingResources.empty()) {
0508                     return;
0509                 }
0510 
0511                 QTimer::singleShot(0, this->d->q, [=] {
0512                     d->q->forgetResources(missingResources);
0513                 });
0514             }).detach();
0515         }
0516         //^
0517 
0518         inline void trim()
0519         {
0520             trim(m_countLimit);
0521         }
0522 
0523         inline void trim(int limit)
0524         {
0525             if (m_items.size() <= limit) {
0526                 return;
0527             }
0528 
0529             // Example:
0530             //   limit is 5,
0531             //   current cache (0, 1, 2, 3, 4, 5, 6, 7), size = 8
0532             // We need to delete from 5 to 7
0533 
0534             d->q->beginRemoveRows(QModelIndex(), limit, m_items.size() - 1);
0535             m_items.erase(m_items.begin() + limit, m_items.end());
0536             d->q->endRemoveRows();
0537         }
0538 
0539     } cache; //^
0540 
0541     struct FixedItemsLessThan {
0542         //_ Compartor that orders the linked items by user-specified order
0543         typedef kamd::utils::member_matcher::placeholder placeholder;
0544 
0545         enum Ordering {
0546             PartialOrdering,
0547             FullOrdering,
0548         };
0549 
0550         FixedItemsLessThan(Ordering ordering,
0551                            const Cache &cache,
0552                            const QString &matchResource = QString())
0553             : cache(cache), matchResource(matchResource), ordering(ordering)
0554         {
0555         }
0556 
0557         bool lessThan(const QString &leftResource, const QString &rightResource) const
0558         {
0559             const auto fixedOrderedItems = cache.fixedOrderedItems();
0560 
0561             const auto indexLeft  = fixedOrderedItems.indexOf(leftResource);
0562             const auto indexRight = fixedOrderedItems.indexOf(rightResource);
0563 
0564             const bool hasLeft  = indexLeft != -1;
0565             const bool hasRight = indexRight != -1;
0566 
0567             /* clang-format off */
0568             return
0569                 ( hasLeft && !hasRight) ? true :
0570                 (!hasLeft &&  hasRight) ? false :
0571                 ( hasLeft &&  hasRight) ? indexLeft < indexRight :
0572                 (ordering == PartialOrdering ? false : leftResource < rightResource);
0573             /* clang-format on */
0574         }
0575 
0576         template <typename T>
0577         bool operator() (const T &left, placeholder) const
0578         {
0579             return lessThan(left.resource(), matchResource);
0580         }
0581 
0582         template <typename T>
0583         bool operator() (placeholder, const T &right) const
0584         {
0585             return lessThan(matchResource, right.resource());
0586         }
0587 
0588         template <typename T, typename V>
0589         bool operator() (const T &left, const V &right) const
0590         {
0591             return lessThan(left.resource(), right.resource());
0592         }
0593 
0594         const Cache &cache;
0595         const QString matchResource;
0596         Ordering ordering;
0597         //^
0598     };
0599 
0600     inline Cache::FindCacheResult destinationFor(const ResultSet::Result &result)
0601     {
0602         using namespace kamd::utils::member_matcher;
0603         using namespace Terms;
0604 
0605         const auto resource    = result.resource();
0606         const auto score       = result.score();
0607         const auto firstUpdate = result.firstUpdate();
0608         const auto lastUpdate  = result.lastUpdate();
0609         const auto linkStatus  = result.linkStatus();
0610 
0611         /* clang-format off */
0612         #define FIXED_ITEMS_LESS_THAN FixedItemsLessThan(FixedItemsLessThan::PartialOrdering, cache, resource)
0613         #define ORDER_BY(Field) member(&ResultSet::Result::Field) > Field
0614         #define ORDER_BY_FULL(Field)                                           \
0615             (query.selection() == Terms::AllResources ?                        \
0616                 cache.lowerBoundWithSkippedResource(                           \
0617                                  FIXED_ITEMS_LESS_THAN                         \
0618                                  && ORDER_BY(linkStatus)                       \
0619                                  && ORDER_BY(Field)                            \
0620                                  && ORDER_BY(resource)) :                      \
0621                 cache.lowerBoundWithSkippedResource(                           \
0622                                  FIXED_ITEMS_LESS_THAN                         \
0623                                  && ORDER_BY(Field)                            \
0624                                  && ORDER_BY(resource))                        \
0625             )
0626 
0627         const auto destination =
0628             query.ordering() == HighScoredFirst      ? ORDER_BY_FULL(score):
0629             query.ordering() == RecentlyUsedFirst    ? ORDER_BY_FULL(lastUpdate):
0630             query.ordering() == RecentlyCreatedFirst ? ORDER_BY_FULL(firstUpdate):
0631             /* otherwise */                            ORDER_BY_FULL(resource)
0632             ;
0633         #undef ORDER_BY
0634         #undef ORDER_BY_FULL
0635         #undef FIXED_ITEMS_LESS_THAN
0636 
0637         /* clang-format on */
0638 
0639         return destination;
0640     }
0641 
0642     inline void removeResult(const Cache::FindCacheResult &result)
0643     {
0644         q->beginRemoveRows(QModelIndex(), result.index, result.index);
0645         cache.removeAt(result);
0646         q->endRemoveRows();
0647 
0648         if (query.selection() != Terms::LinkedResources) {
0649             fetch(cache.size(), 1);
0650         }
0651     }
0652 
0653     inline void repositionResult(const Cache::FindCacheResult &result,
0654                                  const Cache::FindCacheResult &destination)
0655     {
0656         // We already have the resource in the cache
0657         // So, it is the time for a reshuffle
0658         const int oldPosition = result.index;
0659         int position = destination.index;
0660 
0661         Q_EMIT q->dataChanged(q->index(oldPosition), q->index(oldPosition));
0662 
0663         if (oldPosition == position) {
0664             return;
0665         }
0666 
0667         if (position > oldPosition) {
0668             position++;
0669         }
0670 
0671         bool moving
0672             = q->beginMoveRows(QModelIndex(), oldPosition, oldPosition,
0673                                QModelIndex(), position);
0674 
0675         kamd::utils::move_one(result.iterator, destination.iterator);
0676 
0677         if (moving) {
0678             q->endMoveRows();
0679         }
0680     }
0681 
0682     void reload()
0683     {
0684         fetch(FetchReload);
0685     }
0686 
0687     void init()
0688     {
0689         using namespace std::placeholders;
0690 
0691         QObject::connect(
0692             &watcher, &ResultWatcher::resultScoreUpdated,
0693             q, std::bind(&ResultModelPrivate::onResultScoreUpdated, this, _1, _2, _3, _4));
0694         QObject::connect(
0695             &watcher, &ResultWatcher::resultRemoved,
0696             q, std::bind(&ResultModelPrivate::onResultRemoved, this, _1));
0697         QObject::connect(
0698             &watcher, &ResultWatcher::resultLinked,
0699             q, std::bind(&ResultModelPrivate::onResultLinked, this, _1));
0700         QObject::connect(
0701             &watcher, &ResultWatcher::resultUnlinked,
0702             q, std::bind(&ResultModelPrivate::onResultUnlinked, this, _1));
0703 
0704         QObject::connect(
0705             &watcher, &ResultWatcher::resourceTitleChanged,
0706             q, std::bind(&ResultModelPrivate::onResourceTitleChanged, this, _1, _2));
0707         QObject::connect(
0708             &watcher, &ResultWatcher::resourceMimetypeChanged,
0709             q, std::bind(&ResultModelPrivate::onResourceMimetypeChanged, this, _1, _2));
0710 
0711         QObject::connect(
0712             &watcher, &ResultWatcher::resultsInvalidated,
0713             q, std::bind(&ResultModelPrivate::reload, this));
0714 
0715         if (query.activities().contains(CURRENT_ACTIVITY_TAG)) {
0716             QObject::connect(
0717                 &activities, &KActivities::Consumer::currentActivityChanged, q,
0718                 std::bind(&ResultModelPrivate::onCurrentActivityChanged, this, _1));
0719         }
0720 
0721         fetch(FetchReset);
0722     }
0723 
0724     void fetch(int from, int count)
0725     {
0726         using namespace Terms;
0727 
0728         if (from + count > query.limit()) {
0729             count = query.limit() - from;
0730         }
0731 
0732         if (count <= 0) {
0733             return;
0734         }
0735 
0736         // In order to see whether there are more results, we need to pass
0737         // the count increased by one
0738         ResultSet results(query | Offset(from) | Limit(count + 1));
0739 
0740         auto it = results.begin();
0741 
0742         Cache::Items newItems;
0743 
0744         while (count --> 0 && it != results.end()) {
0745             newItems << *it;
0746             ++it;
0747         }
0748 
0749         hasMore = (it != results.end());
0750 
0751         // We need to sort the new items for the linked resources
0752         // user-defined reordering. This needs only to be a partial sort,
0753         // the main sorting is done by sqlite
0754         if (query.selection() != Terms::UsedResources) {
0755             std::stable_sort(
0756                 newItems.begin(), newItems.end(),
0757                 FixedItemsLessThan(FixedItemsLessThan::PartialOrdering, cache));
0758         }
0759 
0760         cache.replace(newItems, from);
0761     }
0762 
0763     void fetch(Fetch mode)
0764     {
0765         if (mode == FetchReset) {
0766             // Removing the previously cached data
0767             // and loading all from scratch
0768             cache.clear();
0769 
0770             /* clang-format off */
0771             const QString activityTag =
0772                 query.activities().contains(CURRENT_ACTIVITY_TAG)
0773                     ? (QStringLiteral("-ForActivity-") + activities.currentActivity())
0774                     : QStringLiteral("-ForAllActivities");
0775             /* clang-format on */
0776 
0777             cache.loadOrderingConfig(activityTag);
0778 
0779             fetch(0, MAX_CHUNK_LOAD_SIZE);
0780 
0781         } else if (mode == FetchReload) {
0782             if (cache.size() > MAX_RELOAD_CACHE_SIZE) {
0783                 // If the cache is big, we are pretending
0784                 // we were asked to reset the model
0785                 fetch(FetchReset);
0786 
0787             } else {
0788                 // We are only updating the currently
0789                 // cached items, nothing more
0790                 fetch(0, cache.size());
0791 
0792             }
0793 
0794         } else { // FetchMore
0795             // Load a new batch of data
0796             fetch(cache.size(), MAX_CHUNK_LOAD_SIZE);
0797         }
0798     }
0799 
0800     void onResultScoreUpdated(const QString &resource, double score,
0801                               uint lastUpdate, uint firstUpdate)
0802     {
0803         QDBG << "ResultModelPrivate::onResultScoreUpdated "
0804              << "result added:" << resource
0805              << "score:" << score
0806              << "last:" << lastUpdate
0807              << "first:" << firstUpdate;
0808 
0809         // This can also be called when the resource score
0810         // has been updated, so we need to check whether
0811         // we already have it in the cache
0812         const auto result = cache.find(resource);
0813 
0814         /* clang-format off */
0815         ResultSet::Result::LinkStatus linkStatus
0816             = result ? result->linkStatus()
0817             : query.selection() != Terms::UsedResources ? ResultSet::Result::Unknown
0818             : query.selection() != Terms::LinkedResources ? ResultSet::Result::Linked
0819             : ResultSet::Result::NotLinked;
0820         /* clang-format on */
0821 
0822         if (result) {
0823             // We are only updating a result we already had,
0824             // lets fill out the data and send the update signal.
0825             // Move it if necessary.
0826 
0827             auto &item = *result.iterator;
0828 
0829             item.setScore(score);
0830             item.setLinkStatus(linkStatus);
0831             item.setLastUpdate(lastUpdate);
0832             item.setFirstUpdate(firstUpdate);
0833 
0834             repositionResult(result, destinationFor(item));
0835 
0836         } else {
0837             // We do not have the resource in the cache,
0838             // lets fill out the data and insert it
0839             // at the desired position
0840 
0841             ResultSet::Result result;
0842             result.setResource(resource);
0843 
0844             result.setTitle(QStringLiteral(" "));
0845             result.setMimetype(QStringLiteral(" "));
0846             fillTitleAndMimetype(result);
0847 
0848             result.setScore(score);
0849             result.setLinkStatus(linkStatus);
0850             result.setLastUpdate(lastUpdate);
0851             result.setFirstUpdate(firstUpdate);
0852 
0853             const auto destination = destinationFor(result);
0854 
0855             q->beginInsertRows(QModelIndex(), destination.index,
0856                                destination.index);
0857 
0858             cache.insertAt(destination, result);
0859 
0860             q->endInsertRows();
0861 
0862             cache.trim();
0863         }
0864     }
0865 
0866     void onResultRemoved(const QString &resource)
0867     {
0868         const auto result = cache.find(resource);
0869 
0870         if (!result) {
0871             return;
0872         }
0873 
0874         if (query.selection() == Terms::UsedResources
0875             || result->linkStatus() != ResultSet::Result::Linked) {
0876             removeResult(result);
0877         }
0878     }
0879 
0880     void onResultLinked(const QString &resource)
0881     {
0882         if (query.selection() != Terms::UsedResources) {
0883             onResultScoreUpdated(resource, 0, 0, 0);
0884         }
0885     }
0886 
0887     void onResultUnlinked(const QString &resource)
0888     {
0889         const auto result = cache.find(resource);
0890 
0891         if (!result) {
0892             return;
0893         }
0894 
0895         if (query.selection() == Terms::LinkedResources) {
0896             removeResult(result);
0897 
0898         } else if (query.selection() == Terms::AllResources) {
0899             // When the result is unlinked, it might go away or not
0900             // depending on its previous usage
0901             reload();
0902         }
0903     }
0904 
0905     Query query;
0906     ResultWatcher watcher;
0907     bool hasMore;
0908 
0909     KActivities::Consumer activities;
0910     Common::Database::Ptr database;
0911 
0912     //_ Title and mimetype functions
0913     void fillTitleAndMimetype(ResultSet::Result &result)
0914     {
0915         if (!database) {
0916             return;
0917         }
0918 
0919         /* clang-format off */
0920         auto query = database->execQuery(
0921                 QStringLiteral("SELECT "
0922                 "title, mimetype "
0923                 "FROM "
0924                 "ResourceInfo "
0925                 "WHERE "
0926                 "targettedResource = '") + result.resource() + QStringLiteral("'")
0927                 );
0928         /* clang-format on */
0929 
0930         // Only one item at most
0931         for (const auto &item: query) {
0932             result.setTitle(item[QStringLiteral("title")].toString());
0933             result.setMimetype(item[QStringLiteral("mimetype")].toString());
0934         }
0935     }
0936 
0937     void onResourceTitleChanged(const QString &resource, const QString &title)
0938     {
0939         const auto result = cache.find(resource);
0940 
0941         if (!result) {
0942             return;
0943         }
0944 
0945         result->setTitle(title);
0946 
0947         Q_EMIT q->dataChanged(q->index(result.index), q->index(result.index));
0948     }
0949 
0950     void onResourceMimetypeChanged(const QString &resource, const QString &mimetype)
0951     {
0952         // TODO: This can add or remove items from the model
0953 
0954         const auto result = cache.find(resource);
0955 
0956         if (!result) {
0957             return;
0958         }
0959 
0960         result->setMimetype(mimetype);
0961 
0962         Q_EMIT q->dataChanged(q->index(result.index), q->index(result.index));
0963     }
0964     //^
0965 
0966     void onCurrentActivityChanged(const QString &activity)
0967     {
0968         Q_UNUSED(activity);
0969         // If the current activity has changed, and
0970         // the query lists items for the ':current' one,
0971         // reset the model (not a simple refresh this time)
0972         if (query.activities().contains(CURRENT_ACTIVITY_TAG)) {
0973             fetch(FetchReset);
0974         }
0975     }
0976 
0977 private:
0978     ResultModel *const q;
0979     static QList<ResultModelPrivate*> s_privates;
0980 
0981 };
0982 
0983 QList<ResultModelPrivate*> ResultModelPrivate::s_privates;
0984 
0985 ResultModel::ResultModel(Query query, QObject *parent)
0986     : QAbstractListModel(parent)
0987     , d(new ResultModelPrivate(query, QString(), this))
0988 {
0989     d->init();
0990 }
0991 
0992 ResultModel::ResultModel(Query query, const QString &clientId, QObject *parent)
0993     : QAbstractListModel(parent)
0994     , d(new ResultModelPrivate(query, clientId, this))
0995 {
0996     d->init();
0997 }
0998 
0999 ResultModel::~ResultModel()
1000 {
1001     delete d;
1002 }
1003 
1004 QHash<int, QByteArray> ResultModel::roleNames() const
1005 {
1006     return {
1007         { ResourceRole         , "resource" },
1008         { TitleRole            , "title" },
1009         { ScoreRole            , "score" },
1010         { FirstUpdateRole      , "created" },
1011         { LastUpdateRole       , "modified" },
1012         { LinkStatusRole       , "linkStatus" },
1013         { LinkedActivitiesRole , "linkedActivities" },
1014         { MimeType             , "mimeType" },
1015     };
1016 }
1017 
1018 QVariant ResultModel::data(const QModelIndex &item, int role) const
1019 {
1020     const auto row = item.row();
1021 
1022     if (row < 0 || row >= d->cache.size()) {
1023         return QVariant();
1024     }
1025 
1026     const auto &result = d->cache[row];
1027 
1028     /* clang-format off */
1029     return role == Qt::DisplayRole ? QString(
1030                result.title() + QStringLiteral(" ") +
1031                result.resource() + QStringLiteral(" - ") +
1032                QString::number(result.linkStatus()) + QStringLiteral(" - ") +
1033                QString::number(result.score())
1034            )
1035          : role == ResourceRole         ? result.resource()
1036          : role == TitleRole            ? result.title()
1037          : role == ScoreRole            ? result.score()
1038          : role == FirstUpdateRole      ? result.firstUpdate()
1039          : role == LastUpdateRole       ? result.lastUpdate()
1040          : role == LinkStatusRole       ? result.linkStatus()
1041          : role == LinkedActivitiesRole ? result.linkedActivities()
1042          : role == MimeType             ? result.mimetype()
1043          : role == Agent                ? result.agent()
1044          : QVariant()
1045          ;
1046     /* clang-format on */
1047 }
1048 
1049 QVariant ResultModel::headerData(int section, Qt::Orientation orientation,
1050                                  int role) const
1051 {
1052     Q_UNUSED(section);
1053     Q_UNUSED(orientation);
1054     Q_UNUSED(role);
1055     return QVariant();
1056 }
1057 
1058 int ResultModel::rowCount(const QModelIndex &parent) const
1059 {
1060     return parent.isValid() ? 0 : d->cache.size();
1061 }
1062 
1063 void ResultModel::fetchMore(const QModelIndex &parent)
1064 {
1065     if (parent.isValid()) {
1066         return;
1067     }
1068     d->fetch(ResultModelPrivate::FetchMore);
1069 }
1070 
1071 bool ResultModel::canFetchMore(const QModelIndex &parent) const
1072 {
1073     return parent.isValid()                    ? false
1074          : d->cache.size() >= d->query.limit() ? false
1075          : d->hasMore;
1076 }
1077 
1078 void ResultModel::forgetResources(const QList<QString> &resources)
1079 {
1080     const auto lstActivities = d->query.activities();
1081     for (const QString &activity : lstActivities) {
1082         const auto lstAgents = d->query.agents();
1083         for (const QString &agent : lstAgents) {
1084             for (const QString &resource : resources) {
1085                 /* clang-format off */
1086                 Stats::forgetResource(
1087                         activity,
1088                         agent == CURRENT_AGENT_TAG ?
1089                             QCoreApplication::applicationName() : agent,
1090                         resource);
1091                 /* clang-format on */
1092             }
1093         }
1094     }
1095 }
1096 
1097 void ResultModel::forgetResource(const QString &resource)
1098 {
1099     ResultModel::forgetResources({ resource });
1100 }
1101 
1102 void ResultModel::forgetResource(int row)
1103 {
1104     if (row >= d->cache.size()) {
1105         return;
1106     }
1107     const auto lstActivities = d->query.activities();
1108     for (const QString &activity : lstActivities) {
1109         const auto lstAgents = d->query.agents();
1110         for (const QString &agent : lstAgents) {
1111             /* clang-format off */
1112             Stats::forgetResource(
1113                     activity,
1114                     agent == CURRENT_AGENT_TAG ?
1115                         QCoreApplication::applicationName() : agent,
1116                     d->cache[row].resource());
1117             /* clang-format on */
1118         }
1119     }
1120 }
1121 
1122 void ResultModel::forgetAllResources()
1123 {
1124     Stats::forgetResources(d->query);
1125 }
1126 
1127 void ResultModel::setResultPosition(const QString &resource, int position)
1128 {
1129     d->cache.setLinkedResultPosition(resource, position);
1130 }
1131 
1132 void ResultModel::sortItems(Qt::SortOrder sortOrder)
1133 {
1134     // TODO
1135     Q_UNUSED(sortOrder);
1136 }
1137 
1138 void ResultModel::linkToActivity(const QUrl &resource,
1139                                  const Terms::Activity &activity,
1140                                  const Terms::Agent &agent)
1141 {
1142     d->watcher.linkToActivity(resource, activity, agent);
1143 }
1144 
1145 void ResultModel::unlinkFromActivity(const QUrl &resource,
1146                                      const Terms::Activity &activity,
1147                                      const Terms::Agent &agent)
1148 {
1149     d->watcher.unlinkFromActivity(resource, activity, agent);
1150 }
1151 
1152 } // namespace Stats
1153 } // namespace KActivities
1154 
1155 #include "moc_resultmodel.cpp"