#include #include #include #include #include #include "global.h" #include "board.h" #include "xmlrpcinterface.h" #include "xmlrpcreply.h" #include "fetchtopicsaction.h" FetchTopicsAction::FetchTopicsAction(int forumId, Board::TopicType type, int start, int end, Board *board) : Action(board), _forumId(forumId), _type(type), _start(start), _end(end) { } bool FetchTopicsAction::isSupersetOf(Action *action) const { FetchTopicsAction *other = qobject_cast(action); if (other) { if (other->_forumId == _forumId && other->_type == _type) { if (_end == FetchAllTopics) { // If this is fetching all topics, then this is a superset // of every other action... except those that also fetch all // topics. return other->_end != FetchAllTopics; } else if (other->_end == FetchAllTopics) { // If the other action fetches all topics, this cannot be a // superset return false; } else if (_start <= other->_start && _end >= other->_end) { // Otherwise, check if the range of posts fetched by this // action fully contains the other action. return true; } } } return false; } void FetchTopicsAction::execute() { int end = _end; if (end == FetchAllTopics) { // Fetch topics in blocks of size 50. end = _start + MAX_FORUM_PAGE_SIZE - 1; // After finishing this action, a new one will be enqueued with the next 50. } switch (_type) { case Board::Normal: _call = _board->service()->asyncCall("get_topic", QString::number(_forumId), _start, end); break; case Board::Sticky: _call = _board->service()->asyncCall("get_topic", QString::number(_forumId), _start, end, QString("TOP")); break; case Board::Announcement: _call = _board->service()->asyncCall("get_topic", QString::number(_forumId), _start, end, QString("ANN")); break; } Q_ASSERT(_call); _call->setParent(this); connect(_call, SIGNAL(finished(XmlRpcPendingCall*)), SLOT(handleFinishedCall())); } void FetchTopicsAction::handleFinishedCall() { XmlRpcReply result(_call); if (result.isValid()) { QVariantMap map = result; QVariantList topics = map["topics"].toList(); QSqlDatabase db = _board->database(); db.transaction(); const int total_topic_num = map["total_topic_num"].toInt(); int expected_end = _end == FetchAllTopics ? _start + MAX_FORUM_PAGE_SIZE - 1 : _end; QSet current_topics = QSet::fromList(getCurrentDbTopics(_start, expected_end)); QSet unchanged_topics = current_topics; if (_start >= total_topic_num) { // Workaround an issue where the service always returns one post. topics.clear(); } Q_ASSERT(_start >= 0); int position = _start; QSqlQuery query(db); query.prepare("INSERT OR REPLACE INTO topics (forum_id, topic_id, topic_type, topic_title, topic_author_id, topic_author_name, is_subscribed, is_closed, icon_url, last_reply_time, reply_number, new_post, position, last_update_time) " "VALUES (:forum_id, :topic_id, :topic_type, :topic_title, :topic_author_id, :topic_author_name, :is_subscribed, :is_closed, :icon_url, :last_reply_time, :reply_number, :new_post, :position, :last_update_time)"); foreach (const QVariant& topic_v, topics) { QVariantMap topic = topic_v.toMap(); bool ok = false; int forum_id = topic["forum_id"].toInt(&ok); if (!ok) { // Not fatal, just use the one we expected. forum_id = _forumId; } int topic_id = topic["topic_id"].toInt(&ok); if (!ok) { qWarning() << "No topic_id in" << topic; // This is now a fatal error. db.rollback(); goto finish; } query.bindValue(":forum_id", forum_id); query.bindValue(":topic_id", topic_id); query.bindValue(":topic_type", _type); query.bindValue(":topic_title", decodeTopicText(topic["topic_title"])); query.bindValue(":topic_author_id", topic["topic_author_id"].toInt()); query.bindValue(":topic_author_name", decodeTopicText(topic["topic_author_name"])); query.bindValue(":is_subscribed", topic["is_subscribed"].toBool() ? 1 : 0); query.bindValue(":is_closed", topic["is_closed"].toBool() ? 1 : 0); query.bindValue(":icon_url", topic["icon_url"].toString()); query.bindValue(":last_reply_time", topic["last_reply_time"].toDateTime().toUTC()); query.bindValue(":reply_number", topic["reply_number"].toInt()); query.bindValue(":new_post", topic["new_post"].toBool() ? 1 : 0); query.bindValue(":position", position); query.bindValue(":last_update_time", QDateTime::currentDateTimeUtc()); position++; if (!query.exec()) { qWarning() << "Failed to store topic info for:" << topic_id; handleDatabaseError("storing topic info", query); db.rollback(); goto finish; } unchanged_topics.remove(topic_id); } int fetched_end = position - 1; bool eof = fetched_end < expected_end; if (eof) { // Service did not return as many topics as we expected, thus we // can safely delete any topics after the current position. query.prepare("DELETE FROM topics " "WHERE forum_id = :forum_id AND topic_type = :topic_type " " AND position >= :position"); query.bindValue(":forum_id", _forumId); query.bindValue(":topic_type", _type); query.bindValue(":position", position); if (query.exec()) { position += query.numRowsAffected(); // Advance the position counter } else { handleDatabaseError("deleting topics", query); db.rollback(); goto finish; } if (!unchanged_topics.empty()) { // The topics that were in the begin..end range but were not updated // need to be removed. query.prepare("DELETE FROM topics WHERE topic_id = :topic_id"); foreach (int topic_id, unchanged_topics) { query.bindValue(":topic_id", topic_id); if (!query.exec()) { handleDatabaseError("deleting topics", query); db.rollback(); goto finish; } position++; } } } else if (!unchanged_topics.empty()) { // The topics that were in the begin..end range but were not updated // need to be renumbered. query.prepare("UPDATE topics SET position = :position WHERE topic_id = :topic_id"); foreach (int topic_id, unchanged_topics) { query.bindValue(":position", position); query.bindValue(":topic_id", topic_id); if (!query.exec()) { handleDatabaseError("updating topic position", query); db.rollback(); goto finish; } position++; } Q_ASSERT(position >= expected_end + 1); } db.commit(); if (position > _start) { _board->notifyForumTopicsChanged(_forumId, _type, _start, position - 1); } if (_end == FetchAllTopics && !eof) { // Ok, let's prepare to fetch the next block of topics because // there are probably more of them int next_start = fetched_end + 1; _board->enqueueAction(new FetchTopicsAction(_forumId, _type, next_start, FetchAllTopics, _board)); } } else { qWarning() << "Could not fetch topics"; // TODO emit error ... } finish: emit finished(this); _call->deleteLater(); } QString FetchTopicsAction::decodeTopicText(const QVariant &v) { QByteArray ba = v.toByteArray(); return QString::fromUtf8(ba.constData(), ba.length()); } QList FetchTopicsAction::getCurrentDbTopics(int start, int end) { QSqlQuery query(_board->database()); query.prepare("SELECT topic_id FROM topics " "WHERE forum_id = :forum_id AND topic_type = :topic_type AND " " position BETWEEN :start AND :end " "ORDER by position ASC"); query.bindValue(":forum_id", _forumId); query.bindValue(":topic_type", _type); query.bindValue(":start", start); query.bindValue(":end", end); if (query.exec()) { QList list; while (query.next()) { list << query.value(0).toInt(); } return list; } else { qWarning() << "Could not get current topics list:" << query.lastError().text(); return QList(); } }