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