diff --git a/src/resultset.cpp b/src/resultset.cpp index d204452..15b82e8 100644 --- a/src/resultset.cpp +++ b/src/resultset.cpp @@ -1,577 +1,587 @@ /* * 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 +#include +#include // Local #include #include #include #include #include "kactivities-stats-logsettings.h" // 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 +QUrl ResultSet::Result::url() const +{ + if (QDir::isAbsolutePath(d->resource)) { + return QUrl::fromLocalFile(d->resource); + } else { + return QUrl(d->resource); + } +} 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()) { qCWarning(KACTIVITIES_STATS_LOG) << "[Error at ResultSetPrivate::initQuery]: " << query.lastError(); } } QString agentClause(const QString &agent) const { if (agent == QLatin1String(":any")) return QStringLiteral("1"); return QStringLiteral("agent = '") + ( agent == QLatin1String(":current") ? QCoreApplication::instance()->applicationName() : agent ) + QStringLiteral("'"); } QString activityClause(const QString &activity) const { if (activity == QLatin1String(":any")) return QStringLiteral("1"); return QStringLiteral("activity = '") + ( activity == QLatin1String(":current") ? ActivitiesSync::currentActivity(activities) : activity ) + QStringLiteral("'"); } 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 QStringLiteral("resource LIKE '") + Common::starPatternToLike(urlFilter) + QStringLiteral("' ESCAPE '\\'"); } QString mimetypeClause(const QString &mimetype) const { if (mimetype == ANY_TYPE_TAG || mimetype == QLatin1String("*")) return QStringLiteral("1"); else if (mimetype == FILES_TYPE_TAG) return QStringLiteral("mimetype != 'inode/directory' AND mimetype != ''"); else if (mimetype == DIRECTORIES_TYPE_TAG) return QStringLiteral("mimetype = 'inode/directory'"); return QStringLiteral("mimetype LIKE '") + Common::starPatternToLike(mimetype) + QStringLiteral("' ESCAPE '\\'"); } QString dateClause(QDate start, QDate end) const { if (end.isNull()) { // only date filtering return QStringLiteral("DATE(re.start, 'unixepoch') = '") + start.toString(Qt::ISODate) + QStringLiteral("' "); } else { // date range filtering return QStringLiteral("DATE(re.start, 'unixepoch') >= '") + start.toString(Qt::ISODate) + QStringLiteral("' AND DATE(re.start, 'unixepoch') <= '") + end.toString(Qt::ISODate) + QStringLiteral("' "); } } QString resourceEventJoinClause() const { return QStringLiteral(R"sql( LEFT JOIN ResourceEvent re ON from_table.targettedResource = re.targettedResource AND from_table.usedActivity = re.usedActivity AND from_table.initiatingAgent = re.initiatingAgent )sql"); } /** * 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 += QStringLiteral(" LIMIT ") + QString::number(limit); const int offset = queryDefinition.offset(); if (offset > 0) { result += QStringLiteral(" 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); QString dateColumn = QStringLiteral("1"), resourceEventJoin; // WHERE clause for access date filtering and ResourceEvent table Join if (!queryDefinition.dateStart().isNull()) { dateColumn = dateClause(queryDefinition.dateStart(), queryDefinition.dateEnd()); resourceEventJoin = resourceEventJoinClause(); } auto queryString = _query; queryString.replace(QStringLiteral("ORDER_BY_CLAUSE"), QStringLiteral("ORDER BY $orderingColumn resource ASC")) .replace(QStringLiteral("LIMIT_CLAUSE"), limitOffsetSuffix()); return kamd::utils::debug_and_return(DEBUG_QUERIES, "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 "))) .replace(QLatin1String("$resourceEventJoin"), resourceEventJoin) .replace(QLatin1String("$dateFilter"), dateColumn) ); } 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 queryString = QStringLiteral(R"sql( SELECT from_table.targettedResource as resource , SUM(rsc.cachedScore) as score , MIN(rsc.firstUpdate) as firstUpdate , MAX(rsc.lastUpdate) as lastUpdate , from_table.usedActivity as activity , from_table.initiatingAgent as agent , COALESCE(ri.title, from_table.targettedResource) as title , ri.mimetype as mimetype , 2 as linkStatus FROM ResourceLink from_table LEFT JOIN ResourceScoreCache rsc ON from_table.targettedResource = rsc.targettedResource AND from_table.usedActivity = rsc.usedActivity AND from_table.initiatingAgent = rsc.initiatingAgent LEFT JOIN ResourceInfo ri ON from_table.targettedResource = ri.targettedResource $resourceEventJoin WHERE ($agentsFilter) AND ($activitiesFilter) AND ($urlFilter) AND ($mimetypeFilter) AND ($dateFilter) GROUP BY resource, title ORDER_BY_CLAUSE LIMIT_CLAUSE )sql") ; 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 queryString = QStringLiteral(R"sql( SELECT from_table.targettedResource as resource , SUM(from_table.cachedScore) as score , MIN(from_table.firstUpdate) as firstUpdate , MAX(from_table.lastUpdate) as lastUpdate , from_table.usedActivity as activity , from_table.initiatingAgent as agent , COALESCE(ri.title, from_table.targettedResource) as title , ri.mimetype as mimetype , 1 as linkStatus FROM ResourceScoreCache from_table LEFT JOIN ResourceInfo ri ON from_table.targettedResource = ri.targettedResource $resourceEventJoin WHERE ($agentsFilter) AND ($activitiesFilter) AND ($urlFilter) AND ($mimetypeFilter) AND ($dateFilter) GROUP BY resource, title ORDER_BY_CLAUSE LIMIT_CLAUSE )sql") ; 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 queryString = QStringLiteral(R"sql( WITH LinkedResourcesResults AS ( SELECT from_table.targettedResource as resource , rsc.cachedScore as score , rsc.firstUpdate as firstUpdate , rsc.lastUpdate as lastUpdate , from_table.usedActivity as activity , from_table.initiatingAgent as agent , 2 as linkStatus FROM ResourceLink from_table LEFT JOIN ResourceScoreCache rsc ON from_table.targettedResource = rsc.targettedResource AND from_table.usedActivity = rsc.usedActivity AND rl.initiatingAgent = rsc.initiatingAgent $resourceEventJoin WHERE ($agentsFilter) AND ($activitiesFilter) AND ($urlFilter) AND ($mimetypeFilter) AND ($dateFilter) ), UsedResourcesResults AS ( SELECT from_table.targettedResource as resource , from_table.cachedScore as score , from_table.firstUpdate as firstUpdate , from_table.lastUpdate as lastUpdate , from_table.usedActivity as activity , from_table.initiatingAgent as agent , 0 as linkStatus FROM ResourceScoreCache from_table $resourceEventJoin WHERE ($agentsFilter) AND ($activitiesFilter) AND ($urlFilter) AND ($mimetypeFilter) AND ($dateFilter) ), 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 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")).toUInt()); result.setFirstUpdate(query.value(QStringLiteral("firstUpdate")).toUInt()); result.setLinkStatus( static_cast(query.value(QStringLiteral("linkStatus")).toUInt())); auto linkedActivitiesQuery = database->createQuery(); linkedActivitiesQuery.prepare(QStringLiteral(R"sql( SELECT usedActivity FROM ResourceLink WHERE targettedResource = :resource )sql")); linkedActivitiesQuery.bindValue(QStringLiteral(":resource"), result.resource()); linkedActivitiesQuery.exec(); QStringList linkedActivities; for (const auto &item: linkedActivitiesQuery) { linkedActivities << item[0].toString(); } result.setLinkedActivities(linkedActivities); // qDebug(KACTIVITIES_STATS_LOG) << result.resource() << "linked to activities" << result.linkedActivities(); return result; } }; ResultSet::ResultSet(Query queryDefinition) : d(new ResultSetPrivate()) { using namespace Common; d->database = Database::instance(Database::ResourcesDatabase, Database::ReadOnly); if (!(d->database)) { qCWarning(KACTIVITIES_STATS_LOG) << "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!"; } 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 { 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/resultset.h b/src/resultset.h index 5b658e9..6dd64c9 100644 --- a/src/resultset.h +++ b/src/resultset.h @@ -1,257 +1,259 @@ /* * 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 . */ #ifndef KACTIVITIES_STATS_RESULTSET #define KACTIVITIES_STATS_RESULTSET #include "query.h" #include namespace KActivities { namespace Stats { class ResultSetPrivate; class ResultSet_ResultPrivate; class ResultSet_IteratorPrivate; /** * @class KActivities::Stats::ResultSet resultset.h * * Class that can query the KActivities usage tracking mechanism * for resources. * * Note: It is important to note that you should not create * long-living instances of ResultSet. It might lock the database * and break proper updating mechanisms. If you want a list of results * that automatically get updated, use ResultModel. * * ResultSet is meant to be used when you just need to fetch a few results * like this: * * @code * auto results = ResultSet(AllResources | Agent("org.kde.kate")); * for (const auto &result: results) { * // ... * } * @endcode */ class KACTIVITIESSTATS_EXPORT ResultSet { public: /** * Structure containing data of one of the results */ class Result { public: Result(); ~Result(); Result(Result &&result); Result(const Result &result); Result &operator=(Result result); enum LinkStatus { NotLinked = 0, Unknown = 1, Linked = 2 }; - QString resource() const; ///< URL of the resource + // TODO: KF6 rething the function names, and maybe their signature, perhaps leverage std::variant or std::optional to add semantics to the API + QString resource() const; ///< String representation of resource (can represent an url or a path) + QUrl url() const; ///< Url representation of a resource based on internal resource, readonly, @since 5.64 QString title() const; ///< Title of the resource, or URL if title is not known QString mimetype() const; ///< Mimetype of the resource, or URL if title is not known double score() const; ///< The score calculated based on the usage statistics uint lastUpdate() const; ///< Timestamp of the last update uint firstUpdate() const; ///< Timestamp of the first update LinkStatus linkStatus() const; ///< Differentiates between linked and non-linked resources in mixed queries QStringList linkedActivities() const; ///< Contains the activities this resource is linked to for the queries that care about resource linking void setResource(QString resource); void setTitle(QString title); void setMimetype(QString mimetype); void setScore(double score); void setLastUpdate(uint lastUpdate); void setFirstUpdate(uint firstUpdate); void setLinkStatus(LinkStatus linkedStatus); void setLinkedActivities(QStringList activities); private: ResultSet_ResultPrivate * d; }; /** * ResultSet is a container. This notifies the generic algorithms * from STLboost, and others of the contained type. */ typedef Result value_type; /** * Creates the ResultSet from the specified query */ ResultSet(Query query); ResultSet(ResultSet && source); ResultSet(const ResultSet &source); ResultSet &operator= (ResultSet source); ~ResultSet(); /** * @returns a result at the specified index * @param index of the result * @note You should use iterators instead */ Result at(int index) const; // Iterators /** * An STL-style constant forward iterator for accessing the results in a ResultSet * TODO: Consider making this to be more than just forward iterator. * Maybe even a random-access one. */ class const_iterator { public: typedef std::random_access_iterator_tag iterator_category; typedef int difference_type; typedef const Result value_type; typedef const Result& reference; typedef const Result* pointer; const_iterator(); const_iterator(const const_iterator &source); const_iterator &operator=(const const_iterator &source); ~const_iterator(); bool isSourceValid() const; reference operator*() const; pointer operator->() const; // prefix const_iterator& operator++(); // postfix const_iterator operator++(int); // prefix const_iterator& operator--(); // postfix const_iterator operator--(int); const_iterator operator+(difference_type n) const; const_iterator& operator+=(difference_type n); const_iterator operator-(difference_type n) const; const_iterator& operator-=(difference_type n); reference operator[](difference_type n) const; friend bool operator==(const const_iterator &left, const const_iterator &right); friend bool operator!=(const const_iterator &left, const const_iterator &right); friend bool operator<(const const_iterator &left, const const_iterator &right); friend bool operator>(const const_iterator &left, const const_iterator &right); friend bool operator<=(const const_iterator &left, const const_iterator &right); friend bool operator>=(const const_iterator &left, const const_iterator &right); friend difference_type operator-(const const_iterator &left, const const_iterator &right); private: const_iterator(const ResultSet *resultSet, int currentRow); friend class ResultSet; ResultSet_IteratorPrivate* const d; }; /** * @returns a constant iterator pointing to the start of the collection * (to the first item) * @note as usual in C++, the range of the collection is [begin, end) */ const_iterator begin() const; /** * @returns a constant iterator pointing to the end of the collection * (after the last item) * @note as usual in C++, the range of the collection is [begin, end) */ const_iterator end() const; /** * Alias for begin */ inline const_iterator cbegin() const { return begin(); } /** * Alias for end */ inline const_iterator cend() const { return end(); } /** * Alias for begin */ inline const_iterator constBegin() const { return cbegin(); } /** * Alias for end */ inline const_iterator constEnd() const { return cend(); } private: friend class ResultSet_IteratorPrivate; ResultSetPrivate *d; }; bool KACTIVITIESSTATS_EXPORT operator==(const ResultSet::const_iterator &left, const ResultSet::const_iterator &right); bool KACTIVITIESSTATS_EXPORT operator!=(const ResultSet::const_iterator &left, const ResultSet::const_iterator &right); bool KACTIVITIESSTATS_EXPORT operator<(const ResultSet::const_iterator &left, const ResultSet::const_iterator &right); bool KACTIVITIESSTATS_EXPORT operator>(const ResultSet::const_iterator &left, const ResultSet::const_iterator &right); bool KACTIVITIESSTATS_EXPORT operator<=(const ResultSet::const_iterator &left, const ResultSet::const_iterator &right); bool KACTIVITIESSTATS_EXPORT operator>=(const ResultSet::const_iterator &left, const ResultSet::const_iterator &right); ResultSet::const_iterator::difference_type KACTIVITIESSTATS_EXPORT operator-(const ResultSet::const_iterator &left, const ResultSet::const_iterator &right); inline QDebug operator<< (QDebug out, const ResultSet::Result &result) { return out << (result.linkStatus() == ResultSet::Result::Linked ? "⊤" : result.linkStatus() == ResultSet::Result::NotLinked ? "⊥" : "?") << result.score() << (result.title() != result.resource() ? result.title() : QString()) << result.lastUpdate() << result.resource().rightRef(20) ; } } // namespace Stats } // namespace KActivities #endif // KACTIVITIES_STATS_RESULTSET