diff options
author | Juha Vuolle <juha.vuolle@qt.io> | 2023-06-12 11:23:19 +0300 |
---|---|---|
committer | Juha Vuolle <juha.vuolle@qt.io> | 2023-12-08 15:53:33 +0200 |
commit | e560adef213301318dcc13d4db155624846e0420 (patch) | |
tree | 237ffa17c837ee0f270885641b781d9bb47c6fd6 | |
parent | f587ba1036164691a0981897397bdcc8f3472438 (diff) |
Add REST client convenience wrappers
[ChangeLog][QtNetwork][QRestAccessManager] Added new convenience
classes QRestAccessManager and QRestReply for typical RESTful
client application usage
Task-number: QTBUG-114637
Task-number: QTBUG-114701
Change-Id: I65057e56bf27f365b54bfd528565efd5f09386aa
Reviewed-by: MÃ¥rten Nordheim <marten.nordheim@qt.io>
Reviewed-by: Mate Barany <mate.barany@qt.io>
Reviewed-by: Marc Mutz <marc.mutz@qt.io>
-rw-r--r-- | src/network/CMakeLists.txt | 2 | ||||
-rw-r--r-- | src/network/access/qrestaccessmanager.cpp | 819 | ||||
-rw-r--r-- | src/network/access/qrestaccessmanager.h | 110 | ||||
-rw-r--r-- | src/network/access/qrestaccessmanager_p.h | 87 | ||||
-rw-r--r-- | src/network/access/qrestreply.cpp | 364 | ||||
-rw-r--r-- | src/network/access/qrestreply.h | 55 | ||||
-rw-r--r-- | src/network/access/qrestreply_p.h | 39 | ||||
-rw-r--r-- | src/network/doc/snippets/code/src_network_access_qrestaccessmanager.cpp | 84 | ||||
-rw-r--r-- | tests/auto/network/access/CMakeLists.txt | 1 | ||||
-rw-r--r-- | tests/auto/network/access/qrestaccessmanager/CMakeLists.txt | 11 | ||||
-rw-r--r-- | tests/auto/network/access/qrestaccessmanager/httptestserver.cpp | 243 | ||||
-rw-r--r-- | tests/auto/network/access/qrestaccessmanager/httptestserver_p.h | 78 | ||||
-rw-r--r-- | tests/auto/network/access/qrestaccessmanager/tst_qrestaccessmanager.cpp | 794 |
13 files changed, 2687 insertions, 0 deletions
diff --git a/src/network/CMakeLists.txt b/src/network/CMakeLists.txt index d1ac08251a..4ef2699ee8 100644 --- a/src/network/CMakeLists.txt +++ b/src/network/CMakeLists.txt @@ -135,6 +135,8 @@ qt_internal_extend_target(Network CONDITION QT_FEATURE_http access/qnetworkreplyhttpimpl.cpp access/qnetworkreplyhttpimpl_p.h access/qnetworkrequestfactory.cpp access/qnetworkrequestfactory_p.h access/qnetworkrequestfactory.h + access/qrestaccessmanager.cpp access/qrestaccessmanager.h access/qrestaccessmanager_p.h + access/qrestreply.cpp access/qrestreply.h access/qrestreply_p.h socket/qhttpsocketengine.cpp socket/qhttpsocketengine_p.h ) diff --git a/src/network/access/qrestaccessmanager.cpp b/src/network/access/qrestaccessmanager.cpp new file mode 100644 index 0000000000..c4efa846a2 --- /dev/null +++ b/src/network/access/qrestaccessmanager.cpp @@ -0,0 +1,819 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "qrestaccessmanager.h" +#include "qrestaccessmanager_p.h" +#include "qrestreply.h" + +#include <QtNetwork/qhttpmultipart.h> +#include <QtNetwork/qnetworkaccessmanager.h> +#include <QtNetwork/qnetworkreply.h> + +#if QT_CONFIG(ssl) +#include <QtNetwork/qsslsocket.h> +#endif + +#include <QtCore/qjsondocument.h> +#include <QtCore/qjsonobject.h> +#include <QtCore/qloggingcategory.h> +#include <QtCore/qthread.h> + +QT_BEGIN_NAMESPACE + +using namespace Qt::StringLiterals; + +Q_LOGGING_CATEGORY(lcQrest, "qt.network.access.rest") + +/*! + + \class QRestAccessManager + \brief The QRestAccessManager is a networking convenience class for RESTful + client applications. + \since 6.7 + + \ingroup network + \inmodule QtNetwork + \reentrant + + QRestAccessManager provides a networking API for typical REST client + applications. It provides the means to issue HTTP requests such as GET + and POST. The responses to these requests can be handled with traditional + Qt signal and slot mechanisms, as well as by providing callbacks + directly - see \l {Issuing Network Requests and Handling Replies}. + + The class is a wrapper on top of QNetworkAccessManager, and it both amends + convenience methods and omits typically less used features. These + features are still accessible by configuring the underlying + QNetworkAccessManager directly. QRestAccessManager is closely related to + the QRestReply class, which it returns when issuing network requests. + + QRestAccessManager and related QRestReply classes can only be used in the + thread they live in. For further information see + \l {QObject#Thread Affinity}{QObject thread affinity} documentation. + + \section1 Issuing Network Requests and Handling Replies + + Network requests are initiated with a function call corresponding to + the desired HTTP method, such as \c get() and \c post(). + + \section2 Using Signals and Slots + + The function returns a QRestReply* object, whose signals can be used + to follow up on the completion of the request in a traditional + Qt-signals-and-slots way. + + Here's an example of how you could send a GET request and handle the + response: + + \snippet code/src_network_access_qrestaccessmanager.cpp 0 + + \section2 Using Callbacks and Context Objects + + The functions also take a context object of QObject (subclass) type + and a callback function as parameters. The callback takes one QRestReply* + as a parameter. The callback can be any callable, incl. a + pointer-to-member-function.. + + These callbacks are invoked when the QRestReply has finished processing + (also in the case the processing finished due to an error). + + The context object can be \c nullptr, although, generally speaking, + this is discouraged. Using a valid context object ensures that if the + context object is destroyed during request processing, the callback will + not be called. Stray callbacks which access a destroyed context is a source + of application misbehavior. + + Here's an example of how you could send a GET request and check the + response: + + \snippet code/src_network_access_qrestaccessmanager.cpp 1 + + Many of the functions take in data for sending to a server. The data is + supplied as the second parameter after the request. + + Here's an example of how you could send a POST request and check the + response: + + \snippet code/src_network_access_qrestaccessmanager.cpp 2 + + \section2 Supported data types + + The following table summarizes the methods and the supported data types. + \c X means support. + + \table + \header + \li Data type + \li \c get() + \li \c post() + \li \c put() + \li \c head() + \li \c deleteResource() + \row + \li No data + \li X + \li - + \li - + \li X + \li X + \row + \li QByteArray + \li X + \li X + \li X + \li - + \li - + \row + \li QJsonObject *) + \li X + \li X + \li X + \li - + \li - + \row + \li QJsonArray *) + \li - + \li X + \li X + \li - + \li - + \row + \li QVariantMap **) + \li - + \li X + \li X + \li - + \li - + \row + \li QHttpMultiPart + \li - + \li X + \li X + \li - + \li - + \row + \li QIODevice + \li X + \li X + \li X + \li - + \li - + \endtable + + *) QJsonObject and QJsonArray are sent in \l QJsonDocument::Compact format, + and the \c Content-Type header is set to \c {application/json} if the + \c Content-Type header was not set + + **) QVariantMap is converted to and treated as a QJsonObject + + \sa QRestReply, QNetworkRequestFactory, QNetworkAccessManager +*/ + +/*! + \fn void QRestAccessManager::authenticationRequired(QRestReply *reply, + QAuthenticator *authenticator) + + This signal is emitted when the final server requires authentication. + The authentication relates to the provided \a reply instance, and any + credentials are to be filled in the provided \a authenticator instance. + + See \l QNetworkAccessManager::authenticationRequired() for details. +*/ + +/*! + \fn void QRestAccessManager::proxyAuthenticationRequired( + const QNetworkProxy &proxy, QAuthenticator *authenticator) + + This signal is emitted when a proxy authentication requires action. + The proxy details are in \a proxy object, and any credentials are + to be filled in the provided \a authenticator object. + + See \l QNetworkAccessManager::proxyAuthenticationRequired() for details. +*/ + +/*! + \fn void QRestAccessManager::requestFinished(QRestReply *reply) + + This signal is emitted whenever a pending network reply is + finished. \a reply parameter will contain a pointer to the + reply that has just finished. This signal is emitted in tandem + with the QRestReply::finished() signal. QRestReply provides + functions for checking the status of the request, as well as for + acquiring any received data. + + \note Do not delete \a reply object in the slot connected to this + signal. Use deleteLater() if needed. See also \l deletesRepliesOnFinished(). + + \sa QRestReply::finished() +*/ + +/*! + \fn template<typename Functor, if_compatible_callback<Functor>> QRestReply *QRestAccessManager::get( + const QNetworkRequest &request, + const ContextTypeForFunctor<Functor> *context, + Functor &&callback) + + Issues an \c {HTTP GET} based on \a request. + + The optional \a callback and \a context object can be provided for + handling the request completion as illustrated below: + + \snippet code/src_network_access_qrestaccessmanager.cpp 3 + + Alternatively the signals of the returned QRestReply* object can be + used. For further information see + \l {Issuing Network Requests and Handling Replies}. + + \sa QRestReply, QRestReply::finished(), + QRestAccessManager::requestFinished() +*/ + +/*! + \fn template<typename Functor, if_compatible_callback<Functor>> QRestReply *QRestAccessManager::get( + const QNetworkRequest &request, const QByteArray &data, + const ContextTypeForFunctor<Functor> *context, + Functor &&callback) + + Issues an \c {HTTP GET} based on \a request and provided \a data. + + The optional \a callback and \a context object can be provided for + handling the request completion as illustrated below: + + \snippet code/src_network_access_qrestaccessmanager.cpp 4 + + Alternatively the signals of the returned QRestReply* object can be + used. For further information see + \l {Issuing Network Requests and Handling Replies}. + + \sa QRestReply, QRestReply::finished(), + QRestAccessManager::requestFinished() +*/ + +/*! + \fn template<typename Functor, if_compatible_callback<Functor>> QRestReply *QRestAccessManager::get( + const QNetworkRequest &request, const QJsonObject &data, + const ContextTypeForFunctor<Functor> *context, + Functor &&callback) + + \overload +*/ + +/*! + \fn template<typename Functor, if_compatible_callback<Functor>> QRestReply *QRestAccessManager::get( + const QNetworkRequest &request, QIODevice *data, + const ContextTypeForFunctor<Functor> *context, + Functor &&callback) + + \overload +*/ + +/*! + \fn template<typename Functor, if_compatible_callback<Functor>> QRestReply *QRestAccessManager::post( + const QNetworkRequest &request, const QJsonObject &data, + const ContextTypeForFunctor<Functor> *context, + Functor &&callback) + + Issues an \c {HTTP POST} based on \a request. + + The optional \a callback and \a context object can be provided for + handling the request completion as illustrated below: + + \snippet code/src_network_access_qrestaccessmanager.cpp 5 + + Alternatively, the signals of the returned QRestReply* object can be + used. For further information see + \l {Issuing Network Requests and Handling Replies}. + + The \c post() method always requires \a data parameter. The following + data types are supported: + \list + \li QByteArray + \li QJsonObject *) + \li QJsonArray *) + \li QVariantMap **) + \li QHttpMultiPart* + \li QIODevice* + \endlist + + *) Sent in \l QJsonDocument::Compact format, and the + \c Content-Type header is set to \c {application/json} if the + \c Content-Type header was not set + **) QVariantMap is converted to and treated as a QJsonObject + + \sa QRestReply, QRestReply::finished(), + QRestAccessManager::requestFinished() +*/ + +/*! + \fn template<typename Functor, if_compatible_callback<Functor>> QRestReply *QRestAccessManager::post( + const QNetworkRequest &request, const QJsonArray &data, + const ContextTypeForFunctor<Functor> *context, + Functor &&callback) + + \overload +*/ + +/*! + \fn template<typename Functor, if_compatible_callback<Functor>> QRestReply *QRestAccessManager::post( + const QNetworkRequest &request, const QVariantMap &data, + const ContextTypeForFunctor<Functor> *context, + Functor &&callback) + + \overload +*/ + +/*! + \fn template<typename Functor, if_compatible_callback<Functor>> QRestReply *QRestAccessManager::post( + const QNetworkRequest &request, const QByteArray &data, + const ContextTypeForFunctor<Functor> *context, + Functor &&callback) + + \overload +*/ + +/*! + \fn template<typename Functor, if_compatible_callback<Functor>> QRestReply *QRestAccessManager::post( + const QNetworkRequest &request, QHttpMultiPart *data, + const ContextTypeForFunctor<Functor> *context, + Functor &&callback) + + \overload +*/ + +/*! + \fn template<typename Functor, if_compatible_callback<Functor>> QRestReply *QRestAccessManager::post( + const QNetworkRequest &request, QIODevice *data, + const ContextTypeForFunctor<Functor> *context, + Functor &&callback) + + \overload +*/ + +/*! + \fn template<typename Functor, if_compatible_callback<Functor>> QRestReply *QRestAccessManager::put( + const QNetworkRequest &request, const QJsonObject &data, + const ContextTypeForFunctor<Functor> *context, + Functor &&callback) + + Issues an \c {HTTP PUT} based on \a request. + + The optional \a callback and \a context object can be provided for + handling the request completion as illustrated below: + + \snippet code/src_network_access_qrestaccessmanager.cpp 6 + + Alternatively the signals of the returned QRestReply* object can be + used. For further information see + \l {Issuing Network Requests and Handling Replies}. + + The \c put() method always requires \a data parameter. The following + data types are supported: + \list + \li QByteArray + \li QJsonObject *) + \li QJsonArray *) + \li QVariantMap **) + \li QHttpMultiPart* + \li QIODevice* + \endlist + + *) Sent in \l QJsonDocument::Compact format, and the + \c Content-Type header is set to \c {application/json} if the + \c Content-Type header was not set + **) QVariantMap is converted to and treated as a QJsonObject + + \sa QRestReply, QRestReply::finished(), QRestAccessManager::requestFinished() +*/ + +/*! + \fn template<typename Functor, if_compatible_callback<Functor>> QRestReply *QRestAccessManager::put( + const QNetworkRequest &request, const QJsonArray &data, + const ContextTypeForFunctor<Functor> *context, + Functor &&callback) + + \overload +*/ + +/*! + \fn template<typename Functor, if_compatible_callback<Functor>> QRestReply *QRestAccessManager::put( + const QNetworkRequest &request, const QVariantMap &data, + const ContextTypeForFunctor<Functor> *context, + Functor &&callback) + + \overload +*/ + +/*! + \fn template<typename Functor, if_compatible_callback<Functor>> QRestReply *QRestAccessManager::put( + const QNetworkRequest &request, const QByteArray &data, + const ContextTypeForFunctor<Functor> *context, + Functor &&callback) + + \overload +*/ + +/*! + \fn template<typename Functor, if_compatible_callback<Functor>> QRestReply *QRestAccessManager::put( + const QNetworkRequest &request, QHttpMultiPart *data, + const ContextTypeForFunctor<Functor> *context, + Functor &&callback) + + \overload +*/ + +/*! + \fn template<typename Functor, if_compatible_callback<Functor>> QRestReply *QRestAccessManager::put( + const QNetworkRequest &request, QIODevice *data, + const ContextTypeForFunctor<Functor> *context, + Functor &&callback) + + \overload +*/ + +/*! + \fn template<typename Functor, if_compatible_callback<Functor>> QRestReply *QRestAccessManager::head( + const QNetworkRequest &request, + const ContextTypeForFunctor<Functor> *context, + Functor &&callback) + + Issues an \c {HTTP HEAD} based on \a request. + + The optional \a callback and \a context object can be provided for + handling the request completion as illustrated below: + + \snippet code/src_network_access_qrestaccessmanager.cpp 7 + + Alternatively the signals of the returned QRestReply* object can be + used. For further information see + \l {Issuing Network Requests and Handling Replies}. + + \c head() request does not support providing data. + + \sa QRestReply, QRestReply::finished(), + QRestAccessManager::requestFinished() +*/ + +/*! + \fn template<typename Functor, if_compatible_callback<Functor>> QRestReply *QRestAccessManager::deleteResource( + const QNetworkRequest &request, + const ContextTypeForFunctor<Functor> *context, + Functor &&callback) + + Issues an \c {HTTP DELETE} based on \a request. + + The optional \a callback and \a context object can be provided for + handling the request completion as illustrated below: + + \snippet code/src_network_access_qrestaccessmanager.cpp 8 + + Alternatively the signals of the returned QRestReply* object can be + used. For further information see + \l {Issuing Network Requests and Handling Replies}. + + \c deleteResource() request does not support providing data. + + \sa QRestReply, QRestReply::finished(), + QRestAccessManager::requestFinished() +*/ + +/* + Memory management/object ownership: + - QRestAM is parent of QNAM and QRestReplies + - QRestReplies are parents of QNetworkReplies +*/ + +/*! + Constructs a QRestAccessManager and sets \a parent as the parent object. +*/ +QRestAccessManager::QRestAccessManager(QObject *parent) + : QObject(*new QRestAccessManagerPrivate, parent) +{ + Q_D(QRestAccessManager); + d->ensureNetworkAccessManager(); +} + +/*! + Destroys the QRestAccessManager object and frees up any + resources, including any unfinished QRestReply objects. +*/ +QRestAccessManager::~QRestAccessManager() + = default; + +/*! + Returns whether QRestAccessManager is currently configured to automatically + delete replies once they have finished. By default this is \c true. + + \sa setDeletesRepliesOnFinished() +*/ +bool QRestAccessManager::deletesRepliesOnFinished() const +{ + Q_D(const QRestAccessManager); + return d->deletesRepliesOnFinished; +} + +/*! + Enables or disables automatic deletion of QRestReply instances + once the request has finished, according to the provided + \a autoDelete parameter. The deletion is done with deleteLater() + so that using the replies in directly-connected slots or callbacks is safe. + + \sa deletesRepliesOnFinished() +*/ +void QRestAccessManager::setDeletesRepliesOnFinished(bool autoDelete) +{ + Q_D(QRestAccessManager); + d->deletesRepliesOnFinished = autoDelete; +} + +/*! + Aborts all unfinished network requests. Calling this function is same + as calling QRestReply::abort() for all individual unfinished requests. + + \sa QRestReply::abort(), QNetworkReply::abort() +*/ +void QRestAccessManager::abortRequests() +{ + Q_D(QRestAccessManager); + + // Make copy of the reply container, as it might get modified when + // aborting individual requests if they finish immediately + const auto requests = d->activeRequests; + for (const auto &[req, _] : requests.asKeyValueRange()) + req->abort(); +} + +/*! + Returns the underlying QNetworkAccessManager instance. The instance + can be used for accessing less-frequently used features and configurations. + + \sa QNetworkAccessManager +*/ +QNetworkAccessManager *QRestAccessManager::networkAccessManager() const +{ + Q_D(const QRestAccessManager); + return d->qnam; +} + +QRestAccessManagerPrivate::QRestAccessManagerPrivate() + = default; + +QRestAccessManagerPrivate::~QRestAccessManagerPrivate() +{ + if (!activeRequests.isEmpty()) { + qCWarning(lcQrest, "Access manager destroyed while %lld requests were still in progress", + qlonglong(activeRequests.size())); + } +} + +QRestReply *QRestAccessManager::postWithDataImpl(const QNetworkRequest &request, + const QJsonObject &data, const QObject *context, + QtPrivate::QSlotObjectBase *slot) +{ + Q_D(QRestAccessManager); + return d->executeRequest([&](auto req, auto json) { return d->qnam->post(req, json); }, + data, request, context, slot); +} + +QRestReply *QRestAccessManager::postWithDataImpl(const QNetworkRequest &request, + const QJsonArray &data, const QObject *context, + QtPrivate::QSlotObjectBase *slot) +{ + Q_D(QRestAccessManager); + return d->executeRequest([&](auto req, auto json) { return d->qnam->post(req, json); }, + data, request, context, slot); +} + +QRestReply *QRestAccessManager::postWithDataImpl(const QNetworkRequest &request, + const QVariantMap &data, const QObject *context, + QtPrivate::QSlotObjectBase *slot) +{ + return postWithDataImpl(request, QJsonObject::fromVariantMap(data), context, slot); +} + +QRestReply *QRestAccessManager::postWithDataImpl(const QNetworkRequest &request, + const QByteArray &data, const QObject *context, + QtPrivate::QSlotObjectBase *slot) +{ + Q_D(QRestAccessManager); + return d->executeRequest([&]() { return d->qnam->post(request, data); }, context, slot); +} + +QRestReply *QRestAccessManager::postWithDataImpl(const QNetworkRequest &request, + QHttpMultiPart *data, const QObject *context, + QtPrivate::QSlotObjectBase *slot) +{ + Q_D(QRestAccessManager); + return d->executeRequest([&]() { return d->qnam->post(request, data); }, context, slot); +} + +QRestReply *QRestAccessManager::postWithDataImpl(const QNetworkRequest &request, + QIODevice *data, const QObject *context, + QtPrivate::QSlotObjectBase *slot) +{ + Q_D(QRestAccessManager); + return d->executeRequest([&]() { return d->qnam->post(request, data); }, context, slot); +} + +QRestReply *QRestAccessManager::getNoDataImpl(const QNetworkRequest &request, + const QObject *context, QtPrivate::QSlotObjectBase *slot) +{ + Q_D(QRestAccessManager); + return d->executeRequest([&]() { return d->qnam->get(request); }, context, slot); +} + +QRestReply *QRestAccessManager::getWithDataImpl(const QNetworkRequest &request, + const QByteArray &data, const QObject *context, + QtPrivate::QSlotObjectBase *slot) +{ + Q_D(QRestAccessManager); + return d->executeRequest([&]() { return d->qnam->get(request, data); }, context, slot); +} + +QRestReply *QRestAccessManager::getWithDataImpl(const QNetworkRequest &request, + const QJsonObject &data, const QObject *context, + QtPrivate::QSlotObjectBase *slot) +{ + Q_D(QRestAccessManager); + return d->executeRequest([&](auto req, auto json) { return d->qnam->get(req, json); }, + data, request, context, slot); +} + +QRestReply *QRestAccessManager::getWithDataImpl(const QNetworkRequest &request, + QIODevice *data, const QObject *context, + QtPrivate::QSlotObjectBase *slot) +{ + Q_D(QRestAccessManager); + return d->executeRequest([&]() { return d->qnam->get(request, data); }, context, slot); +} + +QRestReply *QRestAccessManager::deleteResourceNoDataImpl(const QNetworkRequest &request, + const QObject *context, QtPrivate::QSlotObjectBase *slot) +{ + Q_D(QRestAccessManager); + return d->executeRequest([&]() { return d->qnam->deleteResource(request); }, context, slot); +} + +QRestReply *QRestAccessManager::headNoDataImpl(const QNetworkRequest &request, + const QObject *context, QtPrivate::QSlotObjectBase *slot) +{ + Q_D(QRestAccessManager); + return d->executeRequest([&]() { return d->qnam->head(request); }, context, slot); +} + +QRestReply *QRestAccessManager::putWithDataImpl(const QNetworkRequest &request, + const QJsonObject &data, const QObject *context, + QtPrivate::QSlotObjectBase *slot) +{ + Q_D(QRestAccessManager); + return d->executeRequest([&](auto req, auto json) { return d->qnam->put(req, json); }, + data, request, context, slot); +} + +QRestReply *QRestAccessManager::putWithDataImpl(const QNetworkRequest &request, + const QJsonArray &data, const QObject *context, + QtPrivate::QSlotObjectBase *slot) +{ + Q_D(QRestAccessManager); + return d->executeRequest([&](auto req, auto json) { return d->qnam->put(req, json); }, + data, request, context, slot); +} + +QRestReply *QRestAccessManager::putWithDataImpl(const QNetworkRequest &request, + const QVariantMap &data, const QObject *context, + QtPrivate::QSlotObjectBase *slot) +{ + return putWithDataImpl(request, QJsonObject::fromVariantMap(data), context, slot); +} + +QRestReply *QRestAccessManager::putWithDataImpl(const QNetworkRequest &request, + const QByteArray &data, const QObject *context, + QtPrivate::QSlotObjectBase *slot) +{ + Q_D(QRestAccessManager); + return d->executeRequest([&]() { return d->qnam->put(request, data); }, context, slot); +} + +QRestReply *QRestAccessManager::putWithDataImpl(const QNetworkRequest &request, + QHttpMultiPart *data, const QObject *context, + QtPrivate::QSlotObjectBase *slot) +{ + Q_D(QRestAccessManager); + return d->executeRequest([&]() { return d->qnam->put(request, data); }, context, slot); +} + +QRestReply *QRestAccessManager::putWithDataImpl(const QNetworkRequest &request, QIODevice *data, + const QObject *context, QtPrivate::QSlotObjectBase *slot) +{ + Q_D(QRestAccessManager); + return d->executeRequest([&]() { return d->qnam->put(request, data); }, context, slot); +} + +QRestReply *QRestAccessManagerPrivate::createActiveRequest(QNetworkReply *networkReply, + const QObject *contextObject, + QtPrivate::QSlotObjectBase *slot) +{ + Q_Q(QRestAccessManager); + Q_ASSERT(networkReply); + auto restReply = new QRestReply(networkReply, q); + QtPrivate::SlotObjSharedPtr slotPtr(QtPrivate::SlotObjUniquePtr{slot}); // adopts + activeRequests.insert(restReply, CallerInfo{contextObject, slotPtr}); + + // If context object is provided, use it with connect => context object lifecycle is considered + const QObject *context = contextObject ? contextObject : q; + QObject::connect(networkReply, &QNetworkReply::finished, context, [restReply, this]() { + handleReplyFinished(restReply); + }); + // Safe guard in case reply is destroyed before it's finished + QObject::connect(restReply, &QRestReply::destroyed, q, [restReply, this]() { + activeRequests.remove(restReply); + }); + // If context object is destroyed, clean up any possible replies it had associated with it + if (contextObject) { + QObject::connect(contextObject, &QObject::destroyed, q, [restReply, this]() { + activeRequests.remove(restReply); + }); + } + return restReply; +} + +void QRestAccessManagerPrivate::verifyThreadAffinity(const QObject *contextObject) +{ + Q_Q(QRestAccessManager); + if (QThread::currentThread() != q->thread()) { + qCWarning(lcQrest, "QRestAccessManager can only be called in the thread it belongs to"); + Q_ASSERT(false); + } + if (contextObject && (contextObject->thread() != q->thread())) { + qCWarning(lcQrest, "QRestAccessManager: the context object must reside in the same thread"); + Q_ASSERT(false); + } +} + +void QRestAccessManagerPrivate::ensureNetworkAccessManager() +{ + Q_Q(QRestAccessManager); + if (!qnam) { + qnam = new QNetworkAccessManager(q); + connect(qnam, &QNetworkAccessManager::authenticationRequired, this, + &QRestAccessManagerPrivate::handleAuthenticationRequired); +#ifndef QT_NO_NETWORKPROXY + QObject::connect(qnam, &QNetworkAccessManager::proxyAuthenticationRequired, + q, &QRestAccessManager::proxyAuthenticationRequired); +#endif + } +} + +void QRestAccessManagerPrivate::handleReplyFinished(QRestReply *restReply) +{ + Q_Q(QRestAccessManager); + + auto request = activeRequests.find(restReply); + if (request == activeRequests.end()) { + qCWarning(lcQrest, "Unexpected reply received, ignoring"); + return; + } + + CallerInfo caller = request.value(); + activeRequests.erase(request); + + if (caller.slot) { + // Callback was provided. If we have context object, use it. + // For clarity: being here with a context object means it has not been destroyed + // while the request has been in progress + void *argv[] = { nullptr, &restReply }; + QObject *context = caller.contextObject + ? const_cast<QObject*>(caller.contextObject) : nullptr; + caller.slot->call(context, argv); + } + if (restReply->hasError()) + emit restReply->errorOccurred(restReply); + emit restReply->finished(restReply); + emit q->requestFinished(restReply); + + if (deletesRepliesOnFinished) + restReply->deleteLater(); +} + +void QRestAccessManagerPrivate::handleAuthenticationRequired(QNetworkReply *networkReply, + QAuthenticator *authenticator) +{ + Q_Q(QRestAccessManager); + QRestReply *restReply = restReplyFromNetworkReply(networkReply); + if (restReply) + emit q->authenticationRequired(restReply, authenticator); + else + qCWarning(lcQrest, "No matching QRestReply for authentication, ignoring."); +} + +QRestReply *QRestAccessManagerPrivate::restReplyFromNetworkReply(QNetworkReply *networkReply) +{ + for (const auto &[restReply,_] : activeRequests.asKeyValueRange()) { + if (restReply->networkReply() == networkReply) + return restReply; + } + return nullptr; +} + +QT_END_NAMESPACE + +#include "moc_qrestaccessmanager.cpp" diff --git a/src/network/access/qrestaccessmanager.h b/src/network/access/qrestaccessmanager.h new file mode 100644 index 0000000000..7b67486a2e --- /dev/null +++ b/src/network/access/qrestaccessmanager.h @@ -0,0 +1,110 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef QRESTACCESSMANAGER_H +#define QRESTACCESSMANAGER_H + +#include <QtNetwork/qnetworkaccessmanager.h> + +QT_BEGIN_NAMESPACE + +class QRestReply; + +#define QREST_METHOD_WITH_DATA(METHOD, DATA) \ +public: \ +template <typename Functor, if_compatible_callback<Functor> = true> \ +QRestReply *METHOD(const QNetworkRequest &request, DATA data, \ + const ContextTypeForFunctor<Functor> *context, \ + Functor &&callback) \ +{ \ + return METHOD##WithDataImpl(request, data, context, \ + QtPrivate::makeCallableObject<CallbackPrototype>(std::forward<Functor>(callback))); \ +} \ +QRestReply *METHOD(const QNetworkRequest &request, DATA data) \ +{ \ + return METHOD##WithDataImpl(request, data, nullptr, nullptr); \ +} \ +private: \ +QRestReply *METHOD##WithDataImpl(const QNetworkRequest &request, DATA data, \ + const QObject *context, QtPrivate::QSlotObjectBase *slot); \ +/* end */ + + +#define QREST_METHOD_NO_DATA(METHOD) \ +public: \ +template <typename Functor, if_compatible_callback<Functor> = true> \ +QRestReply *METHOD(const QNetworkRequest &request, \ + const ContextTypeForFunctor<Functor> *context, \ + Functor &&callback) \ +{ \ + return METHOD##NoDataImpl(request, context, \ + QtPrivate::makeCallableObject<CallbackPrototype>(std::forward<Functor>(callback))); \ +} \ +QRestReply *METHOD(const QNetworkRequest &request) \ +{ \ + return METHOD##NoDataImpl(request, nullptr, nullptr); \ +} \ +private: \ +QRestReply *METHOD##NoDataImpl(const QNetworkRequest &request, \ + const QObject *context, QtPrivate::QSlotObjectBase *slot); \ +/* end */ + +class QRestAccessManagerPrivate; +class Q_NETWORK_EXPORT QRestAccessManager : public QObject +{ + Q_OBJECT + + using CallbackPrototype = void(*)(QRestReply*); + template <typename Functor> + using ContextTypeForFunctor = typename QtPrivate::ContextTypeForFunctor<Functor>::ContextType; + template <typename Functor> + using if_compatible_callback = std::enable_if_t< + QtPrivate::AreFunctionsCompatible<CallbackPrototype, Functor>::value, bool>; +public: + explicit QRestAccessManager(QObject *parent = nullptr); + ~QRestAccessManager() override; + + QNetworkAccessManager *networkAccessManager() const; + + bool deletesRepliesOnFinished() const; + void setDeletesRepliesOnFinished(bool autoDelete); + + void abortRequests(); + + QREST_METHOD_NO_DATA(deleteResource) + QREST_METHOD_NO_DATA(head) + QREST_METHOD_NO_DATA(get) + QREST_METHOD_WITH_DATA(get, const QByteArray &) + QREST_METHOD_WITH_DATA(get, const QJsonObject &) + QREST_METHOD_WITH_DATA(get, QIODevice *) + QREST_METHOD_WITH_DATA(post, const QJsonObject &) + QREST_METHOD_WITH_DATA(post, const QJsonArray &) + QREST_METHOD_WITH_DATA(post, const QVariantMap &) + QREST_METHOD_WITH_DATA(post, const QByteArray &) + QREST_METHOD_WITH_DATA(post, QHttpMultiPart *) + QREST_METHOD_WITH_DATA(post, QIODevice *) + QREST_METHOD_WITH_DATA(put, const QJsonObject &) + QREST_METHOD_WITH_DATA(put, const QJsonArray &) + QREST_METHOD_WITH_DATA(put, const QVariantMap &) + QREST_METHOD_WITH_DATA(put, const QByteArray &) + QREST_METHOD_WITH_DATA(put, QHttpMultiPart *) + QREST_METHOD_WITH_DATA(put, QIODevice *) + +Q_SIGNALS: +#ifndef QT_NO_NETWORKPROXY + void proxyAuthenticationRequired(const QNetworkProxy &proxy, QAuthenticator *authenticator); +#endif + void authenticationRequired(QRestReply *reply, QAuthenticator *authenticator); + void requestFinished(QRestReply *reply); + +private: + Q_DECLARE_PRIVATE(QRestAccessManager) + Q_DISABLE_COPY(QRestAccessManager) +}; + +#undef QREST_METHOD_NO_DATA +#undef QREST_METHOD_WITH_DATA + +QT_END_NAMESPACE + +#endif diff --git a/src/network/access/qrestaccessmanager_p.h b/src/network/access/qrestaccessmanager_p.h new file mode 100644 index 0000000000..91c6f89dd0 --- /dev/null +++ b/src/network/access/qrestaccessmanager_p.h @@ -0,0 +1,87 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef QRESTACCESSMANAGER_P_H +#define QRESTACCESSMANAGER_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists for the convenience +// of the Network Access API. This header file may change from +// version to version without notice, or even be removed. +// +// We mean it. +// + +#include "qrestaccessmanager.h" +#include "private/qobject_p.h" + +#include <QtNetwork/qnetworkaccessmanager.h> + +#include <QtCore/qjsonarray.h> +#include <QtCore/qhash.h> +#include <QtCore/qjsondocument.h> +#include <QtCore/qjsonobject.h> + +QT_BEGIN_NAMESPACE + +class QRestReply; +class QRestAccessManagerPrivate : public QObjectPrivate +{ +public: + QRestAccessManagerPrivate(); + ~QRestAccessManagerPrivate() override; + + void ensureNetworkAccessManager(); + + QRestReply *createActiveRequest(QNetworkReply *networkReply, const QObject *contextObject, + QtPrivate::QSlotObjectBase *slot); + + void removeActiveRequest(QRestReply *restReply); + void handleReplyFinished(QRestReply *restReply); + void handleAuthenticationRequired(QNetworkReply *networkReply, QAuthenticator *authenticator); + QRestReply *restReplyFromNetworkReply(QNetworkReply *networkReply); + + template<typename Functor> + QRestReply *executeRequest(Functor requestOperation, + const QObject *context, QtPrivate::QSlotObjectBase *slot) + { + verifyThreadAffinity(context); + QNetworkReply *reply = requestOperation(); + return createActiveRequest(reply, context, slot); + } + + template<typename Functor, typename Json> + QRestReply *executeRequest(Functor requestOperation, Json jsonData, + const QNetworkRequest &request, + const QObject *context, QtPrivate::QSlotObjectBase *slot) + { + verifyThreadAffinity(context); + QNetworkRequest req(request); + if (!request.header(QNetworkRequest::ContentTypeHeader).isValid()) { + req.setHeader(QNetworkRequest::ContentTypeHeader, + QLatin1StringView{"application/json"}); + } + QJsonDocument json(jsonData); + QNetworkReply *reply = requestOperation(req, json.toJson(QJsonDocument::Compact)); + return createActiveRequest(reply, context, slot); + } + + void verifyThreadAffinity(const QObject *contextObject); + + struct CallerInfo { + const QObject *contextObject = nullptr; + QtPrivate::SlotObjSharedPtr slot; + }; + QHash<QRestReply*, CallerInfo> activeRequests; + + QNetworkAccessManager *qnam = nullptr; + bool deletesRepliesOnFinished = true; + Q_DECLARE_PUBLIC(QRestAccessManager) +}; + +QT_END_NAMESPACE + +#endif diff --git a/src/network/access/qrestreply.cpp b/src/network/access/qrestreply.cpp new file mode 100644 index 0000000000..efff394f83 --- /dev/null +++ b/src/network/access/qrestreply.cpp @@ -0,0 +1,364 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "qrestreply.h" +#include "qrestreply_p.h" + +#include <QtCore/qjsonarray.h> +#include <QtCore/qjsondocument.h> +#include <QtCore/qjsonobject.h> +#include <QtCore/qlatin1stringmatcher.h> +#include <QtCore/qloggingcategory.h> +#include <QtCore/qstringconverter.h> + +QT_BEGIN_NAMESPACE + +using namespace Qt::StringLiterals; +Q_DECLARE_LOGGING_CATEGORY(lcQrest) + +/*! + \class QRestReply + \since 6.7 + \brief QRestReply is the class for following up the requests sent with + QRestAccessManager. + + \reentrant + \ingroup network + \inmodule QtNetwork + + QRestReply is a convenience class for typical RESTful client + applications. It wraps the more detailed QNetworkReply and provides + convenience methods for data and status handling. + + \sa QRestAccessManager, QNetworkReply +*/ + +/*! + \fn void QRestReply::finished(QRestReply *reply) + + This signal is emitted when \a reply has finished processing. This + signal is emitted also in cases when the reply finished due to network + or protocol errors (the server did not reply with an HTTP status). + + \sa isFinished(), httpStatus(), error() +*/ + +/*! + \fn void QRestReply::errorOccurred(QRestReply *reply) + + This signal is emitted if, while processing \a reply, an error occurs that + is considered to be a network/protocol error. These errors are + disctinct from HTTP error responses such as \c {500 Internal Server Error}. + This signal is emitted together with the + finished() signal, and often connecting to that is sufficient. + + \sa finished(), isFinished(), httpStatus(), error() +*/ + +QRestReply::QRestReply(QNetworkReply *reply, QObject *parent) + : QObject(*new QRestReplyPrivate, parent) +{ + Q_D(QRestReply); + Q_ASSERT(reply); + d->networkReply = reply; + // Reparent so that destruction of QRestReply destroys QNetworkReply + reply->setParent(this); +} + +/*! + Destroys this QRestReply object. + + \sa abort() +*/ +QRestReply::~QRestReply() + = default; + +/*! + Returns a pointer to the underlying QNetworkReply wrapped by this object. +*/ +QNetworkReply *QRestReply::networkReply() const +{ + Q_D(const QRestReply); + return d->networkReply; +} + +/*! + Aborts the network operation immediately. The finished() signal + will be emitted. + + \sa QRestAccessManager::abortRequests() QNetworkReply::abort() +*/ +void QRestReply::abort() +{ + Q_D(QRestReply); + d->networkReply->abort(); +} + +/*! + Returns the received data as a QJsonObject. Requires the reply to be + finished. + + The returned value is wrapped in \c std::optional. If the conversion + from the received data fails (empty data or JSON parsing error), + \c std::nullopt is returned. + + Calling this function consumes the received data, and any further calls + to get response data will return empty. + + This function returns \c {std::nullopt} and will not consume + any data if the reply is not finished. + + \sa jsonArray(), body(), text(), finished(), isFinished() +*/ +std::optional<QJsonObject> QRestReply::json() +{ + Q_D(QRestReply); + if (!isFinished()) { + qCWarning(lcQrest, "Attempt to read json() of an unfinished reply, ignoring."); + return std::nullopt; + } + const QJsonDocument json = d->replyDataToJson(); + return json.isObject() ? std::optional{json.object()} : std::nullopt; +} + +/*! + Returns the received data as a QJsonArray. Requires the reply to be + finished. + + The returned value is wrapped in \c std::optional. If the conversion + from the received data fails (empty data or JSON parsing error), + \c std::nullopt is returned. + + Calling this function consumes the received data, and any further calls + to get response data will return empty. + + This function returns \c {std::nullopt} and will not consume + any data if the reply is not finished. + + \sa json(), body(), text(), finished(), isFinished() +*/ +std::optional<QJsonArray> QRestReply::jsonArray() +{ + Q_D(QRestReply); + if (!isFinished()) { + qCWarning(lcQrest, "Attempt to read jsonArray() of an unfinished reply, ignoring."); + return std::nullopt; + } + const QJsonDocument json = d->replyDataToJson(); + return json.isArray() ? std::optional{json.array()} : std::nullopt; +} + +/*! + Returns the received data as a QByteArray. + + Calling this function consumes the data received so far, and any further + calls to get response data will return empty until further data has been + received. + + \sa json(), text() +*/ +QByteArray QRestReply::body() +{ + Q_D(QRestReply); + return d->networkReply->readAll(); +} + +/*! + Returns the received data as a QString. Requires the reply to be finished. + + The received data is decoded into a QString (UTF-16). The decoding + uses the \e Content-Type header's \e charset parameter to determine the + source encoding, if available. If the encoding information is not + available or not supported by \l QStringConverter, UTF-8 is used as a + default. + + Calling this function consumes the received data, and any further calls + to get response data will return empty. + + This function returns a default-constructed value and will not consume + any data if the reply is not finished. + + \sa json(), body(), isFinished(), finished() +*/ +QString QRestReply::text() +{ + Q_D(QRestReply); + if (!isFinished()) { + qCWarning(lcQrest, "Attempt to read text() of an unfinished reply, ignoring."); + return {}; + } + QByteArray data = d->networkReply->readAll(); + if (data.isEmpty()) + return {}; + + const QByteArray charset = d->contentCharset(); + QStringDecoder decoder(charset); + if (!decoder.isValid()) { // the decoder may not support the mimetype's charset + qCWarning(lcQrest, "Charset \"%s\" is not supported, defaulting to UTF-8", + charset.constData()); + decoder = QStringDecoder(QStringDecoder::Utf8); + } + return decoder(data); +} + +/*! + Returns the HTTP status received in the server response. + The value is \e 0 if not available (the status line has not been received, + yet). + + \note The HTTP status is reported as indicated by the received HTTP + response. It is possible that an error() occurs after receiving the status, + for instance due to network disconnection while receiving a long response. + These potential subsequent errors are not represented by the reported + HTTP status. + + \sa isSuccess(), hasError(), error() +*/ +int QRestReply::httpStatus() const +{ + Q_D(const QRestReply); + return d->networkReply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); +} + +/*! + \fn bool QRestReply::isSuccess() const + + Returns whether the HTTP status is between 200..299 and no + further errors have occurred while receiving the response (for example + abrupt disconnection while receiving the body data). This function + is a convenient way to check whether the response is considered successful. + + \sa httpStatus(), hasError(), error() +*/ + +/*! + Returns whether the HTTP status is between 200..299. + + \sa isSuccess(), httpStatus(), hasError(), error() +*/ +bool QRestReply::isHttpStatusSuccess() const +{ + const int status = httpStatus(); + return status >= 200 && status < 300; +} + +/*! + Returns whether an error has occurred. This includes errors such as + network and protocol errors, but excludes cases where the server + successfully responded with an HTTP error status (for example + \c {500 Internal Server Error}). Use \l httpStatus() or + \l isHttpStatusSuccess() to get the HTTP status information. + + \sa httpStatus(), isSuccess(), error(), errorString() +*/ +bool QRestReply::hasError() const +{ + Q_D(const QRestReply); + return d->hasNonHttpError(); +} + +/*! + Returns the last error, if any. The errors include + errors such as network and protocol errors, but exclude + cases when the server successfully responded with an HTTP status. + + \sa httpStatus(), isSuccess(), hasError(), errorString() +*/ +QNetworkReply::NetworkError QRestReply::error() const +{ + Q_D(const QRestReply); + if (!hasError()) + return QNetworkReply::NetworkError::NoError; + return d->networkReply->error(); +} + +/*! + Returns a human-readable description of the last network error. + + \sa httpStatus(), isSuccess(), hasError(), error() +*/ +QString QRestReply::errorString() const +{ + Q_D(const QRestReply); + if (hasError()) + return d->networkReply->errorString(); + return {}; +} + +/*! + Returns whether the network request has finished. +*/ +bool QRestReply::isFinished() const +{ + Q_D(const QRestReply); + return d->networkReply->isFinished(); +} + +QRestReplyPrivate::QRestReplyPrivate() + = default; + +QRestReplyPrivate::~QRestReplyPrivate() + = default; + +QByteArray QRestReplyPrivate::contentCharset() const +{ + // Content-type consists of mimetype and optional parameters, of which one may be 'charset' + // Example values and their combinations below are all valid, see RFC 7231 section 3.1.1.5 + // and RFC 2045 section 5.1 + // + // text/plain; charset=utf-8 + // text/plain; charset=utf-8;version=1.7 + // text/plain; charset = utf-8 + // text/plain; charset ="utf-8" + QByteArray contentTypeValue = + networkReply->header(QNetworkRequest::KnownHeaders::ContentTypeHeader).toByteArray(); + // Default to the most commonly used UTF-8. + QByteArray charset{"UTF-8"}; + + QList<QByteArray> parameters = contentTypeValue.split(';'); + if (parameters.size() >= 2) { // Need at least one parameter in addition to the mimetype itself + parameters.removeFirst(); // Exclude the mimetype itself, only interested in parameters + QLatin1StringMatcher matcher("charset="_L1, Qt::CaseSensitivity::CaseInsensitive); + qsizetype matchIndex = -1; + for (auto ¶meter : parameters) { + // Remove whitespaces and parantheses + const QByteArray curated = parameter.replace(" ", "").replace("\"",""); + // Check for match + matchIndex = matcher.indexIn(QLatin1String(curated.constData())); + if (matchIndex >= 0) { + charset = curated.sliced(matchIndex + 8); // 8 is size of "charset=" + break; + } + } + } + return charset; +} + +// Returns true if there's an error that isn't appropriately indicated by the HTTP status +bool QRestReplyPrivate::hasNonHttpError() const +{ + const int status = networkReply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + if (status > 0) { + // The HTTP status is set upon receiving the response headers, but the + // connection might still fail later while receiving the body data. + return networkReply->error() == QNetworkReply::RemoteHostClosedError; + } + return networkReply->error() != QNetworkReply::NoError; +} + +QJsonDocument QRestReplyPrivate::replyDataToJson() +{ + QJsonParseError parseError; + const QByteArray data = networkReply->readAll(); + const QJsonDocument json = QJsonDocument::fromJson(data, &parseError); + + if (parseError.error != QJsonParseError::NoError) { + qCDebug(lcQrest) << "Response data not JSON:" << parseError.errorString() + << "at" << parseError.offset << data; + } + return json; +} + +QT_END_NAMESPACE + +#include "moc_qrestreply.cpp" diff --git a/src/network/access/qrestreply.h b/src/network/access/qrestreply.h new file mode 100644 index 0000000000..6e35b45003 --- /dev/null +++ b/src/network/access/qrestreply.h @@ -0,0 +1,55 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef QRESTREPLY_H +#define QRESTREPLY_H + +#include <QtNetwork/qnetworkreply.h> + +QT_BEGIN_NAMESPACE + +class QRestReplyPrivate; +class Q_NETWORK_EXPORT QRestReply : public QObject +{ + Q_OBJECT + +public: + ~QRestReply() override; + + QNetworkReply *networkReply() const; + + std::optional<QJsonObject> json(); + std::optional<QJsonArray> jsonArray(); + QByteArray body(); + QString text(); + + bool isSuccess() const + { + return !hasError() && isHttpStatusSuccess(); + } + int httpStatus() const; + bool isHttpStatusSuccess() const; + + bool hasError() const; + QNetworkReply::NetworkError error() const; + QString errorString() const; + + bool isFinished() const; + +public Q_SLOTS: + void abort(); + +Q_SIGNALS: + void finished(QRestReply *reply); + void errorOccurred(QRestReply *reply); + +private: + friend class QRestAccessManagerPrivate; + explicit QRestReply(QNetworkReply *reply, QObject *parent = nullptr); + Q_DECLARE_PRIVATE(QRestReply) + Q_DISABLE_COPY_MOVE(QRestReply) +}; + +QT_END_NAMESPACE + +#endif // QRESTREPLY_H diff --git a/src/network/access/qrestreply_p.h b/src/network/access/qrestreply_p.h new file mode 100644 index 0000000000..4b156b3793 --- /dev/null +++ b/src/network/access/qrestreply_p.h @@ -0,0 +1,39 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef QRESTREPLY_P_H +#define QRESTREPLY_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists for the convenience +// of the Network Access API. This header file may change from +// version to version without notice, or even be removed. +// +// We mean it. +// + +#include "private/qobject_p.h" +#include <QtNetwork/qnetworkreply.h> +#include <QtCore/qjsondocument.h> + +QT_BEGIN_NAMESPACE + +class QRestReplyPrivate : public QObjectPrivate +{ +public: + QRestReplyPrivate(); + ~QRestReplyPrivate() override; + + QNetworkReply *networkReply = nullptr; + + QByteArray contentCharset() const; + bool hasNonHttpError() const; + QJsonDocument replyDataToJson(); +}; + +QT_END_NAMESPACE + +#endif diff --git a/src/network/doc/snippets/code/src_network_access_qrestaccessmanager.cpp b/src/network/doc/snippets/code/src_network_access_qrestaccessmanager.cpp new file mode 100644 index 0000000000..89c7348483 --- /dev/null +++ b/src/network/doc/snippets/code/src_network_access_qrestaccessmanager.cpp @@ -0,0 +1,84 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +//! [0] +QRestReply *reply = manager->get(request); +QObject::connect(reply, &QRestReply::finished, this, &MyClass::handleFinished); +//! [0] + + +//! [1] +// With lambda +manager->get(request, this, [this](QRestReply *reply) { + if (reply->isSuccess()) { + // ... + } +}); +// With member function +manager->get(request, this, &MyClass::handleFinished); +//! [1] + + +//! [2] +QJsonObject myJson; +// ... +manager->post(request, myJson, this, [this](QRestReply *reply) { + if (!reply->isSuccess()) { + // ... + } + if (std::optional<QJsonObject> json = reply->json()) { + // use *json + } +}); +//! [2] + + +//! [3] +manager->get(request, this, [this](QRestReply *reply) { + if (!reply->isSuccess()) + // handle error + if (std::optional<QJsonObject> json = reply->json()) + // use *json +}); +//! [3] + + +//! [4] +manager->get(request, myData, this, [this](QRestReply *reply) { + if (reply->isSuccess()) + // ... +}); +//! [4] + + +//! [5] +manager->post(request, myData, this, [this](QRestReply *reply) { + if (reply->isSuccess()) + // ... +}); +//! [5] + + +//! [6] +manager->put(request, myData, this, [this](QRestReply *reply) { + if (reply->isSuccess()) + // ... +}); +//! [6] + + +//! [7] +manager->head(request, this, [this](QRestReply *reply) { + if (reply->isSuccess()) + // ... +}); +//! [7] + + +//! [8] +manager->deleteResource(request, this, [this](QRestReply *reply) { + if (reply->isSuccess()) + // ... +}); +//! [8] + diff --git a/tests/auto/network/access/CMakeLists.txt b/tests/auto/network/access/CMakeLists.txt index 91c886d3fb..741e6b7879 100644 --- a/tests/auto/network/access/CMakeLists.txt +++ b/tests/auto/network/access/CMakeLists.txt @@ -11,6 +11,7 @@ add_subdirectory(qnetworkrequestfactory) add_subdirectory(qnetworkreply) add_subdirectory(qnetworkcachemetadata) add_subdirectory(qabstractnetworkcache) +add_subdirectory(qrestaccessmanager) if(QT_FEATURE_private_tests) add_subdirectory(qhttpheaderparser) add_subdirectory(qhttpnetworkconnection) diff --git a/tests/auto/network/access/qrestaccessmanager/CMakeLists.txt b/tests/auto/network/access/qrestaccessmanager/CMakeLists.txt new file mode 100644 index 0000000000..08e452331f --- /dev/null +++ b/tests/auto/network/access/qrestaccessmanager/CMakeLists.txt @@ -0,0 +1,11 @@ +# Copyright (C) 2023 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause + +qt_internal_add_test(tst_qrestaccessmanager + SOURCES + tst_qrestaccessmanager.cpp + httptestserver.cpp httptestserver_p.h + LIBRARIES + Qt::Network + Qt::CorePrivate +) diff --git a/tests/auto/network/access/qrestaccessmanager/httptestserver.cpp b/tests/auto/network/access/qrestaccessmanager/httptestserver.cpp new file mode 100644 index 0000000000..ff222dbfb2 --- /dev/null +++ b/tests/auto/network/access/qrestaccessmanager/httptestserver.cpp @@ -0,0 +1,243 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +#include "httptestserver_p.h" + +#include <QtNetwork/qtcpsocket.h> + +#include <QtCore/qcoreapplication.h> +#include <private/qlocale_p.h> + +using namespace Qt::StringLiterals; + +static constexpr char CRLF[] = "\r\n"; + +HttpTestServer::HttpTestServer(QObject *parent) : QTcpServer(parent) +{ + QObject::connect(this, &QTcpServer::newConnection, this, &HttpTestServer::handleConnected); + const auto ok = listen(QHostAddress::LocalHost); + Q_ASSERT(ok); +}; + +HttpTestServer::~HttpTestServer() +{ + if (isListening()) + close(); +} + +QUrl HttpTestServer::url() +{ + return QUrl(u"http://127.0.0.1:%1"_s.arg(serverPort())); +} + +void HttpTestServer::setHandler(const Handler &handler) { + m_handler = handler; +} + +void HttpTestServer::handleConnected() +{ + Q_ASSERT(!m_socket); // No socket must exist previously, this is a single-connection server + m_socket = nextPendingConnection(); + Q_ASSERT(m_socket); + QObject::connect(m_socket, &QTcpSocket::readyRead, + this, &HttpTestServer::handleDataAvailable); +} + +void HttpTestServer::handleDataAvailable() +{ + Q_ASSERT(m_socket); + bool ok = true; + + // Parse the incoming request data into the HttpData object + while (m_socket->bytesAvailable()) { + if (state == State::ReadingMethod && !(ok = readMethod(m_socket))) + qWarning("Invalid Method"); + if (ok && state == State::ReadingUrl && !(ok = readUrl(m_socket))) + qWarning("Invalid URL"); + if (ok && state == State::ReadingStatus && !(ok = readStatus(m_socket))) + qWarning("Invalid Status"); + if (ok && state == State::ReadingHeader && !(ok = readHeaders(m_socket))) + qWarning("Invalid Header"); + if (ok && state == State::ReadingBody && !(ok = readBody(m_socket))) + qWarning("Invalid Body"); + } // while bytes available + + Q_ASSERT(ok); + Q_ASSERT(m_handler); + Q_ASSERT(state == State::AllDone); + + if (m_request.headers.contains("Host")) { + const auto parts = m_request.headers["Host"].split(':'); + m_request.url.setHost(parts.at(0)); + if (parts.size() == 2) + m_request.url.setPort(parts.at(1).toUInt()); + } + HttpData response; + // Inform the testcase about request and ask for response data + m_handler(m_request, response); + + if (response.respond) { + QByteArray responseMessage; + responseMessage += "HTTP/1.1 "; + responseMessage += QByteArray::number(response.status); + responseMessage += CRLF; + // Insert headers if any + for (const auto &[name,value] : response.headers.asKeyValueRange()) { + responseMessage += name; + responseMessage += value; + responseMessage += CRLF; + } + responseMessage += CRLF; + responseMessage += response.body; + + /* + qDebug() << "HTTPTestServer received request" + << "\nMethod:" << m_request.method + << "\nHeaders:" << m_request.headers + << "\nBody:" << m_request.body; + qDebug() << "HTTPTestServer sends response:" << responseMessage; + */ + m_socket->write(responseMessage); + } + m_socket->disconnectFromHost(); + m_request = {}; + m_socket = nullptr; // deleted by QTcpServer during destruction + state = State::ReadingMethod; + fragment.clear(); +} + +bool HttpTestServer::readMethod(QTcpSocket *socket) +{ + bool finished = false; + while (socket->bytesAvailable() && !finished) { + const auto c = socket->read(1).at(0); + if (ascii_isspace(c)) + finished = true; + else if (std::isupper(c) && fragment.size() < 8) + fragment += c; + else + return false; + } + if (finished) { + if (fragment == "HEAD") + method = Method::Head; + else if (fragment == "GET") + method = Method::Get; + else if (fragment == "PUT") + method = Method::Put; + else if (fragment == "POST") + method = Method::Post; + else if (fragment == "DELETE") + method = Method::Delete; + else + qWarning("Invalid operation %s", fragment.data()); + + state = State::ReadingUrl; + m_request.method = fragment; + fragment.clear(); + + return method != Method::Unknown; + } + return true; +} + +bool HttpTestServer::readUrl(QTcpSocket *socket) +{ + bool finished = false; + while (socket->bytesAvailable() && !finished) { + const auto c = socket->read(1).at(0); + if (std::isspace(c)) + finished = true; + else + fragment += c; + } + if (finished) { + if (!fragment.startsWith('/')) { + qWarning("Invalid URL path %s", fragment.constData()); + return false; + } + m_request.url = QStringLiteral("http://127.0.0.1:") + QString::number(m_request.port) + + QString::fromUtf8(fragment); + state = State::ReadingStatus; + if (!m_request.url.isValid()) { + qWarning("Invalid URL %s", fragment.constData()); + return false; + } + fragment.clear(); + } + return true; +} + +bool HttpTestServer::readStatus(QTcpSocket *socket) +{ + bool finished = false; + while (socket->bytesAvailable() && !finished) { + fragment += socket->read(1); + if (fragment.endsWith(CRLF)) { + finished = true; + fragment.resize(fragment.size() - 2); + } + } + if (finished) { + if (!std::isdigit(fragment.at(fragment.size() - 3)) || + fragment.at(fragment.size() - 2) != '.' || + !std::isdigit(fragment.at(fragment.size() - 1))) { + qWarning("Invalid version"); + return false; + } + m_request.version = std::pair(fragment.at(fragment.size() - 3) - '0', + fragment.at(fragment.size() - 1) - '0'); + state = State::ReadingHeader; + fragment.clear(); + } + return true; +} + +bool HttpTestServer::readHeaders(QTcpSocket *socket) +{ + while (socket->bytesAvailable()) { + fragment += socket->read(1); + if (fragment.endsWith(CRLF)) { + if (fragment == CRLF) { + state = State::ReadingBody; + fragment.clear(); + return true; + } else { + fragment.chop(2); + const int index = fragment.indexOf(':'); + if (index == -1) + return false; + + QByteArray key = fragment.sliced(0, index).trimmed(); + QByteArray value = fragment.sliced(index + 1).trimmed(); + m_request.headers.insert(std::move(key), std::move(value)); + fragment.clear(); + } + } + } + return true; +} + +bool HttpTestServer::readBody(QTcpSocket *socket) +{ + qint64 bytesLeft = 0; + if (m_request.headers.contains("Content-Length")) { + bool conversionResult; + bytesLeft = m_request.headers["Content-Length"].toInt(&conversionResult); + if (!conversionResult) + return false; + fragment.resize(bytesLeft); + } + while (bytesLeft) { + qint64 got = socket->read(&fragment.data()[fragment.size() - bytesLeft], bytesLeft); + if (got < 0) + return false; // error + bytesLeft -= got; + if (bytesLeft) + qApp->processEvents(); + } + fragment.swap(m_request.body); + state = State::AllDone; + return true; +} + diff --git a/tests/auto/network/access/qrestaccessmanager/httptestserver_p.h b/tests/auto/network/access/qrestaccessmanager/httptestserver_p.h new file mode 100644 index 0000000000..3af34dd0f0 --- /dev/null +++ b/tests/auto/network/access/qrestaccessmanager/httptestserver_p.h @@ -0,0 +1,78 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +#ifndef QRESTACCESSSMANAGER_HTTPTESTSERVER_P_H +#define QRESTACCESSSMANAGER_HTTPTESTSERVER_P_H + +#include <QtNetwork/qtcpserver.h> + +#include <QtCore/qmap.h> +#include <QtCore/qurl.h> + +// This struct is used for parsing the incoming network request data into, as well +// as getting the response data from the testcase +struct HttpData { + QUrl url; + int status = 0; + bool respond = true; + QByteArray body; + QByteArray method; + quint16 port = 0; + QPair<quint8, quint8> version; + QMap<QByteArray, QByteArray> headers; +}; + +// Simple HTTP server. Currently supports only one concurrent connection +class HttpTestServer : public QTcpServer +{ + Q_OBJECT + +public: + explicit HttpTestServer(QObject *parent = nullptr); + ~HttpTestServer() override; + + // Returns this server's URL for the testcase to send requests to + QUrl url(); + + enum class State { + ReadingMethod, + ReadingUrl, + ReadingStatus, + ReadingHeader, + ReadingBody, + AllDone + } state = State::ReadingMethod; + + enum class Method { + Unknown, + Head, + Get, + Put, + Post, + Delete, + } method = Method::Unknown; + + // Parsing helpers for incoming data => HttpData + bool readMethod(QTcpSocket *socket); + bool readUrl(QTcpSocket *socket); + bool readStatus(QTcpSocket *socket); + bool readHeaders(QTcpSocket *socket); + bool readBody(QTcpSocket *socket); + // Parsing-time buffer in case data is received a small chunk at a time (readyRead()) + QByteArray fragment; + + // Settable callback for testcase. Gives the received request data, and takes in response data + using Handler = std::function<void(const HttpData &request, HttpData &response)>; + void setHandler(const Handler &handler); + +private slots: + void handleConnected(); + void handleDataAvailable(); + +private: + QTcpSocket *m_socket = nullptr; + HttpData m_request; + Handler m_handler = nullptr; +}; + +#endif // QRESTACCESSSMANAGER_HTTPTESTSERVER_P_H diff --git a/tests/auto/network/access/qrestaccessmanager/tst_qrestaccessmanager.cpp b/tests/auto/network/access/qrestaccessmanager/tst_qrestaccessmanager.cpp new file mode 100644 index 0000000000..0dd6bdd9c1 --- /dev/null +++ b/tests/auto/network/access/qrestaccessmanager/tst_qrestaccessmanager.cpp @@ -0,0 +1,794 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#include "httptestserver_p.h" + +#include <QtNetwork/qhttpmultipart.h> +#include <QtNetwork/qrestaccessmanager.h> +#include <QtNetwork/qauthenticator.h> +#include <QtNetwork/qrestreply.h> + +#include <QTest> +#include <QtTest/qsignalspy.h> + +#include <QtCore/qbuffer.h> +#include <QtCore/qjsonobject.h> +#include <QtCore/qjsondocument.h> +#include <QtCore/qjsonarray.h> +#include <QtCore/qstringconverter.h> + +using namespace Qt::StringLiterals; + +class tst_QRestAccessManager : public QObject +{ + Q_OBJECT + +private slots: + void initTestCase(); + + void initialization(); + void destruction(); + void callbacks(); + void threading(); + void networkRequestReply(); + void abort(); + void authentication(); + void errors(); + void body(); + void json(); + void text(); + +private: + void memberHandler(QRestReply *reply); + + friend class Transient; + QList<QRestReply*> m_expectedReplies; + QList<QRestReply*> m_actualReplies; +}; + +void tst_QRestAccessManager::initTestCase() +{ + qRegisterMetaType<QRestReply*>(); // For QSignalSpy +} + +void tst_QRestAccessManager::initialization() +{ + QRestAccessManager manager; + QVERIFY(manager.networkAccessManager()); + QCOMPARE(manager.deletesRepliesOnFinished(), true); +} + +#define VERIFY_REPLY_OK(METHOD) \ + QTRY_VERIFY(replyFromServer); \ + QCOMPARE(serverSideRequest.method, METHOD); \ + QVERIFY(replyFromServer->isSuccess()); \ + QVERIFY(!replyFromServer->hasError()); \ + replyFromServer->deleteLater(); \ + replyFromServer = nullptr; \ + +void tst_QRestAccessManager::networkRequestReply() +{ + // A basic test for each HTTP method against the local testserver. + QRestAccessManager manager; + manager.setDeletesRepliesOnFinished(false); + HttpTestServer server; + QTRY_VERIFY(server.isListening()); + QNetworkRequest request(server.url()); + request.setRawHeader("Content-Type"_ba, "text/plain"); // To silence missing content-type warn + QRestReply *replyFromServer = nullptr; + std::unique_ptr<QHttpMultiPart> multiPart; + QHttpPart part; + part.setHeader(QNetworkRequest::ContentDispositionHeader, QVariant("form-data; name=\"text\"")); + part.setBody("multipart_text"); + QByteArray ioDeviceData{"io_device_data"_ba}; + QBuffer bufferIoDevice(&ioDeviceData); + + HttpData serverSideRequest; // The request data the server received + HttpData serverSideResponse; // The response data the server responds with + serverSideResponse.status = 200; + server.setHandler([&](HttpData request, HttpData &response) { + serverSideRequest = request; + response = serverSideResponse; + + }); + auto callback = [&](QRestReply *reply) { replyFromServer = reply; }; + const QByteArray byteArrayData{"some_data"_ba}; + const QJsonObject jsonObjectData{{"key1", "value1"}, {"key2", "value2"}}; + const QJsonArray jsonArrayData{{"arrvalue1", "arrvalue2", QJsonObject{{"key1", "value1"}}}}; + const QVariantMap variantMapData{{"key1", "value1"}, {"key2", "value2"}}; + const QByteArray methodDELETE{"DELETE"_ba}; + const QByteArray methodHEAD{"HEAD"_ba}; + const QByteArray methodPOST{"POST"_ba}; + const QByteArray methodGET{"GET"_ba}; + const QByteArray methodPUT{"PUT"_ba}; + + // DELETE + manager.deleteResource(request, this, callback); + VERIFY_REPLY_OK(methodDELETE); + QCOMPARE(serverSideRequest.body, ""_ba); + + // HEAD + manager.head(request, this, callback); + VERIFY_REPLY_OK(methodHEAD); + QCOMPARE(serverSideRequest.body, ""_ba); + + // GET + manager.get(request, this, callback); + VERIFY_REPLY_OK(methodGET); + QCOMPARE(serverSideRequest.body, ""_ba); + + manager.get(request, byteArrayData, this, callback); + VERIFY_REPLY_OK(methodGET); + QCOMPARE(serverSideRequest.body, byteArrayData); + + manager.get(request, jsonObjectData, this, callback); + VERIFY_REPLY_OK(methodGET); + QCOMPARE(QJsonDocument::fromJson(serverSideRequest.body).object(), jsonObjectData); + + manager.get(request, &bufferIoDevice, this, callback); + VERIFY_REPLY_OK(methodGET); + QCOMPARE(serverSideRequest.body, ioDeviceData); + + // POST + manager.post(request, byteArrayData, this, callback); + VERIFY_REPLY_OK(methodPOST); + QCOMPARE(serverSideRequest.body, byteArrayData); + + manager.post(request, jsonObjectData, this, callback); + VERIFY_REPLY_OK(methodPOST); + QCOMPARE(QJsonDocument::fromJson(serverSideRequest.body).object(), jsonObjectData); + + manager.post(request, jsonArrayData, this, callback); + VERIFY_REPLY_OK(methodPOST); + QCOMPARE(QJsonDocument::fromJson(serverSideRequest.body).array(), jsonArrayData); + + manager.post(request, variantMapData, this, callback); + VERIFY_REPLY_OK(methodPOST); + QCOMPARE(QJsonDocument::fromJson(serverSideRequest.body).object(), jsonObjectData); + + multiPart = std::make_unique<QHttpMultiPart>(QHttpMultiPart::FormDataType); + multiPart->append(part); + manager.post(request, multiPart.get(), this, callback); + VERIFY_REPLY_OK(methodPOST); + QVERIFY(serverSideRequest.body.contains("--boundary"_ba)); + QVERIFY(serverSideRequest.body.contains("multipart_text"_ba)); + + manager.post(request, &bufferIoDevice, this, callback); + VERIFY_REPLY_OK(methodPOST); + QCOMPARE(serverSideRequest.body, ioDeviceData); + + // PUT + manager.put(request, byteArrayData, this, callback); + VERIFY_REPLY_OK(methodPUT); + QCOMPARE(serverSideRequest.body, byteArrayData); + + manager.put(request, jsonObjectData, this, callback); + VERIFY_REPLY_OK(methodPUT); + QCOMPARE(QJsonDocument::fromJson(serverSideRequest.body).object(), jsonObjectData); + + manager.put(request, jsonArrayData, this, callback); + VERIFY_REPLY_OK(methodPUT); + QCOMPARE(QJsonDocument::fromJson(serverSideRequest.body).array(), jsonArrayData); + + manager.put(request, variantMapData, this, callback); + VERIFY_REPLY_OK(methodPUT); + QCOMPARE(QJsonDocument::fromJson(serverSideRequest.body).object(), jsonObjectData); + + multiPart = std::make_unique<QHttpMultiPart>(QHttpMultiPart::FormDataType); + multiPart->append(part); + manager.put(request, multiPart.get(), this, callback); + VERIFY_REPLY_OK(methodPUT); + QVERIFY(serverSideRequest.body.contains("--boundary"_ba)); + QVERIFY(serverSideRequest.body.contains("multipart_text"_ba)); + + manager.put(request, &bufferIoDevice, this, callback); + VERIFY_REPLY_OK(methodPUT); + QCOMPARE(serverSideRequest.body, ioDeviceData); + + //These must NOT compile + //manager.get(request, [](){}); // callback without context object + //manager.get(request, ""_ba, [](){}); // callback without context object + //manager.get(request, QString()); // wrong datatype + //manager.get(request, 123); // wrong datatype + //manager.post(request, QString()); // wrong datatype + //manager.put(request, 123); // wrong datatype + //manager.post(request); // data is required + //manager.put(request, QString()); // wrong datatype + //manager.put(request); // data is required + //manager.deleteResource(request, "f"_ba); // data not allowed + //manager.head(request, "f"_ba); // data not allowed + //manager.post(request, ""_ba, this, [](int param){}); // Wrong callback signature + //manager.get(request, this, [](int param){}); // Wrong callback signature +} + +void tst_QRestAccessManager::abort() +{ + // Test aborting requests + QRestAccessManager manager; + HttpTestServer server; + QTRY_VERIFY(server.isListening()); + QNetworkRequest request(server.url()); + + QSignalSpy finishedSpy(&manager, &QRestAccessManager::requestFinished); + int callbackCount = 0; + auto callback = [&](QRestReply*) { + callbackCount++; + }; + + // Abort without any requests + manager.abortRequests(); + QTest::qWait(20); + QCOMPARE(finishedSpy.size(), 0); + + // Abort immediately after requesting + manager.get(request, this, callback); + manager.abortRequests(); + QTRY_COMPARE(callbackCount, 1); + QTRY_COMPARE(finishedSpy.size(), 1); + + // Abort after request has been sent out + server.setHandler([&](HttpData, HttpData &response) { + response.respond = false; + manager.abortRequests(); + }); + manager.get(request, this, callback); + QTRY_COMPARE(callbackCount, 2); + QTRY_COMPARE(finishedSpy.size(), 2); +} + +void tst_QRestAccessManager::memberHandler(QRestReply *reply) +{ + m_actualReplies.append(reply); +} + +// Class that is destroyed during an active request. +// Used to test that the callbacks won't be called in these cases +class Transient : public QObject +{ + Q_OBJECT +public: + explicit Transient(tst_QRestAccessManager *test) : QObject(test), m_test(test) {} + + void memberHandler(QRestReply *reply) + { + m_test->m_actualReplies.append(reply); + } + +private: + tst_QRestAccessManager *m_test = nullptr; +}; + +template <typename Functor, std::enable_if_t< + QtPrivate::AreFunctionsCompatible<void(*)(QRestReply*), Functor>::value, bool> = true> +inline constexpr bool isCompatibleCallback(Functor &&) { return true; } + +template <typename Functor, std::enable_if_t< + !QtPrivate::AreFunctionsCompatible<void(*)(QRestReply*), Functor>::value, bool> = true, + typename = void> +inline constexpr bool isCompatibleCallback(Functor &&) { return false; } + +void tst_QRestAccessManager::callbacks() +{ + QRestAccessManager manager; + + manager.setDeletesRepliesOnFinished(false); // Don't autodelete so we can compare results later + QNetworkRequest request{u"i_dont_exist"_s}; // Will result in ProtocolUnknown error + QSignalSpy managerFinishedSpy(&manager, &QRestAccessManager::requestFinished); + + auto lambdaHandler = [this](QRestReply *reply) { m_actualReplies.append(reply); }; + QRestReply *reply = nullptr; + Transient *transient = nullptr; + QByteArray data{"some_data"}; + + // Compile-time tests for callback signatures + static_assert(isCompatibleCallback([](QRestReply*){})); // Correct signature + static_assert(isCompatibleCallback(lambdaHandler)); + static_assert(isCompatibleCallback(&Transient::memberHandler)); + static_assert(isCompatibleCallback([](){})); // Less parameters are allowed + + static_assert(!isCompatibleCallback([](QString){})); // Wrong parameter type + static_assert(!isCompatibleCallback([](QNetworkReply*){})); // Wrong parameter type + static_assert(!isCompatibleCallback([](const QString &){})); // Wrong parameter type + static_assert(!isCompatibleCallback([](QRestReply*, QString){})); // Too many parameters + + // -- Test without data + // Without callback + reply = manager.get(request); + QCOMPARE(reply->isFinished(), false); // Test this once here + m_expectedReplies.append(reply); + QObject::connect(reply, &QRestReply::finished, lambdaHandler); + + // With lambda callback, without context object + m_expectedReplies.append(manager.get(request, nullptr, lambdaHandler)); + m_expectedReplies.append(manager.get(request, nullptr, + [this](QRestReply *reply){m_actualReplies.append(reply);})); + // With lambda callback and context object + m_expectedReplies.append(manager.get(request, this, lambdaHandler)); + m_expectedReplies.append(manager.get(request, this, + [this](QRestReply *reply){m_actualReplies.append(reply);})); + // With member callback and context object + m_expectedReplies.append(manager.get(request, this, &tst_QRestAccessManager::memberHandler)); + // With context object that is destroyed, there should be no callback or eg. crash. + transient = new Transient(this); + manager.get(request, transient, &Transient::memberHandler); // Reply not added to expecteds + delete transient; + + // Let requests finish + QTRY_COMPARE(m_actualReplies.size(), m_expectedReplies.size()); + QTRY_COMPARE(managerFinishedSpy.size(), m_actualReplies.size()); + for (auto reply: m_actualReplies) { + QVERIFY(!reply->isSuccess()); + QVERIFY(reply->hasError()); + QCOMPARE(reply->error(), QNetworkReply::ProtocolUnknownError); + QCOMPARE(reply->isFinished(), true); + reply->deleteLater(); + } + m_actualReplies.clear(); + m_expectedReplies.clear(); + managerFinishedSpy.clear(); + + // -- Test with data + reply = manager.post(request, data); + m_expectedReplies.append(reply); + QObject::connect(reply, &QRestReply::finished, lambdaHandler); + + // With lambda callback, without context object + m_expectedReplies.append(manager.post(request, data, nullptr, lambdaHandler)); + m_expectedReplies.append(manager.post(request, data, nullptr, + [this](QRestReply *reply){m_actualReplies.append(reply);})); + // With lambda callback and context object + m_expectedReplies.append(manager.post(request, data, this, lambdaHandler)); + m_expectedReplies.append(manager.post(request, data, this, + [this](QRestReply *reply){m_actualReplies.append(reply);})); + // With member callback and context object + m_expectedReplies.append(manager.post(request, data, + this, &tst_QRestAccessManager::memberHandler)); + // With context object that is destroyed, there should be no callback or eg. crash + transient = new Transient(this); + manager.post(request, data, transient, &Transient::memberHandler); // Note: reply not expected + delete transient; + + // Let requests finish + QTRY_COMPARE(m_actualReplies.size(), m_expectedReplies.size()); + QTRY_COMPARE(managerFinishedSpy.size(), m_actualReplies.size()); + for (auto reply: m_actualReplies) { + QVERIFY(!reply->isSuccess()); + QVERIFY(reply->hasError()); + QCOMPARE(reply->error(), QNetworkReply::ProtocolUnknownError); + QCOMPARE(reply->isFinished(), true); + reply->deleteLater(); + } + m_actualReplies.clear(); + m_expectedReplies.clear(); + managerFinishedSpy.clear(); + + // -- Test GET with data separately, as GET provides methods that are usable with and + // without data, and fairly easy to get the qrestaccessmanager.h template SFINAE subtly wrong + reply = manager.get(request, data); + m_expectedReplies.append(reply); + QObject::connect(reply, &QRestReply::finished, lambdaHandler); + // With lambda callback, without context object + m_expectedReplies.append(manager.get(request, data, nullptr, lambdaHandler)); + m_expectedReplies.append(manager.get(request, data, nullptr, + [this](QRestReply *reply){m_actualReplies.append(reply);})); + // With lambda callback and context object + m_expectedReplies.append(manager.get(request, data, this, lambdaHandler)); + m_expectedReplies.append(manager.get(request, data, this, + [this](QRestReply *reply){m_actualReplies.append(reply);})); + // With member callback and context object + m_expectedReplies.append(manager.get(request, data, + this, &tst_QRestAccessManager::memberHandler)); + // With context object that is destroyed, there should be no callback or eg. crash + transient = new Transient(this); + manager.get(request, data, transient, &Transient::memberHandler); // Reply not added + delete transient; + + // Let requests finish + QTRY_COMPARE(m_actualReplies.size(), m_expectedReplies.size()); + QTRY_COMPARE(managerFinishedSpy.size(), m_actualReplies.size()); + for (auto reply: m_actualReplies) { + QVERIFY(!reply->isSuccess()); + QVERIFY(reply->hasError()); + QCOMPARE(reply->error(), QNetworkReply::ProtocolUnknownError); + QCOMPARE(reply->isFinished(), true); + reply->deleteLater(); + } + m_actualReplies.clear(); + m_expectedReplies.clear(); + managerFinishedSpy.clear(); +} + +class RestWorker : public QObject +{ + Q_OBJECT +public: + explicit RestWorker(QObject *parent = nullptr) : QObject(parent) + { + m_manager = new QRestAccessManager(this); + } + QRestAccessManager *m_manager; + +public slots: + void issueRestRequest() + { + QNetworkRequest request{u"i_dont_exist"_s}; + m_manager->get(request, this, [this](QRestReply *reply){ + emit result(reply->body()); + }); + } +signals: + void result(const QByteArray &data); +}; + +void tst_QRestAccessManager::threading() +{ + // QRestAccessManager and QRestReply are only allowed to use in the thread they live in. + + // A "sanity test" for checking that there are no problems with running the QRestAM + // in another thread. + QThread restWorkThread; + RestWorker restWorker; + restWorker.moveToThread(&restWorkThread); + + QList<QByteArray> results; + QObject::connect(&restWorker, &RestWorker::result, this, [&](const QByteArray &data){ + results.append(data); + }); + restWorkThread.start(); + + QMetaObject::invokeMethod(&restWorker, &RestWorker::issueRestRequest); + QTRY_COMPARE(results.size(), 1); + restWorkThread.quit(); + restWorkThread.wait(); +} + +void tst_QRestAccessManager::destruction() +{ + QRestAccessManager *manager = new QRestAccessManager; + manager->setDeletesRepliesOnFinished(false); // Don't autodelete so we can compare results later + QNetworkRequest request{u"i_dont_exist"_s}; // Will result in ProtocolUnknown error + m_expectedReplies.clear(); + m_actualReplies.clear(); + auto handler = [this](QRestReply *reply) { m_actualReplies.append(reply); }; + + // Delete reply immediately, make sure nothing bad happens and that there is no callback + QRestReply *reply = manager->get(request, this, handler); + delete reply; + QTest::qWait(20); // allow some time for the callback to arrive (it shouldn't) + QCOMPARE(m_actualReplies.size(), m_expectedReplies.size()); // Both should be 0 + + // Delete access manager immediately after request, make sure nothing bad happens + manager->get(request, this, handler); + manager->post(request, "data"_ba, this, handler); + QTest::ignoreMessage(QtWarningMsg, "Access manager destroyed while 2 requests were still" + " in progress"); + delete manager; + QTest::qWait(20); + QCOMPARE(m_actualReplies.size(), m_expectedReplies.size()); // Both should be 0 +} + +void tst_QRestAccessManager::authentication() +{ + // Test the case where server responds with '401' (authentication required). + // The QRestAM emits an authenticationRequired signal, which is used to the username/password. + // The QRestAM/QNAM underneath then automatically resends the request. + QRestAccessManager manager; + manager.setDeletesRepliesOnFinished(false); + HttpTestServer server; + QTRY_VERIFY(server.isListening()); + QNetworkRequest request(server.url()); + QRestReply *replyFromServer = nullptr; + + HttpData serverSideRequest; + server.setHandler([&](HttpData request, HttpData &response) { + if (!request.headers.contains("Authorization"_ba)) { + response.status = 401; + response.headers.insert("WWW-Authenticate: "_ba, "Basic realm=\"secret_place\""_ba); + } else { + response.status = 200; + } + serverSideRequest = request; // store for checking later the 'Authorization' header value + }); + + QObject::connect(&manager, &QRestAccessManager::authenticationRequired, this, + [](QRestReply*, QAuthenticator *authenticator) { + authenticator->setUser(u"a_user"_s); + authenticator->setPassword(u"a_password"_s); + }); + + // Issue a GET request without any authorization data. + int finishedCount = 0; + manager.get(request, this, [&](QRestReply *reply) { + finishedCount++; + replyFromServer = reply; + }); + QTRY_VERIFY(replyFromServer); + // Server and QRestAM/QNAM exchange req/res twice, but finished() should be emitted just once + QCOMPARE(finishedCount, 1); + QCOMPARE(serverSideRequest.headers["Authorization"_ba], "Basic YV91c2VyOmFfcGFzc3dvcmQ="_ba); +} + +#define VERIFY_HTTP_ERROR_STATUS(STATUS) \ + serverSideResponse.status = STATUS; \ + reply = manager.get(request); \ + QObject::connect(reply, &QRestReply::errorOccurred, this, \ + [&](){ errorSignalReceived = true; }); \ + QTRY_VERIFY(reply->isFinished()); \ + QVERIFY(!errorSignalReceived); \ + QVERIFY(!reply->hasError()); \ + QCOMPARE(reply->httpStatus(), serverSideResponse.status); \ + QCOMPARE(reply->error(), QNetworkReply::NetworkError::NoError); \ + QVERIFY(!reply->isSuccess()); \ + reply->deleteLater(); \ + +void tst_QRestAccessManager::errors() +{ + // Tests the distinction between HTTP and other (network/protocol) errors + QRestAccessManager manager; + manager.setDeletesRepliesOnFinished(false); + HttpTestServer server; + QTRY_VERIFY(server.isListening()); + QNetworkRequest request(server.url()); + QRestReply *reply = nullptr; + bool errorSignalReceived = false; + + HttpData serverSideResponse; // The response data the server responds with + server.setHandler([&](HttpData, HttpData &response) { + response = serverSideResponse; + }); + + // Test few HTTP statuses in different categories + VERIFY_HTTP_ERROR_STATUS(103) // QNetworkReply::NoError + VERIFY_HTTP_ERROR_STATUS(301) // QNetworkReply::ProtocolUnknownError + VERIFY_HTTP_ERROR_STATUS(302) // QNetworkReply::ProtocolUnknownError + VERIFY_HTTP_ERROR_STATUS(400) // QNetworkReply::ProtocolInvalidOperationError + VERIFY_HTTP_ERROR_STATUS(401) // QNetworkReply::AuthenticationRequiredEror + VERIFY_HTTP_ERROR_STATUS(402) // QNetworkReply::UnknownContentError + VERIFY_HTTP_ERROR_STATUS(403) // QNetworkReply::ContentAccessDenied + VERIFY_HTTP_ERROR_STATUS(404) // QNetworkReply::ContentNotFoundError + VERIFY_HTTP_ERROR_STATUS(405) // QNetworkReply::ContentOperationNotPermittedError + VERIFY_HTTP_ERROR_STATUS(406) // QNetworkReply::UnknownContentError + VERIFY_HTTP_ERROR_STATUS(407) // QNetworkReply::ProxyAuthenticationRequiredError + VERIFY_HTTP_ERROR_STATUS(408) // QNetworkReply::UnknownContentError + VERIFY_HTTP_ERROR_STATUS(409) // QNetworkReply::ContentConflictError + VERIFY_HTTP_ERROR_STATUS(410) // QNetworkReply::ContentGoneError + VERIFY_HTTP_ERROR_STATUS(500) // QNetworkReply::InternalServerError + VERIFY_HTTP_ERROR_STATUS(501) // QNetworkReply::OperationNotImplementedError + VERIFY_HTTP_ERROR_STATUS(502) // QNetworkReply::UnknownServerError + VERIFY_HTTP_ERROR_STATUS(503) // QNetworkReply::ServiceUnavailableError + VERIFY_HTTP_ERROR_STATUS(504) // QNetworkReply::UnknownServerError + VERIFY_HTTP_ERROR_STATUS(505) // QNetworkReply::UnknownServerError + + // Test that actual network/protocol errors come through + reply = manager.get({}); // Empty url + QObject::connect(reply, &QRestReply::errorOccurred, this, [&](){ errorSignalReceived = true; }); + QTRY_VERIFY(reply->isFinished()); + QTRY_VERIFY(errorSignalReceived); + QVERIFY(reply->hasError()); + QVERIFY(!reply->isSuccess()); + QCOMPARE(reply->error(), QNetworkReply::ProtocolUnknownError); + reply->deleteLater(); + errorSignalReceived = false; + + reply = manager.get(QNetworkRequest{{"http://non-existent.foo.bar.test"}}); + QObject::connect(reply, &QRestReply::errorOccurred, this, [&](){ errorSignalReceived = true; }); + QTRY_VERIFY(reply->isFinished()); + QTRY_VERIFY(errorSignalReceived); + QVERIFY(reply->hasError()); + QVERIFY(!reply->isSuccess()); + QCOMPARE(reply->error(), QNetworkReply::HostNotFoundError); + reply->deleteLater(); + errorSignalReceived = false; + + reply = manager.get(request); + QObject::connect(reply, &QRestReply::errorOccurred, this, [&](){ errorSignalReceived = true; }); + reply->abort(); + QTRY_VERIFY(reply->isFinished()); + QTRY_VERIFY(errorSignalReceived); + QVERIFY(reply->hasError()); + QVERIFY(!reply->isSuccess()); + QCOMPARE(reply->error(), QNetworkReply::OperationCanceledError); + reply->deleteLater(); + errorSignalReceived = false; +} + +void tst_QRestAccessManager::body() +{ + // Test using QRestReply::body() data accessor + QRestAccessManager manager; + manager.setDeletesRepliesOnFinished(false); + HttpTestServer server; + QTRY_VERIFY(server.isListening()); + QNetworkRequest request(server.url()); + QRestReply *replyFromServer = nullptr; + + HttpData serverSideRequest; // The request data the server received + HttpData serverSideResponse; // The response data the server responds with + server.setHandler([&](HttpData request, HttpData &response) { + serverSideRequest = request; + response = serverSideResponse; + }); + + serverSideResponse.status = 200; + serverSideResponse.body = "some_data"_ba; + manager.get(request, this, [&](QRestReply *reply) { replyFromServer = reply; }); + QTRY_VERIFY(replyFromServer); + QCOMPARE(replyFromServer->body(), serverSideResponse.body); + QCOMPARE(replyFromServer->httpStatus(), serverSideResponse.status); + QVERIFY(!replyFromServer->hasError()); + QVERIFY(replyFromServer->isSuccess()); + replyFromServer->deleteLater(); + replyFromServer = nullptr; + + serverSideResponse.body = ""_ba; // Empty + manager.get(request, this, [&](QRestReply *reply) { replyFromServer = reply; }); + QTRY_VERIFY(replyFromServer); + QCOMPARE(replyFromServer->body(), serverSideResponse.body); + replyFromServer->deleteLater(); + replyFromServer = nullptr; + + serverSideResponse.status = 500; + serverSideResponse.body = "some_other_data"_ba; + manager.get(request, this, [&](QRestReply *reply) { replyFromServer = reply; }); + QTRY_VERIFY(replyFromServer); + QCOMPARE(replyFromServer->body(), serverSideResponse.body); + QCOMPARE(replyFromServer->httpStatus(), serverSideResponse.status); + QVERIFY(!replyFromServer->hasError()); + QVERIFY(!replyFromServer->isSuccess()); + replyFromServer->deleteLater(); + replyFromServer = nullptr; +} + +void tst_QRestAccessManager::json() +{ + // Test using QRestReply::json() and jsonArray() data accessors + QRestAccessManager manager; + manager.setDeletesRepliesOnFinished(false); + HttpTestServer server; + QTRY_VERIFY(server.isListening()); + QNetworkRequest request(server.url()); + QRestReply *replyFromServer = nullptr; + QJsonObject responseJsonObject; + QJsonArray responseJsonArray; + + HttpData serverSideRequest; // The request data the server received + HttpData serverSideResponse; // The response data the server responds with + serverSideResponse.status = 200; + server.setHandler([&](HttpData request, HttpData &response) { + serverSideRequest = request; + response = serverSideResponse; + }); + + // Test receiving valid json object + serverSideResponse.body = "{\"key1\":\"value1\",""\"key2\":\"value2\"}\n"_ba; + manager.get(request, this, [&](QRestReply *reply) { replyFromServer = reply; }); + QTRY_VERIFY(replyFromServer); + responseJsonObject = *replyFromServer->json(); + QCOMPARE(responseJsonObject["key1"], "value1"); + QCOMPARE(responseJsonObject["key2"], "value2"); + replyFromServer->deleteLater(); + replyFromServer = nullptr; + + // Test receiving an invalid json object + serverSideResponse.body = "foobar"_ba; + manager.get(request, this, [&](QRestReply *reply) { replyFromServer = reply; }); + QTRY_VERIFY(replyFromServer); + QVERIFY(!replyFromServer->json().has_value()); // std::nullopt returned + replyFromServer->deleteLater(); + replyFromServer = nullptr; + + // Test receiving valid json array + serverSideResponse.body = "[\"foo\", \"bar\"]\n"_ba; + manager.get(request, this, [&](QRestReply *reply) { replyFromServer = reply; }); + QTRY_VERIFY(replyFromServer); + responseJsonArray = *replyFromServer->jsonArray(); + QCOMPARE(responseJsonArray.size(), 2); + QCOMPARE(responseJsonArray[0].toString(), "foo"_L1); + QCOMPARE(responseJsonArray[1].toString(), "bar"_L1); + replyFromServer->deleteLater(); + replyFromServer = nullptr; + + // Test receiving an invalid json array + serverSideResponse.body = "foobar"_ba; + manager.get(request, this, [&](QRestReply *reply) { replyFromServer = reply; }); + QTRY_VERIFY(replyFromServer); + QVERIFY(!replyFromServer->jsonArray().has_value()); // std::nullopt returned + replyFromServer->deleteLater(); + replyFromServer = nullptr; +} + +#define VERIFY_TEXT_REPLY_OK \ + QTRY_VERIFY(replyFromServer); \ + responseString = replyFromServer->text(); \ + QCOMPARE(responseString, sourceString); \ + replyFromServer->deleteLater(); \ + replyFromServer = nullptr; \ + +void tst_QRestAccessManager::text() +{ + // Test using QRestReply::text() data accessor with various text encodings + QRestAccessManager manager; + manager.setDeletesRepliesOnFinished(false); + HttpTestServer server; + QTRY_VERIFY(server.isListening()); + QNetworkRequest request(server.url()); + QRestReply *replyFromServer = nullptr; + QJsonObject responseJsonObject; + + QStringEncoder encUTF8("UTF-8"); + QStringEncoder encUTF16("UTF-16"); + QStringEncoder encUTF32("UTF-32"); + QString responseString; + + HttpData serverSideRequest; // The request data the server received + HttpData serverSideResponse; // The response data the server responds with + serverSideResponse.status = 200; + server.setHandler([&](HttpData request, HttpData &response) { + serverSideRequest = request; + response = serverSideResponse; + }); + + const QString sourceString("this is a string"_L1); + + // Charset parameter of Content-Type header may specify non-UTF-8 character encoding. + // + // QString is UTF-16, and in the tests below we encode the response data to various + // charset encodings (into byte arrays). When we get the response data, the text() + // should consider the indicated charset and convert it to an UTF-16 QString => the returned + // QString from text() should match with the original (UTF-16) QString. + + // Successful UTF-8 + serverSideResponse.headers.insert("Content-Type:"_ba, "text/plain; charset=UTF-8"_ba); + serverSideResponse.body = encUTF8(sourceString); + manager.get(request, this, [&](QRestReply *reply) { replyFromServer = reply; }); + VERIFY_TEXT_REPLY_OK; + + // Successful UTF-16 + serverSideResponse.headers.insert("Content-Type:"_ba, "text/plain; charset=UTF-16"_ba); + serverSideResponse.body = encUTF16(sourceString); + manager.get(request, this, [&](QRestReply *reply) { replyFromServer = reply; }); + VERIFY_TEXT_REPLY_OK; + + // Successful UTF-16, parameter case insensitivity + serverSideResponse.headers.insert("Content-Type:"_ba, "text/plain; chARset=uTf-16"_ba); + serverSideResponse.body = encUTF16(sourceString); + manager.get(request, this, [&](QRestReply *reply) { replyFromServer = reply; }); + VERIFY_TEXT_REPLY_OK; + + // Successful UTF-32 + serverSideResponse.headers.insert("Content-Type:"_ba, "text/plain; charset=UTF-32"_ba); + serverSideResponse.body = encUTF32(sourceString); + manager.get(request, this, [&](QRestReply *reply) { replyFromServer = reply; }); + VERIFY_TEXT_REPLY_OK; + + // Successful UTF-32 with spec-wise allowed extra content in the Content-Type header value + serverSideResponse.headers.insert("Content-Type:"_ba, + "text/plain; charset = \"UTF-32\";extraparameter=bar"_ba); + serverSideResponse.body = encUTF32(sourceString); + manager.get(request, this, [&](QRestReply *reply) { replyFromServer = reply; }); + VERIFY_TEXT_REPLY_OK; + + // Unsuccessful UTF-32, wrong encoding indicated (indicated charset UTF-32 but data is UTF-8) + serverSideResponse.headers.insert("Content-Type:"_ba, "text/plain; charset=UTF-32"_ba); + serverSideResponse.body = encUTF8(sourceString); + manager.get(request, this, [&](QRestReply *reply) { replyFromServer = reply; }); + QTRY_VERIFY(replyFromServer); + responseString = replyFromServer->text(); + QCOMPARE_NE(responseString, sourceString); + replyFromServer->deleteLater(); + replyFromServer = nullptr; + + // Unsupported encoding, defaults to UTF-8 + serverSideResponse.headers.insert("Content-Type:"_ba, "text/plain; charset=foo"_ba); + serverSideResponse.body = encUTF8(sourceString); + manager.get(request, this, [&](QRestReply *reply) { replyFromServer = reply; }); + QTRY_VERIFY(replyFromServer); + QTest::ignoreMessage(QtWarningMsg, "Charset \"foo\" is not supported, defaulting to UTF-8"); + responseString = replyFromServer->text(); + QCOMPARE(responseString, sourceString); + replyFromServer->deleteLater(); + replyFromServer = nullptr; +} + +QTEST_MAIN(tst_QRestAccessManager) +#include "tst_qrestaccessmanager.moc" |