summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorLorenz Haas <lorenz.haas@histomatics.de>2017-11-13 22:26:25 +0100
committerMaurice Kalinowski <maurice.kalinowski@qt.io>2017-11-15 13:18:31 +0000
commit6eca6f914d3f7704e7bd38f3b48f4c7a3f817625 (patch)
tree7a0a6a973e1b511bad406114ad1ce67c0f21f521
parentb9369efc1ea0fdba5a92ef70ec24b9f3c6edc3cd (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>
-rw-r--r--examples/mqtt/quicksubscription/qmlmqttclient.h4
-rw-r--r--examples/mqtt/simpleclient/mainwindow.cpp4
-rw-r--r--examples/mqtt/subscriptions/mainwindow.cpp6
-rw-r--r--examples/mqtt/subscriptions/subscriptionwindow.cpp2
-rw-r--r--src/mqtt/doc/qtmqtt.qdocconf2
-rw-r--r--src/mqtt/mqtt.pro8
-rw-r--r--src/mqtt/qmqttclient.cpp8
-rw-r--r--src/mqtt/qmqttclient.h9
-rw-r--r--src/mqtt/qmqttconnection.cpp60
-rw-r--r--src/mqtt/qmqttconnection_p.h8
-rw-r--r--src/mqtt/qmqttmessage.cpp4
-rw-r--r--src/mqtt/qmqttmessage.h10
-rw-r--r--src/mqtt/qmqttsubscription.cpp4
-rw-r--r--src/mqtt/qmqttsubscription.h8
-rw-r--r--src/mqtt/qmqttsubscription_p.h2
-rw-r--r--src/mqtt/qmqtttopicfilter.cpp314
-rw-r--r--src/mqtt/qmqtttopicfilter.h101
-rw-r--r--src/mqtt/qmqtttopicname.cpp231
-rw-r--r--src/mqtt/qmqtttopicname.h94
-rw-r--r--tests/auto/auto.pro4
-rw-r--r--tests/auto/conformance/tst_conformance.cpp10
-rw-r--r--tests/auto/qmqttclient/tst_qmqttclient.cpp31
-rw-r--r--tests/auto/qmqtttopicfilter/qmqtttopicfilter.pro11
-rw-r--r--tests/auto/qmqtttopicfilter/tst_qmqtttopicfilter.cpp144
-rw-r--r--tests/auto/qmqtttopicname/qmqtttopicname.pro11
-rw-r--r--tests/auto/qmqtttopicname/tst_qmqtttopicname.cpp121
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"