diff --git a/src/resultmodel.cpp b/src/resultmodel.cpp index 815f36d..1685569 100644 --- a/src/resultmodel.cpp +++ b/src/resultmodel.cpp @@ -1,1090 +1,1090 @@ /* * Copyright (C) 2015, 2016 Ivan Cukic * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) version 3, or any * later version accepted by the membership of KDE e.V. (or its * successor approved by the membership of KDE e.V.), which shall * act as a proxy defined in Section 6 of version 3 of the license. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library. * If not, see . */ // Self #include "resultmodel.h" // Qt #include #include #include #include // STL and Boost #include #include #include // KDE #include #include // Local #include #include #include #include #include "resultset.h" #include "resultwatcher.h" #include "cleaning.h" #include "kactivities/consumer.h" #include #define MAX_CHUNK_LOAD_SIZE 50 #define MAX_RELOAD_CACHE_SIZE 50 #define QDBG qDebug() << "KActivitiesStats(" << (void*)this << ")" namespace KActivities { namespace Stats { class ResultModelPrivate { public: ResultModelPrivate(Query query, const QString &clientId, ResultModel *parent) : cache(this, clientId, query.limit()) , query(query) , watcher(query) , hasMore(true) , q(parent) { using Common::Database; database = Database::instance(Database::ResourcesDatabase, Database::ReadOnly); s_privates << this; } ~ResultModelPrivate() { s_privates.removeAll(this); } enum Fetch { FetchReset, // Remove old data and reload FetchReload, // Update all data FetchMore // Load more data if there is any }; class Cache { //_ public: typedef QList Items; Cache(ResultModelPrivate *d, const QString &clientId, int limit) : d(d) , m_countLimit(limit) , m_clientId(clientId) { if (!m_clientId.isEmpty()) { m_configFile = KSharedConfig::openConfig("kactivitymanagerd-statsrc"); } } ~Cache() { } inline int size() const { return m_items.size(); } inline void setLinkedResultPosition(const QString &resourcePath, int position) { if (!m_orderingConfig.isValid()) { qWarning() << "We can not reorder the results, no clientId was specified"; return; } // Preconditions: // - cache is ordered properly, first on the user's desired order, // then on the query specified order // - the resource that needs to be moved is a linked resource, not // one that comes from the stats (there are overly many // corner-cases that need to be covered in order to support // reordering of the statistics-based resources) // - the new position for the resource is not outside of the cache auto resourcePosition = find(resourcePath); if (resourcePosition) { if (resourcePosition.index == position) { return; } if (resourcePosition.iterator->linkStatus() == ResultSet::Result::NotLinked) { return; } } // Lets make a list of linked items - we can only reorder them, // not others QStringList linkedItems; foreach (const ResultSet::Result &item, m_items) { if (item.linkStatus() == ResultSet::Result::NotLinked) break; linkedItems << item.resource(); } // We have two options: // - we are planning to add an item to the desired position, // but the item is not yet in the model // - we want to move an existing item if (!resourcePosition || resourcePosition.iterator->linkStatus() == ResultSet::Result::NotLinked) { linkedItems.insert(position, resourcePath); m_fixedOrderedItems = linkedItems; } else { // We can not accept the new position to be outside // of the linked items area if (position >= linkedItems.size()) { position = linkedItems.size() - 1; } Q_ASSERT(resourcePosition.index == linkedItems.indexOf(resourcePath)); auto oldPosition = linkedItems.indexOf(resourcePath); const auto oldLinkedItems = linkedItems; kamd::utils::move_one( linkedItems.begin() + oldPosition, linkedItems.begin() + position); // When we change this, the cache is not valid anymore, // destinationFor will fail and we can not use it m_fixedOrderedItems = linkedItems; // We are prepared to reorder the cache d->repositionResult(resourcePosition, d->destinationFor(*resourcePosition)); } m_orderingConfig.writeEntry("kactivitiesLinkedItemsOrder", m_fixedOrderedItems); m_orderingConfig.sync(); // We need to notify others to reload for (const auto& other: s_privates) { if (other != d && other->cache.m_clientId == m_clientId) { other->fetch(FetchReset); } } } inline void debug() const { for (const auto& item: m_items) { qDebug() << "Item: " << item; } } void loadOrderingConfig(const QString &activityTag) { if (!m_configFile) { qDebug() << "Nothing to load - the client id is empty"; return; } m_orderingConfig = KConfigGroup(m_configFile, "ResultModel-OrderingFor-" + m_clientId + activityTag); if (m_orderingConfig.hasKey("kactivitiesLinkedItemsOrder")) { // If we have the ordering defined, use it m_fixedOrderedItems = m_orderingConfig.readEntry("kactivitiesLinkedItemsOrder", QStringList()); } else { // Otherwise, copy the order from the previous activity to this one m_orderingConfig.writeEntry("kactivitiesLinkedItemsOrder", m_fixedOrderedItems); m_orderingConfig.sync(); } } private: ResultModelPrivate *const d; QList m_items; int m_countLimit; QString m_clientId; KSharedConfig::Ptr m_configFile; KConfigGroup m_orderingConfig; QStringList m_fixedOrderedItems; friend QDebug operator<< (QDebug out, const Cache &cache) { for (const auto& item: cache.m_items) { out << "Cache item: " << item << "\n"; } return out; } public: inline const QStringList &fixedOrderedItems() const { return m_fixedOrderedItems; } //_ Fancy iterator, find, lowerBound struct FindCacheResult { Cache *const cache; Items::iterator iterator; int index; FindCacheResult(Cache *cache, Items::iterator iterator) : cache(cache) , iterator(iterator) , index(std::distance(cache->m_items.begin(), iterator)) { } operator bool() const { return iterator != cache->m_items.end(); } ResultSet::Result &operator*() const { return *iterator; } ResultSet::Result *operator->() const { return &(*iterator); } }; inline FindCacheResult find(const QString &resource) { using namespace kamd::utils::member_matcher; using boost::find_if; return FindCacheResult( this, find_if(m_items, member(&ResultSet::Result::resource) == resource)); } template inline FindCacheResult lowerBoundWithSkippedResource(Predicate &&lessThanPredicate) { using namespace kamd::utils::member_matcher; const int count = boost::count_if(m_items, [&] (const ResultSet::Result &result) { return lessThanPredicate(result, _); }); return FindCacheResult(this, m_items.begin() + count); // using namespace kamd::utils::member_matcher; // // const auto position = // std::lower_bound(m_items.begin(), m_items.end(), // _, std::forward(lessThanPredicate)); // // // We seem to have found the position for the item. // // The problem is that we might have found the same position // // we were previously at. Since this function is usually used // // to reposition the result, we might not be in a completely // // sorted collection, so the next item(s) could be less than us. // // We could do this with count_if, but it would be slower // // if (position >= m_items.cend() - 1) { // return FindCacheResult(this, position); // // } else if (lessThanPredicate(_, *(position + 1))) { // return FindCacheResult(this, position); // // } else { // return FindCacheResult( // this, std::lower_bound(position + 1, m_items.end(), // _, std::forward(lessThanPredicate))); // } } //^ inline void insertAt(const FindCacheResult &at, const ResultSet::Result &result) { m_items.insert(at.iterator, result); } inline void removeAt(const FindCacheResult &at) { m_items.removeAt(at.index); } inline const ResultSet::Result &operator[] (int index) const { return m_items[index]; } inline void clear() { if (m_items.size() == 0) return; d->q->beginRemoveRows(QModelIndex(), 0, m_items.size()); m_items.clear(); d->q->endRemoveRows(); } // Algorithm to calculate the edit operations to allow //_ replaceing items without model reset inline void replace(const Items &newItems, int from = 0) { using namespace kamd::utils::member_matcher; -#if 1 +#if 0 QDBG << "======"; QDBG << "Old items {"; for (const auto& item: m_items) { QDBG << item; } QDBG << "}"; QDBG << "New items to be added at " << from << " {"; for (const auto& item: newItems) { QDBG << item; } QDBG << "}"; #endif // Based on 'The string to string correction problem // with block moves' paper by Walter F. Tichy // // In essence, it goes like this: // // Take the first element from the new list, and try to find // it in the old one. If you can not find it, it is a new item // item - send the 'inserted' event. // If you did find it, test whether the following items also // match. This detects blocks of items that have moved. // // In this example, we find 'b', and then detect the rest of the // moved block 'b' 'c' 'd' // // Old items: a[b c d]e f g // ^ // / // New items: [b c d]a f g // // After processing one block, just repeat until the end of the // new list is reached. // // Then remove all remaining elements from the old list. // // The main addition here compared to the original papers is that // our 'strings' can not hold two instances of the same element, // and that we support updating from arbitrary position. auto newBlockStart = newItems.cbegin(); // How many items should we add? // This should remove the need for post-replace-trimming // in the case where somebody called this with too much new items. const int maxToReplace = m_countLimit - from; if (maxToReplace <= 0) return; const auto newItemsEnd = newItems.size() <= maxToReplace ? newItems.cend() : newItems.cbegin() + maxToReplace; // Finding the blocks until we reach the end of the newItems list // // from = 4 // Old items: X Y Z U a b c d e f g // ^ oldBlockStart points to the first element // of the currently processed block in the old list // // New items: _ _ _ _ b c d a f g // ^ newBlockStartIndex is the index of the first // element of the block that is currently being // processed (with 'from' offset) while (newBlockStart != newItemsEnd) { const int newBlockStartIndex = from + std::distance(newItems.cbegin(), newBlockStart); const auto oldBlockStart = std::find_if( m_items.begin() + from, m_items.end(), member(&ResultSet::Result::resource) == newBlockStart->resource()); if (oldBlockStart == m_items.end()) { // This item was not found in the old cache, so we are // inserting a new item at the same position it had in // the newItems array d->q->beginInsertRows(QModelIndex(), newBlockStartIndex, newBlockStartIndex); m_items.insert(newBlockStartIndex, *newBlockStart); d->q->endInsertRows(); // This block contained only one item, move on to find // the next block - it starts from the next item ++newBlockStart; } else { // We are searching for a block of matching items. // This is a reimplementation of std::mismatch that // accepts two complete ranges that is available only // since C++14, so we can not use it. auto newBlockEnd = newBlockStart; auto oldBlockEnd = oldBlockStart; while (newBlockEnd != newItemsEnd && oldBlockEnd != m_items.end() && newBlockEnd->resource() == oldBlockEnd->resource()) { ++newBlockEnd; ++oldBlockEnd; } // We have found matching blocks // [newBlockStart, newBlockEnd) and [oldBlockStart, newBlockEnd) const int oldBlockStartIndex = std::distance(m_items.begin() + from, oldBlockStart); const int blockSize = std::distance(oldBlockStart, oldBlockEnd); if (oldBlockStartIndex != newBlockStartIndex) { // If these blocks do not have the same start, // we need to send the move event. // Note: If there is a crash here, it means we // are getting a bad query which has duplicate // results d->q->beginMoveRows(QModelIndex(), oldBlockStartIndex, oldBlockStartIndex + blockSize - 1, QModelIndex(), newBlockStartIndex); // Moving the items from the old location to the new one kamd::utils::slide( oldBlockStart, oldBlockEnd, m_items.begin() + newBlockStartIndex); d->q->endMoveRows(); } // Skip all the items in this block, and continue with // the search newBlockStart = newBlockEnd; } } // We have avoided the need for trimming for the most part, // but if the newItems list was shorter than needed, we still // need to trim the rest. trim(from + newItems.size()); // Check whether we got an item representing a non-existent file, // if so, schedule its removal from the database for (const auto &item: newItems) { if (item.resource().startsWith('/') && !QFile(item.resource()).exists()) { d->q->forgetResource(item.resource()); } } } //^ inline void trim() { trim(m_countLimit); } inline void trim(int limit) { if (m_items.size() <= limit) return; // Example: // limit is 5, // current cache (0, 1, 2, 3, 4, 5, 6, 7), size = 8 // We need to delete from 5 to 7 d->q->beginRemoveRows(QModelIndex(), limit, m_items.size() - 1); m_items.erase(m_items.begin() + limit, m_items.end()); d->q->endRemoveRows(); } } cache; //^ struct FixedItemsLessThan { //_ Compartor that orders the linked items by user-specified order typedef kamd::utils::member_matcher::placeholder placeholder; FixedItemsLessThan(const Cache &cache, const QString &matchResource = QString()) : cache(cache), matchResource(matchResource) { } bool lessThan(const QString &leftResource, const QString &rightResource) const { const auto fixedOrderedItems = cache.fixedOrderedItems(); const auto indexLeft = fixedOrderedItems.indexOf(leftResource); const auto indexRight = fixedOrderedItems.indexOf(rightResource); const bool hasLeft = indexLeft != -1; const bool hasRight = indexRight != -1; return ( hasLeft && !hasRight) ? true : (!hasLeft && hasRight) ? false : ( hasLeft && hasRight) ? indexLeft < indexRight : leftResource < rightResource; } template bool operator() (const T &left, placeholder) const { return lessThan(left.resource(), matchResource); } template bool operator() (placeholder, const T &right) const { return lessThan(matchResource, right.resource()); } template bool operator() (const T &left, const V &right) const { return lessThan(left.resource(), right.resource()); } const Cache &cache; const QString matchResource; //^ }; inline Cache::FindCacheResult destinationFor(const ResultSet::Result &result) { using namespace kamd::utils::member_matcher; using namespace Terms; const auto resource = result.resource(); const auto score = result.score(); const auto firstUpdate = result.firstUpdate(); const auto lastUpdate = result.lastUpdate(); const auto linkStatus = result.linkStatus(); #define ORDER_BY(Field) member(&ResultSet::Result::Field) > Field #define ORDER_BY_FULL(Field) \ (query.selection() == Terms::AllResources ? \ cache.lowerBoundWithSkippedResource( \ FixedItemsLessThan(cache, resource) \ && ORDER_BY(linkStatus) \ && ORDER_BY(Field) \ && ORDER_BY(resource)) : \ cache.lowerBoundWithSkippedResource( \ FixedItemsLessThan(cache, resource) \ && ORDER_BY(Field) \ && ORDER_BY(resource)) \ ) const auto destination = query.ordering() == HighScoredFirst ? ORDER_BY_FULL(score): query.ordering() == RecentlyUsedFirst ? ORDER_BY_FULL(lastUpdate): query.ordering() == RecentlyCreatedFirst ? ORDER_BY_FULL(firstUpdate): /* otherwise */ ORDER_BY_FULL(resource) ; #undef ORDER_BY #undef ORDER_BY_FULL return destination; } inline void removeResult(const Cache::FindCacheResult &result) { q->beginRemoveRows(QModelIndex(), result.index, result.index); cache.removeAt(result); q->endRemoveRows(); if (query.selection() != Terms::LinkedResources) { fetch(cache.size(), 1); } } inline void repositionResult(const Cache::FindCacheResult &result, const Cache::FindCacheResult &destination) { // We already have the resource in the cache // So, it is the time for a reshuffle const int oldPosition = result.index; int position = destination.index; q->dataChanged(q->index(oldPosition), q->index(oldPosition)); if (oldPosition == position) { return; } if (position > oldPosition) { position++; } bool moving = q->beginMoveRows(QModelIndex(), oldPosition, oldPosition, QModelIndex(), position); kamd::utils::move_one(result.iterator, destination.iterator); if (moving) { q->endMoveRows(); } } void reload() { fetch(FetchReload); } void init() { using namespace std::placeholders; QObject::connect( &watcher, &ResultWatcher::resultScoreUpdated, q, std::bind(&ResultModelPrivate::onResultScoreUpdated, this, _1, _2, _3, _4)); QObject::connect( &watcher, &ResultWatcher::resultRemoved, q, std::bind(&ResultModelPrivate::onResultRemoved, this, _1)); QObject::connect( &watcher, &ResultWatcher::resultLinked, q, std::bind(&ResultModelPrivate::onResultLinked, this, _1)); QObject::connect( &watcher, &ResultWatcher::resultUnlinked, q, std::bind(&ResultModelPrivate::onResultUnlinked, this, _1)); QObject::connect( &watcher, &ResultWatcher::resourceTitleChanged, q, std::bind(&ResultModelPrivate::onResourceTitleChanged, this, _1, _2)); QObject::connect( &watcher, &ResultWatcher::resourceMimetypeChanged, q, std::bind(&ResultModelPrivate::onResourceMimetypeChanged, this, _1, _2)); QObject::connect( &watcher, &ResultWatcher::resultsInvalidated, q, std::bind(&ResultModelPrivate::reload, this)); if (query.activities().contains(CURRENT_ACTIVITY_TAG)) { QObject::connect( &activities, &KActivities::Consumer::currentActivityChanged, q, std::bind(&ResultModelPrivate::onCurrentActivityChanged, this, _1)); } fetch(FetchReset); } void fetch(int from, int count) { using namespace Terms; if (from + count > query.limit()) { count = query.limit() - from; } if (count <= 0) return; // In order to see whether there are more results, we need to pass // the count increased by one ResultSet results(query | Offset(from) | Limit(count + 1)); auto it = results.begin(); Cache::Items newItems; while (count --> 0 && it != results.end()) { newItems << *it; ++it; } hasMore = (it != results.end()); // We need to sort the new items for the linked resources // user-defined reordering if (query.selection() != Terms::UsedResources) { std::stable_sort(newItems.begin(), newItems.end(), FixedItemsLessThan(cache)); } cache.replace(newItems, from); } void fetch(Fetch mode) { if (mode == FetchReset) { // Removing the previously cached data // and loading all from scratch cache.clear(); const QString activityTag = query.activities().contains(CURRENT_ACTIVITY_TAG) ? ("-ForActivity-" + activities.currentActivity()) : "-ForAllActivities"; cache.loadOrderingConfig(activityTag); fetch(0, MAX_CHUNK_LOAD_SIZE); } else if (mode == FetchReload) { if (cache.size() > MAX_RELOAD_CACHE_SIZE) { // If the cache is big, we are pretending // we were asked to reset the model fetch(FetchReset); } else { // We are only updating the currently // cached items, nothing more fetch(0, cache.size()); } } else { // FetchMore // Load a new batch of data fetch(cache.size(), MAX_CHUNK_LOAD_SIZE); } } void onResultScoreUpdated(const QString &resource, double score, uint lastUpdate, uint firstUpdate) { QDBG << "ResultModelPrivate::onResultScoreUpdated " << "result added:" << resource << "score:" << score << "last:" << lastUpdate << "first:" << firstUpdate; // This can also be called when the resource score // has been updated, so we need to check whether // we already have it in the cache const auto result = cache.find(resource); ResultSet::Result::LinkStatus linkStatus = result ? result->linkStatus() : query.selection() != Terms::UsedResources ? ResultSet::Result::Unknown : query.selection() != Terms::LinkedResources ? ResultSet::Result::Linked : ResultSet::Result::NotLinked; if (result) { // We are only updating a result we already had, // lets fill out the data and send the update signal. // Move it if necessary. auto &item = *result.iterator; item.setScore(score); item.setLinkStatus(linkStatus); item.setLastUpdate(lastUpdate); item.setFirstUpdate(firstUpdate); repositionResult(result, destinationFor(item)); } else { // We do not have the resource in the cache, // lets fill out the data and insert it // at the desired position ResultSet::Result result; result.setResource(resource); result.setTitle(" "); result.setMimetype(" "); fillTitleAndMimetype(result); result.setScore(score); result.setLinkStatus(linkStatus); result.setLastUpdate(lastUpdate); result.setFirstUpdate(firstUpdate); const auto destination = destinationFor(result); q->beginInsertRows(QModelIndex(), destination.index, destination.index); cache.insertAt(destination, result); q->endInsertRows(); cache.trim(); } } void onResultRemoved(const QString &resource) { const auto result = cache.find(resource); if (!result) return; if (query.selection() == Terms::UsedResources || result->linkStatus() != ResultSet::Result::Linked) { removeResult(result); } } void onResultLinked(const QString &resource) { if (query.selection() != Terms::UsedResources) { onResultScoreUpdated(resource, 0, 0, 0); } } void onResultUnlinked(const QString &resource) { const auto result = cache.find(resource); if (!result) return; if (query.selection() == Terms::LinkedResources) { removeResult(result); } else if (query.selection() == Terms::AllResources) { // When the result is unlinked, it might go away or not // depending on its previous usage reload(); } } Query query; ResultWatcher watcher; bool hasMore; KActivities::Consumer activities; Common::Database::Ptr database; //_ Title and mimetype functions void fillTitleAndMimetype(ResultSet::Result &result) { auto query = database->execQuery( "SELECT " "title, mimetype " "FROM " "ResourceInfo " "WHERE " "targettedResource = '" + result.resource() + "'" ); // Only one item at most for (const auto &item: query) { result.setTitle(item["title"].toString()); result.setMimetype(item["mimetype"].toString()); } } void onResourceTitleChanged(const QString &resource, const QString &title) { const auto result = cache.find(resource); if (!result) return; result->setTitle(title); q->dataChanged(q->index(result.index), q->index(result.index)); } void onResourceMimetypeChanged(const QString &resource, const QString &mimetype) { // TODO: This can add or remove items from the model const auto result = cache.find(resource); if (!result) return; result->setMimetype(mimetype); q->dataChanged(q->index(result.index), q->index(result.index)); } //^ void onCurrentActivityChanged(const QString &activity) { Q_UNUSED(activity); // If the current activity has changed, and // the query lists items for the ':current' one, // reset the model (not a simple refresh this time) if (query.activities().contains(CURRENT_ACTIVITY_TAG)) { fetch(FetchReset); } } private: ResultModel *const q; static QList s_privates; }; QList ResultModelPrivate::s_privates; ResultModel::ResultModel(Query query, QObject *parent) : QAbstractListModel(parent) , d(new ResultModelPrivate(query, QString(), this)) { d->init(); } ResultModel::ResultModel(Query query, const QString &clientId, QObject *parent) : QAbstractListModel(parent) , d(new ResultModelPrivate(query, clientId, this)) { d->init(); } ResultModel::~ResultModel() { delete d; } QHash ResultModel::roleNames() const { return { { ResourceRole , "resource" }, { TitleRole , "title" }, { ScoreRole , "score" }, { FirstUpdateRole , "created" }, { LastUpdateRole , "modified" }, { LinkStatusRole , "linkStatus" }, { LinkedActivitiesRole , "linkedActivities" } }; } QVariant ResultModel::data(const QModelIndex &item, int role) const { const auto row = item.row(); if (row < 0 || row >= d->cache.size()) { return QVariant(); } const auto &result = d->cache[row]; return role == Qt::DisplayRole ? ( result.title() + " " + result.resource() + " - " + QString::number(result.linkStatus()) + " - " + QString::number(result.score()) ) : role == ResourceRole ? result.resource() : role == TitleRole ? result.title() : role == ScoreRole ? result.score() : role == FirstUpdateRole ? result.firstUpdate() : role == LastUpdateRole ? result.lastUpdate() : role == LinkStatusRole ? result.linkStatus() : role == LinkedActivitiesRole ? result.linkedActivities() : QVariant() ; } QVariant ResultModel::headerData(int section, Qt::Orientation orientation, int role) const { Q_UNUSED(section); Q_UNUSED(orientation); Q_UNUSED(role); return QVariant(); } int ResultModel::rowCount(const QModelIndex &parent) const { return parent.isValid() ? 0 : d->cache.size(); } void ResultModel::fetchMore(const QModelIndex &parent) { if (parent.isValid()) return; d->fetch(ResultModelPrivate::FetchMore); } bool ResultModel::canFetchMore(const QModelIndex &parent) const { return parent.isValid() ? false : d->cache.size() >= d->query.limit() ? false : d->hasMore; } void ResultModel::forgetResource(const QString &resource) { foreach (const QString &activity, d->query.activities()) { foreach (const QString &agent, d->query.agents()) { Stats::forgetResource( activity, agent == CURRENT_AGENT_TAG ? QCoreApplication::applicationName() : agent, resource); } } } void ResultModel::forgetResource(int row) { if (row >= d->cache.size()) return; foreach (const QString &activity, d->query.activities()) { foreach (const QString &agent, d->query.agents()) { Stats::forgetResource( activity, agent == CURRENT_AGENT_TAG ? QCoreApplication::applicationName() : agent, d->cache[row].resource()); } } } void ResultModel::forgetAllResources() { Stats::forgetResources(d->query); } void ResultModel::setResultPosition(const QString &resource, int position) { d->cache.setLinkedResultPosition(resource, position); } void ResultModel::sortItems(Qt::SortOrder sortOrder) { // TODO Q_UNUSED(sortOrder); } void ResultModel::linkToActivity(const QUrl &resource, const Terms::Activity &activity, const Terms::Agent &agent) { d->watcher.linkToActivity(resource, activity, agent); } void ResultModel::unlinkFromActivity(const QUrl &resource, const Terms::Activity &activity, const Terms::Agent &agent) { d->watcher.unlinkFromActivity(resource, activity, agent); } } // namespace Stats } // namespace KActivities // #include "resourcemodel.moc" diff --git a/src/resultset.cpp b/src/resultset.cpp index dcf6d13..1275ef5 100644 --- a/src/resultset.cpp +++ b/src/resultset.cpp @@ -1,469 +1,469 @@ /* * Copyright (C) 2015, 2016 Ivan Cukic * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) version 3, or any * later version accepted by the membership of KDE e.V. (or its * successor approved by the membership of KDE e.V.), which shall * act as a proxy defined in Section 6 of version 3 of the license. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library. * If not, see . */ #include "resultset.h" // Qt #include #include #include // Local #include #include #include // Boost and STL #include #include #include #include // KActivities #include "activitiessync_p.h" -#define DEBUG_QUERIES 1 +#define DEBUG_QUERIES 0 namespace KActivities { namespace Stats { using namespace Terms; class ResultSet_ResultPrivate { public: QString resource; QString title; QString mimetype; double score; uint lastUpdate; uint firstUpdate; ResultSet::Result::LinkStatus linkStatus; QStringList linkedActivities; }; ResultSet::Result::Result() : d(new ResultSet_ResultPrivate()) { } ResultSet::Result::Result(Result &&result) : d(result.d) { result.d = nullptr; } ResultSet::Result::Result(const Result &result) : d(new ResultSet_ResultPrivate(*result.d)) { } ResultSet::Result &ResultSet::Result::operator=(Result result) { std::swap(d, result.d); return *this; } ResultSet::Result::~Result() { delete d; } #define CREATE_GETTER_AND_SETTER(Type, Name, Set) \ Type ResultSet::Result::Name() const \ { \ return d->Name; \ } \ \ void ResultSet::Result::Set(Type Name) \ { \ d->Name = Name; \ } CREATE_GETTER_AND_SETTER(QString, resource, setResource) CREATE_GETTER_AND_SETTER(QString, title, setTitle) CREATE_GETTER_AND_SETTER(QString, mimetype, setMimetype) CREATE_GETTER_AND_SETTER(double, score, setScore) CREATE_GETTER_AND_SETTER(uint, lastUpdate, setLastUpdate) CREATE_GETTER_AND_SETTER(uint, firstUpdate, setFirstUpdate) CREATE_GETTER_AND_SETTER(ResultSet::Result::LinkStatus, linkStatus, setLinkStatus) CREATE_GETTER_AND_SETTER(QStringList, linkedActivities, setLinkedActivities) #undef CREATE_GETTER_AND_SETTER class ResultSetPrivate { public: Common::Database::Ptr database; QSqlQuery query; Query queryDefinition; mutable ActivitiesSync::ConsumerPtr activities; void initQuery() { if (!database || query.isActive()) { return; } auto selection = queryDefinition.selection(); query = database->execQuery(replaceQueryParameters( selection == LinkedResources ? linkedResourcesQuery() : selection == UsedResources ? usedResourcesQuery() : selection == AllResources ? allResourcesQuery() : QString())); if (query.lastError().isValid()) { qDebug() << "Error: " << query.lastError(); } Q_ASSERT_X(query.isActive(), "ResultSet initQuery", "Query is not valid"); } QString agentClause(const QString &agent) const { if (agent == QLatin1String(":any")) return QStringLiteral("1"); return "agent = '" + ( agent == QLatin1String(":current") ? QCoreApplication::instance()->applicationName() : agent ) + "'"; } QString activityClause(const QString &activity) const { if (activity == QLatin1String(":any")) return QStringLiteral("1"); return "activity = '" + ( activity == QLatin1String(":current") ? ActivitiesSync::currentActivity(activities) : activity ) + "'"; } inline QString starPattern(const QString &pattern) const { return Common::parseStarPattern(pattern, QStringLiteral("%"), [] (QString str) { return str.replace(QLatin1String("%"), QLatin1String("\\%")).replace(QLatin1String("_"), QLatin1String("\\_")); }); } QString urlFilterClause(const QString &urlFilter) const { if (urlFilter == QLatin1String("*")) return QStringLiteral("1"); return "resource LIKE '" + Common::starPatternToLike(urlFilter) + "' ESCAPE '\\'"; } QString mimetypeClause(const QString &mimetype) const { if (mimetype == QLatin1String(":any") || mimetype == QLatin1String("*")) return QStringLiteral("1"); return "mimetype LIKE '" + Common::starPatternToLike(mimetype) + "' ESCAPE '\\'"; } /** * Transforms the input list's elements with the f member method, * and returns the resulting list */ template inline QStringList transformedList(const QStringList &input, F f) const { using namespace std::placeholders; QStringList result; boost::transform(input, std::back_inserter(result), std::bind(f, this, _1)); return result; } QString limitOffsetSuffix() const { QString result; const int limit = queryDefinition.limit(); if (limit > 0) { result += " LIMIT " + QString::number(limit); const int offset = queryDefinition.offset(); if (offset > 0) { result += " OFFSET " + QString::number(offset); } } return result; } inline QString replaceQueryParameters(const QString &_query) const { // ORDER BY column auto ordering = queryDefinition.ordering(); QString orderingColumn = QStringLiteral("linkStatus DESC, ") + ( ordering == HighScoredFirst ? QStringLiteral("score DESC,") : ordering == RecentlyCreatedFirst ? QStringLiteral("firstUpdate DESC,") : ordering == RecentlyUsedFirst ? QStringLiteral("lastUpdate DESC,") : ordering == OrderByTitle ? QStringLiteral("title ASC,") : QString() ); // WHERE clause for filtering on agents QStringList agentsFilter = transformedList( queryDefinition.agents(), &ResultSetPrivate::agentClause); // WHERE clause for filtering on activities QStringList activitiesFilter = transformedList( queryDefinition.activities(), &ResultSetPrivate::activityClause); // WHERE clause for filtering on resource URLs QStringList urlFilter = transformedList( queryDefinition.urlFilters(), &ResultSetPrivate::urlFilterClause); // WHERE clause for filtering on resource mime QStringList mimetypeFilter = transformedList( queryDefinition.types(), &ResultSetPrivate::mimetypeClause); auto query = _query + "\nORDER BY $orderingColumn resource ASC\n" + limitOffsetSuffix(); return kamd::utils::debug_and_return(DEBUG_QUERIES, "Query: ", query .replace(QLatin1String("$orderingColumn"), orderingColumn) .replace(QLatin1String("$agentsFilter"), agentsFilter.join(QStringLiteral(" OR "))) .replace(QLatin1String("$activitiesFilter"), activitiesFilter.join(QStringLiteral(" OR "))) .replace(QLatin1String("$urlFilter"), urlFilter.join(QStringLiteral(" OR "))) .replace(QLatin1String("$mimetypeFilter"), mimetypeFilter.join(QStringLiteral(" OR "))) ); } static const QString &linkedResourcesQuery() { // TODO: We need to correct the scores based on the time that passed // since the cache was last updated, although, for this query, // scores are not that important. static const QString query = R"sql( SELECT rl.targettedResource as resource , SUM(rsc.cachedScore) as score , MIN(rsc.firstUpdate) as firstUpdate , MAX(rsc.lastUpdate) as lastUpdate , rl.usedActivity as activity , rl.initiatingAgent as agent , COALESCE(ri.title, rl.targettedResource) as title , ri.mimetype as mimetype , 2 as linkStatus FROM ResourceLink rl LEFT JOIN ResourceScoreCache rsc ON rl.targettedResource = rsc.targettedResource AND rl.usedActivity = rsc.usedActivity AND rl.initiatingAgent = rsc.initiatingAgent LEFT JOIN ResourceInfo ri ON rl.targettedResource = ri.targettedResource WHERE ($agentsFilter) AND ($activitiesFilter) AND ($urlFilter) AND ($mimetypeFilter) GROUP BY resource, title )sql" ; return query; } static const QString &usedResourcesQuery() { // TODO: We need to correct the scores based on the time that passed // since the cache was last updated static const QString query = R"sql( SELECT rsc.targettedResource as resource , SUM(rsc.cachedScore) as score , MIN(rsc.firstUpdate) as firstUpdate , MAX(rsc.lastUpdate) as lastUpdate , rsc.usedActivity as activity , rsc.initiatingAgent as agent , COALESCE(ri.title, rsc.targettedResource) as title , ri.mimetype as mimetype , 1 as linkStatus -- Note: this is replaced by allResourcesQuery FROM ResourceScoreCache rsc LEFT JOIN ResourceInfo ri ON rsc.targettedResource = ri.targettedResource WHERE ($agentsFilter) AND ($activitiesFilter) AND ($urlFilter) AND ($mimetypeFilter) GROUP BY resource, title )sql" ; return query; } static const QString &allResourcesQuery() { // TODO: Implement counting of the linked items // int linkedItemsCount = 0; // // if (linkedItemsCount >= limit) { // return linkedResourcesQuery(); // // } else if (linkedItemsCount == 0) { // return usedResourcesQuery(); // // } else { static const QString usedResourcesQuery_ = [] { auto query = usedResourcesQuery(); query.replace(QLatin1String("WHERE"), QLatin1String("WHERE rsc.targettedResource NOT IN " "(SELECT resource FROM LinkedResourcesResults) AND ")) .replace(QLatin1String("1 as linkStatus"), QLatin1String("0 as linkStatus")); return query; }(); static const QString query = "WITH LinkedResourcesResults as (\n" + linkedResourcesQuery() + "\n)\n" + "SELECT * FROM LinkedResourcesResults \n" + "UNION \n" + usedResourcesQuery_; // } return query; } ResultSet::Result currentResult() const { ResultSet::Result result; result.setResource(query.value(QStringLiteral("resource")).toString()); result.setTitle(query.value(QStringLiteral("title")).toString()); result.setMimetype(query.value(QStringLiteral("mimetype")).toString()); result.setScore(query.value(QStringLiteral("score")).toDouble()); result.setLastUpdate(query.value(QStringLiteral("lastUpdate")).toInt()); result.setFirstUpdate(query.value(QStringLiteral("firstUpdate")).toInt()); result.setLinkStatus( (ResultSet::Result::LinkStatus)query.value(QStringLiteral("linkStatus")).toInt()); auto query = database->createQuery(); query.prepare(R"sql( SELECT usedActivity FROM ResourceLink WHERE targettedResource = :resource )sql"); query.bindValue(":resource", result.resource()); query.exec(); QStringList linkedActivities; for (const auto &item: query) { linkedActivities << item[0].toString(); } result.setLinkedActivities(linkedActivities); // qDebug() << result.resource() << "linked to activities" << result.linkedActivities(); return result; } }; ResultSet::ResultSet(Query query) : d(new ResultSetPrivate()) { using namespace Common; d->database = Database::instance(Database::ResourcesDatabase, Database::ReadOnly); if (!(d->database)) { qWarning() << "KActivities ERROR: There is no database. This probably means " "that you do not have the Activity Manager running, or that " "something else is broken on your system. Recent documents and " "alike will not work!"; Q_ASSERT_X((bool)d->database, "ResultSet constructor", "Database is NULL"); } d->queryDefinition = query; d->initQuery(); } ResultSet::ResultSet(ResultSet &&source) : d(nullptr) { std::swap(d, source.d); } ResultSet::ResultSet(const ResultSet &source) : d(new ResultSetPrivate(*source.d)) { } ResultSet &ResultSet::operator= (ResultSet source) { std::swap(d, source.d); return *this; } ResultSet::~ResultSet() { delete d; } ResultSet::Result ResultSet::at(int index) const { Q_ASSERT_X(d->query.isActive(), "ResultSet::at", "Query is not active"); d->query.seek(index); return d->currentResult(); } } // namespace Stats } // namespace KActivities #include "resultset_iterator.cpp"