diff options
author | Lorenz Haas <lorenz.haas@histomatics.de> | 2017-11-13 22:26:25 +0100 |
---|---|---|
committer | Maurice Kalinowski <maurice.kalinowski@qt.io> | 2017-11-15 13:18:31 +0000 |
commit | 6eca6f914d3f7704e7bd38f3b48f4c7a3f817625 (patch) | |
tree | 7a0a6a973e1b511bad406114ad1ce67c0f21f521 | |
parent | b9369efc1ea0fdba5a92ef70ec24b9f3c6edc3cd (diff) |
Introduce specific types for topic names and filters
For "topics" the standard defines topic names and topic filters with
specific characteristics. QMqttTopicName and QMqttTopicFilter implement
this requirements.
[ChangeLog][General] Added QMqttTopicName and QMqttTopicFilter
Change-Id: Ie2b6851ec9249f20d05c4b8df3c2f27afc2be4b9
Reviewed-by: hjk <hjk@qt.io>
26 files changed, 1116 insertions, 95 deletions
diff --git a/examples/mqtt/quicksubscription/qmlmqttclient.h b/examples/mqtt/quicksubscription/qmlmqttclient.h index ffc45ea..2e1041b 100644 --- a/examples/mqtt/quicksubscription/qmlmqttclient.h +++ b/examples/mqtt/quicksubscription/qmlmqttclient.h @@ -60,7 +60,7 @@ class QmlMqttClient; class QmlMqttSubscription : public QObject { Q_OBJECT - Q_PROPERTY(QString topic MEMBER m_topic NOTIFY topicChanged) + Q_PROPERTY(QMqttTopicFilter topic MEMBER m_topic NOTIFY topicChanged) public: QmlMqttSubscription(QMqttSubscription *s, QmlMqttClient *c); ~QmlMqttSubscription(); @@ -76,7 +76,7 @@ private: Q_DISABLE_COPY(QmlMqttSubscription) QMqttSubscription *sub; QmlMqttClient *client; - QString m_topic; + QMqttTopicFilter m_topic; }; class QmlMqttClient : public QMqttClient diff --git a/examples/mqtt/simpleclient/mainwindow.cpp b/examples/mqtt/simpleclient/mainwindow.cpp index 4f5e575..e40820b 100644 --- a/examples/mqtt/simpleclient/mainwindow.cpp +++ b/examples/mqtt/simpleclient/mainwindow.cpp @@ -68,10 +68,10 @@ MainWindow::MainWindow(QWidget *parent) : connect(m_client, &QMqttClient::stateChanged, this, &MainWindow::updateLogStateChange); connect(m_client, &QMqttClient::disconnected, this, &MainWindow::brokerDisconnected); - connect(m_client, &QMqttClient::messageReceived, this, [this](const QByteArray &message, const QString &topic) { + connect(m_client, &QMqttClient::messageReceived, this, [this](const QByteArray &message, const QMqttTopicName &topic) { const QString content = QDateTime::currentDateTime().toString() + QLatin1String(" Received Topic: ") - + topic + + topic.name() + QLatin1String(" Message: ") + message + QLatin1Char('\n'); diff --git a/examples/mqtt/subscriptions/mainwindow.cpp b/examples/mqtt/subscriptions/mainwindow.cpp index 499d46a..e8fea99 100644 --- a/examples/mqtt/subscriptions/mainwindow.cpp +++ b/examples/mqtt/subscriptions/mainwindow.cpp @@ -69,10 +69,10 @@ MainWindow::MainWindow(QWidget *parent) : connect(m_client, &QMqttClient::stateChanged, this, &MainWindow::updateLogStateChange); connect(m_client, &QMqttClient::disconnected, this, &MainWindow::brokerDisconnected); - connect(m_client, &QMqttClient::messageReceived, this, [this](const QByteArray &message, const QString &topic) { + connect(m_client, &QMqttClient::messageReceived, this, [this](const QByteArray &message, const QMqttTopicName &topic) { const QString content = QDateTime::currentDateTime().toString() + QLatin1String(" Received Topic: ") - + topic + + topic.name() + QLatin1String(" Message: ") + message + QLatin1Char('\n'); @@ -156,7 +156,7 @@ void MainWindow::on_buttonSubscribe_clicked() return; } auto subWindow = new SubscriptionWindow(subscription); - subWindow->setWindowTitle(subscription->topic()); + subWindow->setWindowTitle(subscription->topic().filter()); subWindow->show(); } diff --git a/examples/mqtt/subscriptions/subscriptionwindow.cpp b/examples/mqtt/subscriptions/subscriptionwindow.cpp index 0b6e0ba..5a87a51 100644 --- a/examples/mqtt/subscriptions/subscriptionwindow.cpp +++ b/examples/mqtt/subscriptions/subscriptionwindow.cpp @@ -58,7 +58,7 @@ SubscriptionWindow::SubscriptionWindow(QMqttSubscription *sub, QWidget *parent) { ui->setupUi(this); - ui->labelSub->setText(m_sub->topic()); + ui->labelSub->setText(m_sub->topic().filter()); ui->labelQoS->setText(QString::number(m_sub->qos())); updateStatus(m_sub->state()); connect(m_sub, &QMqttSubscription::messageReceived, this, &SubscriptionWindow::updateMessage); diff --git a/src/mqtt/doc/qtmqtt.qdocconf b/src/mqtt/doc/qtmqtt.qdocconf index 6c62142..6fc50b4 100644 --- a/src/mqtt/doc/qtmqtt.qdocconf +++ b/src/mqtt/doc/qtmqtt.qdocconf @@ -37,6 +37,8 @@ imagedirs += images \ ../../../examples/mqtt/doc/images excludedirs += ../qt4support +Cpp.ignoretokens += Q_MQTT_EXPORT + depends += qtcore qtdoc qtnetwork qmake qtwebsockets #add generic thumbnail images for example documentation that does not have an image. diff --git a/src/mqtt/mqtt.pro b/src/mqtt/mqtt.pro index 3e71e07..c7866ba 100644 --- a/src/mqtt/mqtt.pro +++ b/src/mqtt/mqtt.pro @@ -11,7 +11,9 @@ PUBLIC_HEADERS += \ qmqttglobal.h \ qmqttclient.h \ qmqttmessage.h \ - qmqttsubscription.h + qmqttsubscription.h \ + qmqtttopicfilter.h \ + qmqtttopicname.h PRIVATE_HEADERS += \ qmqttclient_p.h \ @@ -23,8 +25,10 @@ SOURCES += \ qmqttclient.cpp \ qmqttconnection.cpp \ qmqttcontrolpacket.cpp \ + qmqttmessage.cpp \ qmqttsubscription.cpp \ - qmqttmessage.cpp + qmqtttopicfilter.cpp \ + qmqtttopicname.cpp HEADERS += $$PUBLIC_HEADERS $$PRIVATE_HEADERS diff --git a/src/mqtt/qmqttclient.cpp b/src/mqtt/qmqttclient.cpp index c61e32e..22ee311 100644 --- a/src/mqtt/qmqttclient.cpp +++ b/src/mqtt/qmqttclient.cpp @@ -236,7 +236,7 @@ QT_BEGIN_NAMESPACE */ /*! - \fn QMqttClient::messageReceived(const QByteArray &message, const QString &topic) + \fn QMqttClient::messageReceived(const QByteArray &message, const QMqttTopicName &topic) This signal is emitted when a new message has been received. The category of the message is specified by \a topic with the content being \a message. @@ -313,7 +313,7 @@ QIODevice *QMqttClient::transport() const is subscribed twice, the return value points to the same subscription instance. The MQTT client is the owner of the subscription. */ -QMqttSubscription *QMqttClient::subscribe(const QString &topic, quint8 qos) +QMqttSubscription *QMqttClient::subscribe(const QMqttTopicFilter &topic, quint8 qos) { Q_D(QMqttClient); @@ -330,7 +330,7 @@ QMqttSubscription *QMqttClient::subscribe(const QString &topic, quint8 qos) \note If a client disconnects from a broker without unsubscribing, the broker will store all messages and publish them on the next reconnect. */ -void QMqttClient::unsubscribe(const QString &topic) +void QMqttClient::unsubscribe(const QMqttTopicFilter &topic) { Q_D(QMqttClient); d->m_connection.sendControlUnsubscribe(topic); @@ -345,7 +345,7 @@ void QMqttClient::unsubscribe(const QString &topic) Returns an ID that is used internally to identify the message. */ -qint32 QMqttClient::publish(const QString &topic, const QByteArray &message, quint8 qos, bool retain) +qint32 QMqttClient::publish(const QMqttTopicName &topic, const QByteArray &message, quint8 qos, bool retain) { Q_D(QMqttClient); if (qos > 2) diff --git a/src/mqtt/qmqttclient.h b/src/mqtt/qmqttclient.h index 9a6990d..873f937 100644 --- a/src/mqtt/qmqttclient.h +++ b/src/mqtt/qmqttclient.h @@ -32,6 +32,7 @@ #include <QtMqtt/qmqttglobal.h> #include <QtMqtt/QMqttSubscription> +#include <QtMqtt/QMqttTopicFilter> #include <QtCore/QIODevice> #include <QtCore/QObject> @@ -97,10 +98,10 @@ public: void setTransport(QIODevice *device, TransportType transport); QIODevice *transport() const; - QMqttSubscription *subscribe(const QString &topic, quint8 qos = 0); - void unsubscribe(const QString &topic); + QMqttSubscription *subscribe(const QMqttTopicFilter &topic, quint8 qos = 0); + void unsubscribe(const QMqttTopicFilter &topic); - Q_INVOKABLE qint32 publish(const QString &topic, const QByteArray &message = QByteArray(), + Q_INVOKABLE qint32 publish(const QMqttTopicName &topic, const QByteArray &message = QByteArray(), quint8 qos = 0, bool retain = false); bool requestPing(); @@ -131,7 +132,7 @@ public: Q_SIGNALS: void connected(); void disconnected(); - void messageReceived(const QByteArray &message, const QString &topic = QString()); + void messageReceived(const QByteArray &message, const QMqttTopicName &topic = QMqttTopicName()); void messageSent(qint32 id); void pingResponseReceived(); void brokerSessionRestored(); diff --git a/src/mqtt/qmqttconnection.cpp b/src/mqtt/qmqttconnection.cpp index a9c1b35..40e3908 100644 --- a/src/mqtt/qmqttconnection.cpp +++ b/src/mqtt/qmqttconnection.cpp @@ -245,12 +245,12 @@ bool QMqttConnection::sendControlConnect() return true; } -qint32 QMqttConnection::sendControlPublish(const QString &topic, const QByteArray &message, quint8 qos, bool retain) +qint32 QMqttConnection::sendControlPublish(const QMqttTopicName &topic, const QByteArray &message, quint8 qos, bool retain) { qCDebug(lcMqttConnection) << Q_FUNC_INFO << topic << " Size:" << message.size() << " bytes." << "QoS:" << qos << " Retain:" << retain; - if (topic.contains(QLatin1Char('#')) || topic.contains('+')) + if (!topic.isValid()) return -1; quint8 header = QMqttControlPacket::PUBLISH; @@ -263,15 +263,7 @@ qint32 QMqttConnection::sendControlPublish(const QString &topic, const QByteArra header |= 0x01; QSharedPointer<QMqttControlPacket> packet(new QMqttControlPacket(header)); - - QByteArray topicArray = topic.toUtf8(); - const std::uint16_t u16max = std::numeric_limits<std::uint16_t>::max(); - if (topicArray.size() > u16max) { - qWarning("Published topic is too long. Need to truncate"); - topicArray.truncate(u16max); - } - - packet->append(topicArray); + packet->append(topic.name().toUtf8()); quint16 identifier = 0; if (qos > 0) { identifier = unusedPacketIdentifier(); @@ -322,7 +314,7 @@ bool QMqttConnection::sendControlPublishComp(quint16 id) return writePacketToTransport(packet); } -QMqttSubscription *QMqttConnection::sendControlSubscribe(const QString &topic, quint8 qos) +QMqttSubscription *QMqttConnection::sendControlSubscribe(const QMqttTopicFilter &topic, quint8 qos) { qCDebug(lcMqttConnection) << Q_FUNC_INFO << " Topic:" << topic << " qos:" << qos; @@ -340,13 +332,12 @@ QMqttSubscription *QMqttConnection::sendControlSubscribe(const QString &topic, q packet.append(identifier); // Overflow protection - QByteArray topicArray = topic.toUtf8(); - if (topicArray.size() > std::numeric_limits<std::uint16_t>::max()) { - qWarning("Subscribed topic is too long."); + if (!topic.isValid()) { + qWarning("Subscribed topic filter is not valid."); return nullptr; } - packet.append(topicArray); + packet.append(topic.filter().toUtf8()); switch (qos) { case 0: packet.append(char(0x0)); break; @@ -356,7 +347,7 @@ QMqttSubscription *QMqttConnection::sendControlSubscribe(const QString &topic, q } auto result = new QMqttSubscription(this); - result->setTopic(QString::fromUtf8(topicArray)); + result->setTopic(topic); result->setClient(m_clientPrivate->m_client); result->setQos(qos); result->setState(QMqttSubscription::SubscriptionPending); @@ -372,12 +363,12 @@ QMqttSubscription *QMqttConnection::sendControlSubscribe(const QString &topic, q return result; } -bool QMqttConnection::sendControlUnsubscribe(const QString &topic) +bool QMqttConnection::sendControlUnsubscribe(const QMqttTopicFilter &topic) { qCDebug(lcMqttConnection) << Q_FUNC_INFO << " Topic:" << topic; // MQTT-3.10.3-2 - if (topic.isEmpty()) + if (!topic.isValid()) return false; if (!m_activeSubscriptions.contains(topic)) @@ -398,7 +389,7 @@ bool QMqttConnection::sendControlUnsubscribe(const QString &topic) packet.append(identifier); - packet.append(topic.toUtf8()); + packet.append(topic.filter().toUtf8()); auto sub = m_activeSubscriptions[topic]; sub->setState(QMqttSubscription::UnsubscriptionPending); @@ -619,7 +610,7 @@ void QMqttConnection::finalize_publish() { // String topic const quint16 topicLength = qFromBigEndian<quint16>(reinterpret_cast<const quint16 *>(readBuffer(2).constData())); - const QString topic = QString::fromUtf8(reinterpret_cast<const char *>(readBuffer(topicLength).constData())); + const QMqttTopicName topic = QString::fromUtf8(reinterpret_cast<const char *>(readBuffer(topicLength).constData())); quint16 id = 0; if (m_currentPublish.qos > 0) { @@ -639,33 +630,8 @@ void QMqttConnection::finalize_publish() m_currentPublish.dup, m_currentPublish.retain); for (auto sub = m_activeSubscriptions.constBegin(); sub != m_activeSubscriptions.constEnd(); sub++) { - const QString subTopic = sub.key(); - - if (subTopic == topic) { - emit sub.value()->messageReceived(qmsg); - continue; - } else if (subTopic.endsWith(QLatin1Char('#')) && topic.startsWith(subTopic.leftRef(subTopic.size() - 1))) { + if (sub.key().match(topic)) emit sub.value()->messageReceived(qmsg); - continue; - } - - if (!subTopic.contains(QLatin1Char('+'))) - continue; - - const QVector<QStringRef> subTopicSplit = subTopic.splitRef(QLatin1Char('/')); - const QVector<QStringRef> topicSplit = topic.splitRef(QLatin1Char('/')); - if (subTopicSplit.size() != topicSplit.size()) - continue; - bool match = true; - for (int i = 0; i < subTopicSplit.size() && match; ++i) { - if (subTopicSplit.at(i) == QLatin1Char('+') || subTopicSplit.at(i) == topicSplit.at(i)) - continue; - match = false; - } - - if (match) { - emit sub.value()->messageReceived(qmsg); - } } if (m_currentPublish.qos == 1) diff --git a/src/mqtt/qmqttconnection_p.h b/src/mqtt/qmqttconnection_p.h index 0a2d676..128573c 100644 --- a/src/mqtt/qmqttconnection_p.h +++ b/src/mqtt/qmqttconnection_p.h @@ -77,13 +77,13 @@ public: bool ensureTransportOpen(const QString &sslPeerName = QString()); bool sendControlConnect(); - qint32 sendControlPublish(const QString &topic, const QByteArray &message, quint8 qos = 0, bool retain = false); + qint32 sendControlPublish(const QMqttTopicName &topic, const QByteArray &message, quint8 qos = 0, bool retain = false); bool sendControlPublishAcknowledge(quint16 id); bool sendControlPublishRelease(quint16 id); bool sendControlPublishReceive(quint16 id); bool sendControlPublishComp(quint16 id); - QMqttSubscription *sendControlSubscribe(const QString &topic, quint8 qos = 0); - bool sendControlUnsubscribe(const QString &topic); + QMqttSubscription *sendControlSubscribe(const QMqttTopicFilter &topic, quint8 qos = 0); + bool sendControlUnsubscribe(const QMqttTopicFilter &topic); bool sendControlPingRequest(); bool sendControlDisconnect(); @@ -129,7 +129,7 @@ private: bool writePacketToTransport(const QMqttControlPacket &p); QMap<quint16, QMqttSubscription *> m_pendingSubscriptionAck; QMap<quint16, QMqttSubscription *> m_pendingUnsubscriptions; - QMap<QString, QMqttSubscription *> m_activeSubscriptions; + QMap<QMqttTopicFilter, QMqttSubscription *> m_activeSubscriptions; QMap<quint16, QSharedPointer<QMqttControlPacket>> m_pendingMessages; QMap<quint16, QSharedPointer<QMqttControlPacket>> m_pendingReleaseMessages; InternalConnectionState m_internalState{BrokerDisconnected}; diff --git a/src/mqtt/qmqttmessage.cpp b/src/mqtt/qmqttmessage.cpp index 1bdbdf7..7f4cc04 100644 --- a/src/mqtt/qmqttmessage.cpp +++ b/src/mqtt/qmqttmessage.cpp @@ -101,7 +101,7 @@ quint16 QMqttMessage::id() const return m_id; } -QString QMqttMessage::topic() const +QMqttTopicName QMqttMessage::topic() const { return m_topic; } @@ -116,7 +116,7 @@ bool QMqttMessage::retain() const return m_retain; } -QMqttMessage::QMqttMessage(const QString &topic, const QByteArray &content, quint16 id, quint8 qos, bool dup, bool retain) +QMqttMessage::QMqttMessage(const QMqttTopicName &topic, const QByteArray &content, quint16 id, quint8 qos, bool dup, bool retain) : m_topic(topic) , m_payload(content) , m_id(id) diff --git a/src/mqtt/qmqttmessage.h b/src/mqtt/qmqttmessage.h index 3f25d08..cedd81a 100644 --- a/src/mqtt/qmqttmessage.h +++ b/src/mqtt/qmqttmessage.h @@ -32,6 +32,8 @@ #include "qmqttglobal.h" +#include <QtMqtt/QMqttTopicName> + #include <QtCore/QObject> QT_BEGIN_NAMESPACE @@ -39,7 +41,7 @@ QT_BEGIN_NAMESPACE class Q_MQTT_EXPORT QMqttMessage { Q_GADGET - Q_PROPERTY(QString topic READ topic CONSTANT) + Q_PROPERTY(QMqttTopicName topic READ topic CONSTANT) Q_PROPERTY(QByteArray payload READ payload CONSTANT) Q_PROPERTY(quint16 id READ id CONSTANT) Q_PROPERTY(quint8 qos READ qos CONSTANT) @@ -49,16 +51,16 @@ public: QByteArray payload() const; quint8 qos() const; quint16 id() const; - QString topic() const; + QMqttTopicName topic() const; bool duplicate() const; bool retain() const; private: friend class QMqttConnection; - explicit QMqttMessage(const QString &topic, const QByteArray &payload, + explicit QMqttMessage(const QMqttTopicName &topic, const QByteArray &payload, quint16 id, quint8 qos, bool dup, bool retain); - QString m_topic; + QMqttTopicName m_topic; QByteArray m_payload; quint16 m_id; quint8 m_qos; diff --git a/src/mqtt/qmqttsubscription.cpp b/src/mqtt/qmqttsubscription.cpp index 80e8aed..750707f 100644 --- a/src/mqtt/qmqttsubscription.cpp +++ b/src/mqtt/qmqttsubscription.cpp @@ -108,7 +108,7 @@ QMqttSubscription::SubscriptionState QMqttSubscription::state() const return d->m_state; } -QString QMqttSubscription::topic() const +QMqttTopicFilter QMqttSubscription::topic() const { Q_D(const QMqttSubscription); return d->m_topic; @@ -143,7 +143,7 @@ void QMqttSubscription::unsubscribe() setState(Unsubscribed); } -void QMqttSubscription::setTopic(const QString &topic) +void QMqttSubscription::setTopic(const QMqttTopicFilter &topic) { Q_D(QMqttSubscription); d->m_topic = topic; diff --git a/src/mqtt/qmqttsubscription.h b/src/mqtt/qmqttsubscription.h index a4dc53b..054e4ea 100644 --- a/src/mqtt/qmqttsubscription.h +++ b/src/mqtt/qmqttsubscription.h @@ -33,6 +33,8 @@ #include "qmqttmessage.h" #include <QtMqtt/qmqttglobal.h> +#include <QtMqtt/QMqttTopicFilter> + #include <QtCore/QObject> QT_BEGIN_NAMESPACE @@ -46,7 +48,7 @@ class Q_MQTT_EXPORT QMqttSubscription : public QObject Q_ENUMS(SubscriptionState) Q_PROPERTY(SubscriptionState state READ state NOTIFY stateChanged) Q_PROPERTY(quint8 qos READ qos NOTIFY qosChanged) - Q_PROPERTY(QString topic READ topic) + Q_PROPERTY(QMqttTopicFilter topic READ topic) public: ~QMqttSubscription() override; enum SubscriptionState { @@ -58,7 +60,7 @@ public: }; SubscriptionState state() const; - QString topic() const; + QMqttTopicFilter topic() const; quint8 qos() const; Q_SIGNALS: @@ -73,7 +75,7 @@ private: Q_DECLARE_PRIVATE(QMqttSubscription) Q_DISABLE_COPY(QMqttSubscription) void setState(SubscriptionState state); - void setTopic(const QString &topic); + void setTopic(const QMqttTopicFilter &topic); void setClient(QMqttClient *client); void setQos(quint8 qos); friend class QMqttConnection; diff --git a/src/mqtt/qmqttsubscription_p.h b/src/mqtt/qmqttsubscription_p.h index 104c4fc..89ffdf0 100644 --- a/src/mqtt/qmqttsubscription_p.h +++ b/src/mqtt/qmqttsubscription_p.h @@ -54,7 +54,7 @@ public: ~QMqttSubscriptionPrivate() override = default; QMqttClient *m_client{nullptr}; QMqttSubscription::SubscriptionState m_state{QMqttSubscription::Unsubscribed}; - QString m_topic; + QMqttTopicFilter m_topic; quint8 m_qos{0}; }; diff --git a/src/mqtt/qmqtttopicfilter.cpp b/src/mqtt/qmqtttopicfilter.cpp new file mode 100644 index 0000000..d2071c7 --- /dev/null +++ b/src/mqtt/qmqtttopicfilter.cpp @@ -0,0 +1,314 @@ +/****************************************************************************** +** +** Copyright (C) 2017 Lorenz Haas +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtMqtt module. +** +** $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 "qmqtttopicfilter.h" + +#include <QtCore/QDebug> +#include <QtCore/QVector> + +QT_BEGIN_NAMESPACE + +/*! + \class QMqttTopicFilter + \inmodule QtMqtt + \reentrant + \ingroup shared + + \brief The QMqttTopicFilter class represents a MQTT topic filter. + + QMqttTopicFilter is a thin wrapper around a QString providing an expressive + data type for MQTT topic filters. Beside the benefits of having a strong + type preventing unintended misuse, QMqttTopicFilter provides convenient + functions related to topic filters like isValid() or match(). + + For example, the following code would fail to compile and prevent a possible + unintended and meaningless matching of two filters, especially if the + variable names were less expressive: + + \code + QMqttTopicFilter globalFilter{"foo/#"}; + QMqttTopicFilter specificFilter{"foo/bar"}; + if (globalFilter.match(specificFilter)) { + //... + } + \endcode + + The usability, however, is not affected since the following snippet compiles + and runs as expected: + + \code + QMqttTopicFilter globalFilter{"foo/#"}; + if (globalFilter.match("foo/bar")) { + //... + } + \endcode + + \sa QMqttTopicName + */ + +/*! + \fn void QMqttTopicFilter::swap(QMqttTopicFilter &other) + Swaps the MQTT topic filter \a other with this MQTT topic filter. This + operation is very fast and never fails. + */ + +/*! + \enum QMqttTopicFilter::MatchOption + + This enum value holds the matching options for the topic filter. + + \value NoMatchOption + No match options are set. + \value WildcardsDontMatchDollarTopicMatchOption + A wildcard at the filter's beginning does not match a topic name that + starts with the dollar sign ($). + */ + +class QMqttTopicFilterPrivate : public QSharedData +{ +public: + QString filter; +}; + +/*! + Creates a new MQTT topic filter with the specified \a filter. + */ +QMqttTopicFilter::QMqttTopicFilter(const QString &filter) : d(new QMqttTopicFilterPrivate) +{ + d->filter = filter; +} + +/*! + Creates a new MQTT topic filter with the specified \a filter. + */ +QMqttTopicFilter::QMqttTopicFilter(const QLatin1String &filter) : d(new QMqttTopicFilterPrivate) +{ + d->filter = filter; +} + +/*! + Creates a new MQTT topic filter as a copy of \a filter. + */ +QMqttTopicFilter::QMqttTopicFilter(const QMqttTopicFilter &filter) : d(filter.d) +{ +} + +/*! + Destroys the QMqttTopicFilter object. + */ +QMqttTopicFilter::~QMqttTopicFilter() +{ +} + +/*! + Assigns the MQTT topic filter \a filter to this object, and returns a + reference to the copy. + */ +QMqttTopicFilter &QMqttTopicFilter::operator=(const QMqttTopicFilter &filter) +{ + d = filter.d; + return *this; +} + +/*! + Returns the topic filter. + */ +QString QMqttTopicFilter::filter() const +{ + return d->filter; +} + +/*! + Sets the topic filter to \a filter. + */ +void QMqttTopicFilter::setFilter(const QString &filter) +{ + d.detach(); + d->filter = filter; +} + +/*! + Returns \c true if the topic filter is valid according to the MQTT standard + section 4.7, or \c false otherwise. + */ +bool QMqttTopicFilter::isValid() const +{ + // MQTT-4.7.3-1, MQTT-4.7.3-3, and MQTT-4.7.3-2 + const int size = d->filter.size(); + if (size == 0 || size > 65535 || d->filter.contains(QChar(QChar::Null))) + return false; + + if (size == 1) + return true; + + // '#' MUST be last and its own level. It MUST NOT appear more than at most once. + const int multiLevelPosition = d->filter.indexOf(QLatin1Char('#')); + if (multiLevelPosition != -1 + && (multiLevelPosition != size - 1 || d->filter.at(size-2) != QLatin1Char('/'))) { + return false; + } + + // '+' MAY occur multiple times but MUST be its own level. + int singleLevelPosition = d->filter.indexOf(QLatin1Char('+')); + while (singleLevelPosition != -1) { + if ((singleLevelPosition != 0 && d->filter.at(singleLevelPosition - 1) != QLatin1Char('/')) + || (singleLevelPosition < size - 1 && d->filter.at(singleLevelPosition + 1) != QLatin1Char('/'))) { + return false; + } + singleLevelPosition = d->filter.indexOf(QLatin1Char('#'), singleLevelPosition + 1); + } + + return true; +} + +/*! + Returns \c true if the topic filter matches the topic name \a name + honoring the given \a matchOptions, or \c false otherwise. + */ +bool QMqttTopicFilter::match(const QMqttTopicName &name, MatchOptions matchOptions) const +{ + if (!name.isValid() || !isValid()) + return false; + + const QString topic = name.name(); + if (topic == d->filter) + return true; + + if (matchOptions.testFlag(WildcardsDontMatchDollarTopicMatchOption) + && topic.startsWith(QLatin1Char('$')) + && (d->filter.startsWith(QLatin1Char('+')) + || d->filter == QLatin1Char('#') + || d->filter == QLatin1String("/#"))) { + return false; + } + + if (d->filter.endsWith(QLatin1Char('#'))) { + QStringRef root = d->filter.leftRef(d->filter.size() - 1); + if (root.endsWith(QLatin1Char('/'))) // '#' also represents the parent level! + root = root.left(root.size() - 1); + return topic.startsWith(root); + } + + if (d->filter.contains(QLatin1Char('+'))) { + const QVector<QStringRef> filterLevels = d->filter.splitRef(QLatin1Char('/')); + const QVector<QStringRef> topicLevels = topic.splitRef(QLatin1Char('/')); + if (filterLevels.size() != topicLevels.size()) + return false; + for (int i = 0; i < filterLevels.size(); ++i) { + const QStringRef &level = filterLevels.at(i); + if (level != QLatin1Char('+') && level != topicLevels.at(i)) + return false; + } + return true; + } + + return false; +} + +/*! + \relates QMqttTopicFilter + + Returns \c true if the topic filters \a lhs and \a rhs are equal, + otherwise returns \c false. + */ +bool operator==(const QMqttTopicFilter &lhs, const QMqttTopicFilter &rhs) Q_DECL_NOTHROW +{ + return (lhs.d == rhs.d) || (lhs.d->filter == rhs.d->filter); +} + +/*! + \fn bool operator!=(const QMqttTopicFilter &lhs, const QMqttTopicFilter &rhs) + \relates QMqttTopicFilter + + Returns \c true if the topic filters \a lhs and \a rhs are different, + otherwise returns \c false. + */ + +/*! + \relates QMqttTopicFilter + + Returns \c true if the topic filter \a lhs is lexically less than the topic + filter \a rhs; otherwise returns \c false. + */ +bool operator<(const QMqttTopicFilter &lhs, const QMqttTopicFilter &rhs) Q_DECL_NOTHROW +{ + return lhs.d->filter < rhs.d->filter; +} + +/*! + \relates QHash + + Returns the hash value for \a filter. If specified, \a seed is used to + initialize the hash. +*/ +uint qHash(const QMqttTopicFilter &filter, uint seed) Q_DECL_NOTHROW +{ + return qHash(filter.d->filter, seed); +} + +#ifndef QT_NO_DATASTREAM +/*! \relates QMqttTopicFilter + + Writes the topic filter \a filter to the stream \a out and returns a + reference to the stream. + + \sa{Serializing Qt Data Types}{Format of the QDataStream operators} +*/ +QDataStream &operator<<(QDataStream &out, const QMqttTopicFilter &filter) +{ + out << filter.filter(); + return out; +} + +/*! \relates QMqttTopicFilter + + Reads a topic filter into \a filter from the stream \a in and returns a + reference to the stream. + + \sa{Serializing Qt Data Types}{Format of the QDataStream operators} +*/ +QDataStream &operator>>(QDataStream &in, QMqttTopicFilter &filter) +{ + QString f; + in >> f; + filter.setFilter(f); + return in; +} +#endif // QT_NO_DATASTREAM + +#ifndef QT_NO_DEBUG_STREAM +QDebug operator<<(QDebug d, const QMqttTopicFilter &filter) +{ + QDebugStateSaver saver(d); + d.nospace() << "QMqttTopicFilter(" << filter.filter() << ')'; + return d; +} +#endif + +QT_END_NAMESPACE diff --git a/src/mqtt/qmqtttopicfilter.h b/src/mqtt/qmqtttopicfilter.h new file mode 100644 index 0000000..8834a9a --- /dev/null +++ b/src/mqtt/qmqtttopicfilter.h @@ -0,0 +1,101 @@ +/****************************************************************************** +** +** Copyright (C) 2017 Lorenz Haas +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtMqtt module. +** +** $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 QMQTTTOPICFILTER_H +#define QMQTTTOPICFILTER_H + +#include "qmqttglobal.h" + +#include <QtMqtt/QMqttTopicName> + +#include <QtCore/QExplicitlySharedDataPointer> +#include <QtCore/QMetaType> +#include <QtCore/QString> + +QT_BEGIN_NAMESPACE + +class QMqttTopicFilterPrivate; + +class QMqttTopicFilter; +// qHash is a friend, but we can't use default arguments for friends (§8.3.6.4) +Q_MQTT_EXPORT uint qHash(const QMqttTopicFilter &name, uint seed = 0) Q_DECL_NOTHROW; + +class Q_MQTT_EXPORT QMqttTopicFilter +{ +public: + enum MatchOption { + NoMatchOption = 0x0000, + WildcardsDontMatchDollarTopicMatchOption = 0x0001 + }; + Q_DECLARE_FLAGS(MatchOptions, MatchOption) + + QMqttTopicFilter(const QString &filter = QString()); + QMqttTopicFilter(const QLatin1String &filter); + QMqttTopicFilter(const QMqttTopicFilter &filter); + ~QMqttTopicFilter(); + QMqttTopicFilter &operator=(const QMqttTopicFilter &filter); + +#ifdef Q_COMPILER_RVALUE_REFS + inline QMqttTopicFilter &operator=(QMqttTopicFilter &&other) Q_DECL_NOTHROW { qSwap(d, other.d); return *this; } +#endif + + inline void swap(QMqttTopicFilter &other) Q_DECL_NOTHROW { qSwap(d, other.d); } + + QString filter() const; + void setFilter(const QString &filter); + + Q_REQUIRED_RESULT bool isValid() const; + Q_REQUIRED_RESULT bool match(const QMqttTopicName &name, MatchOptions matchOptions = NoMatchOption) const; + + friend Q_MQTT_EXPORT bool operator==(const QMqttTopicFilter &lhs, const QMqttTopicFilter &rhs) Q_DECL_NOTHROW; + friend inline bool operator!=(const QMqttTopicFilter &lhs, const QMqttTopicFilter &rhs) Q_DECL_NOTHROW { return !(lhs == rhs); } + friend Q_MQTT_EXPORT bool operator<(const QMqttTopicFilter &lhs, const QMqttTopicFilter &rhs) Q_DECL_NOTHROW; + friend Q_MQTT_EXPORT uint qHash(const QMqttTopicFilter &filter, uint seed) Q_DECL_NOTHROW; + +private: + QExplicitlySharedDataPointer<QMqttTopicFilterPrivate> d; +}; + +Q_DECLARE_SHARED(QMqttTopicFilter) +Q_DECLARE_OPERATORS_FOR_FLAGS(QMqttTopicFilter::MatchOptions) + +#ifndef QT_NO_DATASTREAM +Q_MQTT_EXPORT QDataStream &operator<<(QDataStream &, const QMqttTopicFilter &); +Q_MQTT_EXPORT QDataStream &operator>>(QDataStream &, QMqttTopicFilter &); +#endif + +#ifndef QT_NO_DEBUG_STREAM +Q_MQTT_EXPORT QDebug operator<<(QDebug, const QMqttTopicFilter &); +#endif + +QT_END_NAMESPACE + +Q_DECLARE_METATYPE(QMqttTopicFilter) + +#endif // QMQTTTOPICFILTER_H diff --git a/src/mqtt/qmqtttopicname.cpp b/src/mqtt/qmqtttopicname.cpp new file mode 100644 index 0000000..d414db0 --- /dev/null +++ b/src/mqtt/qmqtttopicname.cpp @@ -0,0 +1,231 @@ +/****************************************************************************** +** +** Copyright (C) 2017 Lorenz Haas +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtMqtt module. +** +** $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 "qmqtttopicname.h" + +#include <QtCore/QDebug> + +QT_BEGIN_NAMESPACE + +/*! + \class QMqttTopicName + \inmodule QtMqtt + \reentrant + \ingroup shared + + \brief The QMqttTopicName class represents a MQTT topic name. + + QMqttTopicName is a thin wrapper around a QString providing an expressive + data type for MQTT topic names. Beside the benefits of having a strong type + preventing unintended misuse, QMqttTopicName provides convenient functions + related to topic names like isValid() or levels(). + + \sa QMqttTopicFilter + */ + +/*! + \fn void QMqttTopicName::swap(QMqttTopicName &other) + Swaps the MQTT topic name \a other with this MQTT topic name. This + operation is very fast and never fails. + */ + +class QMqttTopicNamePrivate : public QSharedData +{ +public: + QString name; +}; + +/*! + Creates a new MQTT topic name with the specified \a name. + */ +QMqttTopicName::QMqttTopicName(const QString &name) : d(new QMqttTopicNamePrivate) +{ + d->name = name; +} + +/*! + Creates a new MQTT topic name with the specified \a name. + */ +QMqttTopicName::QMqttTopicName(const QLatin1String &name) : d(new QMqttTopicNamePrivate) +{ + d->name = name; +} + +/*! + Creates a new MQTT topic name as a copy of \a name. + */ +QMqttTopicName::QMqttTopicName(const QMqttTopicName &name) : d(name.d) +{ +} + +/*! + Destroys the QMqttTopicName object. + */ +QMqttTopicName::~QMqttTopicName() +{ +} + +/*! + Assigns the MQTT topic name \a name to this object, and returns a reference + to the copy. + */ +QMqttTopicName &QMqttTopicName::operator=(const QMqttTopicName &name) +{ + d = name.d; + return *this; +} + +/*! + Returns the topic name. + */ +QString QMqttTopicName::name() const +{ + return d->name; +} + +/*! + Sets the topic name to \a name. + */ +void QMqttTopicName::setName(const QString &name) +{ + d.detach(); + d->name = name; +} + +/*! + Returns \c true if the topic name is valid according to the MQTT standard + section 4.7, or \c false otherwise. + */ +bool QMqttTopicName::isValid() const +{ + const int bytes = d->name.size(); + return bytes > 0 // [MQTT-4.7.3-1] + && bytes < 65536 // [MQTT-4.7.3-3] + && !d->name.contains(QLatin1Char('#')) // [MQTT-4.7.1-1] + && !d->name.contains(QLatin1Char('+')) // [MQTT-4.7.1-1] + && !d->name.contains(QChar(QChar::Null)); // [MQTT-4.7.3-2] +} + +/*! + Returns the total number of topic levels. + */ +int QMqttTopicName::levelCount() const +{ + return d->name.isEmpty() ? 0 : d->name.count(QLatin1Char('/')) + 1; +} + +/*! + Returns the topic levels. + */ +QStringList QMqttTopicName::levels() const +{ + return d->name.split(QLatin1Char('/'), QString::KeepEmptyParts); +} + +/*! + \relates QMqttTopicName + + Returns \c true if the topic names \a lhs and \a rhs are equal, + otherwise returns \c false. + */ +bool operator==(const QMqttTopicName &lhs, const QMqttTopicName &rhs) Q_DECL_NOTHROW +{ + return (lhs.d == rhs.d) || (lhs.d->name == rhs.d->name); +} + +/*! + \fn bool operator!=(const QMqttTopicName &lhs, const QMqttTopicName &rhs) + \relates QMqttTopicName + + Returns \c true if the topic names \a lhs and \a rhs are different, + otherwise returns \c false. + */ + +/*! + \relates QMqttTopicName + + Returns \c true if the topic name \a lhs is lexically less than the topic + name \a rhs; otherwise returns \c false. + */ +bool operator<(const QMqttTopicName &lhs, const QMqttTopicName &rhs) Q_DECL_NOTHROW +{ + return lhs.d->name < rhs.d->name; +} + +/*! + \relates QHash + + Returns the hash value for \a name. If specified, \a seed is used to + initialize the hash. +*/ +uint qHash(const QMqttTopicName &name, uint seed) Q_DECL_NOTHROW +{ + return qHash(name.d->name, seed); +} + +#ifndef QT_NO_DATASTREAM +/*! \relates QMqttTopicName + + Writes the topic name \a name to the stream \a out and returns a reference + to the stream. + + \sa{Serializing Qt Data Types}{Format of the QDataStream operators} +*/ +QDataStream &operator<<(QDataStream &out, const QMqttTopicName &name) +{ + out << name.name(); + return out; +} + +/*! \relates QMqttTopicName + + Reads a topic name into \a name from the stream \a in and returns a + reference to the stream. + + \sa{Serializing Qt Data Types}{Format of the QDataStream operators} +*/ +QDataStream &operator>>(QDataStream &in, QMqttTopicName &name) +{ + QString n; + in >> n; + name.setName(n); + return in; +} +#endif // QT_NO_DATASTREAM + +#ifndef QT_NO_DEBUG_STREAM +QDebug operator<<(QDebug d, const QMqttTopicName &name) +{ + QDebugStateSaver saver(d); + d.nospace() << "QMqttTopicName(" << name.name() << ')'; + return d; +} +#endif + +QT_END_NAMESPACE diff --git a/src/mqtt/qmqtttopicname.h b/src/mqtt/qmqtttopicname.h new file mode 100644 index 0000000..09898bf --- /dev/null +++ b/src/mqtt/qmqtttopicname.h @@ -0,0 +1,94 @@ +/****************************************************************************** +** +** Copyright (C) 2017 Lorenz Haas +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtMqtt module. +** +** $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 QMQTTTOPICNAME_H +#define QMQTTTOPICNAME_H + +#include "qmqttglobal.h" + +#include <QtCore/QExplicitlySharedDataPointer> +#include <QtCore/QMetaType> +#include <QtCore/QString> +#include <QtCore/QStringList> + +QT_BEGIN_NAMESPACE + +class QMqttTopicNamePrivate; + +class QMqttTopicName; +// qHash is a friend, but we can't use default arguments for friends (§8.3.6.4) +Q_MQTT_EXPORT uint qHash(const QMqttTopicName &name, uint seed = 0) Q_DECL_NOTHROW; + +class Q_MQTT_EXPORT QMqttTopicName +{ +public: + QMqttTopicName(const QString &name = QString()); + QMqttTopicName(const QLatin1String &name); + QMqttTopicName(const QMqttTopicName &name); + ~QMqttTopicName(); + QMqttTopicName &operator=(const QMqttTopicName &name); + +#ifdef Q_COMPILER_RVALUE_REFS + inline QMqttTopicName &operator=(QMqttTopicName &&other) Q_DECL_NOTHROW { qSwap(d, other.d); return *this; } +#endif + + inline void swap(QMqttTopicName &other) Q_DECL_NOTHROW { qSwap(d, other.d); } + + QString name() const; + void setName(const QString &name); + + Q_REQUIRED_RESULT bool isValid() const; + Q_REQUIRED_RESULT int levelCount() const; + Q_REQUIRED_RESULT QStringList levels() const; + + friend Q_MQTT_EXPORT bool operator==(const QMqttTopicName &lhs, const QMqttTopicName &rhs) Q_DECL_NOTHROW; + friend inline bool operator!=(const QMqttTopicName &lhs, const QMqttTopicName &rhs) Q_DECL_NOTHROW { return !(lhs == rhs); } + friend Q_MQTT_EXPORT bool operator<(const QMqttTopicName &lhs, const QMqttTopicName &rhs) Q_DECL_NOTHROW; + friend Q_MQTT_EXPORT uint qHash(const QMqttTopicName &name, uint seed) Q_DECL_NOTHROW; + +private: + QExplicitlySharedDataPointer<QMqttTopicNamePrivate> d; +}; + +Q_DECLARE_SHARED(QMqttTopicName) + +#ifndef QT_NO_DATASTREAM +Q_MQTT_EXPORT QDataStream &operator<<(QDataStream &, const QMqttTopicName &); +Q_MQTT_EXPORT QDataStream &operator>>(QDataStream &, QMqttTopicName &); +#endif + +#ifndef QT_NO_DEBUG_STREAM +Q_MQTT_EXPORT QDebug operator<<(QDebug, const QMqttTopicName &); +#endif + +QT_END_NAMESPACE + +Q_DECLARE_METATYPE(QMqttTopicName) + +#endif // QMQTTTOPICNAME_H diff --git a/tests/auto/auto.pro b/tests/auto/auto.pro index dfa905c..ecd1207 100644 --- a/tests/auto/auto.pro +++ b/tests/auto/auto.pro @@ -4,4 +4,6 @@ win32|if(linux:!cross_compile): SUBDIRS += cmake \ conformance \ qmqttcontrolpacket \ qmqttclient \ - qmqttsubscription + qmqttsubscription \ + qmqtttopicname \ + qmqtttopicfilter diff --git a/tests/auto/conformance/tst_conformance.cpp b/tests/auto/conformance/tst_conformance.cpp index c748fb9..ca3cc61 100644 --- a/tests/auto/conformance/tst_conformance.cpp +++ b/tests/auto/conformance/tst_conformance.cpp @@ -274,15 +274,15 @@ void Tst_MqttConformance::offline_message_queueing_test() QTRY_VERIFY2(publisher.state() == QMqttClient::Connected, "Could not connect to broker."); QSignalSpy pubCounter(&publisher, SIGNAL(messageSent(qint32))); - publisher.publish("Qt/offline/foo/bar", "msg1", 1); - publisher.publish("Qt/offline/foo/bar2", "msg2", 1); - publisher.publish("Qt/offline/foo2/bar", "msg3", 1); + publisher.publish(QLatin1String("Qt/offline/foo/bar"), "msg1", 1); + publisher.publish(QLatin1String("Qt/offline/foo/bar2"), "msg2", 1); + publisher.publish(QLatin1String("Qt/offline/foo2/bar"), "msg3", 1); QTRY_VERIFY2(pubCounter.size() == 3, "Could not publish all messages."); publisher.disconnectFromHost(); QTRY_VERIFY2(publisher.state() == QMqttClient::Disconnected, "Could not disconnect."); - QSignalSpy receiveCounter(&client, SIGNAL(messageReceived(QByteArray,QString))); + QSignalSpy receiveCounter(&client, SIGNAL(messageReceived(QByteArray,QMqttTopicName))); client.connectToHost(); QTRY_VERIFY2(client.state() == QMqttClient::Connected, "Could not connect to broker."); @@ -306,7 +306,7 @@ void Tst_MqttConformance::subscribe_failure_test() client.connectToHost(); QTRY_VERIFY2(client.state() == QMqttClient::Connected, "Could not connect to broker."); - auto sub = client.subscribe(forbiddenTopic, 1); + auto sub = client.subscribe(QMqttTopicFilter(forbiddenTopic), 1); QVERIFY2(sub->state() == QMqttSubscription::SubscriptionPending, "Could not initiate subscription"); QTRY_VERIFY2(sub->state() == QMqttSubscription::Error, "Did not receive error state for sub."); diff --git a/tests/auto/qmqttclient/tst_qmqttclient.cpp b/tests/auto/qmqttclient/tst_qmqttclient.cpp index 26f5443..23611f4 100644 --- a/tests/auto/qmqttclient/tst_qmqttclient.cpp +++ b/tests/auto/qmqttclient/tst_qmqttclient.cpp @@ -50,10 +50,11 @@ private Q_SLOTS: void sendReceive(); void retainMessage(); void willMessage(); - void longTopic_data(); - void longTopic(); + void compliantTopic_data(); + void compliantTopic(); void subscribeLongTopic(); void dataIncludingZero(); + void publishLongTopic(); private: QProcess m_brokerProcess; QString m_testBroker; @@ -202,7 +203,7 @@ void Tst_QMqttClient::retainMessage() msgCount++; }); - QSignalSpy messageSpy(&sub, SIGNAL(messageReceived(QByteArray,QString))); + QSignalSpy messageSpy(&sub, SIGNAL(messageReceived(QByteArray,QMqttTopicName))); sub.connectToHost(); QTRY_COMPARE(sub.state(), QMqttClient::Connected); @@ -271,7 +272,7 @@ void Tst_QMqttClient::willMessage() } } -void Tst_QMqttClient::longTopic_data() +void Tst_QMqttClient::compliantTopic_data() { QTest::addColumn<QString>("topic"); QTest::newRow("simple") << QString::fromLatin1("topic"); @@ -280,11 +281,9 @@ void Tst_QMqttClient::longTopic_data() QString l; l.fill(QLatin1Char('T'), std::numeric_limits<std::uint16_t>::max()); QTest::newRow("maxSize") << l; - l.fill(QLatin1Char('M'), 2 * std::numeric_limits<std::uint16_t>::max()); - QTest::newRow("overflow") << l; } -void Tst_QMqttClient::longTopic() +void Tst_QMqttClient::compliantTopic() { QFETCH(QString, topic); QString truncTopic = topic; @@ -308,7 +307,7 @@ void Tst_QMqttClient::longTopic() bool received = false; bool verified = false; - connect(&subscriber, &QMqttClient::messageReceived, [&](const QByteArray &, const QString &t) { + connect(&subscriber, &QMqttClient::messageReceived, [&](const QByteArray &, const QMqttTopicName &t) { received = true; verified = t == truncTopic; }); @@ -373,6 +372,22 @@ void Tst_QMqttClient::dataIncludingZero() QVERIFY2(correctSize, "Subscriber received message of different size"); } +void Tst_QMqttClient::publishLongTopic() +{ + QMqttClient publisher; + publisher.setClientId(QLatin1String("publisher")); + publisher.setHostname(m_testBroker); + publisher.setPort(m_port); + + publisher.connectToHost(); + QTRY_COMPARE(publisher.state(), QMqttClient::Connected); + + QString topic; + topic.fill(QLatin1Char('s'), 2 * std::numeric_limits<std::uint16_t>::max()); + auto pub = publisher.publish(topic); + QCOMPARE(pub, -1); +} + QTEST_MAIN(Tst_QMqttClient) #include "tst_qmqttclient.moc" diff --git a/tests/auto/qmqtttopicfilter/qmqtttopicfilter.pro b/tests/auto/qmqtttopicfilter/qmqtttopicfilter.pro new file mode 100644 index 0000000..3ce2d77 --- /dev/null +++ b/tests/auto/qmqtttopicfilter/qmqtttopicfilter.pro @@ -0,0 +1,11 @@ +CONFIG += testcase +QT += testlib mqtt +QT -= gui +QT_PRIVATE += mqtt-private + +TARGET = tst_qmqtttopicfilter + +SOURCES += \ + tst_qmqtttopicfilter.cpp + +DEFINES += SRCDIR=\\\"$$PWD/\\\" diff --git a/tests/auto/qmqtttopicfilter/tst_qmqtttopicfilter.cpp b/tests/auto/qmqtttopicfilter/tst_qmqtttopicfilter.cpp new file mode 100644 index 0000000..f83999c --- /dev/null +++ b/tests/auto/qmqtttopicfilter/tst_qmqtttopicfilter.cpp @@ -0,0 +1,144 @@ +/****************************************************************************** +** +** Copyright (C) 2017 Lorenz Haas +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtMqtt module. +** +** $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 <QtCore/QHash> +#include <QtCore/QMap> +#include <QtCore/QVector> +#include <QtMqtt/QMqttTopicFilter> +#include <QtTest/QtTest> + +class Tst_QMqttTopicFilter : public QObject +{ + Q_OBJECT + +private Q_SLOTS: + void checkValidity(); + void matches(); + + void usableWithQVector(); + void usableWithQMap(); + void usableWithQHash(); +}; + +void Tst_QMqttTopicFilter::checkValidity() +{ + QVERIFY(QMqttTopicFilter("a").isValid()); + QVERIFY(QMqttTopicFilter("/").isValid()); + QVERIFY(QMqttTopicFilter("a b").isValid()); + QVERIFY(QMqttTopicFilter("#").isValid()); + QVERIFY(QMqttTopicFilter("/#").isValid()); + QVERIFY(QMqttTopicFilter("a/#").isValid()); + QVERIFY(QMqttTopicFilter("/a/#").isValid()); + QVERIFY(QMqttTopicFilter("+").isValid()); + QVERIFY(QMqttTopicFilter("/+").isValid()); + QVERIFY(QMqttTopicFilter("+/").isValid()); + QVERIFY(QMqttTopicFilter("/+/").isValid()); + QVERIFY(QMqttTopicFilter("/+/+").isValid()); + QVERIFY(QMqttTopicFilter("+/#").isValid()); + QVERIFY(QMqttTopicFilter("a/+/b").isValid()); + + QVERIFY(!QMqttTopicFilter("").isValid()); + QVERIFY(!QMqttTopicFilter("#/").isValid()); + QVERIFY(!QMqttTopicFilter("/a/#/").isValid()); + QVERIFY(!QMqttTopicFilter("#/#").isValid()); + QVERIFY(!QMqttTopicFilter("a#").isValid()); + QVERIFY(!QMqttTopicFilter("/a#").isValid()); + + QVERIFY(!QMqttTopicFilter("a+").isValid()); + QVERIFY(!QMqttTopicFilter("+a").isValid()); + QVERIFY(!QMqttTopicFilter("++").isValid()); + + QVERIFY(!QMqttTopicFilter(QString(3, QChar(QChar::Null))).isValid()); +} + +void Tst_QMqttTopicFilter::matches() +{ + // Non normative comment's examples [4.7.1.2] + QMqttTopicFilter filter("sport/tennis/player1/#"); + QVERIFY(filter.match(QMqttTopicName("sport/tennis/player1"))); + QVERIFY(filter.match(QMqttTopicName("sport/tennis/player1/ranking"))); + QVERIFY(filter.match(QMqttTopicName("sport/tennis/player1/score/wimbledon"))); + + filter = QMqttTopicFilter("sport/#"); + QVERIFY(filter.match(QMqttTopicName("sport"))); + + // Non normative comment's examples [4.7.1.3] + filter = QMqttTopicFilter("sport/tennis/+"); + QVERIFY(filter.match(QMqttTopicName("sport/tennis/player1"))); + QVERIFY(!filter.match(QMqttTopicName("sport/tennis/player1/ranking"))); + + filter = QMqttTopicFilter("sport/+"); + QVERIFY(filter.match(QMqttTopicName("sport/"))); + QVERIFY(!filter.match(QMqttTopicName("sport"))); + + QVERIFY(QMqttTopicFilter("+/+").match(QMqttTopicName("/finance"))); + QVERIFY(QMqttTopicFilter("/+").match(QMqttTopicName("/finance"))); + QVERIFY(!QMqttTopicFilter("+").match(QMqttTopicName("/finance"))); + + // Non normative comment's examples [4.7.2] + QVERIFY(QMqttTopicFilter("#").match(QMqttTopicName("$SYS/foo"))); + QVERIFY(!QMqttTopicFilter("#").match(QMqttTopicName("$SYS/foo"), QMqttTopicFilter::WildcardsDontMatchDollarTopicMatchOption)); + + QVERIFY(QMqttTopicFilter("+/monitor/Clients").match(QMqttTopicName("$SYS/monitor/Clients"))); + QVERIFY(!QMqttTopicFilter("+/monitor/Clients").match(QMqttTopicName("$SYS/monitor/Clients"), QMqttTopicFilter::WildcardsDontMatchDollarTopicMatchOption)); + + QVERIFY(QMqttTopicFilter("$SYS/#").match(QMqttTopicName("$SYS/foo"))); + QVERIFY(QMqttTopicFilter("$SYS/#").match(QMqttTopicName("$SYS/foo"), QMqttTopicFilter::WildcardsDontMatchDollarTopicMatchOption)); + + QVERIFY(QMqttTopicFilter("$SYS/monitor/+").match(QMqttTopicName("$SYS/monitor/Clients"))); + QVERIFY(QMqttTopicFilter("$SYS/monitor/+").match(QMqttTopicName("$SYS/monitor/Clients"), QMqttTopicFilter::WildcardsDontMatchDollarTopicMatchOption)); +} + +void Tst_QMqttTopicFilter::usableWithQVector() +{ + const QMqttTopicFilter topic{"a/b"}; + QVector<QMqttTopicFilter> names; + names.append(topic); + QCOMPARE(topic, names.constFirst()); +} + +void Tst_QMqttTopicFilter::usableWithQMap() +{ + const QMqttTopicFilter topic{"a/b"}; + QMap<QMqttTopicFilter, int> names; + names.insert(topic, 42); + QCOMPARE(names[topic], 42); +} + +void Tst_QMqttTopicFilter::usableWithQHash() +{ + const QMqttTopicFilter topic{"a/b"}; + QHash<QMqttTopicFilter, int> names; + names.insert(topic, 42); + QCOMPARE(names[topic], 42); +} + +QTEST_MAIN(Tst_QMqttTopicFilter) + +#include "tst_qmqtttopicfilter.moc" diff --git a/tests/auto/qmqtttopicname/qmqtttopicname.pro b/tests/auto/qmqtttopicname/qmqtttopicname.pro new file mode 100644 index 0000000..30d7d05 --- /dev/null +++ b/tests/auto/qmqtttopicname/qmqtttopicname.pro @@ -0,0 +1,11 @@ +CONFIG += testcase +QT += testlib mqtt +QT -= gui +QT_PRIVATE += mqtt-private + +TARGET = tst_qmqtttopicname + +SOURCES += \ + tst_qmqtttopicname.cpp + +DEFINES += SRCDIR=\\\"$$PWD/\\\" diff --git a/tests/auto/qmqtttopicname/tst_qmqtttopicname.cpp b/tests/auto/qmqtttopicname/tst_qmqtttopicname.cpp new file mode 100644 index 0000000..6f33e8c --- /dev/null +++ b/tests/auto/qmqtttopicname/tst_qmqtttopicname.cpp @@ -0,0 +1,121 @@ +/****************************************************************************** +** +** Copyright (C) 2017 Lorenz Haas +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtMqtt module. +** +** $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 <QtCore/QHash> +#include <QtCore/QMap> +#include <QtCore/QVector> +#include <QtMqtt/QMqttTopicName> +#include <QtTest/QtTest> + +class Tst_QMqttTopicName : public QObject +{ + Q_OBJECT + +private Q_SLOTS: + void checkValidity(); + void checkLevelCount(); + void checkLevels_data(); + void checkLevels(); + void usableWithQVector(); + void usableWithQMap(); + void usableWithQHash(); +}; + +void Tst_QMqttTopicName::checkValidity() +{ + QVERIFY(QMqttTopicName("a").isValid()); + QVERIFY(QMqttTopicName("/").isValid()); + QVERIFY(QMqttTopicName("a b").isValid()); + + QVERIFY(!QMqttTopicName("").isValid()); + QVERIFY(!QMqttTopicName("/a/#").isValid()); + QVERIFY(!QMqttTopicName("/+/a").isValid()); + QVERIFY(!QMqttTopicName(QString(3, QChar(QChar::Null))).isValid()); +} + +void Tst_QMqttTopicName::checkLevelCount() +{ + QCOMPARE(QMqttTopicName("a").levelCount(), 1); + QCOMPARE(QMqttTopicName("/").levelCount(), 2); + QCOMPARE(QMqttTopicName("/a").levelCount(), 2); + QCOMPARE(QMqttTopicName("a/").levelCount(), 2); + QCOMPARE(QMqttTopicName("a/b").levelCount(), 2); + QCOMPARE(QMqttTopicName("a/b/").levelCount(), 3); +} + +void Tst_QMqttTopicName::checkLevels_data() +{ + QTest::addColumn<QMqttTopicName>("name"); + QTest::addColumn<QStringList>("levels"); + + QTest::newRow("1") << QMqttTopicName("a") << QStringList{"a"}; + QTest::newRow("2") << QMqttTopicName("/") << QStringList{"", ""}; + QTest::newRow("3") << QMqttTopicName("//") << QStringList{"", "", ""}; + QTest::newRow("4") << QMqttTopicName("a/") << QStringList{"a", ""}; + QTest::newRow("5") << QMqttTopicName("/a") << QStringList{"", "a"}; + QTest::newRow("6") << QMqttTopicName("a/b") << QStringList{"a", "b"}; + QTest::newRow("7") << QMqttTopicName("a/b/") << QStringList{"a", "b", ""}; + QTest::newRow("8") << QMqttTopicName("/a/b") << QStringList{"", "a", "b"}; +} + +void Tst_QMqttTopicName::checkLevels() +{ + QFETCH(QMqttTopicName, name); + QFETCH(QStringList, levels); + + QCOMPARE(name.levels(), levels); +} + +void Tst_QMqttTopicName::usableWithQVector() +{ + const QMqttTopicName topic{"a/b"}; + QVector<QMqttTopicName> names; + names.append(topic); + QCOMPARE(topic, names.constFirst()); +} + +void Tst_QMqttTopicName::usableWithQMap() +{ + const QMqttTopicName topic{"a/b"}; + QMap<QMqttTopicName, int> names; + names.insert(topic, 42); + QCOMPARE(names[topic], 42); +} + +void Tst_QMqttTopicName::usableWithQHash() +{ + const QMqttTopicName topic{"a/b"}; + QHash<QMqttTopicName, int> names; + names.insert(topic, 42); + QCOMPARE(names[topic], 42); +} + +QTEST_MAIN(Tst_QMqttTopicName) + +#include "tst_qmqtttopicname.moc" |