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

0001 /*
0002     SPDX-FileCopyrightText: 2015, 2016 Ivan Cukic <ivan.cukic(at)kde.org>
0003 
0004     SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
0005 */
0006 
0007 #include "resultset.h"
0008 
0009 // Qt
0010 #include <QSqlQuery>
0011 #include <QSqlError>
0012 #include <QCoreApplication>
0013 #include <QDir>
0014 #include <QUrl>
0015 
0016 // Local
0017 #include <common/database/Database.h>
0018 #include <common/specialvalues.h>
0019 #include <utils/debug_and_return.h>
0020 #include <utils/qsqlquery_iterator.h>
0021 #include "kactivities-stats-logsettings.h"
0022 
0023 // STL
0024 #include <iterator>
0025 #include <functional>
0026 #include <mutex>
0027 
0028 // KActivities
0029 #include "activitiessync_p.h"
0030 
0031 #define DEBUG_QUERIES 0
0032 
0033 namespace KActivities {
0034 namespace Stats {
0035 
0036 using namespace Terms;
0037 
0038 class ResultSet_ResultPrivate {
0039 public:
0040     QString resource;
0041     QString title;
0042     QString mimetype;
0043     double  score;
0044     uint    lastUpdate;
0045     uint    firstUpdate;
0046     ResultSet::Result::LinkStatus linkStatus;
0047     QStringList linkedActivities;
0048     QString agent;
0049 };
0050 
0051 ResultSet::Result::Result()
0052     : d(new ResultSet_ResultPrivate())
0053 {
0054 }
0055 
0056 ResultSet::Result::Result(Result &&result)
0057     : d(result.d)
0058 {
0059     result.d = nullptr;
0060 }
0061 
0062 ResultSet::Result::Result(const Result &result)
0063     : d(new ResultSet_ResultPrivate(*result.d))
0064 {
0065 }
0066 
0067 ResultSet::Result &ResultSet::Result::operator=(Result result)
0068 {
0069     std::swap(d, result.d);
0070 
0071     return *this;
0072 }
0073 
0074 ResultSet::Result::~Result()
0075 {
0076     delete d;
0077 }
0078 
0079 /* clang-format off */
0080 #define CREATE_GETTER_AND_SETTER(Type, Name, Set)                              \
0081     Type ResultSet::Result::Name() const                                       \
0082     {                                                                          \
0083         return d->Name;                                                        \
0084     }                                                                          \
0085                                                                                \
0086     void ResultSet::Result::Set(Type Name)                                     \
0087     {                                                                          \
0088         d->Name = Name;                                                        \
0089     }
0090 /* clang-format on */
0091 
0092 CREATE_GETTER_AND_SETTER(QString, resource, setResource)
0093 CREATE_GETTER_AND_SETTER(QString, title, setTitle)
0094 CREATE_GETTER_AND_SETTER(QString, mimetype, setMimetype)
0095 CREATE_GETTER_AND_SETTER(double, score, setScore)
0096 CREATE_GETTER_AND_SETTER(uint, lastUpdate, setLastUpdate)
0097 CREATE_GETTER_AND_SETTER(uint, firstUpdate, setFirstUpdate)
0098 CREATE_GETTER_AND_SETTER(ResultSet::Result::LinkStatus, linkStatus, setLinkStatus)
0099 CREATE_GETTER_AND_SETTER(QStringList, linkedActivities, setLinkedActivities)
0100 CREATE_GETTER_AND_SETTER(QString, agent, setAgent)
0101 
0102 #undef CREATE_GETTER_AND_SETTER
0103 
0104 QUrl ResultSet::Result::url() const
0105 {
0106     if (QDir::isAbsolutePath(d->resource)) {
0107         return QUrl::fromLocalFile(d->resource);
0108     } else {
0109         return QUrl(d->resource);
0110     }
0111 }
0112 
0113 class ResultSetPrivate {
0114 public:
0115     Common::Database::Ptr database;
0116     QSqlQuery query;
0117     Query queryDefinition;
0118 
0119     mutable ActivitiesSync::ConsumerPtr activities;
0120 
0121     void initQuery()
0122     {
0123         if (!database || query.isActive()) {
0124             return;
0125         }
0126 
0127         auto selection = queryDefinition.selection();
0128 
0129         /* clang-format off */
0130         query = database->execQuery(replaceQueryParameters(
0131                 selection == LinkedResources ? linkedResourcesQuery()
0132               : selection == UsedResources   ? usedResourcesQuery()
0133               : selection == AllResources    ? allResourcesQuery()
0134               : QString()));
0135         /* clang-format on */
0136 
0137         if (query.lastError().isValid()) {
0138             qCWarning(KACTIVITIES_STATS_LOG) << "[Error at ResultSetPrivate::initQuery]: " << query.lastError();
0139         }
0140     }
0141 
0142     QString agentClause(const QString &agent) const
0143     {
0144         if (agent == QLatin1String(":any")) {
0145             return QStringLiteral("1");
0146         }
0147 
0148         return QLatin1String("agent = '")
0149             + Common::escapeSqliteLikePattern(agent == QLatin1String(":current") ? QCoreApplication::instance()->applicationName() : agent)
0150             + QLatin1String("'");
0151     }
0152 
0153     QString activityClause(const QString &activity) const
0154     {
0155         if (activity == QLatin1String(":any")) {
0156             return QStringLiteral("1");
0157         }
0158 
0159         return QLatin1String("activity = '") + //
0160             Common::escapeSqliteLikePattern(activity == QLatin1String(":current") ? ActivitiesSync::currentActivity(activities) : activity)
0161             + QLatin1String("'");
0162     }
0163 
0164     inline QString starPattern(const QString &pattern) const
0165     {
0166         return Common::parseStarPattern(pattern, QStringLiteral("%"), [] (QString str) {
0167             return str.replace(QLatin1String("%"), QLatin1String("\\%")).replace(QLatin1String("_"), QLatin1String("\\_"));
0168         });
0169     }
0170 
0171     QString urlFilterClause(const QString &urlFilter) const
0172     {
0173         if (urlFilter == QLatin1String("*")) {
0174             return QStringLiteral("1");
0175         }
0176 
0177         return QStringLiteral("resource LIKE '") + Common::starPatternToLike(urlFilter) + QStringLiteral("' ESCAPE '\\'");
0178     }
0179 
0180     QString mimetypeClause(const QString &mimetype) const
0181     {
0182         if (mimetype == ANY_TYPE_TAG || mimetype == QLatin1String("*")) {
0183             return QStringLiteral("1");
0184 
0185         } else if (mimetype == FILES_TYPE_TAG) {
0186             return QStringLiteral("mimetype != 'inode/directory' AND mimetype != ''");
0187         } else if (mimetype == DIRECTORIES_TYPE_TAG) {
0188             return QStringLiteral("mimetype = 'inode/directory'");
0189         }
0190 
0191         return QStringLiteral("mimetype LIKE '") + Common::starPatternToLike(mimetype) + QStringLiteral("' ESCAPE '\\'");
0192     }
0193 
0194     QString dateClause(QDate start, QDate end) const {
0195         if (end.isNull()) {
0196             // only date filtering
0197             return QStringLiteral("DATE(re.start, 'unixepoch') = '") +
0198                     start.toString(Qt::ISODate) + QStringLiteral("' ");
0199         } else {
0200             // date range filtering
0201             return QStringLiteral("DATE(re.start, 'unixepoch') >= '") +
0202                     start.toString(Qt::ISODate) + QStringLiteral("' AND DATE(re.start, 'unixepoch') <= '") +
0203                     end.toString(Qt::ISODate) + QStringLiteral("' ");
0204         }
0205     }
0206 
0207     QString resourceEventJoinClause() const {
0208         return QStringLiteral(R"sql(
0209             LEFT JOIN
0210                 ResourceEvent re
0211                 ON  from_table.targettedResource = re.targettedResource
0212                 AND from_table.usedActivity      = re.usedActivity
0213                 AND from_table.initiatingAgent   = re.initiatingAgent
0214         )sql");
0215     }
0216 
0217     /**
0218      * Transforms the input list's elements with the f member method,
0219      * and returns the resulting list
0220      */
0221     template <typename F>
0222     inline
0223     QStringList transformedList(const QStringList &input, F f) const
0224     {
0225         using namespace std::placeholders;
0226 
0227         QStringList result;
0228         std::transform(input.cbegin(), input.cend(),
0229                          std::back_inserter(result),
0230                          std::bind(f, this, _1));
0231 
0232         return result;
0233     }
0234 
0235     QString limitOffsetSuffix() const
0236     {
0237         QString result;
0238 
0239         const int limit = queryDefinition.limit();
0240         if (limit > 0) {
0241             result += QStringLiteral(" LIMIT ") + QString::number(limit);
0242 
0243             const int offset = queryDefinition.offset();
0244             if (offset > 0) {
0245                 result += QStringLiteral(" OFFSET ") + QString::number(offset);
0246             }
0247         }
0248 
0249         return result;
0250     }
0251 
0252     inline QString replaceQueryParameters(const QString &_query) const
0253     {
0254         // ORDER BY column
0255         auto ordering = queryDefinition.ordering();
0256         /* clang-format off */
0257         QString orderingColumn = QStringLiteral("linkStatus DESC, ") + (
0258                 ordering == HighScoredFirst      ? QStringLiteral("score DESC,")
0259               : ordering == RecentlyCreatedFirst ? QStringLiteral("firstUpdate DESC,")
0260               : ordering == RecentlyUsedFirst    ? QStringLiteral("lastUpdate DESC,")
0261               : ordering == OrderByTitle         ? QStringLiteral("title ASC,")
0262               : QString()
0263             );
0264         /* clang-format on */
0265 
0266         // WHERE clause for filtering on agents
0267         QStringList agentsFilter = transformedList(
0268                 queryDefinition.agents(), &ResultSetPrivate::agentClause);
0269 
0270         // WHERE clause for filtering on activities
0271         QStringList activitiesFilter = transformedList(
0272                 queryDefinition.activities(), &ResultSetPrivate::activityClause);
0273 
0274         // WHERE clause for filtering on resource URLs
0275         QStringList urlFilter = transformedList(
0276                 queryDefinition.urlFilters(), &ResultSetPrivate::urlFilterClause);
0277 
0278         // WHERE clause for filtering on resource mime
0279         QStringList mimetypeFilter = transformedList(
0280                 queryDefinition.types(), &ResultSetPrivate::mimetypeClause);
0281 
0282         QString dateColumn = QStringLiteral("1");
0283         QString resourceEventJoin;
0284         // WHERE clause for access date filtering and ResourceEvent table Join
0285         if (!queryDefinition.dateStart().isNull()) {
0286             dateColumn = dateClause(queryDefinition.dateStart(), queryDefinition.dateEnd());
0287 
0288             resourceEventJoin = resourceEventJoinClause();
0289         }
0290 
0291         auto queryString = _query;
0292 
0293         queryString.replace(QStringLiteral("ORDER_BY_CLAUSE"), QStringLiteral("ORDER BY $orderingColumn resource ASC"))
0294                    .replace(QStringLiteral("LIMIT_CLAUSE"), limitOffsetSuffix());
0295 
0296         return kamd::utils::debug_and_return(DEBUG_QUERIES, "Query: ",
0297             queryString
0298                 .replace(QLatin1String("$orderingColumn"), orderingColumn)
0299                 .replace(QLatin1String("$agentsFilter"), agentsFilter.join(QStringLiteral(" OR ")))
0300                 .replace(QLatin1String("$activitiesFilter"), activitiesFilter.join(QStringLiteral(" OR ")))
0301                 .replace(QLatin1String("$urlFilter"), urlFilter.join(QStringLiteral(" OR ")))
0302                 .replace(QLatin1String("$mimetypeFilter"), mimetypeFilter.join(QStringLiteral(" OR ")))
0303                 .replace(QLatin1String("$resourceEventJoin"), resourceEventJoin)
0304                 .replace(QLatin1String("$dateFilter"), dateColumn)
0305             );
0306     }
0307 
0308     static const QString &linkedResourcesQuery()
0309     {
0310         // TODO: We need to correct the scores based on the time that passed
0311         //       since the cache was last updated, although, for this query,
0312         //       scores are not that important.
0313         /* clang-format off */
0314         static const QString queryString =
0315             QStringLiteral(R"sql(
0316             SELECT
0317                 from_table.targettedResource as resource
0318               , SUM(rsc.cachedScore)         as score
0319               , MIN(rsc.firstUpdate)         as firstUpdate
0320               , MAX(rsc.lastUpdate)          as lastUpdate
0321               , from_table.usedActivity      as activity
0322               , from_table.initiatingAgent   as agent
0323               , COALESCE(ri.title, from_table.targettedResource) as title
0324               , ri.mimetype as mimetype
0325               , 2 as linkStatus
0326 
0327             FROM
0328                 ResourceLink from_table
0329             LEFT JOIN
0330                 ResourceScoreCache rsc
0331                 ON  from_table.targettedResource = rsc.targettedResource
0332                 AND from_table.usedActivity      = rsc.usedActivity
0333                 AND from_table.initiatingAgent   = rsc.initiatingAgent
0334             LEFT JOIN
0335                 ResourceInfo ri
0336                 ON from_table.targettedResource = ri.targettedResource
0337 
0338             $resourceEventJoin
0339 
0340             WHERE
0341                 ($agentsFilter)
0342                 AND ($activitiesFilter)
0343                 AND ($urlFilter)
0344                 AND ($mimetypeFilter)
0345                 AND ($dateFilter)
0346 
0347             GROUP BY resource, title
0348 
0349             ORDER_BY_CLAUSE
0350             LIMIT_CLAUSE
0351             )sql")
0352             ;
0353         /* clang-format on */
0354 
0355         return queryString;
0356     }
0357 
0358     static const QString &usedResourcesQuery()
0359     {
0360         // TODO: We need to correct the scores based on the time that passed
0361         //       since the cache was last updated
0362         static const QString queryString =
0363             QStringLiteral(R"sql(
0364             SELECT
0365                 from_table.targettedResource as resource
0366               , SUM(from_table.cachedScore)  as score
0367               , MIN(from_table.firstUpdate)  as firstUpdate
0368               , MAX(from_table.lastUpdate)   as lastUpdate
0369               , from_table.usedActivity      as activity
0370               , from_table.initiatingAgent   as agent
0371               , COALESCE(ri.title, from_table.targettedResource) as title
0372               , ri.mimetype as mimetype
0373               , 1 as linkStatus
0374 
0375             FROM
0376                 ResourceScoreCache from_table
0377             LEFT JOIN
0378                 ResourceInfo ri
0379                 ON from_table.targettedResource = ri.targettedResource
0380 
0381             $resourceEventJoin
0382 
0383             WHERE
0384                 ($agentsFilter)
0385                 AND ($activitiesFilter)
0386                 AND ($urlFilter)
0387                 AND ($mimetypeFilter)
0388                 AND ($dateFilter)
0389 
0390             GROUP BY resource, title
0391 
0392             ORDER_BY_CLAUSE
0393             LIMIT_CLAUSE
0394             )sql")
0395             ;
0396 
0397         return queryString;
0398     }
0399 
0400     static const QString &allResourcesQuery()
0401     {
0402         // TODO: We need to correct the scores based on the time that passed
0403         //       since the cache was last updated, although, for this query,
0404         //       scores are not that important.
0405         static const QString queryString =
0406             QStringLiteral(R"sql(
0407             WITH
0408                 LinkedResourcesResults AS (
0409                     SELECT from_table.targettedResource as resource
0410                          , rsc.cachedScore              as score
0411                          , rsc.firstUpdate              as firstUpdate
0412                          , rsc.lastUpdate               as lastUpdate
0413                          , from_table.usedActivity      as activity
0414                          , from_table.initiatingAgent   as agent
0415                          , 2 as linkStatus
0416 
0417                     FROM
0418                         ResourceLink from_table
0419 
0420                     LEFT JOIN
0421                         ResourceScoreCache rsc
0422                         ON  from_table.targettedResource = rsc.targettedResource
0423                         AND from_table.usedActivity      = rsc.usedActivity
0424                         AND from_table.initiatingAgent   = rsc.initiatingAgent
0425 
0426                     $resourceEventJoin
0427 
0428                     WHERE
0429                         ($agentsFilter)
0430                         AND ($activitiesFilter)
0431                         AND ($urlFilter)
0432                         AND ($mimetypeFilter)
0433                         AND ($dateFilter)
0434                 ),
0435 
0436                 UsedResourcesResults AS (
0437                     SELECT from_table.targettedResource as resource
0438                          , from_table.cachedScore       as score
0439                          , from_table.firstUpdate       as firstUpdate
0440                          , from_table.lastUpdate        as lastUpdate
0441                          , from_table.usedActivity      as activity
0442                          , from_table.initiatingAgent   as agent
0443                          , 0 as linkStatus
0444 
0445                     FROM
0446                         ResourceScoreCache from_table
0447 
0448                     $resourceEventJoin
0449 
0450                     WHERE
0451                         ($agentsFilter)
0452                         AND ($activitiesFilter)
0453                         AND ($urlFilter)
0454                         AND ($mimetypeFilter)
0455                         AND ($dateFilter)
0456                 ),
0457 
0458                 CollectedResults AS (
0459                     SELECT *
0460                     FROM LinkedResourcesResults
0461 
0462                     UNION
0463 
0464                     SELECT *
0465                     FROM UsedResourcesResults
0466                     WHERE resource NOT IN (SELECT resource FROM LinkedResourcesResults)
0467                 )
0468 
0469                 SELECT
0470                     resource
0471                   , SUM(score) as score
0472                   , MIN(firstUpdate) as firstUpdate
0473                   , MAX(lastUpdate) as lastUpdate
0474                   , activity
0475                   , agent
0476                   , COALESCE(ri.title, resource) as title
0477                   , ri.mimetype as mimetype
0478                   , linkStatus
0479 
0480                 FROM CollectedResults cr
0481 
0482                 LEFT JOIN
0483                     ResourceInfo ri
0484                     ON cr.resource = ri.targettedResource
0485 
0486                 GROUP BY resource, title
0487 
0488                 ORDER_BY_CLAUSE
0489                 LIMIT_CLAUSE
0490             )sql")
0491             ;
0492 
0493         return queryString;
0494     }
0495 
0496     ResultSet::Result currentResult() const
0497     {
0498         ResultSet::Result result;
0499 
0500         if (!database || !query.isActive()) {
0501             return result;
0502         }
0503 
0504         result.setResource(query.value(QStringLiteral("resource")).toString());
0505         result.setTitle(query.value(QStringLiteral("title")).toString());
0506         result.setMimetype(query.value(QStringLiteral("mimetype")).toString());
0507         result.setScore(query.value(QStringLiteral("score")).toDouble());
0508         result.setLastUpdate(query.value(QStringLiteral("lastUpdate")).toUInt());
0509         result.setFirstUpdate(query.value(QStringLiteral("firstUpdate")).toUInt());
0510         result.setAgent(query.value(QStringLiteral("agent")).toString());
0511 
0512         result.setLinkStatus(
0513             static_cast<ResultSet::Result::LinkStatus>(query.value(QStringLiteral("linkStatus")).toUInt()));
0514 
0515         auto linkedActivitiesQuery = database->createQuery();
0516 
0517         linkedActivitiesQuery.prepare(QStringLiteral(R"sql(
0518             SELECT usedActivity
0519             FROM   ResourceLink
0520             WHERE  targettedResource = :resource
0521             )sql"));
0522 
0523         linkedActivitiesQuery.bindValue(QStringLiteral(":resource"), result.resource());
0524         linkedActivitiesQuery.exec();
0525 
0526         QStringList linkedActivities;
0527         for (const auto &item: linkedActivitiesQuery) {
0528             linkedActivities << item[0].toString();
0529         }
0530 
0531         result.setLinkedActivities(linkedActivities);
0532         // qDebug(KACTIVITIES_STATS_LOG) << result.resource() << "linked to activities" << result.linkedActivities();
0533 
0534         return result;
0535     }
0536 };
0537 
0538 ResultSet::ResultSet(Query queryDefinition)
0539     : d(new ResultSetPrivate())
0540 {
0541     using namespace Common;
0542 
0543     d->database = Database::instance(Database::ResourcesDatabase, Database::ReadOnly);
0544 
0545     if (!(d->database)) {
0546         qCWarning(KACTIVITIES_STATS_LOG) << "KActivities ERROR: There is no database. This probably means "
0547                       "that you do not have the Activity Manager running, or that "
0548                       "something else is broken on your system. Recent documents and "
0549                       "alike will not work!";
0550     }
0551 
0552     d->queryDefinition = queryDefinition;
0553 
0554     d->initQuery();
0555 }
0556 
0557 ResultSet::ResultSet(ResultSet &&source)
0558     : d(nullptr)
0559 {
0560     std::swap(d, source.d);
0561 }
0562 
0563 ResultSet::ResultSet(const ResultSet &source)
0564     : d(new ResultSetPrivate(*source.d))
0565 {
0566 }
0567 
0568 ResultSet &ResultSet::operator= (ResultSet source)
0569 {
0570     std::swap(d, source.d);
0571     return *this;
0572 }
0573 
0574 ResultSet::~ResultSet()
0575 {
0576     delete d;
0577 }
0578 
0579 ResultSet::Result ResultSet::at(int index) const
0580 {
0581     if (!d->query.isActive()) {
0582         return Result();
0583     }
0584 
0585     d->query.seek(index);
0586 
0587     return d->currentResult();
0588 }
0589 
0590 } // namespace Stats
0591 } // namespace KActivities
0592 
0593 #include "resultset_iterator.cpp"
0594