diff --git a/src/common/database/Database.cpp b/src/common/database/Database.cpp index 1dafeca..e21d267 100644 --- a/src/common/database/Database.cpp +++ b/src/common/database/Database.cpp @@ -1,293 +1,295 @@ /* * Copyright 2014 Ivan Cukic * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 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 14 of version 3 of the license. * * This program 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "Database.h" #include #include #include #include #include #include #include #include #include #include #include #include namespace Common { namespace { #ifdef QT_DEBUG QString lastExecutedQuery; #endif std::mutex databases_mutex; struct DatabaseInfo { Qt::HANDLE thread; Database::OpenMode openMode; }; bool operator<(const DatabaseInfo &left, const DatabaseInfo &right) { return left.thread < right.thread ? true : left.thread > right.thread ? false : left.openMode < right.openMode; } std::map> databases; } class QSqlDatabaseWrapper { private: QSqlDatabase m_database; bool m_open; QString m_connectionName; public: QSqlDatabaseWrapper(const DatabaseInfo &info) : m_open(false) { m_connectionName = "kactivities_db_resources_" // Adding the thread number to the database name + QString::number((quintptr)info.thread) // And whether it is read-only or read-write + (info.openMode == Database::ReadOnly ? "_readonly" : "_readwrite"); m_database = QSqlDatabase::contains(m_connectionName) ? QSqlDatabase::database(m_connectionName) : QSqlDatabase::addDatabase(QStringLiteral("QSQLITE"), m_connectionName); if (info.openMode == Database::ReadOnly) { m_database.setConnectOptions(QStringLiteral("QSQLITE_OPEN_READONLY")); } // We are allowing the database file to be overridden mostly for testing purposes m_database.setDatabaseName(ResourcesDatabaseSchema::path()); m_open = m_database.open(); if (!m_open) { qWarning() << "KActivities: Database is not open: " << m_database.connectionName() << m_database.databaseName() << m_database.lastError(); if (info.openMode == Database::ReadWrite) { qFatal("KActivities: Opening the database in RW mode should always succeed"); } } } ~QSqlDatabaseWrapper() { qDebug() << "Closing SQL connection: " << m_connectionName; } QSqlDatabase &get() { return m_database; } bool isOpen() const { return m_open; } QString connectionName() const { return m_connectionName; } }; class Database::Private { public: Private() { } QSqlQuery query(const QString &query) { return database ? QSqlQuery(query, database->get()) : QSqlQuery(); } QSqlQuery query() { return database ? QSqlQuery(database->get()) : QSqlQuery(); } QScopedPointer database; }; Database::Locker::Locker(Database &database) : m_database(database.d->database->get()) { m_database.transaction(); } Database::Locker::~Locker() { m_database.commit(); } Database::Ptr Database::instance(Source source, OpenMode openMode) { Q_UNUSED(source) // for the time being std::lock_guard lock(databases_mutex); // We are saving instances per thread and per read/write mode DatabaseInfo info; info.thread = QThread::currentThreadId(); info.openMode = openMode; // Do we have an instance matching the request? auto search = databases.find(info); if (search != databases.end()) { auto ptr = search->second.lock(); if (ptr) { return ptr; } } // Creating a new database instance auto ptr = std::make_shared(); ptr->d->database.reset(new QSqlDatabaseWrapper(info)); if (!ptr->d->database->isOpen()) { return nullptr; } databases[info] = ptr; if (info.openMode == ReadOnly) { // From now on, only SELECT queries will work ptr->setPragma(QStringLiteral("query_only = 1")); // These should not make any difference ptr->setPragma(QStringLiteral("synchronous = 0")); } else { // Using the write-ahead log and sync = NORMAL for faster writes ptr->setPragma(QStringLiteral("synchronous = 1")); } // Maybe we should use the write-ahead log auto walResult = ptr->pragma(QStringLiteral("journal_mode = WAL")); if (walResult != "wal") { - qFatal("KActivities: Database can not be opened in WAL mode. Check the " - "SQLite version (required >3.7.0). And whether your filesystem " - "supports shared memory"); + qWarning("KActivities: Database can not be opened in WAL mode. Check the " + "SQLite version (required >3.7.0). And whether your filesystem " + "supports shared memory"); + + return nullptr; } // We don't have a big database, lets flush the WAL when // it reaches 400k, not 4M as is default ptr->setPragma(QStringLiteral("wal_autocheckpoint = 100")); qDebug() << "KActivities: Database connection: " << ptr->d->database->connectionName() << "\n query_only: " << ptr->pragma(QStringLiteral("query_only")) << "\n journal_mode: " << ptr->pragma(QStringLiteral("journal_mode")) << "\n wal_autocheckpoint: " << ptr->pragma(QStringLiteral("wal_autocheckpoint")) << "\n synchronous: " << ptr->pragma(QStringLiteral("synchronous")) ; return ptr; } Database::Database() { } Database::~Database() { } QSqlQuery Database::createQuery() const { return d->query(); } QString Database::lastQuery() const { #ifdef QT_DEBUG return lastExecutedQuery; #endif return QString(); } QSqlQuery Database::execQuery(const QString &query, bool ignoreErrors) const { #ifdef QT_NO_DEBUG Q_UNUSED(ignoreErrors); return d->query(query); #else auto result = d->query(query); lastExecutedQuery = query; if (!ignoreErrors && result.lastError().isValid()) { qWarning() << "SQL: " << "\n error: " << result.lastError() << "\n query: " << query; } return result; #endif } QSqlQuery Database::execQueries(const QStringList &queries) const { QSqlQuery result; for (const auto query: queries) { result = execQuery(query); } return result; } void Database::setPragma(const QString &pragma) { execQuery(QStringLiteral("PRAGMA ") + pragma); } QVariant Database::pragma(const QString &pragma) const { return value("PRAGMA " + pragma); } QVariant Database::value(const QString &query) const { auto result = execQuery(query); return result.next() ? result.value(0) : QVariant(); } } // namespace Common diff --git a/src/resultmodel.cpp b/src/resultmodel.cpp index 7290b95..077edc8 100644 --- a/src/resultmodel.cpp +++ b/src/resultmodel.cpp @@ -1,1101 +1,1103 @@ /* * 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 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; enum Ordering { PartialOrdering, FullOrdering }; FixedItemsLessThan(Ordering ordering, const Cache &cache, const QString &matchResource = QString()) : cache(cache), matchResource(matchResource), ordering(ordering) { } 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 : (ordering == PartialOrdering ? false : 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; Ordering ordering; //^ }; 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 FIXED_ITEMS_LESS_THAN FixedItemsLessThan(FixedItemsLessThan::FullOrdering, cache, resource) #define ORDER_BY(Field) member(&ResultSet::Result::Field) > Field #define ORDER_BY_FULL(Field) \ (query.selection() == Terms::AllResources ? \ cache.lowerBoundWithSkippedResource( \ FIXED_ITEMS_LESS_THAN \ && ORDER_BY(linkStatus) \ && ORDER_BY(Field) \ && ORDER_BY(resource)) : \ cache.lowerBoundWithSkippedResource( \ FIXED_ITEMS_LESS_THAN \ && 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 #undef FIXED_ITEMS_LESS_THAN 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. This needs only to be a partial sort, // the main sorting is done by sqlite if (query.selection() != Terms::UsedResources) { std::stable_sort( newItems.begin(), newItems.end(), FixedItemsLessThan(FixedItemsLessThan::PartialOrdering, 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) { + if (!database) return; + 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 744bb61..2fc081b 100644 --- a/src/resultset.cpp +++ b/src/resultset.cpp @@ -1,529 +1,529 @@ /* * 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 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(); + qWarning() << "[Error at ResultSetPrivate::initQuery]: " << 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; + auto queryString = _query; - query.replace("ORDER_BY_CLAUSE", "ORDER BY $orderingColumn resource ASC") - .replace("LIMIT_CLAUSE", limitOffsetSuffix()); + queryString.replace("ORDER_BY_CLAUSE", "ORDER BY $orderingColumn resource ASC") + .replace("LIMIT_CLAUSE", limitOffsetSuffix()); return kamd::utils::debug_and_return(DEBUG_QUERIES, "Query: ", - query + queryString .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 = + static const QString queryString = 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 ORDER_BY_CLAUSE LIMIT_CLAUSE )sql" ; - return query; + return queryString; } 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 = + static const QString queryString = 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 FROM ResourceScoreCache rsc LEFT JOIN ResourceInfo ri ON rsc.targettedResource = ri.targettedResource WHERE ($agentsFilter) AND ($activitiesFilter) AND ($urlFilter) AND ($mimetypeFilter) GROUP BY resource, title ORDER_BY_CLAUSE LIMIT_CLAUSE )sql" ; - return query; + return queryString; } static const QString &allResourcesQuery() { // 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 = + static const QString queryString = R"sql( WITH LinkedResourcesResults AS ( SELECT rl.targettedResource as resource , rsc.cachedScore as score , rsc.firstUpdate as firstUpdate , rsc.lastUpdate as lastUpdate , rl.usedActivity as activity , rl.initiatingAgent as agent , 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 WHERE ($agentsFilter) AND ($activitiesFilter) AND ($urlFilter) AND ($mimetypeFilter) ), UsedResourcesResults AS ( SELECT rsc.targettedResource as resource , rsc.cachedScore as score , rsc.firstUpdate as firstUpdate , rsc.lastUpdate as lastUpdate , rsc.usedActivity as activity , rsc.initiatingAgent as agent , 0 as linkStatus FROM ResourceScoreCache rsc WHERE ($agentsFilter) AND ($activitiesFilter) AND ($urlFilter) AND ($mimetypeFilter) ), CollectedResults AS ( SELECT * FROM LinkedResourcesResults UNION SELECT * FROM UsedResourcesResults WHERE resource NOT IN (SELECT resource FROM LinkedResourcesResults) ) SELECT resource , SUM(score) as score , MIN(firstUpdate) as firstUpdate , MAX(lastUpdate) as lastUpdate , activity , agent , COALESCE(ri.title, resource) as title , ri.mimetype as mimetype , linkStatus FROM CollectedResults cr LEFT JOIN ResourceInfo ri ON cr.resource = ri.targettedResource GROUP BY resource, title ORDER_BY_CLAUSE LIMIT_CLAUSE )sql" ; - return query; + return queryString; } ResultSet::Result currentResult() const { ResultSet::Result result; + + if (!database || !query.isActive()) return 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(); + auto linkedActivitiesQuery = database->createQuery(); - query.prepare(R"sql( + linkedActivitiesQuery.prepare(R"sql( SELECT usedActivity FROM ResourceLink WHERE targettedResource = :resource )sql"); - query.bindValue(":resource", result.resource()); - query.exec(); + linkedActivitiesQuery.bindValue(":resource", result.resource()); + linkedActivitiesQuery.exec(); QStringList linkedActivities; - for (const auto &item: query) { + for (const auto &item: linkedActivitiesQuery) { linkedActivities << item[0].toString(); } result.setLinkedActivities(linkedActivities); // qDebug() << result.resource() << "linked to activities" << result.linkedActivities(); return result; } }; -ResultSet::ResultSet(Query query) +ResultSet::ResultSet(Query queryDefinition) : 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->queryDefinition = queryDefinition; 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"); + if (!d->query.isActive()) return Result(); d->query.seek(index); return d->currentResult(); } } // namespace Stats } // namespace KActivities #include "resultset_iterator.cpp" diff --git a/src/resultwatcher.cpp b/src/resultwatcher.cpp index 32ffe29..5cf3b4c 100644 --- a/src/resultwatcher.cpp +++ b/src/resultwatcher.cpp @@ -1,388 +1,392 @@ /* * 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 "resultwatcher.h" // Qt #include #include #include #include #include // Local #include #include // Boost and STL #include #include #include #include #include // KActivities #include #include "resourceslinking_interface.h" #include "resourcesscoring_interface.h" #include "common/dbus/common.h" #include "common/specialvalues.h" #include "activitiessync_p.h" #include "utils/lazy_val.h" #include "utils/qsqlquery_iterator.h" #include #define QDBG qDebug() << "KActivitiesStats(" << (void*)this << ")" namespace KActivities { namespace Stats { // Main class class ResultWatcherPrivate { public: mutable ActivitiesSync::ConsumerPtr activities; QList urlFilters; ResultWatcherPrivate(ResultWatcher *parent, Query query) : linking(new KAMD_DBUS_CLASS_INTERFACE(Resources/Linking, ResourcesLinking, nullptr)) , scoring(new KAMD_DBUS_CLASS_INTERFACE(Resources/Scoring, ResourcesScoring, nullptr)) , q(parent) , query(query) { for (const auto& urlFilter: query.urlFilters()) { urlFilters << Common::starPatternToRegex(urlFilter); } m_resultInvalidationTimer.setSingleShot(true); m_resultInvalidationTimer.setInterval(200); QObject::connect(&m_resultInvalidationTimer, &QTimer::timeout, q, emit &ResultWatcher::resultsInvalidated); } // Like boost any_of, but returning true if the range is empty template inline bool any_of(const Collection &collection, Predicate &&predicate) const { const auto begin = collection.cbegin(); const auto end = collection.cend(); return begin == end || std::any_of(begin, end, std::forward(predicate)); } #define DEBUG_MATCHERS 0 // Processing the list of activities as specified by the query. // If it contains :any, we are returning true, otherwise // we want to match a specific activity (be it the current // activity or not). The :global special value is not special here bool activityMatches(const QString &activity) const { #if DEBUG_MATCHERS qDebug() << "Activity " << activity << "matching against" << query.activities(); #endif return kamd::utils::debug_and_return(DEBUG_MATCHERS, " -> returning ", activity == ANY_ACTIVITY_TAG || any_of(query.activities(), [&] (const QString &matcher) { return matcher == ANY_ACTIVITY_TAG ? true : matcher == CURRENT_ACTIVITY_TAG ? (matcher == activity || activity == ActivitiesSync::currentActivity(activities)) : activity == matcher; } )); } // Same as above, but for agents bool agentMatches(const QString &agent) const { #if DEBUG_MATCHERS qDebug() << "Agent " << agent << "matching against" << query.agents(); #endif return kamd::utils::debug_and_return(DEBUG_MATCHERS, " -> returning ", agent == ANY_AGENT_TAG || any_of(query.agents(), [&] (const QString &matcher) { return matcher == ANY_AGENT_TAG ? true : matcher == CURRENT_AGENT_TAG ? (matcher == agent || agent == QCoreApplication::applicationName()) : agent == matcher; } )); } // Same as above, but for urls bool urlMatches(const QString &url) const { #if DEBUG_MATCHERS qDebug() << "Url " << url << "matching against" << urlFilters; #endif return kamd::utils::debug_and_return(DEBUG_MATCHERS, " -> returning ", any_of(urlFilters, [&] (const QRegExp &matcher) { return matcher.exactMatch(url); } )); } bool typeMatches(const QString &resource) const { // We don't necessarily need to retrieve the type from // the database. If we do, get it only once auto type = kamd::utils::make_lazy_val([&] () -> QString { using Common::Database; - auto query + auto database = Database::instance(Database::ResourcesDatabase, - Database::ReadOnly) - ->execQuery("SELECT mimetype FROM ResourceInfo WHERE " - "targettedResource = '" + resource + "'"); + Database::ReadOnly); + + if (!database) return QString(); + + auto query + = database->execQuery("SELECT mimetype FROM ResourceInfo WHERE " + "targettedResource = '" + resource + "'"); for (const auto &item : query) { return item[0].toString(); } return QString(); }); #if DEBUG_MATCHERS qDebug() << "Type " << "...type..." << "matching against" << query.types(); qDebug() << "ANY_TYPE_TAG" << ANY_TYPE_TAG; #endif return kamd::utils::debug_and_return(DEBUG_MATCHERS, " -> returning ", any_of(query.types(), [&] (const QString &matcher) { return matcher == ANY_TYPE_TAG || matcher == type; } )); } bool eventMatches(const QString &agent, const QString &resource, const QString &activity) const { // The order of checks is not arbitrary, it is sorted // from the cheapest, to the most expensive return kamd::utils::debug_and_return(DEBUG_MATCHERS, "event matches?", agentMatches(agent) && activityMatches(activity) && urlMatches(resource) && typeMatches(resource) ); } void onResourceLinkedToActivity(const QString &agent, const QString &resource, const QString &activity) { #if DEBUG_MATCHERS qDebug() << "Resource has been linked: " << agent << resource << activity; #endif // The used resources do not really care about the linked ones if (query.selection() == Terms::UsedResources) return; if (!eventMatches(agent, resource, activity)) return; // TODO: See whether it makes sense to have // lastUpdate/firstUpdate here as well emit q->resultLinked(resource); } void onResourceUnlinkedFromActivity(const QString &agent, const QString &resource, const QString &activity) { #if DEBUG_MATCHERS qDebug() << "Resource unlinked: " << agent << resource << activity; #endif // The used resources do not really care about the linked ones if (query.selection() == Terms::UsedResources) return; if (!eventMatches(agent, resource, activity)) return; emit q->resultUnlinked(resource); } #undef DEBUG_MATCHERS void onResourceScoreUpdated(const QString &activity, const QString &agent, const QString &resource, double score, uint lastUpdate, uint firstUpdate) { Q_ASSERT_X(activity == "00000000-0000-0000-0000-000000000000" || !QUuid(activity).isNull(), "ResultWatcher::onResourceScoreUpdated", "The activity should be always specified here, no magic values"); // The linked resources do not really care about the stats if (query.selection() == Terms::LinkedResources) return; if (!eventMatches(agent, resource, activity)) return; emit q->resultScoreUpdated(resource, score, lastUpdate, firstUpdate); } void onEarlierStatsDeleted(QString, int) { // The linked resources do not really care about the stats if (query.selection() == Terms::LinkedResources) return; scheduleResultsInvalidation(); } void onRecentStatsDeleted(QString, int, QString) { // The linked resources do not really care about the stats if (query.selection() == Terms::LinkedResources) return; scheduleResultsInvalidation(); } void onStatsForResourceDeleted(const QString &activity, const QString &agent, const QString &resource) { if (query.selection() == Terms::LinkedResources) return; if (activityMatches(activity) && agentMatches(agent)) { if (resource.contains('*')) { scheduleResultsInvalidation(); } else if (typeMatches(resource)) { if (!m_resultInvalidationTimer.isActive()) { // Remove a result only if we haven't an invalidation // request scheduled q->resultRemoved(resource); } } } } // Lets not send a lot of invalidation events at once QTimer m_resultInvalidationTimer; void scheduleResultsInvalidation() { QDBG << "Scheduling invalidation"; m_resultInvalidationTimer.start(); } QScopedPointer linking; QScopedPointer scoring; ResultWatcher * const q; Query query; }; ResultWatcher::ResultWatcher(Query query, QObject *parent) : QObject(parent) , d(new ResultWatcherPrivate(this, query)) { using namespace org::kde::ActivityManager; using namespace std::placeholders; // There is no need for private slots, when we have bind // Connecting the linking service QObject::connect( d->linking.data(), &ResourcesLinking::ResourceLinkedToActivity, this, std::bind(&ResultWatcherPrivate::onResourceLinkedToActivity, d, _1, _2, _3)); QObject::connect( d->linking.data(), &ResourcesLinking::ResourceUnlinkedFromActivity, this, std::bind(&ResultWatcherPrivate::onResourceUnlinkedFromActivity, d, _1, _2, _3)); // Connecting the scoring service QObject::connect( d->scoring.data(), &ResourcesScoring::ResourceScoreUpdated, this, std::bind(&ResultWatcherPrivate::onResourceScoreUpdated, d, _1, _2, _3, _4, _5, _6)); QObject::connect( d->scoring.data(), &ResourcesScoring::ResourceScoreDeleted, this, std::bind(&ResultWatcherPrivate::onStatsForResourceDeleted, d, _1, _2, _3)); QObject::connect( d->scoring.data(), &ResourcesScoring::RecentStatsDeleted, this, std::bind(&ResultWatcherPrivate::onRecentStatsDeleted, d, _1, _2, _3)); QObject::connect( d->scoring.data(), &ResourcesScoring::EarlierStatsDeleted, this, std::bind(&ResultWatcherPrivate::onEarlierStatsDeleted, d, _1, _2)); } ResultWatcher::~ResultWatcher() { delete d; } void ResultWatcher::linkToActivity(const QUrl &resource, const Terms::Activity &activity, const Terms::Agent &agent) { const auto activities = (!activity.values.isEmpty()) ? activity.values : (!d->query.activities().isEmpty()) ? d->query.activities() : Terms::Activity::current().values; const auto agents = (!agent.values.isEmpty()) ? agent.values : (!d->query.agents().isEmpty()) ? d->query.agents() : Terms::Agent::current().values; for (const auto &activity : activities) { for (const auto &agent : agents) { d->linking->LinkResourceToActivity(agent, resource.toString(), activity); } } } void ResultWatcher::unlinkFromActivity(const QUrl &resource, const Terms::Activity &activity, const Terms::Agent &agent) { const auto activities = !activity.values.isEmpty() ? activity.values : !d->query.activities().isEmpty() ? d->query.activities() : Terms::Activity::current().values; const auto agents = !agent.values.isEmpty() ? agent.values : !d->query.agents().isEmpty() ? d->query.agents() : Terms::Agent::current().values; for (const auto &activity : activities) { for (const auto &agent : agents) { qDebug() << "Unlink " << agent << resource << activity; d->linking->UnlinkResourceFromActivity(agent, resource.toString(), activity); } } } } // namespace Stats } // namespace KActivities