aboutsummaryrefslogtreecommitdiff
path: root/jobs/basejob.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'jobs/basejob.cpp')
-rw-r--r--jobs/basejob.cpp508
1 files changed, 0 insertions, 508 deletions
diff --git a/jobs/basejob.cpp b/jobs/basejob.cpp
deleted file mode 100644
index 3cde7c50..00000000
--- a/jobs/basejob.cpp
+++ /dev/null
@@ -1,508 +0,0 @@
-/******************************************************************************
- * Copyright (C) 2015 Felix Rohrbach <kde@fxrh.de>
- *
- * 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) any later version.
- *
- * 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, write to the Free Software
- * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
- */
-
-#include "basejob.h"
-
-#include "connectiondata.h"
-
-#include <QtNetwork/QNetworkAccessManager>
-#include <QtNetwork/QNetworkRequest>
-#include <QtNetwork/QNetworkReply>
-#include <QtCore/QTimer>
-#include <QtCore/QRegularExpression>
-//#include <QtCore/QStringBuilder>
-
-#include <array>
-
-using namespace QMatrixClient;
-
-struct NetworkReplyDeleter : public QScopedPointerDeleteLater
-{
- static inline void cleanup(QNetworkReply* reply)
- {
- if (reply && reply->isRunning())
- reply->abort();
- QScopedPointerDeleteLater::cleanup(reply);
- }
-};
-
-class BaseJob::Private
-{
- public:
- // Using an idiom from clang-tidy:
- // http://clang.llvm.org/extra/clang-tidy/checks/modernize-pass-by-value.html
- Private(HttpVerb v, QString endpoint, QUrlQuery q, Data&& data, bool nt)
- : verb(v), apiEndpoint(std::move(endpoint))
- , requestQuery(std::move(q)), requestData(std::move(data))
- , needsToken(nt)
- { }
-
- void sendRequest();
- const JobTimeoutConfig& getCurrentTimeoutConfig() const;
-
- const ConnectionData* connection = nullptr;
-
- // Contents for the network request
- HttpVerb verb;
- QString apiEndpoint;
- QHash<QByteArray, QByteArray> requestHeaders;
- QUrlQuery requestQuery;
- Data requestData;
- bool needsToken;
-
- // There's no use of QMimeType here because we don't want to match
- // content types against the known MIME type hierarchy; and at the same
- // type QMimeType is of little help with MIME type globs (`text/*` etc.)
- QByteArrayList expectedContentTypes;
-
- QScopedPointer<QNetworkReply, NetworkReplyDeleter> reply;
- Status status = Pending;
-
- QTimer timer;
- QTimer retryTimer;
-
- QVector<JobTimeoutConfig> errorStrategy =
- { { 90, 5 }, { 90, 10 }, { 120, 30 } };
- int maxRetries = errorStrategy.size();
- int retriesTaken = 0;
-
- LoggingCategory logCat = JOBS;
-};
-
-BaseJob::BaseJob(HttpVerb verb, const QString& name, const QString& endpoint, bool needsToken)
- : BaseJob(verb, name, endpoint, Query { }, Data { }, needsToken)
-{ }
-
-BaseJob::BaseJob(HttpVerb verb, const QString& name, const QString& endpoint,
- const Query& query, Data&& data, bool needsToken)
- : d(new Private(verb, endpoint, query, std::move(data), needsToken))
-{
- setObjectName(name);
- setExpectedContentTypes({ "application/json" });
- d->timer.setSingleShot(true);
- connect (&d->timer, &QTimer::timeout, this, &BaseJob::timeout);
- d->retryTimer.setSingleShot(true);
- connect (&d->retryTimer, &QTimer::timeout, this, &BaseJob::sendRequest);
-}
-
-BaseJob::~BaseJob()
-{
- stop();
- qCDebug(d->logCat) << this << "destroyed";
-}
-
-const QString& BaseJob::apiEndpoint() const
-{
- return d->apiEndpoint;
-}
-
-void BaseJob::setApiEndpoint(const QString& apiEndpoint)
-{
- d->apiEndpoint = apiEndpoint;
-}
-
-const BaseJob::headers_t&BaseJob::requestHeaders() const
-{
- return d->requestHeaders;
-}
-
-void BaseJob::setRequestHeader(const headers_t::key_type& headerName,
- const headers_t::mapped_type& headerValue)
-{
- d->requestHeaders[headerName] = headerValue;
-}
-
-void BaseJob::setRequestHeaders(const BaseJob::headers_t& headers)
-{
- d->requestHeaders = headers;
-}
-
-const QUrlQuery& BaseJob::query() const
-{
- return d->requestQuery;
-}
-
-void BaseJob::setRequestQuery(const QUrlQuery& query)
-{
- d->requestQuery = query;
-}
-
-const BaseJob::Data& BaseJob::requestData() const
-{
- return d->requestData;
-}
-
-void BaseJob::setRequestData(Data&& data)
-{
- std::swap(d->requestData, data);
-}
-
-const QByteArrayList& BaseJob::expectedContentTypes() const
-{
- return d->expectedContentTypes;
-}
-
-void BaseJob::addExpectedContentType(const QByteArray& contentType)
-{
- d->expectedContentTypes << contentType;
-}
-
-void BaseJob::setExpectedContentTypes(const QByteArrayList& contentTypes)
-{
- d->expectedContentTypes = contentTypes;
-}
-
-QUrl BaseJob::makeRequestUrl(QUrl baseUrl,
- const QString& path, const QUrlQuery& query)
-{
- auto pathBase = baseUrl.path();
- if (!pathBase.endsWith('/') && !path.startsWith('/'))
- pathBase.push_back('/');
-
- baseUrl.setPath( pathBase + path );
- baseUrl.setQuery(query);
- return baseUrl;
-}
-
-void BaseJob::Private::sendRequest()
-{
- QNetworkRequest req
- { makeRequestUrl(connection->baseUrl(), apiEndpoint, requestQuery) };
- if (!requestHeaders.contains("Content-Type"))
- req.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
- req.setRawHeader(QByteArray("Authorization"),
- QByteArray("Bearer ") + connection->accessToken());
-#if (QT_VERSION >= QT_VERSION_CHECK(5, 6, 0))
- req.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true);
- req.setMaximumRedirectsAllowed(10);
-#endif
- for (auto it = requestHeaders.cbegin(); it != requestHeaders.cend(); ++it)
- req.setRawHeader(it.key(), it.value());
- switch( verb )
- {
- case HttpVerb::Get:
- reply.reset( connection->nam()->get(req) );
- break;
- case HttpVerb::Post:
- reply.reset( connection->nam()->post(req, requestData.source()) );
- break;
- case HttpVerb::Put:
- reply.reset( connection->nam()->put(req, requestData.source()) );
- break;
- case HttpVerb::Delete:
- reply.reset( connection->nam()->deleteResource(req) );
- break;
- }
-}
-
-void BaseJob::beforeStart(const ConnectionData*)
-{ }
-
-void BaseJob::afterStart(const ConnectionData*, QNetworkReply*)
-{ }
-
-void BaseJob::beforeAbandon(QNetworkReply*)
-{ }
-
-void BaseJob::start(const ConnectionData* connData)
-{
- d->connection = connData;
- beforeStart(connData);
- if (status().good())
- sendRequest();
- if (status().good())
- afterStart(connData, d->reply.data());
- if (!status().good())
- QTimer::singleShot(0, this, &BaseJob::finishJob);
-}
-
-void BaseJob::sendRequest()
-{
- emit aboutToStart();
- d->retryTimer.stop(); // In case we were counting down at the moment
- qCDebug(d->logCat) << this << "sending request to" << d->apiEndpoint;
- if (!d->requestQuery.isEmpty())
- qCDebug(d->logCat) << " query:" << d->requestQuery.toString();
- d->sendRequest();
- connect( d->reply.data(), &QNetworkReply::finished, this, &BaseJob::gotReply );
- if (d->reply->isRunning())
- {
- connect( d->reply.data(), &QNetworkReply::uploadProgress,
- this, &BaseJob::uploadProgress);
- connect( d->reply.data(), &QNetworkReply::downloadProgress,
- this, &BaseJob::downloadProgress);
- d->timer.start(getCurrentTimeout());
- qCDebug(d->logCat) << this << "request has been sent";
- emit started();
- }
- else
- qCWarning(d->logCat) << this << "request could not start";
-}
-
-void BaseJob::gotReply()
-{
- setStatus(checkReply(d->reply.data()));
- if (status().good())
- setStatus(parseReply(d->reply.data()));
- else
- {
- const auto body = d->reply->readAll();
- if (!body.isEmpty())
- {
- qCDebug(d->logCat).noquote() << "Error body:" << body;
- auto json = QJsonDocument::fromJson(body).object();
- if (json.isEmpty())
- setStatus(IncorrectRequestError, body);
- else {
- if (error() == TooManyRequestsError ||
- json.value("errcode").toString() == "M_LIMIT_EXCEEDED")
- {
- QString msg = tr("Too many requests");
- auto retryInterval = json.value("retry_after_ms").toInt(-1);
- if (retryInterval != -1)
- msg += tr(", next retry advised after %1 ms")
- .arg(retryInterval);
- else // We still have to figure some reasonable interval
- retryInterval = getNextRetryInterval();
-
- setStatus(TooManyRequestsError, msg);
-
- // Shortcut to retry instead of executing finishJob()
- stop();
- qCWarning(d->logCat)
- << this << "will retry in" << retryInterval;
- d->retryTimer.start(retryInterval);
- emit retryScheduled(d->retriesTaken, retryInterval);
- return;
- }
- setStatus(IncorrectRequestError, json.value("error").toString());
- }
- }
- }
-
- finishJob();
-}
-
-bool checkContentType(const QByteArray& type, const QByteArrayList& patterns)
-{
- if (patterns.isEmpty())
- return true;
-
- // ignore possible appendixes of the content type
- const auto ctype = type.split(';').front();
-
- for (const auto& pattern: patterns)
- {
- if (pattern.startsWith('*') || ctype == pattern) // Fast lane
- return true;
-
- auto patternParts = pattern.split('/');
- Q_ASSERT_X(patternParts.size() <= 2, __FUNCTION__,
- "BaseJob: Expected content type should have up to two"
- " /-separated parts; violating pattern: " + pattern);
-
- if (ctype.split('/').front() == patternParts.front() &&
- patternParts.back() == "*")
- return true; // Exact match already went on fast lane
- }
-
- return false;
-}
-
-BaseJob::Status BaseJob::checkReply(QNetworkReply* reply) const
-{
- const auto httpCode =
- reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
- qCDebug(d->logCat).nospace().noquote() << this << " returned HTTP code "
- << httpCode << ": "
- << (reply->error() == QNetworkReply::NoError ?
- "Success" : reply->errorString())
- << " (URL: " << reply->url().toDisplayString() << ")";
-
- if (httpCode == 429) // Qt doesn't know about it yet
- return { TooManyRequestsError, tr("Too many requests") };
-
- // Should we check httpCode instead? Maybe even use it in BaseJob::Status?
- // That would make codes in logs slightly more readable.
- switch( reply->error() )
- {
- case QNetworkReply::NoError:
- if (checkContentType(reply->rawHeader("Content-Type"),
- d->expectedContentTypes))
- return NoError;
- else // A warning in the logs might be more proper instead
- return { IncorrectResponseError,
- "Incorrect content type of the response" };
-
- case QNetworkReply::AuthenticationRequiredError:
- case QNetworkReply::ContentAccessDenied:
- case QNetworkReply::ContentOperationNotPermittedError:
- return { ContentAccessError, reply->errorString() };
-
- case QNetworkReply::ProtocolInvalidOperationError:
- case QNetworkReply::UnknownContentError:
- return { IncorrectRequestError, reply->errorString() };
-
- case QNetworkReply::ContentNotFoundError:
- return { NotFoundError, reply->errorString() };
-
- default:
- return { NetworkError, reply->errorString() };
- }
-}
-
-BaseJob::Status BaseJob::parseReply(QNetworkReply* reply)
-{
- QJsonParseError error;
- QJsonDocument json = QJsonDocument::fromJson(reply->readAll(), &error);
- if( error.error == QJsonParseError::NoError )
- return parseJson(json);
- else
- return { JsonParseError, error.errorString() };
-}
-
-BaseJob::Status BaseJob::parseJson(const QJsonDocument&)
-{
- return Success;
-}
-
-void BaseJob::stop()
-{
- d->timer.stop();
- if (d->reply)
- {
- d->reply->disconnect(this); // Ignore whatever comes from the reply
- if (d->reply->isRunning())
- {
- qCWarning(d->logCat) << this << "stopped without ready network reply";
- d->reply->abort();
- }
- }
- else
- qCWarning(d->logCat) << this << "stopped with empty network reply";
-}
-
-void BaseJob::finishJob()
-{
- stop();
- if ((error() == NetworkError || error() == TimeoutError)
- && d->retriesTaken < d->maxRetries)
- {
- // TODO: The whole retrying thing should be put to ConnectionManager
- // otherwise independently retrying jobs make a bit of notification
- // storm towards the UI.
- const auto retryInterval =
- error() == TimeoutError ? 0 : getNextRetryInterval();
- ++d->retriesTaken;
- qCWarning(d->logCat) << this << "will retry" << d->retriesTaken
- << "in" << retryInterval/1000 << "s";
- d->retryTimer.start(retryInterval);
- emit retryScheduled(d->retriesTaken, retryInterval);
- return;
- }
-
- // Notify those interested in any completion of the job (including killing)
- emit finished(this);
-
- emit result(this);
- if (error())
- emit failure(this);
- else
- emit success(this);
-
- deleteLater();
-}
-
-const JobTimeoutConfig& BaseJob::Private::getCurrentTimeoutConfig() const
-{
- return errorStrategy[std::min(retriesTaken, errorStrategy.size() - 1)];
-}
-
-BaseJob::duration_t BaseJob::getCurrentTimeout() const
-{
- return d->getCurrentTimeoutConfig().jobTimeout * 1000;
-}
-
-BaseJob::duration_t BaseJob::getNextRetryInterval() const
-{
- return d->getCurrentTimeoutConfig().nextRetryInterval * 1000;
-}
-
-BaseJob::duration_t BaseJob::millisToRetry() const
-{
- return d->retryTimer.isActive() ? d->retryTimer.remainingTime() : 0;
-}
-
-int BaseJob::maxRetries() const
-{
- return d->maxRetries;
-}
-
-void BaseJob::setMaxRetries(int newMaxRetries)
-{
- d->maxRetries = newMaxRetries;
-}
-
-BaseJob::Status BaseJob::status() const
-{
- return d->status;
-}
-
-int BaseJob::error() const
-{
- return d->status.code;
-}
-
-QString BaseJob::errorString() const
-{
- return d->status.message;
-}
-
-void BaseJob::setStatus(Status s)
-{
- d->status = s;
- if (!s.good())
- qCWarning(d->logCat) << this << "status" << s;
-}
-
-void BaseJob::setStatus(int code, QString message)
-{
- message.replace(d->connection->accessToken(), "(REDACTED)");
- setStatus({ code, message });
-}
-
-void BaseJob::abandon()
-{
- beforeAbandon(d->reply.data());
- setStatus(Abandoned);
- this->disconnect();
- if (d->reply)
- d->reply->disconnect(this);
- deleteLater();
-}
-
-void BaseJob::timeout()
-{
- setStatus( TimeoutError, "The job has timed out" );
- finishJob();
-}
-
-void BaseJob::setLoggingCategory(LoggingCategory lcf)
-{
- d->logCat = lcf;
-}