diff --git a/src/httpserver/CMakeLists.txt b/src/httpserver/CMakeLists.txt index efcce0d..a552ca2 100644 --- a/src/httpserver/CMakeLists.txt +++ b/src/httpserver/CMakeLists.txt @@ -47,3 +47,10 @@ qt_extend_target(HttpServer CONDITION QT_FEATURE_ssl PUBLIC_LIBRARIES Qt::SslServer ) + +qt_extend_target(HttpServer CONDITION TARGET Qt::Concurrent + SOURCES + qhttpserverfutureresponse.cpp qhttpserverfutureresponse.h + PUBLIC_LIBRARIES + Qt::Concurrent +) diff --git a/src/httpserver/httpserver.pro b/src/httpserver/httpserver.pro index fd1a231..9e9d17a 100644 --- a/src/httpserver/httpserver.pro +++ b/src/httpserver/httpserver.pro @@ -37,6 +37,12 @@ SOURCES += \ qhttpserverrouter.cpp \ qhttpserverrouterrule.cpp +qtHaveModule(concurrent) { + QT += concurrent + HEADERS += qhttpserverfutureresponse.h + SOURCES += qhttpserverfutureresponse.cpp +} + include(../3rdparty/http-parser.pri) load(qt_module) diff --git a/src/httpserver/qhttpserver.h b/src/httpserver/qhttpserver.h index 97c5bc5..5274d0f 100644 --- a/src/httpserver/qhttpserver.h +++ b/src/httpserver/qhttpserver.h @@ -59,6 +59,15 @@ class Q_HTTPSERVER_EXPORT QHttpServer final : public QAbstractHttpServer using Type = typename VariadicTypeAt<sizeof ... (Ts) - 1, Ts...>::Type; }; + + template<typename T> + using ResponseType = + typename std::conditional< + std::is_base_of<QHttpServerResponse, T>::value, + T, + QHttpServerResponse + >::type; + public: explicit QHttpServer(QObject *parent = nullptr); ~QHttpServer(); @@ -160,7 +169,16 @@ private: const QHttpServerRequest &request, QTcpSocket *socket) { - QHttpServerResponse response(boundViewHandler()); + ResponseType<typename ViewTraits::ReturnType> response(boundViewHandler()); + sendResponse(std::move(response), request, socket); + } + + template<typename ViewTraits, typename T> + typename std::enable_if<ViewTraits::Arguments::Last::IsRequest::Value && + ViewTraits::Arguments::PlaceholdersCount == 1, void>::type + responseImpl(T &boundViewHandler, const QHttpServerRequest &request, QTcpSocket *socket) + { + ResponseType<typename ViewTraits::ReturnType> response(boundViewHandler(request)); sendResponse(std::move(response), request, socket); } @@ -172,15 +190,6 @@ private: boundViewHandler(makeResponder(request, socket), request); } - template<typename ViewTraits, typename T> - typename std::enable_if<ViewTraits::Arguments::Last::IsRequest::Value && - ViewTraits::Arguments::PlaceholdersCount == 1, void>::type - responseImpl(T &boundViewHandler, const QHttpServerRequest &request, QTcpSocket *socket) - { - QHttpServerResponse response(boundViewHandler(request)); - sendResponse(std::move(response), request, socket); - } - template<typename ViewTraits, typename T> typename std::enable_if<ViewTraits::Arguments::Last::IsResponder::Value && ViewTraits::Arguments::PlaceholdersCount == 2, void>::type diff --git a/src/httpserver/qhttpserverfutureresponse.cpp b/src/httpserver/qhttpserverfutureresponse.cpp new file mode 100644 index 0000000..3d2784b --- /dev/null +++ b/src/httpserver/qhttpserverfutureresponse.cpp @@ -0,0 +1,139 @@ +/**************************************************************************** +** +** Copyright (C) 2020 Mikhail Svetkin <mikhail.svetkin@gmail.com> +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtHttpServer module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:GPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 or (at your option) any later version +** approved by the KDE Free Qt Foundation. The licenses are as published by +** the Free Software Foundation and appearing in the file LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "qhttpserverfutureresponse.h" + +#include <QtCore/qfuture.h> +#include <QtCore/qfuturewatcher.h> + +#include <QtNetwork/qtcpsocket.h> + +#include <QtHttpServer/qhttpserverresponder.h> + +#include <private/qhttpserverresponse_p.h> + + +QT_BEGIN_NAMESPACE + +/*! + \class QHttpServerFutureResponse + \brief QHttpServerFutureResponse is a simplified API for asynchronous responses. + + \code + + QHttpServer server; + + server.route("/feature/", [] (int id) -> QHttpServerFutureResponse { + auto future = QtConcurrent::run([] () { + return QHttpServerResponse("the future is coming"); + }); + + return future; + }); + server.listen(); + + \endcode +*/ + +struct QResponseWatcher : public QFutureWatcher<QHttpServerResponse> +{ + Q_OBJECT + +public: + QResponseWatcher(QHttpServerResponder &&_responder) + : QFutureWatcher<QHttpServerResponse>(), + responder(std::move(_responder)) { + } + + QHttpServerResponder responder; +}; + +class QHttpServerFutureResponsePrivate : public QHttpServerResponsePrivate +{ +public: + QHttpServerFutureResponsePrivate(const QFuture<QHttpServerResponse> &futureResponse) + : QHttpServerResponsePrivate(), + futureResp(futureResponse) + { + } + + QFuture<QHttpServerResponse> futureResp; +}; + +/*! + Constructs a new QHttpServerFutureResponse with the \a future response. +*/ +QHttpServerFutureResponse::QHttpServerFutureResponse(const QFuture<QHttpServerResponse> &futureResp) + : QHttpServerFutureResponse(new QHttpServerFutureResponsePrivate{futureResp}) +{ +} + +/*! + \internal +*/ +QHttpServerFutureResponse::QHttpServerFutureResponse(QHttpServerFutureResponsePrivate *d) + : QHttpServerResponse(d) +{ +} + +/*! + \reimp +*/ +void QHttpServerFutureResponse::write(QHttpServerResponder &&responder) const +{ + if (!d_ptr->derived) { + QHttpServerResponse::write(std::move(responder)); + return; + } + + Q_D(const QHttpServerFutureResponse); + + auto socket = responder.socket(); + auto futureWatcher = new QResponseWatcher(std::move(responder)); + + QObject::connect(socket, &QObject::destroyed, + futureWatcher, &QObject::deleteLater); + QObject::connect(futureWatcher, &QFutureWatcherBase::finished, + socket, + [futureWatcher] () mutable { +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) + auto resp = futureWatcher->future().d.takeResult(); +#else + auto resp = futureWatcher->future().takeResult(); +#endif + resp.write(std::move(futureWatcher->responder)); + futureWatcher->deleteLater(); + }); + + futureWatcher->setFuture(d->futureResp); +} + +QT_END_NAMESPACE + +#include "qhttpserverfutureresponse.moc" diff --git a/src/httpserver/qhttpserverfutureresponse.h b/src/httpserver/qhttpserverfutureresponse.h new file mode 100644 index 0000000..45dc376 --- /dev/null +++ b/src/httpserver/qhttpserverfutureresponse.h @@ -0,0 +1,184 @@ +/**************************************************************************** +** +** Copyright (C) 2020 Mikhail Svetkin <mikhail.svetkin@gmail.com> +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtHttpServer module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:GPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 or (at your option) any later version +** approved by the KDE Free Qt Foundation. The licenses are as published by +** the Free Software Foundation and appearing in the file LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef QHTTPSERVERFUTURERESPONSE_H +#define QHTTPSERVERFUTURERESPONSE_H + +#include <QtHttpServer/qhttpserverresponse.h> + +#include <QtCore/qbytearray.h> +#include <QtCore/qfuture.h> + +#include <QtConcurrent> + +#include <mutex> + +QT_BEGIN_NAMESPACE + +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) + +template <> +class QFutureInterface<QHttpServerResponse> : public QFutureInterfaceBase +{ +public: + QFutureInterface(State initialState = NoState) + : QFutureInterfaceBase(initialState) + { + refT(); + } + QFutureInterface(const QFutureInterface &other) + : QFutureInterfaceBase(other) + { + refT(); + } + ~QFutureInterface() + { + if (!derefT()) + resultStoreBase().template clear<QHttpServerResponse>(); + } + + static QFutureInterface canceledResult() + { return QFutureInterface(State(Started | Finished | Canceled)); } + + QFutureInterface &operator=(const QFutureInterface &other) + { + other.refT(); + if (!derefT()) + resultStoreBase().template clear<QHttpServerResponse>(); + QFutureInterfaceBase::operator=(other); + return *this; + } + + inline QFuture<QHttpServerResponse> future() + { + return QFuture<QHttpServerResponse>(this); + } + + void reportAndMoveResult(QHttpServerResponse &&result, int index = -1) + { +#if QT_VERSION < QT_VERSION_CHECK(5, 14, 0) + std::lock_guard<QMutex> locker{*mutex()}; +#else + std::lock_guard<QMutex> locker{mutex(0)}; +#endif + if (queryState(Canceled) || queryState(Finished)) + return; + + QtPrivate::ResultStoreBase &store = resultStoreBase(); + + const int oldResultCount = store.count(); + const int insertIndex = store.addResult( + index, static_cast<void *>(new QHttpServerResponse(std::move_if_noexcept(result)))); + if (!store.filterMode() || oldResultCount < store.count()) // Let's make sure it's not in pending results. + reportResultsReady(insertIndex, store.count()); + } + + void reportFinished() + { + QFutureInterfaceBase::reportFinished(); + } + + QHttpServerResponse takeResult() + { + if (isCanceled()) { + exceptionStore().throwPossibleException(); + return QHttpServerResponse::StatusCode::NotFound; + } + + // Note: we wait for all, this is intentional, + // not to mess with other unready results. + waitForResult(-1); + +#if QT_VERSION < QT_VERSION_CHECK(5, 14, 0) + std::lock_guard<QMutex> locker{*mutex()}; +#else + std::lock_guard<QMutex> locker{mutex(0)}; +#endif + QtPrivate::ResultIteratorBase position = resultStoreBase().resultAt(0); + auto ret = std::move_if_noexcept( + *const_cast<QHttpServerResponse *>(position.pointer<QHttpServerResponse>())); + resultStoreBase().template clear<QHttpServerResponse>(); + + return ret; + } +}; + +#endif // QT_VERSION < QT_VERSION_CHECK(6, 0, 0) + +namespace QtConcurrent { + +template <> +class RunFunctionTask<QHttpServerResponse> : public RunFunctionTaskBase<QHttpServerResponse> +{ +public: + void run() override + { + if (this->isCanceled()) { + this->reportFinished(); + return; + } +#ifndef QT_NO_EXCEPTIONS + try { +#endif + this->runFunctor(); +#ifndef QT_NO_EXCEPTIONS + } catch (QException &e) { + QFutureInterface<QHttpServerResponse>::reportException(e); + } catch (...) { + QFutureInterface<QHttpServerResponse>::reportException(QUnhandledException()); + } +#endif + this->reportAndMoveResult(std::move_if_noexcept(result)); + this->reportFinished(); + } + + QHttpServerResponse result{QHttpServerResponse::StatusCode::NotFound}; +}; + +} + +class QHttpServerFutureResponsePrivate; +class Q_HTTPSERVER_EXPORT QHttpServerFutureResponse : public QHttpServerResponse +{ + Q_DECLARE_PRIVATE(QHttpServerFutureResponse) + +public: + using QHttpServerResponse::QHttpServerResponse; + + QHttpServerFutureResponse(const QFuture<QHttpServerResponse> &futureResponse); + + virtual void write(QHttpServerResponder &&responder) const override; + +protected: + QHttpServerFutureResponse(QHttpServerFutureResponsePrivate *d); +}; + +QT_END_NAMESPACE + +#endif // QHTTPSERVERFUTURERESPONSE_H diff --git a/src/httpserver/qhttpserverresponse.cpp b/src/httpserver/qhttpserverresponse.cpp index 5bd510b..47c3b49 100644 --- a/src/httpserver/qhttpserverresponse.cpp +++ b/src/httpserver/qhttpserverresponse.cpp @@ -41,6 +41,16 @@ QT_BEGIN_NAMESPACE +QHttpServerResponsePrivate::QHttpServerResponsePrivate( + QByteArray &&d, const QHttpServerResponse::StatusCode sc) + : data(std::move(d)), + statusCode(sc) +{ } + +QHttpServerResponsePrivate::QHttpServerResponsePrivate(const QHttpServerResponse::StatusCode sc) + : statusCode(sc) +{ } + QHttpServerResponse::QHttpServerResponse(QHttpServerResponse &&other) noexcept : d_ptr(other.d_ptr.take()) { @@ -100,35 +110,33 @@ QHttpServerResponse::QHttpServerResponse(const QJsonArray &data) QHttpServerResponse::QHttpServerResponse(const QByteArray &mimeType, const QByteArray &data, const StatusCode status) - : QHttpServerResponse(mimeType, - new QHttpServerResponsePrivate{data, status, {}}) + : d_ptr(new QHttpServerResponsePrivate(QByteArray(data), status)) { + setHeader(QHttpServerLiterals::contentTypeHeader(), mimeType); } QHttpServerResponse::QHttpServerResponse(QByteArray &&mimeType, const QByteArray &data, const StatusCode status) - : QHttpServerResponse(std::move(mimeType), - new QHttpServerResponsePrivate{data, status, {}}) + : d_ptr(new QHttpServerResponsePrivate(QByteArray(data), status)) { + setHeader(QHttpServerLiterals::contentTypeHeader(), std::move(mimeType)); } QHttpServerResponse::QHttpServerResponse(const QByteArray &mimeType, QByteArray &&data, const StatusCode status) - : QHttpServerResponse( - mimeType, - new QHttpServerResponsePrivate{std::move(data), status, {}}) + : d_ptr(new QHttpServerResponsePrivate(std::move(data), status)) { + setHeader(QHttpServerLiterals::contentTypeHeader(), mimeType); } QHttpServerResponse::QHttpServerResponse(QByteArray &&mimeType, QByteArray &&data, const StatusCode status) - : QHttpServerResponse( - std::move(mimeType), - new QHttpServerResponsePrivate{std::move(data), status, {}}) + : d_ptr(new QHttpServerResponsePrivate(std::move(data), status)) { + setHeader(QHttpServerLiterals::contentTypeHeader(), std::move(mimeType)); } QHttpServerResponse::~QHttpServerResponse() @@ -146,19 +154,10 @@ QHttpServerResponse QHttpServerResponse::fromFile(const QString &fileName) return QHttpServerResponse(mimeType, data); } -QHttpServerResponse::QHttpServerResponse(const QByteArray &mimeType, - QHttpServerResponsePrivate *d) +QHttpServerResponse::QHttpServerResponse(QHttpServerResponsePrivate *d) : d_ptr(d) { - setHeader(QHttpServerLiterals::contentTypeHeader(), mimeType); -} - -QHttpServerResponse::QHttpServerResponse(QByteArray &&mimeType, - QHttpServerResponsePrivate *d) - : d_ptr(d) -{ - setHeader(QHttpServerLiterals::contentTypeHeader(), - std::move(mimeType)); + d->derived = true; } /*! diff --git a/src/httpserver/qhttpserverresponse.h b/src/httpserver/qhttpserverresponse.h index df748cb..6d2bc1d 100644 --- a/src/httpserver/qhttpserverresponse.h +++ b/src/httpserver/qhttpserverresponse.h @@ -118,12 +118,8 @@ public: virtual void write(QHttpServerResponder &&responder) const; -private: - QHttpServerResponse(const QByteArray &mimeType, - QHttpServerResponsePrivate *d); - - QHttpServerResponse(QByteArray &&mimeType, - QHttpServerResponsePrivate *d); +protected: + QHttpServerResponse(QHttpServerResponsePrivate *d); QScopedPointer<QHttpServerResponsePrivate> d_ptr; }; diff --git a/src/httpserver/qhttpserverresponse_p.h b/src/httpserver/qhttpserverresponse_p.h index 8b81ad0..5011552 100644 --- a/src/httpserver/qhttpserverresponse_p.h +++ b/src/httpserver/qhttpserverresponse_p.h @@ -59,10 +59,16 @@ class QHttpServerResponsePrivate }; public: + explicit QHttpServerResponsePrivate() = default; + virtual ~QHttpServerResponsePrivate() = default; + + QHttpServerResponsePrivate(QByteArray &&d, const QHttpServerResponse::StatusCode sc); + QHttpServerResponsePrivate(const QHttpServerResponse::StatusCode sc); + QByteArray data; QHttpServerResponse::StatusCode statusCode; - std::unordered_multimap<QByteArray, QByteArray, HashHelper> headers; + bool derived{false}; }; QT_END_NAMESPACE diff --git a/src/httpserver/qhttpserverrouterviewtraits.h b/src/httpserver/qhttpserverrouterviewtraits.h index 2e812a5..b572675 100644 --- a/src/httpserver/qhttpserverrouterviewtraits.h +++ b/src/httpserver/qhttpserverrouterviewtraits.h @@ -151,6 +151,7 @@ template <typename ViewHandler, bool DisableStaticAssert = false> struct QHttpServerRouterViewTraits { using Helpers = typename QtPrivate::RouterViewTraitsHelper<ViewHandler, DisableStaticAssert>; + using ReturnType = typename Helpers::FunctionTraits::ReturnType; using Arguments = decltype(Helpers::Arguments::eval(typename Helpers::ArgumentIndexes{})); using BindableType = decltype( Helpers::template BindType<Arguments::CapturableCount>::eval( diff --git a/tests/auto/qhttpserver/tst_qhttpserver.cpp b/tests/auto/qhttpserver/tst_qhttpserver.cpp index c7176ff..cf5c43d 100644 --- a/tests/auto/qhttpserver/tst_qhttpserver.cpp +++ b/tests/auto/qhttpserver/tst_qhttpserver.cpp @@ -31,6 +31,10 @@ #include <QtHttpServer/qhttpserverrequest.h> #include <QtHttpServer/qhttpserverrouterrule.h> +#if QT_CONFIG(concurrent) +# include <QtHttpServer/qhttpserverfutureresponse.h> +#endif + #include <private/qhttpserverrouterrule_p.h> #include <private/qhttpserverliterals_p.h> @@ -296,6 +300,20 @@ void tst_QHttpServer::initTestCase() return std::move(resp); }); +#if QT_CONFIG(concurrent) + httpserver.route("/future/", [] (int id) -> QHttpServerFutureResponse { + if (id == 0) + return QHttpServerResponse::StatusCode::NotFound; + + auto future = QtConcurrent::run([] () { + QTest::qSleep(500); + return QHttpServerResponse("future is coming"); + }); + + return future; + }); +#endif + quint16 port = httpserver.listen(); if (!port) qCritical() << "Http server listen failed"; @@ -526,6 +544,20 @@ void tst_QHttpServer::routeGet_data() << "text/plain" << "part 1 of the message, part 2 of the message"; +#if QT_CONFIG(concurrent) + QTest::addRow("future") + << urlBase.arg("/future/1") + << 200 + << "text/plain" + << "future is coming"; + + QTest::addRow("future-not-found") + << urlBase.arg("/future/0") + << 404 + << "application/x-empty" + << ""; +#endif + #if QT_CONFIG(ssl) QTest::addRow("hello world, ssl")