* Copyright (C) 2015 Ivan Cukic <ivan.cukic(at)kde.org>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2,
* or (at your option) any later version, as published by the Free
* Software Foundation
*
* 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, write to the
* Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
#include "resultmodel.h"
#include <QDebug>
#include <QDateTime>
#include <QCoreApplication>
#include <QFile>
#include <functional>
#include <boost/range/algorithm/find_if.hpp>
#include <boost/range/algorithm/lower_bound.hpp>
#include <KConfig>
#include <KConfigGroup>
#include <common/database/Database.h>
#include <utils/qsqlquery_iterator.h>
#include <utils/slide.h>
#include <utils/member_matcher.h>
#include "resultset.h"
#include "resultwatcher.h"
#include "cleaning.h"
#include "kactivities/consumer.h"
#include <common/specialvalues.h>
#define MAX_CHUNK_LOAD_SIZE 50
#define MAX_RELOAD_CACHE_SIZE 50
#define QDBG qDebug() << "KActivitiesStats(" << (void*)this << ")"
namespace KActivities {
namespace Experimental {
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);
}
enum Fetch {
FetchReset,
FetchReload,
FetchMore
};
class Cache {
public:
typedef QList<ResultSet::Result> Items;
Cache(ResultModelPrivate *d, const QString &clientId, int limit)
: d(d)
, m_countLimit(limit)
, m_clientId(clientId)
{
if (!m_clientId.isEmpty()) {
m_configFile = new KConfig(
QStandardPaths::writableLocation(QStandardPaths::ConfigLocation)
+ QStringLiteral("/kactivitymanagerd-statsrc"));
m_config = KConfigGroup(m_configFile, "ResultModel-OrderingFor-" + clientId);
if (m_config.isValid()) {
m_fixedItems = m_config.readEntry("kactivitiesLinkedItemsOrder",
QStringList());
}
qDebug() << "Configuration activated " << m_configFile->name();
}
}
~Cache()
{
delete m_configFile;
}
inline int size() const
{
return m_items.size();
}
inline void setLinkedResultPosition(const QString &resource,
int position)
{
if (!m_config.isValid()) {
qWarning() << "We can not reorder the results, no clientId was specified";
return;
}
qDebug() << "Searching for " << resource;
auto resourcePosition = find(resource);
qDebug() << "Was resource found? " << (bool)resourcePosition;
if (resourcePosition) {
qDebug() << "What is the status? " << resourcePosition.iterator->linkStatus();
}
if (!resourcePosition
|| resourcePosition.iterator->linkStatus() == ResultSet::Result::NotLinked) {
qWarning("Trying to reposition a resource that we do not have, or is not linked");
return;
}
QStringList linkedItems;
foreach (const ResultSet::Result &item, m_items) {
if (item.linkStatus() == ResultSet::Result::NotLinked) break;
linkedItems << item.resource();
}
if (position > linkedItems.size()) {
position = linkedItems.size();
}
auto oldPosition = linkedItems.indexOf(resource);
kamd::utils::slide_one(
linkedItems.begin() + oldPosition,
linkedItems.begin() + position);
m_fixedItems = linkedItems;
m_config.writeEntry("kactivitiesLinkedItemsOrder", m_fixedItems);
m_config.sync();
d->repositionResult(resourcePosition,
d->destinationFor(*resourcePosition));
}
private:
ResultModelPrivate *const d;
QList<ResultSet::Result> m_items;
int m_countLimit;
QString m_clientId;
KConfig *m_configFile;
KConfigGroup m_config;
QStringList m_fixedItems;
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 &fixedItems() const
{
return m_fixedItems;
}
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 <typename What, typename Predicate>
inline FindCacheResult lowerBound(What &&what, Predicate &&predicate)
{
return FindCacheResult(
this, boost::lower_bound(m_items, std::forward<What>(what),
std::forward<Predicate>(predicate)));
}
template <typename Predicate>
inline FindCacheResult lowerBound(Predicate &&predicate)
{
using namespace kamd::utils::member_matcher;
return FindCacheResult(
this, boost::lower_bound(m_items, _,
std::forward<Predicate>(predicate)));
}
inline int indexOf(const FindCacheResult &result)
{
return std::distance(m_items.begin(), result.iterator);
}
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();
}
inline void replace(const Items &newItems, int from = 0)
{
using namespace kamd::utils::member_matcher;
#if 0
QDBG << "===\nOld 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
auto newBlockStart = newItems.cbegin();
const int maxToReplace = m_countLimit - from;
if (maxToReplace <= 0) return;
const auto newItemsEnd =
newItems.size() <= maxToReplace ? newItems.cend() :
newItems.cbegin() + maxToReplace;
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()) {
d->q->beginInsertRows(QModelIndex(), newBlockStartIndex,
newBlockStartIndex);
m_items.insert(newBlockStartIndex, *newBlockStart);
d->q->endInsertRows();
++newBlockStart;
} else {
auto newBlockEnd = newBlockStart;
auto oldBlockEnd = oldBlockStart;
while (newBlockEnd != newItemsEnd &&
oldBlockEnd != m_items.end() &&
newBlockEnd->resource() == oldBlockEnd->resource()) {
++newBlockEnd;
++oldBlockEnd;
}
const int oldBlockStartIndex
= std::distance(m_items.begin() + from, oldBlockStart);
const int blockSize
= std::distance(oldBlockStart, oldBlockEnd);
if (oldBlockStartIndex != newBlockStartIndex) {
d->q->beginMoveRows(QModelIndex(), oldBlockStartIndex,
oldBlockStartIndex + blockSize - 1,
QModelIndex(), newBlockStartIndex);
kamd::utils::slide(
oldBlockStart, oldBlockEnd,
m_items.begin() + newBlockStartIndex);
d->q->endMoveRows();
}
newBlockStart = newBlockEnd;
}
}
trim(from + newItems.size());
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;
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 {
typedef kamd::utils::member_matcher::placeholder placeholder;
FixedItemsLessThan(const Cache &cache,
const QString &matchResource = QString())
: cache(cache), matchResource(matchResource)
{
}
bool compare (const QString &leftResource, const QString &rightResource) const
{
const bool hasLeft = cache.fixedItems().contains(leftResource);
const bool hasRight = cache.fixedItems().contains(rightResource);
return
( hasLeft && !hasRight) ? true :
(!hasLeft && hasRight) ? false :
( hasLeft && hasRight) ? cache.fixedItems().indexOf(leftResource) <
cache.fixedItems().indexOf(rightResource) :
false;
}
template <typename T>
bool operator() (const T &left, placeholder) const
{
return compare(left.resource(), matchResource);
}
template <typename T>
bool operator() (placeholder, const T &right) const
{
return compare(matchResource, right.resource());
}
template <typename T, typename V>
bool operator() (const T &left, const V &right) const
{
return compare(left.resource(), right.resource());
}
const Cache &cache;
const QString matchResource;
};
inline Cache::FindCacheResult destinationFor(const ResultSet::Result &result)
{
using namespace kamd::utils::member_matcher;
using namespace Terms;
const auto resource = result.resource();
const auto score = result.score();
const auto firstUpdate = result.firstUpdate();
const auto lastUpdate = result.lastUpdate();
const auto linkStatus = result.linkStatus();
#define ORDER_BY(Field) member(&ResultSet::Result::Field) > Field
#define ORDER_BY_FULL(Field) \
cache.lowerBound(FixedItemsLessThan(cache, resource) \
&& ORDER_BY(linkStatus) \
&& 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):
ORDER_BY_FULL(resource)
;
#undef ORDER_BY
#undef ORDER_BY_FULL
return destination;
}
inline void removeResult(const Cache::FindCacheResult &result)
{
q->beginRemoveRows(QModelIndex(), result.index, result.index);
cache.removeAt(result);
q->endRemoveRows();
fetch(cache.size(), 1);
}
inline void repositionResult(const Cache::FindCacheResult &result,
const Cache::FindCacheResult &destination)
{
using kamd::utils::slide_one;
const int currentIndex = result.index;
q->dataChanged(q->index(currentIndex), q->index(currentIndex));
bool moving
= q->beginMoveRows(QModelIndex(), currentIndex, currentIndex,
QModelIndex(), destination.index);
slide_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;
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());
if (query.selection() != Terms::UsedResources) {
std::stable_sort(newItems.begin(), newItems.end(),
FixedItemsLessThan(cache));
}
cache.replace(newItems, from);
}
void fetch(Fetch mode)
{
if (mode == FetchReset) {
cache.clear();
fetch(0, MAX_CHUNK_LOAD_SIZE);
} else if (mode == FetchReload) {
if (cache.size() > MAX_RELOAD_CACHE_SIZE) {
fetch(FetchReset);
} else {
fetch(0, cache.size());
}
} else {
fetch(cache.size(), MAX_CHUNK_LOAD_SIZE);
}
}
void onResultScoreUpdated(const QString &resource, double score,
uint lastUpdate, uint firstUpdate)
{
using boost::lower_bound;
QDBG << "ResultModelPrivate::onResultAdded "
<< "result added:" << resource
<< "score:" << score
<< "last:" << lastUpdate
<< "first:" << firstUpdate;
const auto result = cache.find(resource);
ResultSet::Result::LinkStatus linkStatus
= result ? result->linkStatus()
: ResultSet::Result::NotLinked;
if (result) {
auto &item = *result.iterator;
item.setScore(score);
item.setLinkStatus(linkStatus);
item.setLastUpdate(lastUpdate);
item.setFirstUpdate(firstUpdate);
const auto destination = destinationFor(item);
repositionResult(result, destination);
} else {
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)
{
const auto result = cache.find(resource);
if (!result) return;
if (query.selection() == Terms::LinkedResources) {
onResultScoreUpdated(resource, 0, 0, 0);
} else if (query.selection() == Terms::AllResources) {
result->setLinkStatus(ResultSet::Result::Linked);
repositionResult(result, destinationFor(*result));
}
}
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) {
result->setLinkStatus(ResultSet::Result::NotLinked);
repositionResult(result, destinationFor(*result));
}
}
Query query;
ResultWatcher watcher;
bool hasMore;
KActivities::Consumer activities;
Common::Database::Ptr database;
void fillTitleAndMimetype(ResultSet::Result &result)
{
auto query = database->execQuery(
"SELECT "
"title, mimetype "
"FROM "
"ResourceInfo "
"WHERE "
"targettedResource = '" + result.resource() + "'"
);
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)
{
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 (query.activities().contains(CURRENT_ACTIVITY_TAG)) {
fetch(FetchReset);
}
}
private:
ResultModel *const q;
};
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<int, QByteArray> ResultModel::roleNames() const
{
return {
{ ResourceRole , "resource" },
{ TitleRole , "title" },
{ ScoreRole , "score" },
{ FirstUpdateRole , "created" },
{ LastUpdateRole , "modified" },
{ LinkStatusRole , "linkStatus" }
};
}
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()
: 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)
{
}
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);
}
}
}
}