summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorLiang Qi <liang.qi@qt.io>2018-02-05 10:50:21 +0100
committerLiang Qi <liang.qi@qt.io>2018-02-05 10:50:21 +0100
commit65f542f4600f469bca2c539716e20ba73eb6275a (patch)
tree8fe78f8a46e64a31f5966081269359a826f13828
parentcd1d60ec71836d6c4e9cbfbf344ef9334a22cdc4 (diff)
parent2c3c2a41c55a179332ec2a076856990f36dd5ef9 (diff)
Merge remote-tracking branch 'origin/5.10' into dev
-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.cpp27
-rw-r--r--src/mqtt/qmqttclient.h9
-rw-r--r--src/mqtt/qmqttclient_p.h3
-rw-r--r--src/mqtt/qmqttconnection.cpp193
-rw-r--r--src/mqtt/qmqttconnection_p.h13
-rw-r--r--src/mqtt/qmqttcontrolpacket.cpp4
-rw-r--r--src/mqtt/qmqttmessage.cpp103
-rw-r--r--src/mqtt/qmqttmessage.h31
-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--sync.profile2
-rw-r--r--tests/README.txt21
-rw-r--r--tests/auto/auto.pro4
-rw-r--r--tests/auto/conformance/tst_conformance.cpp10
-rw-r--r--tests/auto/qmqttclient/tst_qmqttclient.cpp126
-rw-r--r--tests/auto/qmqttcontrolpacket/tst_qmqttcontrolpacket.cpp13
-rw-r--r--tests/auto/qmqttsubscription/tst_qmqttsubscription.cpp76
-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
32 files changed, 1529 insertions, 173 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 69af887..23fb00f 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.
@@ -271,10 +271,10 @@ QT_BEGIN_NAMESPACE
/*!
Creates a new MQTT client instance with the specified \a parent.
*/
-QMqttClient::QMqttClient(QObject *parent) : QObject(*(new QMqttClientPrivate), parent)
+QMqttClient::QMqttClient(QObject *parent) : QObject(*(new QMqttClientPrivate(this)), parent)
{
Q_D(QMqttClient);
- d->m_connection.setClient(this, d);
+ d->m_connection.setClientPrivate(d);
}
/*!
@@ -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)
@@ -419,8 +419,12 @@ void QMqttClient::connectToHost(bool encrypted, const QString &sslPeerName)
d->setStateAndError(Disconnected, TransportInvalid);
return;
}
+ d->m_error = QMqttClient::NoError; // Fresh reconnect, unset error
d->setStateAndError(Connecting);
+ if (d->m_cleanSession)
+ d->m_connection.cleanSubscriptions();
+
if (!d->m_connection.ensureTransportOpen(sslPeerName)) {
qWarning("Could not ensure that connection is open");
d->setStateAndError(Disconnected, TransportInvalid);
@@ -670,13 +674,14 @@ void QMqttClient::setError(ClientError e)
emit errorChanged(d->m_error);
}
-QMqttClientPrivate::QMqttClientPrivate()
+QMqttClientPrivate::QMqttClientPrivate(QMqttClient *c)
: QObjectPrivate()
{
+ m_client = c;
m_clientId = QUuid::createUuid().toString();
- m_clientId.remove(QLatin1Char('{'), Qt::CaseInsensitive);
- m_clientId.remove(QLatin1Char('}'), Qt::CaseInsensitive);
- m_clientId.remove(QLatin1Char('-'), Qt::CaseInsensitive);
+ m_clientId.remove(QLatin1Char('{'));
+ m_clientId.remove(QLatin1Char('}'));
+ m_clientId.remove(QLatin1Char('-'));
m_clientId.resize(23);
}
@@ -690,7 +695,7 @@ void QMqttClientPrivate::setStateAndError(QMqttClient::ClientState s, QMqttClien
if (s != m_state)
q->setState(s);
- if (m_error != QMqttClient::NoError && m_error != e)
+ if (e != QMqttClient::NoError && m_error != e)
q->setError(e);
}
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/qmqttclient_p.h b/src/mqtt/qmqttclient_p.h
index 8ae8d61..dc5760c 100644
--- a/src/mqtt/qmqttclient_p.h
+++ b/src/mqtt/qmqttclient_p.h
@@ -54,9 +54,10 @@ class QMqttClientPrivate : public QObjectPrivate
{
Q_DECLARE_PUBLIC(QMqttClient)
public:
- QMqttClientPrivate();
+ QMqttClientPrivate(QMqttClient *c);
~QMqttClientPrivate() override;
void setStateAndError(QMqttClient::ClientState s, QMqttClient::ClientError e = QMqttClient::NoError);
+ QMqttClient *m_client{nullptr};
QString m_hostname;
quint16 m_port{0};
QMqttConnection m_connection;
diff --git a/src/mqtt/qmqttconnection.cpp b/src/mqtt/qmqttconnection.cpp
index 03ddab1..4db53c1 100644
--- a/src/mqtt/qmqttconnection.cpp
+++ b/src/mqtt/qmqttconnection.cpp
@@ -87,11 +87,15 @@ bool QMqttConnection::ensureTransport(bool createSecureIfNeeded)
{
qCDebug(lcMqttConnection) << Q_FUNC_INFO << m_transport;
- if (m_transport)
- return true;
+ if (m_transport) {
+ if (m_ownTransport)
+ delete m_transport;
+ else
+ return true;
+ }
// We are asked to create a transport layer
- if (m_client->hostname().isEmpty() || m_client->port() == 0) {
+ if (m_clientPrivate->m_hostname.isEmpty() || m_clientPrivate->m_port == 0) {
qWarning("Trying to create a transport layer, but no hostname is specified");
return false;
}
@@ -102,7 +106,11 @@ bool QMqttConnection::ensureTransport(bool createSecureIfNeeded)
new QTcpSocket();
m_transport = socket;
m_ownTransport = true;
- m_transportType = createSecureIfNeeded ? QMqttClient::SecureSocket : QMqttClient::AbstractSocket;
+ m_transportType =
+#ifndef QT_NO_SSL
+ createSecureIfNeeded ? QMqttClient::SecureSocket :
+#endif
+ QMqttClient::AbstractSocket;
connect(socket, &QAbstractSocket::connected, this, &QMqttConnection::transportConnectionEstablished);
connect(socket, &QAbstractSocket::disconnected, this, &QMqttConnection::transportConnectionClosed);
@@ -132,7 +140,7 @@ bool QMqttConnection::ensureTransportOpen(const QString &sslPeerName)
return sendControlConnect();
m_internalState = BrokerConnecting;
- socket->connectToHost(m_client->hostname(), m_client->port());
+ socket->connectToHost(m_clientPrivate->m_hostname, m_clientPrivate->m_port);
}
#ifndef QT_NO_SSL
else if (m_transportType == QMqttClient::SecureSocket) {
@@ -142,7 +150,7 @@ bool QMqttConnection::ensureTransportOpen(const QString &sslPeerName)
return true;
m_internalState = BrokerConnecting;
- socket->connectToHostEncrypted(m_client->hostname(), m_client->port(), sslPeerName);
+ socket->connectToHostEncrypted(m_clientPrivate->m_hostname, m_clientPrivate->m_port, sslPeerName);
if (!socket->waitForConnected()) {
qWarning("Could not establish socket connection for transport");
@@ -170,50 +178,54 @@ bool QMqttConnection::sendControlConnect()
// Variable header
// 3.1.2.1 Protocol Name
// 3.1.2.2 Protocol Level
- const quint8 protocolVersion = m_client->protocolVersion();
- if (protocolVersion == 3) {
+ switch (m_clientPrivate->m_protocolVersion) {
+ case QMqttClient::MQTT_3_1:
packet.append("MQIsdp");
packet.append(char(3)); // Version 3.1
- } else if (protocolVersion == 4) {
+ break;
+ case QMqttClient::MQTT_3_1_1:
packet.append("MQTT");
packet.append(char(4)); // Version 3.1.1
- } else {
- qFatal("Illegal MQTT VERSION");
+ break;
+ default:
+ qCWarning(lcMqttConnection) << "Illegal MQTT Version";
+ m_clientPrivate->setStateAndError(QMqttClient::Disconnected, QMqttClient::InvalidProtocolVersion);
+ return false;
}
// 3.1.2.3 Connect Flags
quint8 flags = 0;
// Clean session
- if (m_client->cleanSession())
+ if (m_clientPrivate->m_cleanSession)
flags |= 1 << 1;
- if (!m_client->willMessage().isEmpty()) {
+ if (!m_clientPrivate->m_willMessage.isEmpty()) {
flags |= 1 << 2;
- if (m_client->willQoS() > 2) {
+ if (m_clientPrivate->m_willQoS > 2) {
qWarning("Will QoS does not have a valid value");
return false;
}
- if (m_client->willQoS() == 1)
+ if (m_clientPrivate->m_willQoS == 1)
flags |= 1 << 3;
- else if (m_client->willQoS() == 2)
+ else if (m_clientPrivate->m_willQoS == 2)
flags |= 1 << 4;
- if (m_client->willRetain())
+ if (m_clientPrivate->m_willRetain)
flags |= 1 << 5;
}
- if (m_client->username().size())
+ if (m_clientPrivate->m_username.size())
flags |= 1 << 7;
- if (m_client->password().size())
+ if (m_clientPrivate->m_password.size())
flags |= 1 << 6;
packet.append(char(flags));
// 3.1.2.10 Keep Alive
- packet.append(m_client->keepAlive());
+ packet.append(m_clientPrivate->m_keepAlive);
// 3.1.3 Payload
// 3.1.3.1 Client Identifier
- const QByteArray clientStringArray = m_client->clientId().toUtf8();
+ const QByteArray clientStringArray = m_clientPrivate->m_clientId.toUtf8();
if (clientStringArray.size()) {
packet.append(clientStringArray);
} else {
@@ -221,16 +233,16 @@ bool QMqttConnection::sendControlConnect()
packet.append(char(0));
}
- if (!m_client->willMessage().isEmpty()) {
- packet.append(m_client->willTopic().toUtf8());
- packet.append(m_client->willMessage());
+ if (!m_clientPrivate->m_willMessage.isEmpty()) {
+ packet.append(m_clientPrivate->m_willTopic.toUtf8());
+ packet.append(m_clientPrivate->m_willMessage);
}
- if (m_client->username().size())
- packet.append(m_client->username().toUtf8());
+ if (m_clientPrivate->m_username.size())
+ packet.append(m_clientPrivate->m_username.toUtf8());
- if (m_client->password().size())
- packet.append(m_client->password().toUtf8());
+ if (m_clientPrivate->m_password.size())
+ packet.append(m_clientPrivate->m_password.toUtf8());
if (!writePacketToTransport(packet)) {
qWarning("Could not write CONNECT frame to transport");
@@ -241,12 +253,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;
@@ -259,15 +271,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();
@@ -318,7 +322,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;
@@ -336,13 +340,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;
@@ -352,8 +355,8 @@ QMqttSubscription *QMqttConnection::sendControlSubscribe(const QString &topic, q
}
auto result = new QMqttSubscription(this);
- result->setTopic(QString::fromUtf8(topicArray));
- result->setClient(m_client);
+ result->setTopic(topic);
+ result->setClient(m_clientPrivate->m_client);
result->setQos(qos);
result->setState(QMqttSubscription::SubscriptionPending);
@@ -368,12 +371,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))
@@ -394,7 +397,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);
@@ -446,9 +449,8 @@ bool QMqttConnection::sendControlDisconnect()
return false;
}
-void QMqttConnection::setClient(QMqttClient *client, QMqttClientPrivate *clientPrivate)
+void QMqttConnection::setClientPrivate(QMqttClientPrivate *clientPrivate)
{
- m_client = client;
m_clientPrivate = clientPrivate;
}
@@ -477,6 +479,21 @@ quint16 QMqttConnection::unusedPacketIdentifier() const
return packetIdentifierCounter;
}
+void QMqttConnection::cleanSubscriptions()
+{
+ for (auto item : m_pendingSubscriptionAck)
+ item->setState(QMqttSubscription::Unsubscribed);
+ m_pendingSubscriptionAck.clear();
+
+ for (auto item : m_pendingUnsubscriptions)
+ item->setState(QMqttSubscription::Unsubscribed);
+ m_pendingUnsubscriptions.clear();
+
+ for (auto item : m_activeSubscriptions)
+ item->setState(QMqttSubscription::Unsubscribed);
+ m_activeSubscriptions.clear();
+}
+
void QMqttConnection::transportConnectionEstablished()
{
if (m_internalState != BrokerConnecting) {
@@ -495,8 +512,7 @@ void QMqttConnection::transportConnectionClosed()
{
m_readBuffer.clear();
m_pingTimer.stop();
- m_client->setState(QMqttClient::Disconnected);
- m_client->setError(QMqttClient::TransportInvalid);
+ m_clientPrivate->setStateAndError(QMqttClient::Disconnected, QMqttClient::TransportInvalid);
}
void QMqttConnection::transportReadReady()
@@ -544,9 +560,13 @@ void QMqttConnection::finalize_connack()
// MQTT-3.2.2-1 & MQTT-3.2.2-2
if (sessionPresent) {
- emit m_client->brokerSessionRestored();
- if (m_client->cleanSession())
+ emit m_clientPrivate->m_client->brokerSessionRestored();
+ if (m_clientPrivate->m_cleanSession)
qWarning("Connected with a clean session, ack contains session present");
+ } else {
+ // MQTT-4.1.0.-1 MQTT-4.1.0-2 Session not stored on broker side
+ // regardless whether cleanSession is false
+ cleanSubscriptions();
}
quint8 connectResultValue;
@@ -562,9 +582,9 @@ void QMqttConnection::finalize_connack()
return;
}
m_internalState = BrokerConnected;
- m_client->setState(QMqttClient::Connected);
+ m_clientPrivate->setStateAndError(QMqttClient::Connected);
- m_pingTimer.setInterval(m_client->keepAlive() * 1000);
+ m_pingTimer.setInterval(m_clientPrivate->m_keepAlive * 1000);
m_pingTimer.start();
}
@@ -617,7 +637,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) {
@@ -631,39 +651,14 @@ void QMqttConnection::finalize_publish()
qCDebug(lcMqttConnectionVerbose) << "Finalize PUBLISH: topic:" << topic
<< " payloadLength:" << payloadLength;;
- emit m_client->messageReceived(message, topic);
+ emit m_clientPrivate->m_client->messageReceived(message, topic);
QMqttMessage qmsg(topic, message, id, m_currentPublish.qos,
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))) {
- 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) {
+ if (sub.key().match(topic))
emit sub.value()->messageReceived(qmsg);
- }
}
if (m_currentPublish.qos == 1)
@@ -684,7 +679,7 @@ void QMqttConnection::finalize_pubAckRecComp()
auto pendingRelease = m_pendingReleaseMessages.take(id);
if (!pendingRelease)
qWarning("Received PUBCOMP for unknown released message");
- emit m_client->messageSent(id);
+ emit m_clientPrivate->m_client->messageSent(id);
return;
}
@@ -699,7 +694,7 @@ void QMqttConnection::finalize_pubAckRecComp()
sendControlPublishRelease(id);
} else {
qCDebug(lcMqttConnectionVerbose) << " PUBACK:" << id;
- emit m_client->messageSent(id);
+ emit m_clientPrivate->m_client->messageSent(id);
}
}
@@ -726,7 +721,7 @@ void QMqttConnection::finalize_pingresp()
closeConnection(QMqttClient::ProtocolViolation);
return;
}
- emit m_client->pingResponseReceived();
+ emit m_clientPrivate->m_client->pingResponseReceived();
}
void QMqttConnection::processData()
@@ -769,12 +764,24 @@ void QMqttConnection::processData()
// MQTT-2.2 A fixed header of a control packet must be at least 2 bytes. If the payload is
// longer than 127 bytes the header can be up to 5 bytes long.
- const int readBufferSize = m_readBuffer.size();
- if (readBufferSize < 2
- || (readBufferSize == 2 && (m_readBuffer.at(1) & 128) != 0)
- || (readBufferSize == 3 && (m_readBuffer.at(2) & 128) != 0)
- || (readBufferSize == 4 && (m_readBuffer.at(3) & 128) != 0)) {
+ switch (m_readBuffer.size()) {
+ case 0:
+ case 1:
return;
+ case 2:
+ if ((m_readBuffer.at(1) & 128) != 0)
+ return;
+ break;
+ case 3:
+ if ((m_readBuffer.at(1) & 128) != 0 && (m_readBuffer.at(2) & 128) != 0)
+ return;
+ break;
+ case 4:
+ if ((m_readBuffer.at(1) & 128) != 0 && (m_readBuffer.at(2) & 128) != 0 && (m_readBuffer.at(3) & 128) != 0)
+ return;
+ break;
+ default:
+ break;
}
readBuffer((char*)&m_currentPacket, 1);
diff --git a/src/mqtt/qmqttconnection_p.h b/src/mqtt/qmqttconnection_p.h
index d747771..cf9c912 100644
--- a/src/mqtt/qmqttconnection_p.h
+++ b/src/mqtt/qmqttconnection_p.h
@@ -77,21 +77,23 @@ 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();
- void setClient(QMqttClient *client, QMqttClientPrivate *clientPrivate);
+ void setClientPrivate(QMqttClientPrivate *clientPrivate);
inline quint16 unusedPacketIdentifier() const;
inline InternalConnectionState internalState() const { return m_internalState; }
+ void cleanSubscriptions();
+
public Q_SLOTS:
void transportConnectionEstablished();
void transportConnectionClosed();
@@ -101,7 +103,6 @@ public:
QIODevice *m_transport{nullptr};
QMqttClient::TransportType m_transportType{QMqttClient::IODevice};
bool m_ownTransport{false};
- QMqttClient *m_client{nullptr};
QMqttClientPrivate *m_clientPrivate{nullptr};
private:
Q_DISABLE_COPY(QMqttConnection)
@@ -130,7 +131,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/qmqttcontrolpacket.cpp b/src/mqtt/qmqttcontrolpacket.cpp
index f929f37..3d0db8d 100644
--- a/src/mqtt/qmqttcontrolpacket.cpp
+++ b/src/mqtt/qmqttcontrolpacket.cpp
@@ -80,12 +80,12 @@ void QMqttControlPacket::append(quint16 value)
void QMqttControlPacket::append(const QByteArray &data)
{
append(static_cast<quint16>(data.size()));
- m_payload.append(data.constData());
+ m_payload.append(data);
}
void QMqttControlPacket::appendRaw(const QByteArray &data)
{
- m_payload.append(data.constData());
+ m_payload.append(data);
}
QByteArray QMqttControlPacket::serialize() const
diff --git a/src/mqtt/qmqttmessage.cpp b/src/mqtt/qmqttmessage.cpp
index 1bdbdf7..25ed768 100644
--- a/src/mqtt/qmqttmessage.cpp
+++ b/src/mqtt/qmqttmessage.cpp
@@ -86,44 +86,117 @@ QT_BEGIN_NAMESPACE
live update. A broker can store only one retained message per topic.
*/
-QByteArray QMqttMessage::payload() const
+class QMqttMessagePrivate : public QSharedData
{
- return m_payload;
+public:
+ bool operator==(const QMqttMessagePrivate &other) {
+ return m_topic == other.m_topic &&
+ m_payload == other.m_payload &&
+ m_id == other.m_id &&
+ m_qos == other.m_qos &&
+ m_duplicate == other.m_duplicate &&
+ m_retain == other.m_retain;
+ }
+ QMqttTopicName m_topic;
+ QByteArray m_payload;
+ quint16 m_id{0};
+ quint8 m_qos{0};
+ bool m_duplicate{false};
+ bool m_retain{false};
+};
+
+/*!
+ Creates a new MQTT message.
+*/
+QMqttMessage::QMqttMessage()
+ : d(new QMqttMessagePrivate())
+{
+}
+
+/*!
+ Constructs a new MQTT message that is a copy of \a other.
+*/
+QMqttMessage::QMqttMessage(const QMqttMessage &other)
+ : d(other.d)
+{
+}
+
+QMqttMessage::~QMqttMessage()
+{
+}
+
+/*!
+ Makes this object a copy of \a other and returns the new value of this object.
+*/
+QMqttMessage &QMqttMessage::operator=(const QMqttMessage &other)
+{
+ d = other.d;
+ return *this;
+}
+
+/*!
+ Returns \c true if the message and \a other are equal, otherwise returns \c false.
+*/
+bool QMqttMessage::operator==(const QMqttMessage &other) const
+{
+ return d == other.d;
+}
+
+/*!
+ Returns \c true if the message and \a other are not equal, otherwise returns \c false.
+*/
+bool QMqttMessage::operator!=(const QMqttMessage &other) const
+{
+ return !(*this == other);
+}
+
+const QByteArray &QMqttMessage::payload() const
+{
+ return d->m_payload;
}
quint8 QMqttMessage::qos() const
{
- return m_qos;
+ return d->m_qos;
}
quint16 QMqttMessage::id() const
{
- return m_id;
+ return d->m_id;
}
-QString QMqttMessage::topic() const
+QMqttTopicName QMqttMessage::topic() const
{
- return m_topic;
+ return d->m_topic;
}
bool QMqttMessage::duplicate() const
{
- return m_duplicate;
+ return d->m_duplicate;
}
bool QMqttMessage::retain() const
{
- return m_retain;
+ return d->m_retain;
}
-QMqttMessage::QMqttMessage(const QString &topic, const QByteArray &content, quint16 id, quint8 qos, bool dup, bool retain)
- : m_topic(topic)
- , m_payload(content)
- , m_id(id)
- , m_qos(qos)
- , m_duplicate(dup)
- , m_retain(retain)
+/*!
+ \internal
+ Constructs a new MQTT message with \a content on the topic \a topic.
+ Furthermore the properties \a id, \a qos, \a dup, \a retain must be specified.
+
+ This constructor is mostly used internally to construct a QMqttMessage at message
+ receival.
+*/
+QMqttMessage::QMqttMessage(const QMqttTopicName &topic, const QByteArray &content, quint16 id, quint8 qos, bool dup, bool retain)
+ : d(new QMqttMessagePrivate)
{
+ d->m_topic = topic;
+ d->m_payload = content;
+ d->m_id = id;
+ d->m_qos = qos;
+ d->m_duplicate = dup;
+ d->m_retain = retain;
}
QT_END_NAMESPACE
diff --git a/src/mqtt/qmqttmessage.h b/src/mqtt/qmqttmessage.h
index 3f25d08..509e631 100644
--- a/src/mqtt/qmqttmessage.h
+++ b/src/mqtt/qmqttmessage.h
@@ -32,40 +32,51 @@
#include "qmqttglobal.h"
+#include <QtMqtt/QMqttTopicName>
+
#include <QtCore/QObject>
+#include <QtCore/QSharedDataPointer>
QT_BEGIN_NAMESPACE
+class QMqttMessagePrivate;
+
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)
Q_PROPERTY(bool duplicate READ duplicate CONSTANT)
Q_PROPERTY(bool retain READ retain CONSTANT)
+
public:
- QByteArray payload() const;
+ QMqttMessage();
+ QMqttMessage(const QMqttMessage& other);
+ ~QMqttMessage();
+
+ QMqttMessage& operator=(const QMqttMessage &other);
+ bool operator==(const QMqttMessage &other) const;
+ inline bool operator!=(const QMqttMessage &other) const;
+
+ const 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,
+ QMqttMessage(const QMqttTopicName &topic, const QByteArray &payload,
quint16 id, quint8 qos,
bool dup, bool retain);
- QString m_topic;
- QByteArray m_payload;
- quint16 m_id;
- quint8 m_qos;
- bool m_duplicate;
- bool m_retain;
+ QExplicitlySharedDataPointer<QMqttMessagePrivate> d;
};
QT_END_NAMESPACE
+Q_DECLARE_METATYPE(QMqttMessage)
+
#endif // QMQTTMESSAGE_H
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/sync.profile b/sync.profile
index 77d0e2e..a9e8018 100644
--- a/sync.profile
+++ b/sync.profile
@@ -3,5 +3,5 @@
);
%dependencies = (
- "qtbase" => ""
+ "qtbase" => "",
);
diff --git a/tests/README.txt b/tests/README.txt
new file mode 100644
index 0000000..3a8bf51
--- /dev/null
+++ b/tests/README.txt
@@ -0,0 +1,21 @@
+The tests included in the subdirectories check for functionality and
+conformance of the Qt MQTT module.
+
+To be able to run the tests successfully, a broker needs to be available and
+reachable.
+
+The continuous integration utilized the paho conformance test broker. It can
+be obtained at this location:
+https://github.com/eclipse/paho.mqtt.testing
+
+For the unit tests being able to locate this script, use the
+MQTT_TEST_BROKER_LOCATION environment variable and set it
+to “<install-path>/interoperability/startbroker.py”.
+
+Alternatively, any broker can be instantiated and passed to the auto tests
+by using the environment variable MQTT_TEST_BROKER. This must point to a
+valid url. The broker must run on the standardized port 1883.
+
+Note, that the unit tests verify functionality against the MQTT 3.1.1 version
+of the standard, hence the broker needs to be compliant to the adherent
+specifications.
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 58290e4..6c70007 100644
--- a/tests/auto/qmqttclient/tst_qmqttclient.cpp
+++ b/tests/auto/qmqttclient/tst_qmqttclient.cpp
@@ -29,6 +29,7 @@
#include "broker_connection.h"
#include <QtCore/QString>
+#include <QtNetwork/QTcpServer>
#include <QtTest/QtTest>
#include <QtTest/QSignalSpy>
#include <QtMqtt/QMqttClient>
@@ -50,9 +51,12 @@ 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();
+ void reconnect_QTBUG65726();
private:
QProcess m_brokerProcess;
QString m_testBroker;
@@ -201,7 +205,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);
@@ -270,7 +274,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");
@@ -279,11 +283,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;
@@ -307,7 +309,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;
});
@@ -337,6 +339,114 @@ void Tst_QMqttClient::subscribeLongTopic()
QCOMPARE(sub, nullptr);
}
+void Tst_QMqttClient::dataIncludingZero()
+{
+ QByteArray data;
+ const int dataSize = 200;
+ data.fill('A', dataSize);
+ data[100] = '\0';
+
+ QMqttClient client;
+ client.setHostname(m_testBroker);
+ client.setPort(m_port);
+
+ client.connectToHost();
+ QTRY_COMPARE(client.state(), QMqttClient::Connected);
+
+ bool received = false;
+ bool verified = false;
+ bool correctSize = false;
+ const QString testTopic(QLatin1String("some/topic"));
+ auto sub = client.subscribe(testTopic, 1);
+ QVERIFY(sub);
+ connect(sub, &QMqttSubscription::messageReceived, [&](QMqttMessage msg) {
+ verified = msg.payload() == data;
+ correctSize = msg.payload().size() == dataSize;
+ received = true;
+ });
+
+ QTRY_COMPARE(sub->state(), QMqttSubscription::Subscribed);
+
+ client.publish(testTopic, data, 1);
+
+ QTRY_VERIFY2(received, "Subscriber did not receive message");
+ QVERIFY2(verified, "Subscriber received different message");
+ 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);
+}
+
+class FakeServer : public QObject
+{
+ Q_OBJECT
+public:
+ FakeServer() {
+ server = new QTcpServer();
+ connect(server, &QTcpServer::newConnection, this, &FakeServer::createSocket);
+ server->listen(QHostAddress::Any, 5726);
+ }
+public slots:
+ void createSocket() {
+ socket = server->nextPendingConnection();
+ connect(socket, &QTcpSocket::readyRead, this, &FakeServer::connectionRequested);
+ }
+
+ void connectionRequested() {
+ // We assume it is always a connect statement, so no verification is done
+ socket->readAll();
+ QByteArray response;
+ response += 0x20;
+ response += quint8(2); // Payload size
+ if (!connectionSuccess) {
+ response += quint8(255); // Causes ProtocolViolation
+ response += quint8(13);
+ } else {
+ response += char(0); // ackFlags
+ response += char(0); // result
+ }
+ qDebug() << "Fake server response:" << connectionSuccess;
+ socket->write(response);
+ }
+public:
+ QTcpServer *server;
+ QTcpSocket *socket;
+ bool connectionSuccess{false};
+};
+
+void Tst_QMqttClient::reconnect_QTBUG65726()
+{
+ FakeServer server;
+
+ QMqttClient client;
+ client.setClientId(QLatin1String("bugclient"));
+ client.setHostname(QLatin1String("localhost"));
+ client.setPort(5726);
+
+ client.connectToHost();
+ QTRY_COMPARE(client.state(), QMqttClient::Disconnected);
+ QTRY_COMPARE(client.error(), QMqttClient::ProtocolViolation);
+
+ server.connectionSuccess = true;
+
+ client.connectToHost();
+ QTRY_COMPARE(client.state(), QMqttClient::Connected);
+ QTRY_COMPARE(client.error(), QMqttClient::NoError);
+}
+
QTEST_MAIN(Tst_QMqttClient)
#include "tst_qmqttclient.moc"
diff --git a/tests/auto/qmqttcontrolpacket/tst_qmqttcontrolpacket.cpp b/tests/auto/qmqttcontrolpacket/tst_qmqttcontrolpacket.cpp
index 7a0827d..de0e404 100644
--- a/tests/auto/qmqttcontrolpacket/tst_qmqttcontrolpacket.cpp
+++ b/tests/auto/qmqttcontrolpacket/tst_qmqttcontrolpacket.cpp
@@ -123,6 +123,19 @@ void Tst_QMqttControlPacket::append()
payload = packet.payload();
QCOMPARE(payload.size(), data.size());
QCOMPARE(payload, data);
+
+ const QByteArray containsZero("Some data\0 with zero", 21);
+ packet.clear();
+ packet.appendRaw(containsZero);
+ payload = packet.payload();
+ QCOMPARE(containsZero, payload);
+ QCOMPARE(payload.size(), 21);
+
+ packet.clear();
+ packet.append(containsZero);
+ payload = packet.payload().mid(2); // mid because size got prepended
+ QCOMPARE(containsZero, payload);
+ QCOMPARE(payload.size(), 21);
#else
QSKIP("This test requires a Qt -developer-build.");
#endif
diff --git a/tests/auto/qmqttsubscription/tst_qmqttsubscription.cpp b/tests/auto/qmqttsubscription/tst_qmqttsubscription.cpp
index 7f51dd5..fda8ef6 100644
--- a/tests/auto/qmqttsubscription/tst_qmqttsubscription.cpp
+++ b/tests/auto/qmqttsubscription/tst_qmqttsubscription.cpp
@@ -47,6 +47,7 @@ private Q_SLOTS:
void getSetCheck();
void wildCards_data();
void wildCards();
+ void reconnect();
private:
QProcess m_brokerProcess;
QString m_testBroker;
@@ -141,6 +142,81 @@ void Tst_QMqttSubscription::wildCards()
QTRY_VERIFY2(publisher.state() == QMqttClient::Disconnected, "Could not disconnect.");
}
+void Tst_QMqttSubscription::reconnect()
+{
+ // QTBUG-64042
+ // - Connect with clean session
+ QMqttClient client;
+
+ client.setHostname(m_testBroker);
+ client.setPort(m_port);
+ client.setCleanSession(true);
+ client.connectToHost();
+ QTRY_VERIFY2(client.state() == QMqttClient::Connected, "Could not connect to broker.");
+
+ // - Subscribe to topic A
+ const QString subscription("topics/resub");
+ auto sub = client.subscribe(subscription, 1);
+ QTRY_VERIFY2(sub->state() == QMqttSubscription::Subscribed, "Could not subscribe to topic.");
+
+ // - Loose connection / connection drop
+ QAbstractSocket *transport = qobject_cast<QAbstractSocket *>(client.transport());
+ QVERIFY2(transport, "Transport has to be QAbstractSocket-based.");
+ transport->disconnectFromHost();
+ QTRY_VERIFY2(client.state() == QMqttClient::Disconnected, "State not correctly switched.");
+
+ // - Reconnect (keeping cleansession)
+ client.connectToHost();
+ QTRY_VERIFY2(client.state() == QMqttClient::Connected, "Could not connect to broker.");
+ // old subs should get updated / invalidated
+ QCOMPARE(sub->state(), QMqttSubscription::Unsubscribed);
+
+ // - Resubscribe
+ auto reSub = client.subscribe(subscription, 1);
+ QTRY_VERIFY2(reSub->state() == QMqttSubscription::Subscribed, "Could not re-subscribe to topic.");
+ QSignalSpy receivalSpy(reSub, SIGNAL(messageReceived(QMqttMessage)));
+
+ QSignalSpy pubSpy(&client, SIGNAL(messageSent(qint32)));
+ client.publish(subscription, "Sending after reconnect 1", 1);
+ QTRY_VERIFY2(pubSpy.size() == 1, "Could not publish message.");
+
+ QTRY_VERIFY2(receivalSpy.size() == 1, "Did not receive message on re-subscribe.");
+
+ // - Loose connection / connection drop
+ transport = qobject_cast<QAbstractSocket *>(client.transport());
+ QVERIFY2(transport, "Transport has to be QAbstractSocket-based.");
+ transport->disconnectFromHost();
+ QTRY_VERIFY2(client.state() == QMqttClient::Disconnected, "State not correctly switched.");
+
+ // - Reconnect (no cleansession)
+ QSignalSpy restoredSpy(&client, SIGNAL(brokerSessionRestored()));
+
+ client.setCleanSession(false);
+ client.connectToHost();
+ QTRY_VERIFY2(client.state() == QMqttClient::Connected, "Could not connect to broker.");
+ // Could not identify a broker doing this. The specs states:
+ // [MQTT-4.1.0-1] The Client and Server MUST store Session state for the entire
+ // duration of the Session.
+ // [MQTT-4.1.0-2] A Session MUST last at least as long it has an active Network Connection.
+ // All testbrokers delete the session at transport disconnect, regardless of DISCONNECT been
+ // send before or not.
+ if (restoredSpy.count() > 0) {
+ QCOMPARE(reSub->state(), QMqttSubscription::Subscribed);
+ pubSpy.clear();
+ receivalSpy.clear();
+ client.publish(subscription, "Sending after reconnect 2", 1);
+ QTRY_VERIFY2(pubSpy.size() == 1, "Could not publish message.");
+ QTRY_VERIFY2(receivalSpy.size() == 1, "Did not receive message on re-subscribe.");
+ } else {
+ // No need to test this
+ qDebug() << "Test broker does not support long-livety sessions.";
+ }
+ // - Old subscription is still active
+
+ client.disconnectFromHost();
+ QTRY_VERIFY2(client.state() == QMqttClient::Disconnected, "Could not disconnect.");
+}
+
QTEST_MAIN(Tst_QMqttSubscription)
#include "tst_qmqttsubscription.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"