libQuotient
A Qt library for building matrix clients
Loading...
Searching...
No Matches
basejob.h
Go to the documentation of this file.
1// SPDX-FileCopyrightText: 2015 Felix Rohrbach <kde@fxrh.de>
2// SPDX-FileCopyrightText: 2016 Kitsune Ral <Kitsune-Ral@users.sf.net>
3// SPDX-License-Identifier: LGPL-2.1-or-later
4
5#pragma once
6
7#include "requestdata.h"
8
9#include <QtCore/QObject>
10#include <QtCore/QStringBuilder>
11#include <QtCore/QLoggingCategory>
12#include <QtCore/QFuture>
13
14#include <Quotient/converters.h> // Common for csapi/ headers even though not used here
15#include <Quotient/quotient_common.h> // For DECL_DEPRECATED_ENUMERATOR
16
17class QNetworkRequest;
18class QNetworkReply;
19class QSslError;
20
21namespace Quotient {
22class ConnectionData;
23
24enum class HttpVerb { Get, Put, Post, Delete };
25
30 //! How many times a network request should be tried; std::nullopt means keep trying forever
31 std::optional<decltype(jobTimeouts)::size_type> maxRetries = jobTimeouts.size();
32};
33
38
40 template <int N>
41 static auto encodeIfParam(const char (&literalPart)[N])
42 {
43 return literalPart;
44 }
45
46public:
47 //! \brief Job status codes
48 //!
49 //! Every job is created in Unprepared status; upon calling Connection::prepare(), if things are
50 //! fine, it becomes Pending and remains so until the reply arrives; then the status code is
51 //! set according to the job result. At any point in time the job can be abandon()ed, causing
52 //! it to become Abandoned for a brief period before deletion.
57 WarningLevel = 20, //!< Warnings have codes starting from this
60 Unprepared = 25, //!< Initial job state is incomplete, hence warning level
61 Abandoned = 50, //!< A tiny period between abandoning and object deletion
62 ErrorLevel = 100, //!< Errors have codes starting from this
81 };
83
84 template <typename... StrTs>
86 {
87 return (base % ... % encodeIfParam(std::forward<StrTs>(parts)));
88 }
89
90 //! \brief The status of a job
91 //!
92 //! The status consists of a code that is described (but not delimited) by StatusCode, and
93 //! a freeform message.
94 //!
95 //! To extend the list of error codes, define an (anonymous) enum along the lines of StatusCode,
96 //! with additional values starting at UserDefinedError.
97 struct Status {
99 Status(int c, QString m) : code(c), message(std::move(m)) {}
100
103 {
104 return { fromHttpCode(httpCode), std::move(msg) };
105 }
106
107 bool good() const { return code < ErrorLevel; }
109 friend QDebug operator<<(const QDebug& dbg, const Status& s)
110 {
111 return s.dumpToLog(dbg);
112 }
113
114 bool operator==(const Status& other) const
115 {
116 return code == other.code && message == other.message;
117 }
118 bool operator!=(const Status& other) const
119 {
120 return !operator==(other);
121 }
122 bool operator==(int otherCode) const
123 {
124 return code == otherCode;
125 }
126 bool operator!=(int otherCode) const
127 {
128 return !operator==(otherCode);
129 }
130
131 int code;
133 };
134
135public:
137 bool needsToken = true);
139 const QUrlQuery& query, RequestData&& data = {},
140 bool needsToken = true);
141
143 bool isBackground() const;
144
145 //! Current status of the job
146 Status status() const;
147
148 //! Short human-friendly message on the job status
150
151 //! \brief Get first bytes of the raw response body as received from the server
152 //! \param bytesAtMost the number of leftmost bytes to return
153 //! \sa rawDataSample
155
156 //! Access the whole response body as received from the server
157 const QByteArray& rawData() const;
158
159 //! \brief Get UI-friendly sample of raw data
160 //!
161 //! This is almost the same as rawData but appends the "truncated" suffix if not all data fit in
162 //! bytesAtMost. This call is recommended to present a sample of raw data as "details" next to
163 //! error messages. Note that the default \p bytesAtMost value is also tailored to UI cases.
164 //!
165 //! \sa //! rawData
167
168 //! \brief Get the response body as a JSON object
169 //!
170 //! If the job's returned content type is not `application/json` or if the top-level JSON entity
171 //! is not an object, an empty object is returned.
173
174 //! \brief Get the response body as a JSON array
175 //!
176 //! If the job's returned content type is not `application/json` or if the top-level JSON entity
177 //! is not an array, an empty array is returned.
179
180 //! \brief Load the property from the JSON response assuming a given C++ type
181 //!
182 //! If there's no top-level JSON object in the response or if there's
183 //! no node with the key \p keyName, \p defaultValue is returned.
184 template <typename T>
185 T loadFromJson(auto keyName, T&& defaultValue = {}) const
186 {
187 const auto& jv = jsonData().value(keyName);
188 return jv.isUndefined() ? std::forward<T>(defaultValue) : fromJson<T>(jv);
189 }
190
191 //! \brief Load the property from the JSON response and delete it from JSON
192 //!
193 //! If there's no top-level JSON object in the response or if there's
194 //! no node with the key \p keyName, \p defaultValue is returned.
195 template <typename T>
197 {
198 if (const auto& jv = takeValueFromJson(key); !jv.isUndefined())
199 return fromJson<T>(jv);
200
201 return std::forward<T>(defaultValue);
202 }
203
204 //! \brief Error (more generally, status) code
205 //!
206 //! Equivalent to status().code
207 //! \sa status, StatusCode
208 int error() const;
209
210 //! Error-specific message, as returned by the server
211 virtual QString errorString() const;
212
213 //! A URL to help/clarify the error, if provided by the server
214 QUrl errorUrl() const;
215
216 //! Get the back-off strategy for this job instance
218 //! Set the back-off strategy for this specific job instance
220
221 //! Get the default back-off strategy used for any newly created job
223 //! \brief Set the default back-off strategy to use for any newly created job
224 //! \note This back-off strategy does not apply to SyncJob; it has a separate default but you
225 //! can still override it per job instance after creating it
227
228 using duration_ms_t = std::chrono::milliseconds::rep; // normally int64_t
229
236
237 friend QDebug operator<<(QDebug dbg, const BaseJob* j)
238 {
239 return dbg << j->objectName();
240 }
241
242public Q_SLOTS:
244
245 //! \brief Abandon the result of this job, arrived or unarrived.
246 //!
247 //! This aborts waiting for a reply from the server (if there was
248 //! any pending) and deletes the job object. No result signals
249 //! (result, success, failure) are emitted, only finished() is.
250 void abandon();
251
253 //! \brief The job is about to send a network request
254 //!
255 //! This signal is emitted every time a network request is made (which can
256 //! occur several times due to job retries). You can use it to change
257 //! the request parameters (such as redirect policy) if necessary. If you
258 //! need to set additional request headers or query items, do that using
259 //! setRequestHeaders() and setRequestQuery() instead.
260 //! \note \p req is not guaranteed to exist (i.e. it may point to garbage)
261 //! unless this signal is handled via a DirectConnection (or
262 //! BlockingQueuedConnection if in another thread), i.e.,
263 //! synchronously.
264 //! \sa setRequestHeaders, setRequestQuery
266
267 //! The job has sent a network request
269
270 //! The job has changed its status
272
273 //! \brief A retry of the network request is scheduled after the previous request failed
274 //! \param nextRetryNumber the number of the next retry, starting from 1
275 //! \param inMilliseconds the interval after which the next attempt will be taken
277
278 //! \brief The job has been rate-limited
279 //!
280 //! The previous network request has been rate-limited; the next attempt will be queued and run
281 //! sometime later. Since other jobs may already wait in the queue, it's not possible to predict
282 //! the wait time.
284
285 //! \brief The job has finished - either with a result, or abandoned
286 //!
287 //! Emitted when the job is finished, in any case. It is used to notify
288 //! observers that the job is terminated and that progress can be hidden.
289 //!
290 //! This should not be emitted directly by subclasses; use finishJob() instead.
291 //!
292 //! In general, to be notified of a job's completion, client code should connect to result(),
293 //! success(), or failure() rather than finished(). However if you need to track the job's
294 //! lifecycle you should connect to this instead of result(); in particular, only this signal
295 //! will be emitted on abandoning, the others won't.
296 //!
297 //! \param job the job that emitted this signal
298 //!
299 //! \sa result, success, failure
301
302 //! \brief The job has finished with a result, successful or unsuccessful
303 //!
304 //! Use error() or status().good() to know if the job has finished successfully.
305 //!
306 //! \param job the job that emitted this signal
307 //!
308 //! \sa success, failure
310
311 //! \brief The job has finished with a successful result
312 //! \sa result, failure
314
315 //! \brief The job has finished with a failure result
316 //! Emitted together with result() when the job resulted in an error. Mutually exclusive with
317 //! success(): after result() is emitted, exactly one of success() and failure() will be emitted
318 //! next. Will not be emitted in case of abandon()ing.
319 //!
320 //! \sa result, success
322
325
326protected:
328
331 const headers_t& requestHeaders() const;
337 const RequestData& requestData() const;
345
346 const QNetworkReply* reply() const;
348
349 //! \brief Construct a URL out of baseUrl, path and query
350 //!
351 //! The function ensures exactly one '/' between the path component of
352 //! \p baseUrl and \p path. The query component of \p baseUrl is ignored.
353 //! \note Unlike most of BaseJob, this function is thread-safe
355 const QUrlQuery& query = {});
356
357 //! \brief Prepare the job for execution
358 //!
359 //! This method is called no more than once per job lifecycle, when it's first scheduled
360 //! for execution; in particular, it is not called on retries.
361 virtual void doPrepare(const ConnectionData*);
362
363 //! \brief Postprocessing after the network request has been sent
364 //!
365 //! This method is called every time the job receives a running
366 //! QNetworkReply object from NetworkAccessManager - basically, after
367 //! successfully sending a network request (including retries).
369
370 virtual void beforeAbandon();
371
372 //! \brief Check the pending or received reply for upfront issues
373 //!
374 //! This is invoked when headers are first received and also once the complete reply is
375 //! obtained; the base implementation checks the HTTP headers to detect general issues such as
376 //! network errors or access denial and it's strongly recommended to call it from overrides, as
377 //! early as possible.
378 //!
379 //! This slot is const and cannot read the response body from the reply. If you need to read the
380 //! body on the fly, override onSentRequest() and connect in it to reply->readyRead(); and if
381 //! you only need to validate the body after it fully arrived, use prepareResult() for that.
382 //! Returning anything except NoError/Success switches further processing from prepareResult()
383 //! to prepareError().
384 //!
385 //! \return the result of checking the reply
386 //!
387 //! \sa gotReply
388 virtual Status checkReply(const QNetworkReply* reply) const;
389
390 //! \brief An extension point for additional reply processing
391 //!
392 //! The base implementation simply returns Success without doing anything else.
393 //!
394 //! \sa gotReply
396
397 //! \brief Process details of the error
398 //!
399 //! The function processes the reply in case when status from checkReply() was not good (usually
400 //! because of an unsuccessful HTTP code). The base implementation assumes Matrix JSON error
401 //! object in the body; overrides are strongly recommended to call it for all stock Matrix
402 //! responses as early as possible and only then process custom errors, with JSON or non-JSON
403 //! payload.
404 //!
405 //! \return updated (if necessary) job status
407
408 //! \brief Retrieve a value for one specific key and delete it from the JSON response object
409 //!
410 //! This allows to implement deserialisation with "move" semantics for parts
411 //! of the response. Assuming that the response body is a valid JSON object,
412 //! the function calls QJsonObject::take(key) on it and returns the result.
413 //!
414 //! \return QJsonValue::Undefined if the response content is not a JSON object or it doesn't
415 //! have \p key; the value for \p key otherwise.
416 //!
417 //! \sa takeFromJson
419
422
423 //! \brief Force completion of the job for sake of testing
424 //!
425 //! Normal jobs should never use; this is only meant to be used in test mocks.
426 //! \sa Mocked
428
429 //! \brief Set the logging category for the given job instance
430 //!
431 //! \param lcf The logging category function to provide the category -
432 //! the one you define with Q_LOGGING_CATEGORY (without
433 //! parentheses, BaseJob will call it for you)
435
436 // Job objects should only be deleted via QObject::deleteLater
438
439protected Q_SLOTS:
440 void timeout();
441
442private Q_SLOTS:
443 void sendRequest();
444 void gotReply();
445
446private:
447 friend class ConnectionData; // to provide access to sendRequest()
448 template <class JobT>
449 friend class JobHandle;
450
451 void stop();
452 void finishJob();
453 QFuture<void> future();
454
455 class Private;
457};
458
460{
461 return job && job->error() == BaseJob::Pending;
462}
463
464template <typename JobT>
465constexpr inline auto doCollectResponse = nullptr;
466
467//! \brief Get a job response in a single structure
468//!
469//! Use this to get all parts of the job response in a single C++ type, defined by the job class.
470//! It can be either an aggregate of individual response parts returned by job accessors, or, if
471//! the response is already singular, a type of this response. The default implementation caters to
472//! generated jobs, where the code has to be generic enough to work with copyable and movable-only
473//! responses. For manually written code, simply overload collectResponse() for the respective
474//! job type, with appropriate constness.
475template <std::derived_from<BaseJob> JobT>
477 requires requires { doCollectResponse<JobT>(job); }
478{
479 return doCollectResponse<JobT>(job);
480}
481
482template <std::derived_from<BaseJob> JobT>
483class Mocked : public JobT {
484public:
485 using JobT::JobT;
486 void setResult(QJsonDocument d) { JobT::forceResult(std::move(d)); }
487};
488
489} // namespace Quotient
void setResult(QJsonDocument d)
Definition basejob.h:486
auto collectResponse(GetOneRoomEventJob *job)
Definition rooms.h:37
constexpr auto doCollectResponse
Definition basejob.h:465
#define QUOTIENT_API
QVector< duration_t > nextRetryIntervals
Definition basejob.h:29
QVector< duration_t > jobTimeouts
Definition basejob.h:28
std::optional< decltype(jobTimeouts)::size_type > maxRetries
How many times a network request should be tried; std::nullopt means keep trying forever.
Definition basejob.h:31