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 "resultwatcher.h"
0008 
0009 // Qt
0010 #include <QCoreApplication>
0011 #include <QList>
0012 #include <QRegularExpression>
0013 #include <QSqlError>
0014 #include <QSqlQuery>
0015 
0016 // Local
0017 #include "kactivities-stats-logsettings.h"
0018 #include <common/database/Database.h>
0019 #include <utils/debug_and_return.h>
0020 
0021 // STL
0022 #include <functional>
0023 #include <iterator>
0024 #include <limits>
0025 #include <mutex>
0026 
0027 // KActivities
0028 #include <kactivities/consumer.h>
0029 
0030 #include "activitiessync_p.h"
0031 #include "common/dbus/common.h"
0032 #include "common/specialvalues.h"
0033 #include "resourceslinking_interface.h"
0034 #include "resourcesscoring_interface.h"
0035 #include "utils/lazy_val.h"
0036 #include "utils/qsqlquery_iterator.h"
0037 
0038 #include <algorithm>
0039 
0040 #define QDBG qCDebug(KACTIVITIES_STATS_LOG) << "KActivitiesStats(" << (void *)this << ")"
0041 
0042 namespace KActivities
0043 {
0044 namespace Stats
0045 {
0046 // Main class
0047 
0048 class ResultWatcherPrivate
0049 {
0050 public:
0051     mutable ActivitiesSync::ConsumerPtr activities;
0052     QList<QRegularExpression> urlFilters;
0053 
0054     ResultWatcherPrivate(ResultWatcher *parent, Query query)
0055         : linking(new KAMD_DBUS_CLASS_INTERFACE("Resources/Linking", ResourcesLinking, nullptr))
0056         , scoring(new KAMD_DBUS_CLASS_INTERFACE("Resources/Scoring", ResourcesScoring, nullptr))
0057         , q(parent)
0058         , query(query)
0059     {
0060         for (const auto &urlFilter : query.urlFilters()) {
0061             urlFilters << Common::starPatternToRegex(urlFilter);
0062         }
0063 
0064         m_resultInvalidationTimer.setSingleShot(true);
0065         m_resultInvalidationTimer.setInterval(200);
0066         QObject::connect(&m_resultInvalidationTimer, &QTimer::timeout, q, Q_EMIT & ResultWatcher::resultsInvalidated);
0067     }
0068 
0069     template<typename Collection, typename Predicate>
0070     inline bool any_of(const Collection &collection, Predicate &&predicate) const
0071     {
0072         const auto begin = collection.cbegin();
0073         const auto end = collection.cend();
0074 
0075         return begin == end || std::any_of(begin, end, std::forward<Predicate>(predicate));
0076     }
0077 
0078 #define DEBUG_MATCHERS 0
0079 
0080     // Processing the list of activities as specified by the query.
0081     // If it contains :any, we are returning true, otherwise
0082     // we want to match a specific activity (be it the current
0083     // activity or not). The :global special value is not special here
0084     bool activityMatches(const QString &activity) const
0085     {
0086 #if DEBUG_MATCHERS
0087         qCDebug(KACTIVITIES_STATS_LOG) << "Activity " << activity << "matching against" << query.activities();
0088 #endif
0089 
0090         return kamd::utils::debug_and_return(DEBUG_MATCHERS,
0091                                              " -> returning ",
0092                                              activity == ANY_ACTIVITY_TAG || any_of(query.activities(), [&](const QString &matcher) {
0093                                                  return matcher == ANY_ACTIVITY_TAG ? true
0094                                                      : matcher == CURRENT_ACTIVITY_TAG
0095                                                      ? (matcher == activity || activity == ActivitiesSync::currentActivity(activities))
0096                                                      : activity == matcher;
0097                                              }));
0098     }
0099 
0100     // Same as above, but for agents
0101     bool agentMatches(const QString &agent) const
0102     {
0103 #if DEBUG_MATCHERS
0104         qCDebug(KACTIVITIES_STATS_LOG) << "Agent " << agent << "matching against" << query.agents();
0105 #endif
0106 
0107         return kamd::utils::debug_and_return(DEBUG_MATCHERS, " -> returning ", agent == ANY_AGENT_TAG || any_of(query.agents(), [&](const QString &matcher) {
0108                                                                                    return matcher == ANY_AGENT_TAG ? true
0109                                                                                        : matcher == CURRENT_AGENT_TAG
0110                                                                                        ? (matcher == agent || agent == QCoreApplication::applicationName())
0111                                                                                        : agent == matcher;
0112                                                                                }));
0113     }
0114 
0115     // Same as above, but for urls
0116     bool urlMatches(const QString &url) const
0117     {
0118 #if DEBUG_MATCHERS
0119         qCDebug(KACTIVITIES_STATS_LOG) << "Url " << url << "matching against" << urlFilters;
0120 #endif
0121 
0122         return kamd::utils::debug_and_return(DEBUG_MATCHERS, " -> returning ", any_of(urlFilters, [&](const QRegularExpression &matcher) {
0123                                                  return matcher.match(url).hasMatch();
0124                                              }));
0125     }
0126 
0127     bool typeMatches(const QString &resource) const
0128     {
0129         // We don't necessarily need to retrieve the type from
0130         // the database. If we do, get it only once
0131         auto type = kamd::utils::make_lazy_val([&]() -> QString {
0132             using Common::Database;
0133 
0134             auto database = Database::instance(Database::ResourcesDatabase, Database::ReadOnly);
0135 
0136             if (!database) {
0137                 return QString();
0138             }
0139 
0140             auto query = database->execQuery(QStringLiteral("SELECT mimetype FROM ResourceInfo WHERE "
0141                                                             "targettedResource = '")
0142                                              + resource + QStringLiteral("'"));
0143 
0144             for (const auto &item : query) {
0145                 return item[0].toString();
0146             }
0147 
0148             return QString();
0149         });
0150 
0151 #if DEBUG_MATCHERS
0152         qCDebug(KACTIVITIES_STATS_LOG) << "Type "
0153                                        << "...type..."
0154                                        << "matching against" << query.types();
0155         qCDebug(KACTIVITIES_STATS_LOG) << "ANY_TYPE_TAG" << ANY_TYPE_TAG;
0156 #endif
0157 
0158         return kamd::utils::debug_and_return(DEBUG_MATCHERS, " -> returning ", any_of(query.types(), [&](const QString &matcher) {
0159                                                  if (matcher == ANY_TYPE_TAG) {
0160                                                      return true;
0161                                                  }
0162 
0163                                                  const QString _type = type;
0164                                                  return matcher == ANY_TYPE_TAG
0165                                                      || (matcher == FILES_TYPE_TAG && !_type.isEmpty() && _type != QStringLiteral("inode/directory"))
0166                                                      || (matcher == DIRECTORIES_TYPE_TAG && _type == QLatin1String("inode/directory")) || matcher == type;
0167                                              }));
0168     }
0169 
0170     bool eventMatches(const QString &agent, const QString &resource, const QString &activity) const
0171     {
0172         // The order of checks is not arbitrary, it is sorted
0173         // from the cheapest, to the most expensive
0174         return kamd::utils::debug_and_return(DEBUG_MATCHERS,
0175                                              "event matches?",
0176                                              agentMatches(agent) && activityMatches(activity) && urlMatches(resource) && typeMatches(resource));
0177     }
0178 
0179     void onResourceLinkedToActivity(const QString &agent, const QString &resource, const QString &activity)
0180     {
0181 #if DEBUG_MATCHERS
0182         qCDebug(KACTIVITIES_STATS_LOG) << "Resource has been linked: " << agent << resource << activity;
0183 #endif
0184 
0185         // The used resources do not really care about the linked ones
0186         if (query.selection() == Terms::UsedResources) {
0187             return;
0188         }
0189 
0190         if (!eventMatches(agent, resource, activity)) {
0191             return;
0192         }
0193 
0194         // TODO: See whether it makes sense to have
0195         //       lastUpdate/firstUpdate here as well
0196         Q_EMIT q->resultLinked(resource);
0197     }
0198 
0199     void onResourceUnlinkedFromActivity(const QString &agent, const QString &resource, const QString &activity)
0200     {
0201 #if DEBUG_MATCHERS
0202         qCDebug(KACTIVITIES_STATS_LOG) << "Resource unlinked: " << agent << resource << activity;
0203 #endif
0204 
0205         // The used resources do not really care about the linked ones
0206         if (query.selection() == Terms::UsedResources) {
0207             return;
0208         }
0209 
0210         if (!eventMatches(agent, resource, activity)) {
0211             return;
0212         }
0213 
0214         Q_EMIT q->resultUnlinked(resource);
0215     }
0216 
0217 #undef DEBUG_MATCHERS
0218 
0219     void onResourceScoreUpdated(const QString &activity, const QString &agent, const QString &resource, double score, uint lastUpdate, uint firstUpdate)
0220     {
0221         Q_ASSERT_X(activity == QLatin1String("00000000-0000-0000-0000-000000000000") || !QUuid(activity).isNull(),
0222                    "ResultWatcher::onResourceScoreUpdated",
0223                    "The activity should be always specified here, no magic values");
0224 
0225         // The linked resources do not really care about the stats
0226         if (query.selection() == Terms::LinkedResources) {
0227             return;
0228         }
0229 
0230         if (!eventMatches(agent, resource, activity)) {
0231             return;
0232         }
0233 
0234         Q_EMIT q->resultScoreUpdated(resource, score, lastUpdate, firstUpdate);
0235     }
0236 
0237     void onEarlierStatsDeleted(QString, int)
0238     {
0239         // The linked resources do not really care about the stats
0240         if (query.selection() == Terms::LinkedResources) {
0241             return;
0242         }
0243 
0244         scheduleResultsInvalidation();
0245     }
0246 
0247     void onRecentStatsDeleted(QString, int, QString)
0248     {
0249         // The linked resources do not really care about the stats
0250         if (query.selection() == Terms::LinkedResources) {
0251             return;
0252         }
0253 
0254         scheduleResultsInvalidation();
0255     }
0256 
0257     void onStatsForResourceDeleted(const QString &activity, const QString &agent, const QString &resource)
0258     {
0259         if (query.selection() == Terms::LinkedResources) {
0260             return;
0261         }
0262 
0263         if (activityMatches(activity) && agentMatches(agent)) {
0264             if (resource.contains(QLatin1Char('*'))) {
0265                 scheduleResultsInvalidation();
0266 
0267             } else if (typeMatches(resource)) {
0268                 if (!m_resultInvalidationTimer.isActive()) {
0269                     // Remove a result only if we haven't an invalidation
0270                     // request scheduled
0271                     q->resultRemoved(resource);
0272                 }
0273             }
0274         }
0275     }
0276 
0277     // Lets not send a lot of invalidation events at once
0278     QTimer m_resultInvalidationTimer;
0279     void scheduleResultsInvalidation()
0280     {
0281         QDBG << "Scheduling invalidation";
0282         m_resultInvalidationTimer.start();
0283     }
0284 
0285     std::unique_ptr<org::kde::ActivityManager::ResourcesLinking> linking;
0286     std::unique_ptr<org::kde::ActivityManager::ResourcesScoring> scoring;
0287 
0288     ResultWatcher *const q;
0289     Query query;
0290 };
0291 
0292 ResultWatcher::ResultWatcher(Query query, QObject *parent)
0293     : QObject(parent)
0294     , d(new ResultWatcherPrivate(this, query))
0295 {
0296     using namespace org::kde::ActivityManager;
0297     using namespace std::placeholders;
0298 
0299     // There is no need for private slots, when we have bind
0300 
0301     // Connecting the linking service
0302     QObject::connect(d->linking.get(),
0303                      &ResourcesLinking::ResourceLinkedToActivity,
0304                      this,
0305                      std::bind(&ResultWatcherPrivate::onResourceLinkedToActivity, d, _1, _2, _3));
0306     QObject::connect(d->linking.get(),
0307                      &ResourcesLinking::ResourceUnlinkedFromActivity,
0308                      this,
0309                      std::bind(&ResultWatcherPrivate::onResourceUnlinkedFromActivity, d, _1, _2, _3));
0310 
0311     // Connecting the scoring service
0312     QObject::connect(d->scoring.get(),
0313                      &ResourcesScoring::ResourceScoreUpdated,
0314                      this,
0315                      std::bind(&ResultWatcherPrivate::onResourceScoreUpdated, d, _1, _2, _3, _4, _5, _6));
0316     QObject::connect(d->scoring.get(),
0317                      &ResourcesScoring::ResourceScoreDeleted,
0318                      this,
0319                      std::bind(&ResultWatcherPrivate::onStatsForResourceDeleted, d, _1, _2, _3));
0320     QObject::connect(d->scoring.get(), &ResourcesScoring::RecentStatsDeleted, this, std::bind(&ResultWatcherPrivate::onRecentStatsDeleted, d, _1, _2, _3));
0321     QObject::connect(d->scoring.get(), &ResourcesScoring::EarlierStatsDeleted, this, std::bind(&ResultWatcherPrivate::onEarlierStatsDeleted, d, _1, _2));
0322 }
0323 
0324 ResultWatcher::~ResultWatcher()
0325 {
0326     delete d;
0327 }
0328 
0329 void ResultWatcher::linkToActivity(const QUrl &resource, const Terms::Activity &activity, const Terms::Agent &agent)
0330 {
0331     const auto activities = (!activity.values.isEmpty()) ? activity.values
0332         : (!d->query.activities().isEmpty())             ? d->query.activities()
0333                                                          : Terms::Activity::current().values;
0334     const auto agents = (!agent.values.isEmpty()) ? agent.values : (!d->query.agents().isEmpty()) ? d->query.agents() : Terms::Agent::current().values;
0335 
0336     for (const auto &activity : activities) {
0337         for (const auto &agent : agents) {
0338             d->linking->LinkResourceToActivity(agent, resource.toString(), activity);
0339         }
0340     }
0341 }
0342 
0343 void ResultWatcher::unlinkFromActivity(const QUrl &resource, const Terms::Activity &activity, const Terms::Agent &agent)
0344 {
0345     const auto activities = !activity.values.isEmpty() ? activity.values
0346         : !d->query.activities().isEmpty()             ? d->query.activities()
0347                                                        : Terms::Activity::current().values;
0348     const auto agents = !agent.values.isEmpty() ? agent.values : !d->query.agents().isEmpty() ? d->query.agents() : Terms::Agent::current().values;
0349 
0350     for (const auto &activity : activities) {
0351         for (const auto &agent : agents) {
0352             qCDebug(KACTIVITIES_STATS_LOG) << "Unlink " << agent << resource << activity;
0353             d->linking->UnlinkResourceFromActivity(agent, resource.toString(), activity);
0354         }
0355     }
0356 }
0357 
0358 } // namespace Stats
0359 } // namespace KActivities
0360 
0361 #include "moc_resultwatcher.cpp"