diff --git a/include/QHttpEngine/qibytearray.h b/include/QHttpEngine/qibytearray.h index 63ee1fb..befd70a 100644 --- a/include/QHttpEngine/qibytearray.h +++ b/include/QHttpEngine/qibytearray.h @@ -40,6 +40,7 @@ class QHTTPENGINE_EXPORT QIByteArray : public QByteArray { public: + /// \{ QIByteArray() {} QIByteArray(const QByteArray &other) : QByteArray(other) {} QIByteArray(const QIByteArray &other) : QByteArray(other) {} @@ -55,6 +56,7 @@ public: bool contains(char c) const { return toLower().contains(tolower(c)); } bool contains(const char *c) const { return toLower().contains(QByteArray(c).toLower()); } bool contains(const QByteArray &a) const { return toLower().contains(a.toLower()); } + /// \} }; inline bool operator==(const QIByteArray &a1, const char *a2) { return a1.toLower() == QByteArray(a2).toLower(); } diff --git a/include/QHttpEngine/qobjecthandler.h b/include/QHttpEngine/qobjecthandler.h index 73e892c..06444ae 100644 --- a/include/QHttpEngine/qobjecthandler.h +++ b/include/QHttpEngine/qobjecthandler.h @@ -23,13 +23,12 @@ #ifndef QHTTPENGINE_QOBJECTHANDLER_H #define QHTTPENGINE_QOBJECTHANDLER_H -#include - #include -#include #include "qhttpengine_global.h" +class QHttpSocket; +class QJsonDocument; class QHTTPENGINE_EXPORT QObjectHandlerPrivate; /** @@ -85,8 +84,11 @@ public: * @brief Register a method * * This overload uses the traditional connection syntax with macros. + * + * The readAll parameter determines whether all data must be received by + * the socket before invoking the slot. */ - void registerMethod(const QString &name, QObject *receiver, const char *method); + void registerMethod(const QString &name, QObject *receiver, const char *method, bool readAll = true); #ifdef DOXYGEN /** @@ -94,26 +96,27 @@ public: * * This overload uses the new connection syntax with member pointers. */ - void registerMethod(const QString &name, QObject *receiver, PointerToMemberFunction method); + void registerMethod(const QString &name, QObject *receiver, PointerToMemberFunction method, bool readAll = true); /** * @brief Register a method * * This overload uses the new functor syntax (without context). */ - void registerMethod(const QString &name, Functor functor); + void registerMethod(const QString &name, Functor functor, bool readAll = true); /** * @brief Register a method * * This overload uses the new functor syntax (with context). */ - void registerMethod(const QString &name, QObject *receiver, Functor functor); + void registerMethod(const QString &name, QObject *receiver, Functor functor, bool readAll = true); #else template inline void registerMethod(const QString &name, typename QtPrivate::FunctionPointer::Object *receiver, - Func1 slot) { + Func1 slot, + bool readAll = true) { typedef QtPrivate::FunctionPointer SlotType; @@ -126,25 +129,39 @@ public: "The slot parameters do not match"); // Invoke the implementation - registerMethodImpl(name, receiver, new QtPrivate::QSlotObject(slot)); + registerMethodImpl(name, receiver, new QtPrivate::QSlotObject(slot), readAll); } template - inline void registerMethod(const QString &name, Func1 slot) { - registerMethod(name, Q_NULLPTR, slot); + inline typename QtPrivate::QEnableIf::value, void>::Type + registerMethod(const QString &name, Func1 slot, bool readAll = true) { + registerMethod(name, Q_NULLPTR, slot, readAll); } template inline typename QtPrivate::QEnableIf::IsPointerToMemberFunction && !QtPrivate::is_same::value, void>::Type - registerMethod(const QString &name, QObject *context, Func1 slot) { + registerMethod(const QString &name, QObject *context, Func1 slot, bool readAll = true) { // There is an easier way to do this but then the header wouldn't // compile on non-C++11 compilers - return registerMethod_functor(name, context, slot, &Func1::operator()); + return registerMethod_functor(name, context, slot, &Func1::operator(), readAll); } #endif + /** + * @brief Read the request data from the socket as a JSON document + * + * If an error occurs reading the request, an error is automatically + * written to the socket. + */ + static bool readJson(QHttpSocket *socket, QJsonDocument &document); + + /** + * @brief Write the response to the socket as a JSON document + */ + static void writeJson(QHttpSocket *socket, const QJsonDocument &document); + protected: /** @@ -155,7 +172,7 @@ protected: private: template - inline void registerMethod_functor(const QString &name, QObject *context, Func1 slot, Func1Operator) { + inline void registerMethod_functor(const QString &name, QObject *context, Func1 slot, Func1Operator, bool readAll) { typedef QtPrivate::FunctionPointer SlotType; @@ -168,10 +185,11 @@ private: "The slot parameters do not match"); registerMethodImpl(name, context, - new QtPrivate::QFunctorSlotObject(slot)); + new QtPrivate::QFunctorSlotObject(slot), + readAll); } - void registerMethodImpl(const QString &name, QObject *receiver, QtPrivate::QSlotObjectBase *slotObj); + void registerMethodImpl(const QString &name, QObject *receiver, QtPrivate::QSlotObjectBase *slotObj, bool readAll); QObjectHandlerPrivate *const d; friend class QObjectHandlerPrivate; diff --git a/src/qobjecthandler.cpp b/src/qobjecthandler.cpp index bbe9d20..8565aed 100644 --- a/src/qobjecthandler.cpp +++ b/src/qobjecthandler.cpp @@ -21,8 +21,10 @@ */ #include +#include #include +#include #include #include "qobjecthandler_p.h" @@ -39,16 +41,8 @@ QObjectHandler::QObjectHandler(QObject *parent) { } -void QObjectHandler::process(QHttpSocket *socket, const QString &path) +void QObjectHandlerPrivate::invokeSlot(QHttpSocket *socket, Method m) { - // Ensure the method has been registered - if (!d->map.contains(path)) { - socket->writeError(QHttpSocket::NotFound); - return; - } - - QObjectHandlerPrivate::Method m = d->map.value(path); - // Invoke the slot if (m.oldSlot) { @@ -83,12 +77,55 @@ void QObjectHandler::process(QHttpSocket *socket, const QString &path) } } -void QObjectHandler::registerMethod(const QString &name, QObject *receiver, const char *method) +bool QObjectHandler::readJson(QHttpSocket *socket, QJsonDocument &document) { - d->map.insert(name, QObjectHandlerPrivate::Method(receiver, method)); + QJsonParseError error; + document = QJsonDocument::fromJson(socket->readAll(), &error); + + if (error.error != QJsonParseError::NoError) { + socket->writeError(QHttpSocket::BadRequest); + return false; + } + + return true; } -void QObjectHandler::registerMethodImpl(const QString &name, QObject *receiver, QtPrivate::QSlotObjectBase *slotObj) +void QObjectHandler::writeJson(QHttpSocket *socket, const QJsonDocument &document) { - d->map.insert(name, QObjectHandlerPrivate::Method(receiver, slotObj)); + QByteArray data = document.toJson(); + socket->setHeader("Content-Length", QByteArray::number(data.length())); + socket->setHeader("Content-Type", "application/json"); + socket->write(data); + socket->close(); +} + +void QObjectHandler::process(QHttpSocket *socket, const QString &path) +{ + // Ensure the method has been registered + if (!d->map.contains(path)) { + socket->writeError(QHttpSocket::NotFound); + return; + } + + QObjectHandlerPrivate::Method m = d->map.value(path); + + // If the slot requires all data to be received, check to see if this is + // already the case, otherwise, wait until the rest of it arrives + if (!m.readAll || socket->bytesAvailable() >= socket->contentLength()) { + d->invokeSlot(socket, m); + } else { + connect(socket, &QHttpSocket::readChannelFinished, [this, socket, m]() { + d->invokeSlot(socket, m); + }); + } +} + +void QObjectHandler::registerMethod(const QString &name, QObject *receiver, const char *method, bool readAll) +{ + d->map.insert(name, QObjectHandlerPrivate::Method(receiver, method, readAll)); +} + +void QObjectHandler::registerMethodImpl(const QString &name, QObject *receiver, QtPrivate::QSlotObjectBase *slotObj, bool readAll) +{ + d->map.insert(name, QObjectHandlerPrivate::Method(receiver, slotObj, readAll)); } diff --git a/src/qobjecthandler_p.h b/src/qobjecthandler_p.h index c314b61..414f44d 100644 --- a/src/qobjecthandler_p.h +++ b/src/qobjecthandler_p.h @@ -43,10 +43,10 @@ public: class Method { public: Method() {} - Method(QObject *receiver, const char *method) - : receiver(receiver), oldSlot(true), slot(method) {} - Method(QObject *receiver, QtPrivate::QSlotObjectBase *slotObj) - : receiver(receiver), oldSlot(false), slot(slotObj) {} + Method(QObject *receiver, const char *method, bool readAll) + : receiver(receiver), oldSlot(true), slot(method), readAll(readAll) {} + Method(QObject *receiver, QtPrivate::QSlotObjectBase *slotObj, bool readAll) + : receiver(receiver), oldSlot(false), slot(slotObj), readAll(readAll) {} QObject *receiver; bool oldSlot; @@ -57,8 +57,11 @@ public: const char *method; QtPrivate::QSlotObjectBase *slotObj; } slot; + bool readAll; }; + void invokeSlot(QHttpSocket *socket, Method m); + QMap map; private: diff --git a/tests/TestQObjectHandler.cpp b/tests/TestQObjectHandler.cpp index 5e44867..5ec532f 100644 --- a/tests/TestQObjectHandler.cpp +++ b/tests/TestQObjectHandler.cpp @@ -20,6 +20,8 @@ * IN THE SOFTWARE. */ +#include +#include #include #include @@ -40,6 +42,12 @@ public Q_SLOTS: void valid(QHttpSocket *socket) { socket->writeError(QHttpSocket::OK); } + void echo(QHttpSocket *socket) { + QJsonDocument document; + if (QObjectHandler::readJson(socket, document)) { + QObjectHandler::writeJson(socket, document); + } + } }; class TestQObjectHandler : public QObject @@ -55,24 +63,46 @@ private Q_SLOTS: void TestQObjectHandler::testOldConnection_data() { + QTest::addColumn("sendObject"); + QTest::addColumn("object"); QTest::addColumn("slot"); QTest::addColumn("statusCode"); + QTest::newRow("invalid slot") + << false + << QJsonObject() + << QByteArray(SLOT(invalid())) + << static_cast(QHttpSocket::InternalServerError); + QTest::newRow("wrong argument count") + << false + << QJsonObject() << QByteArray(SLOT(wrongArgumentCount())) << static_cast(QHttpSocket::InternalServerError); QTest::newRow("wrong argument type") + << false + << QJsonObject() << QByteArray(SLOT(wrongArgumentType(int))) << static_cast(QHttpSocket::InternalServerError); QTest::newRow("valid") + << false + << QJsonObject() << QByteArray(SLOT(valid(QHttpSocket*))) << static_cast(QHttpSocket::OK); + + QTest::newRow("json") + << true + << QJsonObject{{"a", "b"}, {"c", 1}} + << QByteArray(SLOT(echo(QHttpSocket*))) + << static_cast(QHttpSocket::OK); } void TestQObjectHandler::testOldConnection() { + QFETCH(bool, sendObject); + QFETCH(QJsonObject, object); QFETCH(QByteArray, slot); QFETCH(int, statusCode); @@ -87,11 +117,28 @@ void TestQObjectHandler::testOldConnection() QSimpleHttpClient client(pair.client()); QHttpSocket socket(pair.server(), &pair); - client.sendHeaders("GET", "test"); + if (sendObject) { + QByteArray data = QJsonDocument(object).toJson(); + client.sendHeaders("POST", "test", QHttpSocket::QHttpHeaderMap{ + {"Content-Length", QByteArray::number(data.length())}, + {"Content-Type", "application/json"} + }); + client.sendData(data); + } else { + client.sendHeaders("GET", "test"); + } + QTRY_VERIFY(socket.isHeadersParsed()); handler.route(&socket, socket.path()); QTRY_COMPARE(client.statusCode(), statusCode); + + if (sendObject) { + QTRY_VERIFY(client.isDataReceived()); + + QJsonObject outObject = QJsonDocument::fromJson(client.data()).object(); + QCOMPARE(object, outObject); + } } void TestQObjectHandler::testNewConnection() diff --git a/tests/common/qsimplehttpclient.cpp b/tests/common/qsimplehttpclient.cpp index 9c06f24..b4e3b96 100644 --- a/tests/common/qsimplehttpclient.cpp +++ b/tests/common/qsimplehttpclient.cpp @@ -66,3 +66,9 @@ void QSimpleHttpClient::onReadyRead() } } } + +bool QSimpleHttpClient::isDataReceived() const +{ + return mHeaders.contains("Content-Length") && + mData.length() >= mHeaders.value("Content-Length").toInt(); +} diff --git a/tests/common/qsimplehttpclient.h b/tests/common/qsimplehttpclient.h index 00050bb..a698dac 100644 --- a/tests/common/qsimplehttpclient.h +++ b/tests/common/qsimplehttpclient.h @@ -64,6 +64,8 @@ public: return mData; } + bool isDataReceived() const; + private Q_SLOTS: void onReadyRead();