From 2723b0ddd3cafb3a14cef0d11b76da3213ce68ef Mon Sep 17 00:00:00 2001 From: candi Date: Thu, 4 Sep 2025 03:46:51 +0800 Subject: [PATCH] init --- Android.bp | 16 + .../paho/mqttv5/client/BufferedMessage.java | 42 + .../paho/mqttv5/client/ConnectCallback.java | 5 + .../mqttv5/client/ConnectionLostCallback.java | 5 + .../client/ConnectionRecoveredCallback.java | 5 + .../client/DisconnectedBufferOptions.java | 91 + .../mqttv5/client/FunctionalMqttClient.java | 7 + .../paho/mqttv5/client/IMqttAsyncClient.java | 1151 +++++++++++ .../paho/mqttv5/client/IMqttClient.java | 657 ++++++ .../mqttv5/client/IMqttDeliveryToken.java | 44 + .../mqttv5/client/IMqttMessageListener.java | 58 + .../paho/mqttv5/client/IMqttToken.java | 206 ++ .../mqttv5/client/MqttActionListener.java | 30 + .../paho/mqttv5/client/MqttAsyncClient.java | 1813 +++++++++++++++++ .../paho/mqttv5/client/MqttCallback.java | 132 ++ .../paho/mqttv5/client/MqttClient.java | 744 +++++++ .../mqttv5/client/MqttClientException.java | 164 ++ .../mqttv5/client/MqttClientInterface.java | 9 + .../mqttv5/client/MqttClientPersistence.java | 104 + .../mqttv5/client/MqttConnectionOptions.java | 971 +++++++++ .../client/MqttConnectionOptionsBuilder.java | 109 + .../paho/mqttv5/client/MqttDeliveryToken.java | 56 + .../mqttv5/client/MqttDisconnectResponse.java | 97 + .../paho/mqttv5/client/MqttPingSender.java | 52 + .../eclipse/paho/mqttv5/client/MqttToken.java | 148 ++ .../eclipse/paho/mqttv5/client/MqttTopic.java | 126 ++ .../paho/mqttv5/client/TimerPingSender.java | 121 ++ .../mqttv5/client/internal/ClientComms.java | 978 +++++++++ .../mqttv5/client/internal/ClientState.java | 1625 +++++++++++++++ .../mqttv5/client/internal/CommsCallback.java | 670 ++++++ .../mqttv5/client/internal/CommsReceiver.java | 241 +++ .../mqttv5/client/internal/CommsSender.java | 210 ++ .../client/internal/CommsTokenStore.java | 254 +++ .../internal/ConnectActionListener.java | 263 +++ .../client/internal/DestinationProvider.java | 31 + .../internal/DisconnectedMessageBuffer.java | 135 ++ .../client/internal/ExceptionHelper.java | 63 + .../paho/mqttv5/client/internal/FileLock.java | 96 + .../internal/IDisconnectedBufferCallback.java | 25 + .../client/internal/MessageCatalog.java | 46 + .../client/internal/MqttConnectionState.java | 185 ++ .../client/internal/MqttPersistentData.java | 98 + .../client/internal/MqttSessionState.java | 39 + .../mqttv5/client/internal/MqttState.java | 114 ++ .../mqttv5/client/internal/NetworkModule.java | 35 + .../client/internal/NetworkModuleService.java | 158 ++ .../internal/ResourceBundleCatalog.java | 37 + .../client/internal/SSLNetworkModule.java | 161 ++ .../internal/SSLNetworkModuleFactory.java | 88 + .../client/internal/TCPNetworkModule.java | 133 ++ .../internal/TCPNetworkModuleFactory.java | 64 + .../paho/mqttv5/client/internal/Token.java | 516 +++++ .../mqttv5/client/logging/JSR47Logger.java | 278 +++ .../paho/mqttv5/client/logging/Logger.java | 579 ++++++ .../mqttv5/client/logging/LoggerFactory.java | 155 ++ .../client/logging/SimpleLogFormatter.java | 104 + .../mqttv5/client/logging/jsr47min.properties | 83 + .../paho/mqttv5/client/logging/package.html | 5 + .../client/persist/MemoryPersistence.java | 111 + .../persist/MqttDefaultFilePersistence.java | 300 +++ .../client/persist/PersistenceFileFilter.java | 18 + .../persist/PersistenceFileNameFilter.java | 18 + .../security/SSLSocketFactoryFactory.java | 1358 ++++++++++++ .../client/security/SimpleBase64Encoder.java | 127 ++ .../client/spi/NetworkModuleFactory.java | 56 + .../paho/mqttv5/client/spi/package-info.java | 6 + .../paho/mqttv5/client/util/Debug.java | 183 ++ .../paho/mqttv5/client/websocket/Base64.java | 95 + .../ExtendedByteArrayOutputStream.java | 47 + .../websocket/HandshakeFailedException.java | 22 + .../client/websocket/WebSocketFrame.java | 306 +++ .../client/websocket/WebSocketHandshake.java | 230 +++ .../websocket/WebSocketNetworkModule.java | 111 + .../WebSocketNetworkModuleFactory.java | 61 + .../client/websocket/WebSocketReceiver.java | 149 ++ .../WebSocketSecureNetworkModule.java | 111 + .../WebSocketSecureNetworkModuleFactory.java | 82 + .../mqttv5/client/wire/MqttInputStream.java | 166 ++ .../mqttv5/client/wire/MqttOutputStream.java | 101 + .../paho/mqttv5/common/ExceptionHelper.java | 54 + .../paho/mqttv5/common/MqttException.java | 158 ++ .../paho/mqttv5/common/MqttMessage.java | 298 +++ .../paho/mqttv5/common/MqttPersistable.java | 111 + .../common/MqttPersistenceException.java | 45 + .../mqttv5/common/MqttSecurityException.java | 52 + .../paho/mqttv5/common/MqttSubscription.java | 166 ++ .../paho/mqttv5/common/Validators.java | 28 + .../paho/mqttv5/common/packet/MqttAck.java | 31 + .../paho/mqttv5/common/packet/MqttAuth.java | 123 ++ .../mqttv5/common/packet/MqttConnAck.java | 154 ++ .../mqttv5/common/packet/MqttConnect.java | 337 +++ .../mqttv5/common/packet/MqttDataTypes.java | 253 +++ .../mqttv5/common/packet/MqttDisconnect.java | 129 ++ .../common/packet/MqttPacketException.java | 43 + .../packet/MqttPersistableWireMessage.java | 76 + .../mqttv5/common/packet/MqttPingReq.java | 52 + .../mqttv5/common/packet/MqttPingResp.java | 52 + .../mqttv5/common/packet/MqttProperties.java | 1419 +++++++++++++ .../paho/mqttv5/common/packet/MqttPubAck.java | 116 ++ .../mqttv5/common/packet/MqttPubComp.java | 118 ++ .../paho/mqttv5/common/packet/MqttPubRec.java | 121 ++ .../paho/mqttv5/common/packet/MqttPubRel.java | 123 ++ .../mqttv5/common/packet/MqttPublish.java | 242 +++ .../common/packet/MqttReceivedMessage.java | 38 + .../mqttv5/common/packet/MqttReturnCode.java | 77 + .../paho/mqttv5/common/packet/MqttSubAck.java | 134 ++ .../mqttv5/common/packet/MqttSubscribe.java | 217 ++ .../mqttv5/common/packet/MqttUnsubAck.java | 130 ++ .../mqttv5/common/packet/MqttUnsubscribe.java | 138 ++ .../mqttv5/common/packet/MqttWireMessage.java | 417 ++++ .../mqttv5/common/packet/UserProperty.java | 40 + .../packet/util/CountingInputStream.java | 58 + .../util/MultiByteArrayInputStream.java | 57 + .../packet/util/VariableByteInteger.java | 53 + .../common/util/MqttTopicValidator.java | 225 ++ .../paho/mqttv5/common/util/Strings.java | 173 ++ ...aho.mqttv5.client.spi.NetworkModuleFactory | 5 + src/main/resources/bundle.properties | 15 + .../client/internal/nls/logcat.properties | 189 ++ .../paho/mqttv5/common/nls/logcat.properties | 1 + .../mqttv5/common/nls/messages.properties | 88 + .../mqttv5/common/nls/messages_cs.properties | 35 + .../mqttv5/common/nls/messages_de.properties | 35 + .../mqttv5/common/nls/messages_es.properties | 35 + .../mqttv5/common/nls/messages_fr.properties | 35 + .../mqttv5/common/nls/messages_hu.properties | 35 + .../mqttv5/common/nls/messages_it.properties | 35 + .../mqttv5/common/nls/messages_ja.properties | 35 + .../mqttv5/common/nls/messages_ko.properties | 35 + .../mqttv5/common/nls/messages_pl.properties | 35 + .../common/nls/messages_pt_BR.properties | 35 + .../mqttv5/common/nls/messages_ru.properties | 35 + .../common/nls/messages_zh_CN.properties | 35 + .../common/nls/messages_zh_TW.properties | 35 + 134 files changed, 24901 insertions(+) create mode 100644 Android.bp create mode 100644 src/main/java/org/eclipse/paho/mqttv5/client/BufferedMessage.java create mode 100644 src/main/java/org/eclipse/paho/mqttv5/client/ConnectCallback.java create mode 100644 src/main/java/org/eclipse/paho/mqttv5/client/ConnectionLostCallback.java create mode 100644 src/main/java/org/eclipse/paho/mqttv5/client/ConnectionRecoveredCallback.java create mode 100644 src/main/java/org/eclipse/paho/mqttv5/client/DisconnectedBufferOptions.java create mode 100644 src/main/java/org/eclipse/paho/mqttv5/client/FunctionalMqttClient.java create mode 100644 src/main/java/org/eclipse/paho/mqttv5/client/IMqttAsyncClient.java create mode 100644 src/main/java/org/eclipse/paho/mqttv5/client/IMqttClient.java create mode 100644 src/main/java/org/eclipse/paho/mqttv5/client/IMqttDeliveryToken.java create mode 100644 src/main/java/org/eclipse/paho/mqttv5/client/IMqttMessageListener.java create mode 100644 src/main/java/org/eclipse/paho/mqttv5/client/IMqttToken.java create mode 100644 src/main/java/org/eclipse/paho/mqttv5/client/MqttActionListener.java create mode 100644 src/main/java/org/eclipse/paho/mqttv5/client/MqttAsyncClient.java create mode 100644 src/main/java/org/eclipse/paho/mqttv5/client/MqttCallback.java create mode 100644 src/main/java/org/eclipse/paho/mqttv5/client/MqttClient.java create mode 100644 src/main/java/org/eclipse/paho/mqttv5/client/MqttClientException.java create mode 100644 src/main/java/org/eclipse/paho/mqttv5/client/MqttClientInterface.java create mode 100644 src/main/java/org/eclipse/paho/mqttv5/client/MqttClientPersistence.java create mode 100644 src/main/java/org/eclipse/paho/mqttv5/client/MqttConnectionOptions.java create mode 100644 src/main/java/org/eclipse/paho/mqttv5/client/MqttConnectionOptionsBuilder.java create mode 100644 src/main/java/org/eclipse/paho/mqttv5/client/MqttDeliveryToken.java create mode 100644 src/main/java/org/eclipse/paho/mqttv5/client/MqttDisconnectResponse.java create mode 100644 src/main/java/org/eclipse/paho/mqttv5/client/MqttPingSender.java create mode 100644 src/main/java/org/eclipse/paho/mqttv5/client/MqttToken.java create mode 100644 src/main/java/org/eclipse/paho/mqttv5/client/MqttTopic.java create mode 100644 src/main/java/org/eclipse/paho/mqttv5/client/TimerPingSender.java create mode 100644 src/main/java/org/eclipse/paho/mqttv5/client/internal/ClientComms.java create mode 100644 src/main/java/org/eclipse/paho/mqttv5/client/internal/ClientState.java create mode 100644 src/main/java/org/eclipse/paho/mqttv5/client/internal/CommsCallback.java create mode 100644 src/main/java/org/eclipse/paho/mqttv5/client/internal/CommsReceiver.java create mode 100644 src/main/java/org/eclipse/paho/mqttv5/client/internal/CommsSender.java create mode 100644 src/main/java/org/eclipse/paho/mqttv5/client/internal/CommsTokenStore.java create mode 100644 src/main/java/org/eclipse/paho/mqttv5/client/internal/ConnectActionListener.java create mode 100644 src/main/java/org/eclipse/paho/mqttv5/client/internal/DestinationProvider.java create mode 100644 src/main/java/org/eclipse/paho/mqttv5/client/internal/DisconnectedMessageBuffer.java create mode 100644 src/main/java/org/eclipse/paho/mqttv5/client/internal/ExceptionHelper.java create mode 100644 src/main/java/org/eclipse/paho/mqttv5/client/internal/FileLock.java create mode 100644 src/main/java/org/eclipse/paho/mqttv5/client/internal/IDisconnectedBufferCallback.java create mode 100644 src/main/java/org/eclipse/paho/mqttv5/client/internal/MessageCatalog.java create mode 100644 src/main/java/org/eclipse/paho/mqttv5/client/internal/MqttConnectionState.java create mode 100644 src/main/java/org/eclipse/paho/mqttv5/client/internal/MqttPersistentData.java create mode 100644 src/main/java/org/eclipse/paho/mqttv5/client/internal/MqttSessionState.java create mode 100644 src/main/java/org/eclipse/paho/mqttv5/client/internal/MqttState.java create mode 100644 src/main/java/org/eclipse/paho/mqttv5/client/internal/NetworkModule.java create mode 100644 src/main/java/org/eclipse/paho/mqttv5/client/internal/NetworkModuleService.java create mode 100644 src/main/java/org/eclipse/paho/mqttv5/client/internal/ResourceBundleCatalog.java create mode 100644 src/main/java/org/eclipse/paho/mqttv5/client/internal/SSLNetworkModule.java create mode 100644 src/main/java/org/eclipse/paho/mqttv5/client/internal/SSLNetworkModuleFactory.java create mode 100644 src/main/java/org/eclipse/paho/mqttv5/client/internal/TCPNetworkModule.java create mode 100644 src/main/java/org/eclipse/paho/mqttv5/client/internal/TCPNetworkModuleFactory.java create mode 100644 src/main/java/org/eclipse/paho/mqttv5/client/internal/Token.java create mode 100644 src/main/java/org/eclipse/paho/mqttv5/client/logging/JSR47Logger.java create mode 100644 src/main/java/org/eclipse/paho/mqttv5/client/logging/Logger.java create mode 100644 src/main/java/org/eclipse/paho/mqttv5/client/logging/LoggerFactory.java create mode 100644 src/main/java/org/eclipse/paho/mqttv5/client/logging/SimpleLogFormatter.java create mode 100644 src/main/java/org/eclipse/paho/mqttv5/client/logging/jsr47min.properties create mode 100644 src/main/java/org/eclipse/paho/mqttv5/client/logging/package.html create mode 100644 src/main/java/org/eclipse/paho/mqttv5/client/persist/MemoryPersistence.java create mode 100644 src/main/java/org/eclipse/paho/mqttv5/client/persist/MqttDefaultFilePersistence.java create mode 100644 src/main/java/org/eclipse/paho/mqttv5/client/persist/PersistenceFileFilter.java create mode 100644 src/main/java/org/eclipse/paho/mqttv5/client/persist/PersistenceFileNameFilter.java create mode 100644 src/main/java/org/eclipse/paho/mqttv5/client/security/SSLSocketFactoryFactory.java create mode 100644 src/main/java/org/eclipse/paho/mqttv5/client/security/SimpleBase64Encoder.java create mode 100644 src/main/java/org/eclipse/paho/mqttv5/client/spi/NetworkModuleFactory.java create mode 100644 src/main/java/org/eclipse/paho/mqttv5/client/spi/package-info.java create mode 100644 src/main/java/org/eclipse/paho/mqttv5/client/util/Debug.java create mode 100644 src/main/java/org/eclipse/paho/mqttv5/client/websocket/Base64.java create mode 100644 src/main/java/org/eclipse/paho/mqttv5/client/websocket/ExtendedByteArrayOutputStream.java create mode 100644 src/main/java/org/eclipse/paho/mqttv5/client/websocket/HandshakeFailedException.java create mode 100644 src/main/java/org/eclipse/paho/mqttv5/client/websocket/WebSocketFrame.java create mode 100644 src/main/java/org/eclipse/paho/mqttv5/client/websocket/WebSocketHandshake.java create mode 100644 src/main/java/org/eclipse/paho/mqttv5/client/websocket/WebSocketNetworkModule.java create mode 100644 src/main/java/org/eclipse/paho/mqttv5/client/websocket/WebSocketNetworkModuleFactory.java create mode 100644 src/main/java/org/eclipse/paho/mqttv5/client/websocket/WebSocketReceiver.java create mode 100644 src/main/java/org/eclipse/paho/mqttv5/client/websocket/WebSocketSecureNetworkModule.java create mode 100644 src/main/java/org/eclipse/paho/mqttv5/client/websocket/WebSocketSecureNetworkModuleFactory.java create mode 100644 src/main/java/org/eclipse/paho/mqttv5/client/wire/MqttInputStream.java create mode 100644 src/main/java/org/eclipse/paho/mqttv5/client/wire/MqttOutputStream.java create mode 100644 src/main/java/org/eclipse/paho/mqttv5/common/ExceptionHelper.java create mode 100644 src/main/java/org/eclipse/paho/mqttv5/common/MqttException.java create mode 100644 src/main/java/org/eclipse/paho/mqttv5/common/MqttMessage.java create mode 100644 src/main/java/org/eclipse/paho/mqttv5/common/MqttPersistable.java create mode 100644 src/main/java/org/eclipse/paho/mqttv5/common/MqttPersistenceException.java create mode 100644 src/main/java/org/eclipse/paho/mqttv5/common/MqttSecurityException.java create mode 100644 src/main/java/org/eclipse/paho/mqttv5/common/MqttSubscription.java create mode 100644 src/main/java/org/eclipse/paho/mqttv5/common/Validators.java create mode 100644 src/main/java/org/eclipse/paho/mqttv5/common/packet/MqttAck.java create mode 100644 src/main/java/org/eclipse/paho/mqttv5/common/packet/MqttAuth.java create mode 100644 src/main/java/org/eclipse/paho/mqttv5/common/packet/MqttConnAck.java create mode 100644 src/main/java/org/eclipse/paho/mqttv5/common/packet/MqttConnect.java create mode 100644 src/main/java/org/eclipse/paho/mqttv5/common/packet/MqttDataTypes.java create mode 100644 src/main/java/org/eclipse/paho/mqttv5/common/packet/MqttDisconnect.java create mode 100644 src/main/java/org/eclipse/paho/mqttv5/common/packet/MqttPacketException.java create mode 100644 src/main/java/org/eclipse/paho/mqttv5/common/packet/MqttPersistableWireMessage.java create mode 100644 src/main/java/org/eclipse/paho/mqttv5/common/packet/MqttPingReq.java create mode 100644 src/main/java/org/eclipse/paho/mqttv5/common/packet/MqttPingResp.java create mode 100644 src/main/java/org/eclipse/paho/mqttv5/common/packet/MqttProperties.java create mode 100644 src/main/java/org/eclipse/paho/mqttv5/common/packet/MqttPubAck.java create mode 100644 src/main/java/org/eclipse/paho/mqttv5/common/packet/MqttPubComp.java create mode 100644 src/main/java/org/eclipse/paho/mqttv5/common/packet/MqttPubRec.java create mode 100644 src/main/java/org/eclipse/paho/mqttv5/common/packet/MqttPubRel.java create mode 100644 src/main/java/org/eclipse/paho/mqttv5/common/packet/MqttPublish.java create mode 100644 src/main/java/org/eclipse/paho/mqttv5/common/packet/MqttReceivedMessage.java create mode 100644 src/main/java/org/eclipse/paho/mqttv5/common/packet/MqttReturnCode.java create mode 100644 src/main/java/org/eclipse/paho/mqttv5/common/packet/MqttSubAck.java create mode 100644 src/main/java/org/eclipse/paho/mqttv5/common/packet/MqttSubscribe.java create mode 100644 src/main/java/org/eclipse/paho/mqttv5/common/packet/MqttUnsubAck.java create mode 100644 src/main/java/org/eclipse/paho/mqttv5/common/packet/MqttUnsubscribe.java create mode 100644 src/main/java/org/eclipse/paho/mqttv5/common/packet/MqttWireMessage.java create mode 100644 src/main/java/org/eclipse/paho/mqttv5/common/packet/UserProperty.java create mode 100644 src/main/java/org/eclipse/paho/mqttv5/common/packet/util/CountingInputStream.java create mode 100644 src/main/java/org/eclipse/paho/mqttv5/common/packet/util/MultiByteArrayInputStream.java create mode 100644 src/main/java/org/eclipse/paho/mqttv5/common/packet/util/VariableByteInteger.java create mode 100644 src/main/java/org/eclipse/paho/mqttv5/common/util/MqttTopicValidator.java create mode 100644 src/main/java/org/eclipse/paho/mqttv5/common/util/Strings.java create mode 100644 src/main/resources/META-INF/services/org.eclipse.paho.mqttv5.client.spi.NetworkModuleFactory create mode 100644 src/main/resources/bundle.properties create mode 100644 src/main/resources/org/eclipse/paho/mqttv5/client/internal/nls/logcat.properties create mode 100644 src/main/resources/org/eclipse/paho/mqttv5/common/nls/logcat.properties create mode 100644 src/main/resources/org/eclipse/paho/mqttv5/common/nls/messages.properties create mode 100644 src/main/resources/org/eclipse/paho/mqttv5/common/nls/messages_cs.properties create mode 100644 src/main/resources/org/eclipse/paho/mqttv5/common/nls/messages_de.properties create mode 100644 src/main/resources/org/eclipse/paho/mqttv5/common/nls/messages_es.properties create mode 100644 src/main/resources/org/eclipse/paho/mqttv5/common/nls/messages_fr.properties create mode 100644 src/main/resources/org/eclipse/paho/mqttv5/common/nls/messages_hu.properties create mode 100644 src/main/resources/org/eclipse/paho/mqttv5/common/nls/messages_it.properties create mode 100644 src/main/resources/org/eclipse/paho/mqttv5/common/nls/messages_ja.properties create mode 100644 src/main/resources/org/eclipse/paho/mqttv5/common/nls/messages_ko.properties create mode 100644 src/main/resources/org/eclipse/paho/mqttv5/common/nls/messages_pl.properties create mode 100644 src/main/resources/org/eclipse/paho/mqttv5/common/nls/messages_pt_BR.properties create mode 100644 src/main/resources/org/eclipse/paho/mqttv5/common/nls/messages_ru.properties create mode 100644 src/main/resources/org/eclipse/paho/mqttv5/common/nls/messages_zh_CN.properties create mode 100644 src/main/resources/org/eclipse/paho/mqttv5/common/nls/messages_zh_TW.properties diff --git a/Android.bp b/Android.bp new file mode 100644 index 0000000..9a35d03 --- /dev/null +++ b/Android.bp @@ -0,0 +1,16 @@ +java_library_static { + name: "libmqttv5", + srcs: [ + "src/main/java/org/eclipse/paho/mqttv5/client/**/*.java", + "src/main/java/org/eclipse/paho/mqttv5/common/**/*.java", + "src/main/java/org/eclipse/paho/mqttv5/internal/**/*.java", + "src/main/java/org/eclipse/paho/mqttv5/util/**/*.java", + ], + javac_flags: [ + "-Xlint:all", + "-Werror", + ], + visibility: [ + "//frameworks/base/services/core", + ], +} diff --git a/src/main/java/org/eclipse/paho/mqttv5/client/BufferedMessage.java b/src/main/java/org/eclipse/paho/mqttv5/client/BufferedMessage.java new file mode 100644 index 0000000..f97c8b3 --- /dev/null +++ b/src/main/java/org/eclipse/paho/mqttv5/client/BufferedMessage.java @@ -0,0 +1,42 @@ +/******************************************************************************* + * Copyright (c) 2016 IBM Corp. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * and Eclipse Distribution License v1.0 which accompany this distribution. + * + * The Eclipse Public License is available at + * https://www.eclipse.org/legal/epl-2.0 + * and the Eclipse Distribution License is available at + * https://www.eclipse.org/org/documents/edl-v10.php + * + * Contributors: + * James Sutton - Initial Contribution for Automatic Reconnect & Offline Buffering + */ +package org.eclipse.paho.mqttv5.client; + +import org.eclipse.paho.mqttv5.common.packet.MqttWireMessage; + +/** + * A BufferedMessage contains an MqttWire Message and token + * it allows both message and token to be buffered when the client + * is in resting state + */ +public class BufferedMessage { + + private MqttWireMessage message; + private MqttToken token; + + public BufferedMessage(MqttWireMessage message, MqttToken token){ + this.message = message; + this.token = token; + } + + public MqttWireMessage getMessage() { + return message; + } + + public MqttToken getToken() { + return token; + } +} diff --git a/src/main/java/org/eclipse/paho/mqttv5/client/ConnectCallback.java b/src/main/java/org/eclipse/paho/mqttv5/client/ConnectCallback.java new file mode 100644 index 0000000..3393b4a --- /dev/null +++ b/src/main/java/org/eclipse/paho/mqttv5/client/ConnectCallback.java @@ -0,0 +1,5 @@ +package org.eclipse.paho.mqttv5.client; + +public interface ConnectCallback { + +} diff --git a/src/main/java/org/eclipse/paho/mqttv5/client/ConnectionLostCallback.java b/src/main/java/org/eclipse/paho/mqttv5/client/ConnectionLostCallback.java new file mode 100644 index 0000000..cb548d1 --- /dev/null +++ b/src/main/java/org/eclipse/paho/mqttv5/client/ConnectionLostCallback.java @@ -0,0 +1,5 @@ +package org.eclipse.paho.mqttv5.client; + +public interface ConnectionLostCallback { + +} diff --git a/src/main/java/org/eclipse/paho/mqttv5/client/ConnectionRecoveredCallback.java b/src/main/java/org/eclipse/paho/mqttv5/client/ConnectionRecoveredCallback.java new file mode 100644 index 0000000..f1d6c7b --- /dev/null +++ b/src/main/java/org/eclipse/paho/mqttv5/client/ConnectionRecoveredCallback.java @@ -0,0 +1,5 @@ +package org.eclipse.paho.mqttv5.client; + +public interface ConnectionRecoveredCallback { + +} diff --git a/src/main/java/org/eclipse/paho/mqttv5/client/DisconnectedBufferOptions.java b/src/main/java/org/eclipse/paho/mqttv5/client/DisconnectedBufferOptions.java new file mode 100644 index 0000000..1daa66d --- /dev/null +++ b/src/main/java/org/eclipse/paho/mqttv5/client/DisconnectedBufferOptions.java @@ -0,0 +1,91 @@ +/******************************************************************************* + * Copyright (c) 2016 IBM Corp. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * and Eclipse Distribution License v1.0 which accompany this distribution. + * + * The Eclipse Public License is available at + * https://www.eclipse.org/legal/epl-2.0 + * and the Eclipse Distribution License is available at + * https://www.eclipse.org/org/documents/edl-v10.php + * + * Contributors: + * James Sutton - Initial Contribution for Automatic Reconnect & Offline Buffering + */ +package org.eclipse.paho.mqttv5.client; + +/** + * Holds the set of options that govern the behaviour + * of Offline (or Disconnected) buffering of messages + */ +public class DisconnectedBufferOptions { + + /** + * The default size of the disconnected buffer + */ + public static final int DISCONNECTED_BUFFER_SIZE_DEFAULT = 5000; + + public static final boolean DISCONNECTED_BUFFER_ENABLED_DEFAULT = false; + + public static final boolean PERSIST_DISCONNECTED_BUFFER_DEFAULT = false; + + public static final boolean DELETE_OLDEST_MESSAGES_DEFAULT = false; + + private int bufferSize = DISCONNECTED_BUFFER_SIZE_DEFAULT; + private boolean bufferEnabled = DISCONNECTED_BUFFER_ENABLED_DEFAULT; + private boolean persistBuffer = PERSIST_DISCONNECTED_BUFFER_DEFAULT; + private boolean deleteOldestMessages = DELETE_OLDEST_MESSAGES_DEFAULT; + + /** + * Constructs a new DisconnectedBufferOptions object using the + * default values. + * + * The defaults are: + * + * More information about these values can be found in the setter methods. + */ + public DisconnectedBufferOptions() { + // Do Nothing. + } + + public int getBufferSize() { + return bufferSize; + } + + public void setBufferSize(int bufferSize) { + if (bufferSize < 1) { + throw new IllegalArgumentException(); + } + this.bufferSize = bufferSize; + } + + public boolean isBufferEnabled() { + return bufferEnabled; + } + + public void setBufferEnabled(boolean bufferEnabled) { + this.bufferEnabled = bufferEnabled; + } + + public boolean isPersistBuffer() { + return persistBuffer; + } + + public void setPersistBuffer(boolean persistBuffer) { + this.persistBuffer = persistBuffer; + } + + public boolean isDeleteOldestMessages() { + return deleteOldestMessages; + } + + public void setDeleteOldestMessages(boolean deleteOldestMessages) { + this.deleteOldestMessages = deleteOldestMessages; + } +} diff --git a/src/main/java/org/eclipse/paho/mqttv5/client/FunctionalMqttClient.java b/src/main/java/org/eclipse/paho/mqttv5/client/FunctionalMqttClient.java new file mode 100644 index 0000000..f9c55d4 --- /dev/null +++ b/src/main/java/org/eclipse/paho/mqttv5/client/FunctionalMqttClient.java @@ -0,0 +1,7 @@ +package org.eclipse.paho.mqttv5.client; + +@FunctionalInterface +public interface FunctionalMqttClient { + int operation(int a, int b); + +} diff --git a/src/main/java/org/eclipse/paho/mqttv5/client/IMqttAsyncClient.java b/src/main/java/org/eclipse/paho/mqttv5/client/IMqttAsyncClient.java new file mode 100644 index 0000000..6f64e0e --- /dev/null +++ b/src/main/java/org/eclipse/paho/mqttv5/client/IMqttAsyncClient.java @@ -0,0 +1,1151 @@ +/******************************************************************************* + * Copyright (c) 2009, 2018 IBM Corp. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * and Eclipse Distribution License v1.0 which accompany this distribution. + * + * The Eclipse Public License is available at + * https://www.eclipse.org/legal/epl-2.0 + * and the Eclipse Distribution License is available at + * https://www.eclipse.org/org/documents/edl-v10.php + * + * Contributors: + * James Sutton - MQTT V5 support + */ + +package org.eclipse.paho.mqttv5.client; + +import org.eclipse.paho.mqttv5.client.util.Debug; +import org.eclipse.paho.mqttv5.common.MqttException; +import org.eclipse.paho.mqttv5.common.MqttMessage; +import org.eclipse.paho.mqttv5.common.MqttPersistenceException; +import org.eclipse.paho.mqttv5.common.MqttSecurityException; +import org.eclipse.paho.mqttv5.common.MqttSubscription; +import org.eclipse.paho.mqttv5.common.packet.MqttProperties; + +public interface IMqttAsyncClient { + + /** + * Connects to an MQTT server using the default options. + *

+ * The default options are specified in {@link MqttConnectionOptions} class. + *

+ * + * @param userContext + * optional object used to pass context to the callback. Use null if + * not required. + * @param callback + * optional listener that will be notified when the connect + * completes. Use null if not required. + * @throws MqttSecurityException + * for security related problems + * @throws MqttException + * for non security related problems + * @return token used to track and wait for the connect to complete. The token + * will be passed to any callback that has been set. + * @see #connect(MqttConnectionOptions, Object, MqttActionListener) + */ + public IMqttToken connect(Object userContext, MqttActionListener callback) throws MqttException, MqttSecurityException; + + /** + * Connects to an MQTT server using the default options. + *

+ * The default options are specified in {@link MqttConnectionOptions} class. + *

+ * + * @throws MqttSecurityException + * for security related problems + * @throws MqttException + * for non security related problems + * @return token used to track and wait for the connect to complete. The token + * will be passed to the callback methods if a callback is set. + * @see #connect(MqttConnectionOptions, Object, MqttActionListener) + */ + public IMqttToken connect() throws MqttException, MqttSecurityException; + + /** + * Connects to an MQTT server using the provided connect options. + *

+ * The connection will be established using the options specified in the + * {@link MqttConnectionOptions} parameter. + *

+ * + * @param options + * a set of connection parameters that override the defaults. + * @throws MqttSecurityException + * for security related problems + * @throws MqttException + * for non security related problems + * @return token used to track and wait for the connect to complete. The token + * will be passed to any callback that has been set. + * @see #connect(MqttConnectionOptions, Object, MqttActionListener) + */ + public IMqttToken connect(MqttConnectionOptions options) throws MqttException, MqttSecurityException; + + /** + * Connects to an MQTT server using the specified options. + *

+ * The server to connect to is specified on the constructor. It is recommended + * to call {@link #setCallback(MqttCallback)} prior to connecting in order that + * messages destined for the client can be accepted as soon as the client is + * connected. + *

+ *

+ * The method returns control before the connect completes. Completion can be + * tracked by: + *

+ * + * + * @param options + * a set of connection parameters that override the defaults. + * @param userContext + * optional object for used to pass context to the callback. Use null + * if not required. + * @param callback + * optional listener that will be notified when the connect + * completes. Use null if not required. + * @return token used to track and wait for the connect to complete. The token + * will be passed to any callback that has been set. + * @throws MqttSecurityException + * for security related problems + * @throws MqttException + * for non security related problems including communication errors + */ + public IMqttToken connect(MqttConnectionOptions options, Object userContext, MqttActionListener callback) + throws MqttException, MqttSecurityException; + + /** + * Disconnects from the server. + *

+ * An attempt is made to quiesce the client allowing outstanding work to + * complete before disconnecting. It will wait for a maximum of 30 seconds for + * work to quiesce before disconnecting. This method must not be called from + * inside {@link MqttCallback} methods. + *

+ * + * @param userContext + * optional object used to pass context to the callback. Use null if + * not required. + * @param callback + * optional listener that will be notified when the disconnect + * completes. Use null if not required. + * @return token used to track and wait for the disconnect to complete. The + * token will be passed to any callback that has been set. + * @throws MqttException + * for problems encountered while disconnecting + * @see #disconnect(long, Object, MqttActionListener, int, MqttProperties) + * + */ + public IMqttToken disconnect(Object userContext, MqttActionListener callback) throws MqttException; + + /** + * Disconnects from the server. + *

+ * An attempt is made to quiesce the client allowing outstanding work to + * complete before disconnecting. It will wait for a maximum of 30 seconds for + * work to quiesce before disconnecting. This method must not be called from + * inside {@link MqttCallback} methods. + *

+ * + * @return token used to track and wait for disconnect to complete. The token + * will be passed to any callback that has been set. + * @throws MqttException + * for problems encountered while disconnecting + * @see #disconnect(long, Object, MqttActionListener, int, MqttProperties) + */ + public IMqttToken disconnect() throws MqttException; + + /** + * Disconnects from the server. + *

+ * An attempt is made to quiesce the client allowing outstanding work to + * complete before disconnecting. It will wait for a maximum of the specified + * quiesce time for work to complete before disconnecting. This method must not + * be called from inside {@link MqttCallback} methods. + *

+ * + * @param quiesceTimeout + * the amount of time in milliseconds to allow for existing work to + * finish before disconnecting. A value of zero or less means the + * client will not quiesce. + * @return token used to track and wait for disconnect to complete. The token + * will be passed to the callback methods if a callback is set. + * @throws MqttException + * for problems encountered while disconnecting + * @see #disconnect(long, Object, MqttActionListener, int, MqttProperties) + */ + public IMqttToken disconnect(long quiesceTimeout) throws MqttException; + + /** + * Disconnects from the server. + *

+ * The client will wait for {@link MqttCallback} methods to complete. It will + * then wait for up to the quiesce timeout to allow for work which has already + * been initiated to complete. For instance when a QoS 2 message has started + * flowing to the server but the QoS 2 flow has not completed.It prevents new + * messages being accepted and does not send any messages that have been + * accepted but not yet started delivery across the network to the server. When + * work has completed or after the quiesce timeout, the client will disconnect + * from the server. If the cleanStart flag was set to false and is set to + * false the next time a connection is made QoS 1 and 2 messages that were not + * previously delivered will be delivered. + *

+ *

+ * This method must not be called from inside {@link MqttCallback} methods. + *

+ *

+ * The method returns control before the disconnect completes. Completion can be + * tracked by: + *

+ * + * + * @param quiesceTimeout + * the amount of time in milliseconds to allow for existing work to + * finish before disconnecting. A value of zero or less means the + * client will not quiesce. + * @param userContext + * optional object used to pass context to the callback. Use null if + * not required. + * @param callback + * optional listener that will be notified when the disconnect + * completes. Use null if not required. + * @param reasonCode + * the disconnection reason code. + * @param disconnectProperties + * The {@link MqttProperties} to be sent. + * @return token used to track and wait for the connect to complete. The token + * will be passed to any callback that has been set. + * @throws MqttException + * for problems encountered while disconnecting + */ + public IMqttToken disconnect(long quiesceTimeout, Object userContext, MqttActionListener callback, int reasonCode, + MqttProperties disconnectProperties) throws MqttException; + + /** + * Disconnects from the server forcibly to reset all the states. Could be useful + * when disconnect attempt failed. + *

+ * Because the client is able to establish the TCP/IP connection to a none MQTT + * server and it will certainly fail to send the disconnect packet. It will wait + * for a maximum of 30 seconds for work to quiesce before disconnecting and wait + * for a maximum of 10 seconds for sending the disconnect packet to server. + * + * @throws MqttException + * if any unexpected error + * @since 0.4.1 + */ + public void disconnectForcibly() throws MqttException; + + /** + * Disconnects from the server forcibly to reset all the states. Could be useful + * when disconnect attempt failed. + *

+ * Because the client is able to establish the TCP/IP connection to a none MQTT + * server and it will certainly fail to send the disconnect packet. It will wait + * for a maximum of 30 seconds for work to quiesce before disconnecting. + * + * @param disconnectTimeout + * the amount of time in milliseconds to allow send disconnect packet + * to server. + * @throws MqttException + * if any unexpected error + * @since 0.4.1 + */ + public void disconnectForcibly(long disconnectTimeout) throws MqttException; + + /** + * Disconnects from the server forcibly to reset all the states. Could be useful + * when disconnect attempt failed. + *

+ * Because the client is able to establish the TCP/IP connection to a none MQTT + * server and it will certainly fail to send the disconnect packet. + * + * @param quiesceTimeout + * the amount of time in milliseconds to allow for existing work to + * finish before disconnecting. A value of zero or less means the + * client will not quiesce. + * @param disconnectTimeout + * the amount of time in milliseconds to allow send disconnect packet + * to server. + * @param reasonCode + * the disconnection reason code. + * @param disconnectProperties + * The {@link MqttProperties} to be sent. + * @throws MqttException + * if any unexpected error + * @since 0.4.1 + */ + public void disconnectForcibly(long quiesceTimeout, long disconnectTimeout, int reasonCode, + MqttProperties disconnectProperties) throws MqttException; + + /** + * Disconnects from the server forcibly to reset all the states. Could be useful + * when disconnect attempt failed. + *

+ * Because the client is able to establish the TCP/IP connection to a none MQTT + * server and it will certainly fail to send the disconnect packet. + * + * @param quiesceTimeout + * the amount of time in milliseconds to allow for existing work to + * finish before disconnecting. A value of zero or less means the + * client will not quiesce. + * @param disconnectTimeout + * the amount of time in milliseconds to allow send disconnect packet + * to server. + * @param sendDisconnectPacket + * if true, will send the disconnect packet to the server + * @throws MqttException + * if any unexpected error + */ + public void disconnectForcibly(long quiesceTimeout, long disconnectTimeout, boolean sendDisconnectPacket) + throws MqttException; + + /** + * Determines if this client is currently connected to the server. + * + * @return true if connected, false otherwise. + */ + public boolean isConnected(); + + /** + * Returns the client ID used by this client. + *

+ * All clients connected to the same server or server farm must have a unique + * ID. + *

+ * + * @return the client ID used by this client. + */ + public String getClientId(); + + public void setClientId(String clientId); + + /** + * Returns the address of the server used by this client. + *

+ * The format of the returned String is the same as that used on the + * constructor. + *

+ * + * @return the server's address, as a URI String. + * @see MqttAsyncClient#MqttAsyncClient(String, String) + */ + public String getServerURI(); + + /** + * Returns the currently connected Server URI Implemented due to: + * https://bugs.eclipse.org/bugs/show_bug.cgi?id=481097 + * + * Where getServerURI only returns the URI that was provided in + * MqttAsyncClient's constructor, getCurrentServerURI returns the URI of the + * Server that the client is currently connected to. This would be different in + * scenarios where multiple server URIs have been provided to the + * MqttConnectOptions. + * + * @return the currently connected server URI + */ + public String getCurrentServerURI(); + + /* + * (non-Javadoc) Check and send a ping if needed.

By default, client sends + * PingReq to server to keep the connection to server. For some platforms which + * cannot use this mechanism, such as Android, developer needs to handle the + * ping request manually with this method.

+ * + * @throws MqttException for other errors encountered while publishing the + * message. + */ + public IMqttToken checkPing(Object userContext, MqttActionListener callback) throws MqttException; + + /** + * Subscribe to a topic, which may include wildcards. + * + * @see #subscribe(MqttSubscription[], Object, MqttActionListener, + * MqttProperties) throws MqttException + * + * @param topicFilter + * the topic to subscribe to, which can include wildcards. + * @param qos + * the maximum quality of service at which to subscribe. Messages + * published at a lower quality of service will be received at the + * published QoS. Messages published at a higher quality of service + * will be received using the QoS specified on the subscribe. + * @param userContext + * optional object used to pass context to the callback. Use null if + * not required. + * @param callback + * optional listener that will be notified when subscribe has + * completed + * @return token used to track and wait for the subscribe to complete. The token + * will be passed to callback methods if set. + * @throws MqttException + * if there was an error registering the subscription. + */ + public IMqttToken subscribe(String topicFilter, int qos, Object userContext, MqttActionListener callback) + throws MqttException; + + public IMqttToken subscribe(String[] topicFilters, int[] qos, Object userContext, MqttActionListener callback) + throws MqttException; + + /** + * Subscribe to a topic, which may include wildcards. + * + * @see #subscribe(MqttSubscription[], Object, MqttActionListener, + * MqttProperties) + * + * @param topicFilter + * the topic to subscribe to, which can include wildcards. + * @param qos + * the maximum quality of service at which to subscribe. Messages + * published at a lower quality of service will be received at the + * published QoS. Messages published at a higher quality of service + * will be received using the QoS specified on the subscribe. + * @return token used to track and wait for the subscribe to complete. The token + * will be passed to callback methods if set. + * @throws MqttException + * if there was an error registering the subscription. + */ + public IMqttToken subscribe(String topicFilter, int qos) throws MqttException; + + public IMqttToken subscribe(String[] topicFilters, int[] qos) throws MqttException; + + /** + * Subscribe to multiple topics, each of which may include wildcards. + * + *

+ * Provides an optimized way to subscribe to multiple topics compared to + * subscribing to each one individually. + *

+ * + * @see #subscribe(MqttSubscription[], Object, MqttActionListener, + * MqttProperties) + * + * @param subscriptions + * one or more {@link MqttSubscription} defining the subscription to + * be made. + * @return token used to track and wait for the subscribe to complete. The token + * will be passed to callback methods if set. + * @throws MqttException + * if there was an error registering the subscription. + */ + public IMqttToken subscribe(MqttSubscription[] subscriptions) throws MqttException; + + /** + * Subscribes to multiple topics, each of which may include wildcards. + *

+ * Provides an optimized way to subscribe to multiple topics compared to + * subscribing to each one individually. + *

+ *

+ * The {@link #setCallback(MqttCallback)} method should be called before this + * method, otherwise any received messages will be discarded. + *

+ *

+ * If (@link MqttConnectOptions#setCleanStart(boolean)} was set to true when + * when connecting to the server then the subscription remains in place until + * either: + *

+ * + * + * + *

+ * If (@link MqttConnectOptions#setCleanStart(boolean)} was set to false when + * connecting to the server then the subscription remains in place until either: + *

+ * + *

+ * With cleanStart set to false the MQTT server will store messages on behalf + * of the client when the client is not connected. The next time the client + * connects with the same client ID the server will deliver the stored + * messages to the client. + *

+ * + *

+ * The "topic filter" string used when subscribing may contain special + * characters, which allow you to subscribe to multiple topics at once. + *

+ *

+ * The topic level separator is used to introduce structure into the topic, and + * can therefore be specified within the topic for that purpose. The multi-level + * wildcard and single-level wildcard can be used for subscriptions, but they + * cannot be used within a topic by the publisher of a message. + *

+ *
Topic level separator
+ *
The forward slash (/) is used to separate each level within a topic tree + * and provide a hierarchical structure to the topic space. The use of the topic + * level separator is significant when the two wildcard characters are + * encountered in topics specified by subscribers.
+ * + *
Multi-level wildcard
+ *
+ *

+ * The number sign (#) is a wildcard character that matches any number of levels + * within a topic. For example, if you subscribe to + * finance/stock/ibm/#, you receive + * messages on these topics: + *

+ *
    + *
  • finance/stock/ibm
  • + *
  • finance/stock/ibm/closingprice
  • + *
  • finance/stock/ibm/currentprice
  • + *
+ *

+ * The multi-level wildcard can represent zero or more levels. Therefore, + * finance/# can also match the singular finance, where + * # represents zero levels. The topic level separator is meaningless + * in this context, because there are no levels to separate. + *

+ * + *

+ * The multi-level wildcard can be specified only on its own or + * next to the topic level separator character. Therefore, # and + * finance/# are both valid, but finance# is not valid. + * The multi-level wildcard must be the last character used within the + * topic tree. For example, finance/# is valid but + * finance/#/closingprice is not valid. + *

+ *
+ * + *
Single-level wildcard
+ *
+ *

+ * The plus sign (+) is a wildcard character that matches only one topic level. + * For example, finance/stock/+ matches finance/stock/ibm and + * finance/stock/xyz, but not finance/stock/ibm/closingprice. + * Also, because the single-level wildcard matches only a single level, + * finance/+ does not match finance. + *

+ * + *

+ * Use the single-level wildcard at any level in the topic tree, and in + * conjunction with the multilevel wildcard. Specify the single-level wildcard + * next to the topic level separator, except when it is specified on its own. + * Therefore, + and finance/+ are both valid, but + * finance+ is not valid. The single-level wildcard can be used + * at the end of the topic tree or within the topic tree. For example, + * finance/+ and finance/+/ibm are both valid. + *

+ *
+ *
+ *

+ * The method returns control before the subscribe completes. Completion can be + * tracked by: + *

+ * + * + * @param subscriptions + * one or more {@link MqttSubscription} defining the subscription to + * be made. + * @param userContext + * optional object used to pass context to the callback. Use null if + * not required. + * @param callback + * optional listener that will be notified when subscribe has + * completed + * @param subscriptionProperties + * The {@link MqttProperties} to be sent. + * @return token used to track and wait for the subscribe to complete. The token + * will be passed to callback methods if set. + * @throws MqttException + * if there was an error registering the subscription. + * @throws IllegalArgumentException + * if the two supplied arrays are not the same size. + */ + public IMqttToken subscribe(MqttSubscription[] subscriptions, Object userContext, MqttActionListener callback, + MqttProperties subscriptionProperties) throws MqttException; + + /** + * Subscribe to a topic, which may include wildcards. + * + * @see #subscribe(MqttSubscription[], Object, MqttActionListener, + * MqttProperties) + * + * @param mqttSubscription + * a {@link MqttSubscription} defining the subscription to be made. + * @param userContext + * optional object used to pass context to the callback. Use null if + * not required. + * @param callback + * optional listener that will be notified when subscribe has + * completed + * @param messageListener + * a callback to handle incoming messages + * @param subscriptionProperties + * The {@link MqttProperties} to be sent. + * @return token used to track and wait for the subscribe to complete. The token + * will be passed to callback methods if set. + * @throws MqttException + * if there was an error registering the subscription. + */ + public IMqttToken subscribe(MqttSubscription mqttSubscription, Object userContext, MqttActionListener callback, + IMqttMessageListener messageListener, MqttProperties subscriptionProperties) throws MqttException; + + /** + * Subscribe to a topic, which may include wildcards. + * + * @see #subscribe(MqttSubscription[], Object, MqttActionListener, + * MqttProperties) + * + * @param subscription + * a {@link MqttSubscription} defining the subscription to be made. + * @param messageListener + * a callback to handle incoming messages + * @return token used to track and wait for the subscribe to complete. The token + * will be passed to callback methods if set. + * @throws MqttException + * if there was an error registering the subscription. + */ + public IMqttToken subscribe(MqttSubscription subscription, IMqttMessageListener messageListener) throws MqttException; + + /** + * Subscribe to multiple topics, each of which may include wildcards. + * + *

+ * Provides an optimized way to subscribe to multiple topics compared to + * subscribing to each one individually. + *

+ * + * @see #subscribe(MqttSubscription[], Object, MqttActionListener, + * MqttProperties) + * + ** @param subscriptions + * one or more {@link MqttSubscription} defining the subscription to + * be made. + * @param messageListener + * a callback to handle incoming messages + * @return token used to track and wait for the subscribe to complete. The token + * will be passed to callback methods if set. + * @throws MqttException + * if there was an error registering the subscription. + */ + public IMqttToken subscribe(MqttSubscription[] subscriptions, IMqttMessageListener messageListener) throws MqttException; + + /** + * Subscribe to multiple topics, each of which may include wildcards. + * + *

+ * Provides an optimized way to subscribe to multiple topics compared to + * subscribing to each one individually. + *

+ * + * @see #subscribe(MqttSubscription[], Object, MqttActionListener, + * MqttProperties) + * + * @param subscriptions + * one or more {@link MqttSubscription} defining the subscription to + * be made. + * @param userContext + * optional object used to pass context to the callback. Use null if + * not required. + * @param callback + * optional listener that will be notified when subscribe has + * completed + * @param messageListeners + * one or more callbacks to handle incoming messages. + * @param subscriptionProperties + * The {@link MqttProperties} to be sent. + * @return token used to track and wait for the subscribe to complete. The token + * will be passed to callback methods if set. + * @throws MqttException + * if there was an error registering the subscription. + */ + public IMqttToken subscribe(MqttSubscription[] subscriptions, Object userContext, MqttActionListener callback, + IMqttMessageListener[] messageListeners, MqttProperties subscriptionProperties) throws MqttException; + + /** + * Subscribe to multiple topics, each of which may include wildcards. + * + *

+ * Provides an optimized way to subscribe to multiple topics compared to + * subscribing to each one individually. + *

+ * + * @see #subscribe(MqttSubscription[], Object, MqttActionListener, + * MqttProperties) + * + * @param subscriptions + * one or more {@link MqttSubscription} defining the subscription to + * be made. + * @param userContext + * optional object used to pass context to the callback. Use null if + * not required. + * @param callback + * optional listener that will be notified when subscribe has + * completed + * @param messageListener + * a callback to handle incoming messages. + * @param subscriptionProperties + * The {@link MqttProperties} to be sent. + * @return token used to track and wait for the subscribe to complete. The token + * will be passed to callback methods if set. + * @throws MqttException + * if there was an error registering the subscription. + */ + public IMqttToken subscribe(MqttSubscription[] subscriptions, Object userContext, MqttActionListener callback, + IMqttMessageListener messageListener, MqttProperties subscriptionProperties) throws MqttException; + + /** + * Requests the server unsubscribe the client from a topics. + * + * @see #unsubscribe(String[], Object, MqttActionListener, MqttProperties) + * + * @param topicFilter + * the topic to unsubscribe from. It must match a topicFilter + * specified on an earlier subscribe. + * @param userContext + * optional object used to pass context to the callback. Use null if + * not required. + * @param callback + * optional listener that will be notified when unsubscribe has + * completed + * @return token used to track and wait for the unsubscribe to complete. The + * token will be passed to callback methods if set. + * @throws MqttException + * if there was an error unregistering the subscription. + */ + public IMqttToken unsubscribe(String topicFilter, Object userContext, MqttActionListener callback) throws MqttException; + + /** + * Requests the server unsubscribe the client from a topic. + * + * @see #unsubscribe(String[], Object, MqttActionListener, MqttProperties) + * @param topicFilter + * the topic to unsubscribe from. It must match a topicFilter + * specified on an earlier subscribe. + * @return token used to track and wait for the unsubscribe to complete. The + * token will be passed to callback methods if set. + * @throws MqttException + * if there was an error unregistering the subscription. + */ + public IMqttToken unsubscribe(String topicFilter) throws MqttException; + + /** + * Requests the server unsubscribe the client from one or more topics. + * + * @see #unsubscribe(String[], Object, MqttActionListener, MqttProperties) + * + * @param topicFilters + * one or more topics to unsubscribe from. Each topicFilter must + * match one specified on an earlier subscribe. * + * @return token used to track and wait for the unsubscribe to complete. The + * token will be passed to callback methods if set. + * @throws MqttException + * if there was an error unregistering the subscription. + */ + public IMqttToken unsubscribe(String[] topicFilters) throws MqttException; + + /** + * Requests the server unsubscribe the client from one or more topics. + *

+ * Unsubcribing is the opposite of subscribing. When the server receives the + * unsubscribe request it looks to see if it can find a matching subscription + * for the client and then removes it. After this point the server will send no + * more messages to the client for this subscription. + *

+ *

+ * The topic(s) specified on the unsubscribe must match the topic(s) specified + * in the original subscribe request for the unsubscribe to succeed + *

+ *

+ * The method returns control before the unsubscribe completes. Completion can + * be tracked by: + *

+ * + * + * + * @param topicFilters + * one or more topics to unsubscribe from. Each topicFilter must + * match one specified on an earlier subscribe. + * @param userContext + * optional object used to pass context to the callback. Use null if + * not required. + * @param callback + * optional listener that will be notified when unsubscribe has + * completed + * @param unsubscribeProperties + * The {@link MqttProperties} to be sent. + * @return token used to track and wait for the unsubscribe to complete. The + * token will be passed to callback methods if set. + * @throws MqttException + * if there was an error unregistering the subscription. + */ + public IMqttToken unsubscribe(String[] topicFilters, Object userContext, MqttActionListener callback, + MqttProperties unsubscribeProperties) throws MqttException; + + /** + * Sets a callback listener to use for events that happen asynchronously. + *

+ * There are a number of events that the listener will be notified about. These + * include: + *

+ * + *

+ * Other events that track the progress of an individual operation such as + * connect and subscribe can be tracked using the {@link MqttToken} returned + * from each non-blocking method or using setting a {@link MqttActionListener} + * on the non-blocking method. + *

+ * + * @see MqttCallback + * @param callback + * which will be invoked for certain asynchronous events + */ + public void setCallback(MqttCallback callback); + + /** + * If manualAcks is set to true, then on completion of the messageArrived + * callback the MQTT acknowledgements are not sent. You must call + * messageArrivedComplete to send those acknowledgements. This allows finer + * control over when the acks are sent. The default behaviour, when manualAcks + * is false, is to send the MQTT acknowledgements automatically at the + * successful completion of the messageArrived callback method. + * + * @param manualAcks + * if set to true MQTT acknowledgements are not sent + */ + public void setManualAcks(boolean manualAcks); + + /** + * Indicate that the application has completed processing the message with id + * messageId. This will cause the MQTT acknowledgement to be sent to the server. + * + * @param messageId + * the MQTT message id to be acknowledged + * @param qos + * the MQTT QoS of the message to be acknowledged + * @throws MqttException + * if there was a problem sending the acknowledgement + */ + public void messageArrivedComplete(int messageId, int qos) throws MqttException; + + /** + * Returns the delivery tokens for any outstanding publish operations. + *

+ * If a client has been restarted and there are messages that were in the + * process of being delivered when the client stopped this method returns a + * token for each in-flight message enabling the delivery to be tracked + * Alternately the {@link MqttCallback#deliveryComplete(IMqttToken)} + * callback can be used to track the delivery of outstanding messages. + *

+ *

+ * If a client connects with cleanStart true then there will be no delivery + * tokens as the cleanStart option deletes all earlier state. For state to be + * remembered the client must connect with cleanStart set to false + *

+ * + * @return zero or more delivery tokens + */ + public IMqttToken[] getPendingTokens(); + + /** + * Publishes a message to a topic on the server. + *

+ * A convenience method, which will create a new {@link MqttMessage} object with + * a byte array payload and the specified QoS, and then publish it. + *

+ * + * @param topic + * to deliver the message to, for example "finance/stock/ibm". + * @param payload + * the byte array to use as the payload + * @param qos + * the Quality of Service to deliver the message at. Valid values are + * 0, 1 or 2. + * @param retained + * whether or not this message should be retained by the server. + * @param userContext + * optional object used to pass context to the callback. Use null if + * not required. + * @param callback + * optional listener that will be notified when message delivery hsa + * completed to the requested quality of service + * @return token used to track and wait for the publish to complete. The token + * will be passed to any callback that has been set. + * @throws MqttPersistenceException + * when a problem occurs storing the message + * @throws IllegalArgumentException + * if value of QoS is not 0, 1 or 2. + * @throws MqttException + * for other errors encountered while publishing the message. For + * instance client not connected. + * @see #publish(String, MqttMessage, Object, MqttActionListener) + * @see MqttMessage#setQos(int) + * @see MqttMessage#setRetained(boolean) + */ + public IMqttToken publish(String topic, byte[] payload, int qos, boolean retained, Object userContext, + MqttActionListener callback) throws MqttException, MqttPersistenceException; + + /** + * Publishes a message to a topic on the server. + *

+ * A convenience method, which will create a new {@link MqttMessage} object with + * a byte array payload and the specified QoS, and then publish it. + *

+ * + * @param topic + * to deliver the message to, for example "finance/stock/ibm". + * @param payload + * the byte array to use as the payload + * @param qos + * the Quality of Service to deliver the message at. Valid values are + * 0, 1 or 2. + * @param retained + * whether or not this message should be retained by the server. + * @return token used to track and wait for the publish to complete. The token + * will be passed to any callback that has been set. + * @throws MqttPersistenceException + * when a problem occurs storing the message + * @throws IllegalArgumentException + * if value of QoS is not 0, 1 or 2. + * @throws MqttException + * for other errors encountered while publishing the message. For + * instance if too many messages are being processed. + * @see #publish(String, MqttMessage, Object, MqttActionListener) + * @see MqttMessage#setQos(int) + * @see MqttMessage#setRetained(boolean) + */ + public IMqttToken publish(String topic, byte[] payload, int qos, boolean retained) + throws MqttException, MqttPersistenceException; + + /** + * Publishes a message to a topic on the server. Takes an {@link MqttMessage} + * message and delivers it to the server at the requested quality of service. + * + * @param topic + * to deliver the message to, for example "finance/stock/ibm". + * @param message + * to deliver to the server + * @return token used to track and wait for the publish to complete. The token + * will be passed to any callback that has been set. + * @throws MqttPersistenceException + * when a problem occurs storing the message + * @throws IllegalArgumentException + * if value of QoS is not 0, 1 or 2. + * @throws MqttException + * for other errors encountered while publishing the message. For + * instance client not connected. + * @see #publish(String, MqttMessage, Object, MqttActionListener) + */ + public IMqttToken publish(String topic, MqttMessage message) throws MqttException, MqttPersistenceException; + + /** + * Publishes a message to a topic on the server. + *

+ * Once this method has returned cleanly, the message has been accepted for + * publication by the client and will be delivered on a background thread. In + * the event the connection fails or the client stops. Messages will be + * delivered to the requested quality of service once the connection is + * re-established to the server on condition that: + *

+ * + * + *

+ * When building an application, the design of the topic tree should take into + * account the following principles of topic name syntax and semantics: + *

+ * + * + * + *

+ * The following principles apply to the construction and content of a topic + * tree: + *

+ * + * + *

+ * The method returns control before the publish completes. Completion can be + * tracked by: + *

+ * + * + * @param topic + * to deliver the message to, for example "finance/stock/ibm". + * @param message + * to deliver to the server + * @param userContext + * optional object used to pass context to the callback. Use null if + * not required. + * @param callback + * optional listener that will be notified when message delivery has + * completed to the requested quality of service. + * @return token used to track and wait for the publish to complete. The token + * will be passed to callback methods if set. + * @throws MqttPersistenceException + * when a problem occurs storing the message + * @throws IllegalArgumentException + * if value of QoS is not 0, 1 or 2. + * @throws MqttException + * for other errors encountered while publishing the message. For + * instance client not connected. + * @see MqttMessage + */ + public IMqttToken publish(String topic, MqttMessage message, Object userContext, MqttActionListener callback) + throws MqttException, MqttPersistenceException; + + /** + * An AUTH Packet is sent from Client to Server or Server to Client as part of + * an extended authentication exchange, such as challenge / response + * authentication. It is a protocol error for the Client or Server to send an + * AUTH packet if the CONNECT packet did not contain the same Authentication + * Method. + * + * @param reasonCode + * The Reason code, can be Success (0), Continue authentication (24) + * or Re-authenticate (25). + * @param userContext + * optional object used to pass context to the callback. Use null if + * not required. + * @param properties + * The {@link MqttProperties} to be sent, containing the + * Authentication Method, Authentication Data and any required User + * Defined Properties. + * @return token used to track and wait for the authentication to complete. + * @throws MqttException if an exception occurs whilst sending the authenticate packet. + */ + public IMqttToken authenticate(int reasonCode, Object userContext, MqttProperties properties) throws MqttException; + + /** + * User triggered attempt to reconnect + * + * @throws MqttException + * if there is an issue with reconnecting + */ + public void reconnect() throws MqttException; + + /** + * Sets the DisconnectedBufferOptions for this client + * + * @param bufferOpts + * the {@link DisconnectedBufferOptions} + */ + public void setBufferOpts(DisconnectedBufferOptions bufferOpts); + + /** + * Returns the number of messages in the Disconnected Message Buffer + * + * @return Count of messages in the buffer + */ + public int getBufferedMessageCount(); + + /** + * Returns a message from the Disconnected Message Buffer + * + * @param bufferIndex + * the index of the message to be retrieved. + * @return the message located at the bufferIndex + */ + public MqttMessage getBufferedMessage(int bufferIndex); + + /** + * Deletes a message from the Disconnected Message Buffer + * + * @param bufferIndex + * the index of the message to be deleted. + */ + public void deleteBufferedMessage(int bufferIndex); + + /** + * Returns the current number of outgoing in-flight messages being sent by the + * client. Note that this number cannot be guaranteed to be 100% accurate as + * some messages may have been sent or queued in the time taken for this method + * to return. + * + * @return the current number of in-flight messages. + */ + public int getInFlightMessageCount(); + + /** + * Close the client Releases all resource associated with the client. After the + * client has been closed it cannot be reused. For instance attempts to connect + * will fail. + * + * @throws MqttException + * if the client is not disconnected. + */ + public void close() throws MqttException; + + /** + * Close the client Releases all resource associated with the client. After the + * client has been closed it cannot be reused. For instance attempts to connect + * will fail. + * + * @param force + * - Will force the connection to close. + * + * @throws MqttException + * if the client is not disconnected. + */ + public void close(boolean force) throws MqttException; + + /** + * Return a debug object that can be used to help solve problems. + * + * @return the {@link Debug} object + */ + public Debug getDebug(); + + public IMqttToken subscribe(MqttSubscription subscription) throws MqttException; + +} diff --git a/src/main/java/org/eclipse/paho/mqttv5/client/IMqttClient.java b/src/main/java/org/eclipse/paho/mqttv5/client/IMqttClient.java new file mode 100644 index 0000000..c8735ac --- /dev/null +++ b/src/main/java/org/eclipse/paho/mqttv5/client/IMqttClient.java @@ -0,0 +1,657 @@ +/******************************************************************************* + * Copyright (c) 2009, 2019 IBM Corp. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * and Eclipse Distribution License v1.0 which accompany this distribution. + * + * The Eclipse Public License is available at + * https://www.eclipse.org/legal/epl-2.0 + * and the Eclipse Distribution License is available at + * https://www.eclipse.org/org/documents/edl-v10.php + * + * Contributors: + * James Sutton - MQTT V5 support + */ + +package org.eclipse.paho.mqttv5.client; + +import org.eclipse.paho.mqttv5.common.MqttException; +import org.eclipse.paho.mqttv5.common.MqttMessage; +import org.eclipse.paho.mqttv5.common.MqttPersistenceException; +import org.eclipse.paho.mqttv5.common.MqttSecurityException; +import org.eclipse.paho.mqttv5.common.MqttSubscription; + +/** + * Enables an application to communicate with an MQTT server using blocking methods. + *

+ * This interface allows applications to utilize all features of the MQTT version 3.1 + * specification including:

+ * + *

+ * There are two styles of MQTT client, this one and {@link IMqttAsyncClient}.

+ * + *

+ * The non-blocking client can also be used in a blocking form by turning a non-blocking + * method into a blocking invocation using the following pattern:

+ *
+ *     IMqttToken token;
+ *     token = asyncClient.method(parms).waitForCompletion();
+ *     
+ *

+ * Using the non-blocking client allows an application to use a mixture of blocking and + * non-blocking styles. Using the blocking client only allows an application to use one + * style. The blocking client provides compatibility with earlier versions + * of the MQTT client.

+ */ +public interface IMqttClient { //extends IMqttAsyncClient { + /** + * Connects to an MQTT server using the default options. + *

The default options are specified in {@link MqttConnectionOptions} class. + *

+ * + * @throws MqttSecurityException when the server rejects the connect for security + * reasons + * @throws MqttException for non security related problems + * @see #connect(MqttConnectionOptions) + */ + public void connect() throws MqttSecurityException, MqttException; + + /** + * Connects to an MQTT server using the specified options. + *

The server to connect to is specified on the constructor. + * It is recommended to call {@link #setCallback(MqttCallback)} prior to + * connecting in order that messages destined for the client can be accepted + * as soon as the client is connected. + *

+ *

This is a blocking method that returns once connect completes

+ * + * @param options a set of connection parameters that override the defaults. + * @throws MqttSecurityException when the server rejects the connect for security + * reasons + * @throws MqttException for non security related problems including communication errors + */ + public void connect(MqttConnectionOptions options) throws MqttSecurityException, MqttException; + + /** + * Connects to an MQTT server using the specified options. + *

The server to connect to is specified on the constructor. + * It is recommended to call {@link #setCallback(MqttCallback)} prior to + * connecting in order that messages destined for the client can be accepted + * as soon as the client is connected. + *

+ *

This is a blocking method that returns once connect completes

+ * + * @param options a set of connection parameters that override the defaults. + * @return the MqttToken used for the call. Can be used to obtain the session present flag + * @throws MqttSecurityException when the server rejects the connect for security + * reasons + * @throws MqttException for non security related problems including communication errors + */ + public IMqttToken connectWithResult(MqttConnectionOptions options) throws MqttSecurityException, MqttException; + + /** + * Disconnects from the server. + *

An attempt is made to quiesce the client allowing outstanding + * work to complete before disconnecting. It will wait + * for a maximum of 30 seconds for work to quiesce before disconnecting. + * This method must not be called from inside {@link MqttCallback} methods. + *

+ * + *

This is a blocking method that returns once disconnect completes

+ * + * @throws MqttException if a problem is encountered while disconnecting + */ + public void disconnect() throws MqttException; + + /** + * Disconnects from the server. + *

+ * The client will wait for all {@link MqttCallback} methods to + * complete. It will then wait for up to the quiesce timeout to allow for + * work which has already been initiated to complete - for example, it will + * wait for the QoS 2 flows from earlier publications to complete. When work has + * completed or after the quiesce timeout, the client will disconnect from + * the server. If the cleanStart flag was set to false and is set to false the + * next time a connection is made QoS 1 and 2 messages that + * were not previously delivered will be delivered.

+ * + *

This is a blocking method that returns once disconnect completes

+ * + * @param quiesceTimeout the amount of time in milliseconds to allow for + * existing work to finish before disconnecting. A value of zero or less + * means the client will not quiesce. + * @throws MqttException if a problem is encountered while disconnecting + */ + public void disconnect(long quiesceTimeout) throws MqttException; + + /** + * Disconnects from the server forcibly to reset all the states. Could be useful when disconnect attempt failed. + *

+ * Because the client is able to establish the TCP/IP connection to a none MQTT server and it will certainly fail to + * send the disconnect packet. It will wait for a maximum of 30 seconds for work to quiesce before disconnecting and + * wait for a maximum of 10 seconds for sending the disconnect packet to server. + * + * @throws MqttException if any unexpected error + * @since 0.4.1 + */ + public void disconnectForcibly() throws MqttException; + + /** + * Disconnects from the server forcibly to reset all the states. Could be useful when disconnect attempt failed. + *

+ * Because the client is able to establish the TCP/IP connection to a none MQTT server and it will certainly fail to + * send the disconnect packet. It will wait for a maximum of 30 seconds for work to quiesce before disconnecting. + * + * @param disconnectTimeout the amount of time in milliseconds to allow send disconnect packet to server. + * @throws MqttException if any unexpected error + * @since 0.4.1 + */ + public void disconnectForcibly(long disconnectTimeout) throws MqttException; + + /** + * Disconnects from the server forcibly to reset all the states. Could be useful when disconnect attempt failed. + *

+ * Because the client is able to establish the TCP/IP connection to a none MQTT server and it will certainly fail to + * send the disconnect packet. + * + * @param quiesceTimeout the amount of time in milliseconds to allow for existing work to finish before + * disconnecting. A value of zero or less means the client will not quiesce. + * @param disconnectTimeout the amount of time in milliseconds to allow send disconnect packet to server. + * @throws MqttException if any unexpected error + * @since 0.4.1 + */ + public void disconnectForcibly(long quiesceTimeout, long disconnectTimeout) throws MqttException; + + /** + * Subscribe to a topic, which may include wildcards. + * + * @see #subscribe(String[], int[]) + * + * @param topicFilter the topic to subscribe to, which can include wildcards. + * @param qos the maximum quality of service at which to subscribe. Messages + * published at a lower quality of service will be received at the published + * QoS. Messages published at a higher quality of service will be received using + * the QoS specified on the subscribe. + * @return a token + * @throws MqttException if there was an error registering the subscription. + */ + public IMqttToken subscribe(String topicFilter, int qos) throws MqttException; + + /** + * Subscribes to multiple topics, each of which may include wildcards. + *

The {@link #setCallback(MqttCallback)} method + * should be called before this method, otherwise any received messages + * will be discarded. + *

+ *

+ * If (@link MqttConnectOptions#setCleanStart(boolean)} was set to true + * when when connecting to the server then the subscription remains in place + * until either: + *

+ * + *

+ * If (@link MqttConnectOptions#setCleanStart(boolean)} was set to false + * when when connecting to the server then the subscription remains in place + * until either:

+ * + *

+ * With cleanStart set to false the MQTT server will store messages on + * behalf of the client when the client is not connected. The next time the + * client connects with the same client ID the server will + * deliver the stored messages to the client. + *

+ * + *

The "topic filter" string used when subscribing + * may contain special characters, which allow you to subscribe to multiple topics + * at once.

+ *

The topic level separator is used to introduce structure into the topic, and + * can therefore be specified within the topic for that purpose. The multi-level + * wildcard and single-level wildcard can be used for subscriptions, but they + * cannot be used within a topic by the publisher of a message. + *

+ *
Topic level separator
+ *
The forward slash (/) is used to separate each level within + * a topic tree and provide a hierarchical structure to the topic space. The + * use of the topic level separator is significant when the two wildcard characters + * are encountered in topics specified by subscribers.
+ * + *
Multi-level wildcard
+ *

The number sign (#) is a wildcard character that matches + * any number of levels within a topic. For example, if you subscribe to + * finance/stock/ibm/#, you receive + * messages on these topics:

+ *
    + *
  • finance/stock/ibm
  • + *
  • finance/stock/ibm/closingprice
  • + *
  • finance/stock/ibm/currentprice
  • + *
+ * + *

The multi-level wildcard + * can represent zero or more levels. Therefore, finance/# can also match + * the singular finance, where # represents zero levels. The topic + * level separator is meaningless in this context, because there are no levels + * to separate.

+ * + *

The multi-level wildcard can + * be specified only on its own or next to the topic level separator character. + * Therefore, # and finance/# are both valid, but finance# is + * not valid. The multi-level wildcard must be the last character + * used within the topic tree. For example, finance/# is valid but + * finance/#/closingprice is not valid.

+ * + *
Single-level wildcard
+ *

The plus sign (+) is a wildcard character that matches only one topic + * level. For example, finance/stock/+ matches + * finance/stock/ibm and finance/stock/xyz, + * but not finance/stock/ibm/closingprice. Also, because the single-level + * wildcard matches only a single level, finance/+ does not match finance.

+ * + *

Use + * the single-level wildcard at any level in the topic tree, and in conjunction + * with the multilevel wildcard. Specify the single-level wildcard next to the + * topic level separator, except when it is specified on its own. Therefore, + * + and finance/+ are both valid, but finance+ is + * not valid. The single-level wildcard can be used at the end of the + * topic tree or within the topic tree. + * For example, finance/+ and finance/+/ibm are both valid.

+ *
+ *
+ * + *

This is a blocking method that returns once subscribe completes

+ * + * @param topicFilters one or more topics to subscribe to, which can include wildcards. + * @param qos the maximum quality of service to subscribe each topic at.Messages + * published at a lower quality of service will be received at the published + * QoS. Messages published at a higher quality of service will be received using + * the QoS specified on the subscribe. + * @return a token + * @throws MqttException if there was an error registering the subscription. + * @throws IllegalArgumentException if the two supplied arrays are not the same size. + */ + public IMqttToken subscribe(String[] topicFilters, int[] qos) throws MqttException; + + /** + * Subscribes to a one or more topics, which may include wildcards using a QoS of 1. + * + * @see #subscribe(String[], int[]) + * + * @param topicFilter the topic to subscribe to, which can include wildcards. + * @param qos QoS + * @param messageListener one callbacks to handle incoming messages + * @return a token + * @throws MqttException if there was an error registering the subscription. + */ + public IMqttToken subscribe(String topicFilter, int qos, IMqttMessageListener messageListener) throws MqttException; + + /** + * Subscribes to multiple topics, each of which may include wildcards. + *

The {@link #setCallback(MqttCallback)} method + * should be called before this method, otherwise any received messages + * will be discarded. + *

+ *

+ * If (@link MqttConnectOptions#setCleanStart(boolean)} was set to true + * when when connecting to the server then the subscription remains in place + * until either:

+ * + *

+ * If (@link MqttConnectOptions#setCleanStart(boolean)} was set to false + * when when connecting to the server then the subscription remains in place + * until either:

+ * + *

+ * With cleanStart set to false the MQTT server will store messages on + * behalf of the client when the client is not connected. The next time the + * client connects with the same client ID the server will + * deliver the stored messages to the client. + *

+ * + *

The "topic filter" string used when subscribing + * may contain special characters, which allow you to subscribe to multiple topics + * at once.

+ *

The topic level separator is used to introduce structure into the topic, and + * can therefore be specified within the topic for that purpose. The multi-level + * wildcard and single-level wildcard can be used for subscriptions, but they + * cannot be used within a topic by the publisher of a message. + *

+ *
Topic level separator
+ *
The forward slash (/) is used to separate each level within + * a topic tree and provide a hierarchical structure to the topic space. The + * use of the topic level separator is significant when the two wildcard characters + * are encountered in topics specified by subscribers.
+ * + *
Multi-level wildcard
+ *

The number sign (#) is a wildcard character that matches + * any number of levels within a topic. For example, if you subscribe to + * finance/stock/ibm/#, you receive + * messages on these topics:

+ *
    + *
  • finance/stock/ibm
  • + *
  • finance/stock/ibm/closingprice
  • + *
  • finance/stock/ibm/currentprice
  • + *
+ *

The multi-level wildcard + * can represent zero or more levels. Therefore, finance/# can also match + * the singular finance, where # represents zero levels. The topic + * level separator is meaningless in this context, because there are no levels + * to separate.

+ * + *

The multi-level wildcard can + * be specified only on its own or next to the topic level separator character. + * Therefore, # and finance/# are both valid, but finance# is + * not valid. The multi-level wildcard must be the last character + * used within the topic tree. For example, finance/# is valid but + * finance/#/closingprice is not valid.

+ * + *
Single-level wildcard
+ *

The plus sign (+) is a wildcard character that matches only one topic + * level. For example, finance/stock/+ matches + * finance/stock/ibm and finance/stock/xyz, + * but not finance/stock/ibm/closingprice. Also, because the single-level + * wildcard matches only a single level, finance/+ does not match finance.

+ * + *

Use + * the single-level wildcard at any level in the topic tree, and in conjunction + * with the multilevel wildcard. Specify the single-level wildcard next to the + * topic level separator, except when it is specified on its own. Therefore, + * + and finance/+ are both valid, but finance+ is + * not valid. The single-level wildcard can be used at the end of the + * topic tree or within the topic tree. + * For example, finance/+ and finance/+/ibm are both valid.

+ *
+ *
+ * + *

This is a blocking method that returns once subscribe completes

+ * + * @param topicFilters one or more topics to subscribe to, which can include wildcards. + * @param qos the maximum quality of service to subscribe each topic at.Messages + * published at a lower quality of service will be received at the published + * QoS. Messages published at a higher quality of service will be received using + * the QoS specified on the subscribe. + * @param messageListeners one or more callbacks to handle incoming messages + * @return a token + * @throws MqttException if there was an error registering the subscription. + * @throws IllegalArgumentException if the two supplied arrays are not the same size. + */ + public IMqttToken subscribe(String[] topicFilters, int[] qos, IMqttMessageListener[] messageListeners) throws MqttException; + + /** + * Requests the server unsubscribe the client from a topic. + * + * @see #unsubscribe(String[]) + * @param topicFilter the topic to unsubscribe from. It must match a topicFilter + * specified on the subscribe. + * @throws MqttException if there was an error unregistering the subscription. + */ + public void unsubscribe(String topicFilter) throws MqttException; + + /** + * Requests the server unsubscribe the client from one or more topics. + *

+ * Unsubcribing is the opposite of subscribing. When the server receives the + * unsubscribe request it looks to see if it can find a subscription for the + * client and then removes it. After this point the server will send no more + * messages to the client for this subscription. + *

+ *

The topic(s) specified on the unsubscribe must match the topic(s) + * specified in the original subscribe request for the subscribe to succeed + *

+ * + *

This is a blocking method that returns once unsubscribe completes

+ * + * @param topicFilters one or more topics to unsubscribe from. Each topicFilter + * must match one specified on a subscribe + * @throws MqttException if there was an error unregistering the subscription. + */ + public void unsubscribe(String[] topicFilters) throws MqttException; + + + /** + * Publishes a message to a topic on the server and return once it is delivered. + *

This is a convenience method, which will + * create a new {@link MqttMessage} object with a byte array payload and the + * specified QoS, and then publish it. All other values in the + * message will be set to the defaults. + *

+ * + * @param topic to deliver the message to, for example "finance/stock/ibm". + * @param payload the byte array to use as the payload + * @param qos the Quality of Service to deliver the message at. Valid values are 0, 1 or 2. + * @param retained whether or not this message should be retained by the server. + * @throws MqttPersistenceException when a problem with storing the message + * @throws IllegalArgumentException if value of QoS is not 0, 1 or 2. + * @throws MqttException for other errors encountered while publishing the message. + * For instance client not connected. + * @see #publish(String, MqttMessage) + * @see MqttMessage#setQos(int) + * @see MqttMessage#setRetained(boolean) + */ + public void publish(String topic, byte[] payload, int qos, boolean retained) throws MqttException, MqttPersistenceException; + + /** + * Publishes a message to a topic on the server. + *

+ * Delivers a message to the server at the requested quality of service and returns control + * once the message has been delivered. In the event the connection fails or the client + * stops, any messages that are in the process of being delivered will be delivered once + * a connection is re-established to the server on condition that:

+ * + *

In the event that the connection breaks or the client stops it is still possible to determine + * when the delivery of the message completes. Prior to re-establishing the connection to the server:

+ * + * + *

When building an application, + * the design of the topic tree should take into account the following principles + * of topic name syntax and semantics:

+ * + * + * + *

The following principles apply to the construction and content of a topic + * tree:

+ * + * + * + * + *

This is a blocking method that returns once publish completes

* + * + * @param topic to deliver the message to, for example "finance/stock/ibm". + * @param message to delivery to the server + * @throws MqttPersistenceException when a problem with storing the message + * @throws MqttException for other errors encountered while publishing the message. + * For instance client not connected. + */ + public void publish(String topic, MqttMessage message) throws MqttException, MqttPersistenceException; + + /** + * Sets the callback listener to use for events that happen asynchronously. + *

There are a number of events that listener will be notified about. These include:

+ * + *

Other events that track the progress of an individual operation such + * as connect and subscribe can be tracked using the {@link MqttToken} passed to the + * operation

+ * @see MqttCallback + * @param callback the class to callback when for events related to the client + */ + public void setCallback(MqttCallback callback); + + /** + * Get a topic object which can be used to publish messages. + *

An alternative method that should be used in preference to this one when publishing a message is:

+ * + *

When building an application, + * the design of the topic tree should take into account the following principles + * of topic name syntax and semantics:

+ * + * + * + *

The following principles apply to the construction and content of a topic + * tree:

+ * + * + * + * @param topic the topic to use, for example "finance/stock/ibm". + * @return an MqttTopic object, which can be used to publish messages to + * the topic. + * @throws IllegalArgumentException if the topic contains a '+' or '#' + * wildcard character. + */ + public MqttTopic getTopic(String topic); + + /** + * Determines if this client is currently connected to the server. + * + * @return true if connected, false otherwise. + */ + public boolean isConnected(); + + /** + * Returns the client ID used by this client. + *

All clients connected to the + * same server or server farm must have a unique ID. + *

+ * + * @return the client ID used by this client. + */ + public String getClientId(); + + /** + * Returns the address of the server used by this client, as a URI. + *

The format is the same as specified on the constructor. + *

+ * + * @return the server's address, as a URI String. + * @see MqttAsyncClient#MqttAsyncClient(String, String) + */ + public String getServerURI(); + + /** + * Returns the delivery tokens for any outstanding publish operations. + *

If a client has been restarted and there are messages that were in the + * process of being delivered when the client stopped this method will + * return a token for each message enabling the delivery to be tracked + * Alternately the {@link MqttCallback#deliveryComplete(IMqttToken)} + * callback can be used to track the delivery of outstanding messages. + *

+ *

If a client connects with cleanStart true then there will be no + * delivery tokens as the cleanStart option deletes all earlier state. + * For state to be remembered the client must connect with cleanStart + * set to false

+ * @return zero or more delivery tokens + */ + public IMqttToken[] getPendingTokens(); + + /** + * If manualAcks is set to true, then on completion of the messageArrived callback + * the MQTT acknowledgements are not sent. You must call messageArrivedComplete + * to send those acknowledgements. This allows finer control over when the acks are + * sent. The default behaviour, when manualAcks is false, is to send the MQTT + * acknowledgements automatically at the successful completion of the messageArrived + * callback method. + * @param manualAcks if set to true, MQTT acknowledgements are not sent. + */ + public void setManualAcks(boolean manualAcks); + + /** + * Will attempt to reconnect to the server after the client has lost connection. + * @throws MqttException if an error occurs attempting to reconnect + */ + public void reconnect() throws MqttException; + + /** + * Indicate that the application has completed processing the message with id messageId. + * This will cause the MQTT acknowledgement to be sent to the server. + * @param messageId the MQTT message id to be acknowledged + * @param qos the MQTT QoS of the message to be acknowledged + * @throws MqttException if there was a problem sending the acknowledgement + */ + public void messageArrivedComplete(int messageId, int qos) throws MqttException; + + /** + * Close the client + * Releases all resource associated with the client. After the client has + * been closed it cannot be reused. For instance attempts to connect will fail. + * @throws MqttException if the client is not disconnected. + */ + public void close() throws MqttException; +} diff --git a/src/main/java/org/eclipse/paho/mqttv5/client/IMqttDeliveryToken.java b/src/main/java/org/eclipse/paho/mqttv5/client/IMqttDeliveryToken.java new file mode 100644 index 0000000..af78bfd --- /dev/null +++ b/src/main/java/org/eclipse/paho/mqttv5/client/IMqttDeliveryToken.java @@ -0,0 +1,44 @@ +package org.eclipse.paho.mqttv5.client; + +import org.eclipse.paho.mqttv5.common.MqttException; +import org.eclipse.paho.mqttv5.common.MqttMessage; + +/** + * Provides a mechanism for tracking the delivery of a message. + * + *

A subclass of IMqttToken that allows the delivery of a message to be tracked. + * Unlike instances of IMqttToken delivery tokens can be used across connection + * and client restarts. This enables the delivery of a messages to be tracked + * after failures. There are two approaches + *

+ *

+ * An action is in progress until either:

+ * + * + */ + +public interface IMqttDeliveryToken extends IMqttToken { + /** + * Returns the message associated with this token. + *

Until the message has been delivered, the message being delivered will + * be returned. Once the message has been delivered null will be + * returned. + * @return the message associated with this token or null if already delivered. + * @throws MqttException if there was a problem completing retrieving the message + */ + MqttMessage getMessage() throws MqttException; +} diff --git a/src/main/java/org/eclipse/paho/mqttv5/client/IMqttMessageListener.java b/src/main/java/org/eclipse/paho/mqttv5/client/IMqttMessageListener.java new file mode 100644 index 0000000..bab5dca --- /dev/null +++ b/src/main/java/org/eclipse/paho/mqttv5/client/IMqttMessageListener.java @@ -0,0 +1,58 @@ +/******************************************************************************* + * Copyright (c) 2015 IBM Corp. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * and Eclipse Distribution License v1.0 which accompany this distribution. + * + * The Eclipse Public License is available at + * https://www.eclipse.org/legal/epl-2.0 + * and the Eclipse Distribution License is available at + * https://www.eclipse.org/org/documents/edl-v10.php + * + * Contributors: + * Ian Craggs - initial API and implementation and/or initial documentation + */ + +package org.eclipse.paho.mqttv5.client; + +import org.eclipse.paho.mqttv5.common.MqttMessage; + +/** + * Implementers of this interface will be notified when a message arrives. + * + */ +public interface IMqttMessageListener { + /** + * This method is called when a message arrives from the server. + * + *

+ * This method is invoked synchronously by the MQTT client. An + * acknowledgment is not sent back to the server until this + * method returns cleanly.

+ *

+ * If an implementation of this method throws an Exception, then the + * client will be shut down. When the client is next re-connected, any QoS + * 1 or 2 messages will be redelivered by the server.

+ *

+ * Any additional messages which arrive while an + * implementation of this method is running, will build up in memory, and + * will then back up on the network.

+ *

+ * If an application needs to persist data, then it + * should ensure the data is persisted prior to returning from this method, as + * after returning from this method, the message is considered to have been + * delivered, and will not be reproducible.

+ *

+ * It is possible to send a new message within an implementation of this callback + * (for example, a response to this message), but the implementation must not + * disconnect the client, as it will be impossible to send an acknowledgment for + * the message being processed, and a deadlock will occur.

+ * + * @param topic name of the topic on the message was published to + * @param message the actual message. + * @throws Exception if a terminal error has occurred, and the client should be + * shut down. + */ + public void messageArrived(String topic, MqttMessage message) throws Exception; +} diff --git a/src/main/java/org/eclipse/paho/mqttv5/client/IMqttToken.java b/src/main/java/org/eclipse/paho/mqttv5/client/IMqttToken.java new file mode 100644 index 0000000..57747f7 --- /dev/null +++ b/src/main/java/org/eclipse/paho/mqttv5/client/IMqttToken.java @@ -0,0 +1,206 @@ +/************************************************************************** + * Copyright (c) 2009, 2012 IBM Corp. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * and Eclipse Distribution License v1.0 which accompany this distribution. + * + * The Eclipse Public License is available at + * https://www.eclipse.org/legal/epl-2.0 + * and the Eclipse Distribution License is available at + * https://www.eclipse.org/org/documents/edl-v10.php + * + * Contributors: + * Dave Locke - initial API and implementation and/or initial documentation + * Ian Craggs - MQTT 3.1.1 support + */ +package org.eclipse.paho.mqttv5.client; + +import org.eclipse.paho.mqttv5.common.MqttException; +import org.eclipse.paho.mqttv5.common.MqttMessage; +import org.eclipse.paho.mqttv5.common.packet.MqttProperties; +import org.eclipse.paho.mqttv5.common.packet.MqttWireMessage; + +/** + * Provides a mechanism for tracking the completion of an asynchronous task. + * + *

When using the asynchronous/non-blocking MQTT programming interface all + * methods/operations that take any time (and in particular those that involve + * any network operation) return control to the caller immediately. The operation + * then proceeds to run in the background so as not to block the invoking thread. + * An IMqttToken is used to track the state of the operation. An application can use the + * token to wait for an operation to complete. A token is passed to callbacks + * once the operation completes and provides context linking it to the original + * request. A token is associated with a single operation.

+ *

+ * An action is in progress until either:

+ * + * + */ +public interface IMqttToken { + + /** + * Blocks the current thread until the action this token is associated with has + * completed. + * + * @throws MqttException if there was a problem with the action associated with the token. + * @see #waitForCompletion(long) + */ + public void waitForCompletion() throws MqttException; + + /** + * Blocks the current thread until the action this token is associated with has + * completed. + *

The timeout specifies the maximum time it will block for. If the action + * completes before the timeout then control returns immediately, if not + * it will block until the timeout expires.

+ *

If the action being tracked fails or the timeout expires an exception will + * be thrown. In the event of a timeout the action may complete after timeout. + *

+ * + * @param timeout the maximum amount of time to wait for, in milliseconds. + * @throws MqttException if there was a problem with the action associated with the token. + */ + public void waitForCompletion(long timeout) throws MqttException; + + /** + * Returns whether or not the action has finished. + *

True will be returned both in the case where the action finished successfully + * and in the case where it failed. If the action failed {@link #getException()} will + * be non null. + *

+ * @return whether or not the action has finished. + */ + public boolean isComplete(); + + /** + * Returns an exception providing more detail if an operation failed. + *

While an action in in progress and when an action completes successfully + * null will be returned. Certain errors like timeout or shutting down will not + * set the exception as the action has not failed or completed at that time + *

+ * @return exception may return an exception if the operation failed. Null will be + * returned while action is in progress and if action completes successfully. + */ + public MqttException getException(); + + /** + * Register a listener to be notified when an action completes. + *

Once a listener is registered it will be invoked when the action the token + * is associated with either succeeds or fails. + *

+ * @param listener to be invoked once the action completes + */ + public void setActionCallback(MqttActionListener listener); + + /** + * Return the async listener for this token. + * @return listener that is set on the token or null if a listener is not registered. + */ + public MqttActionListener getActionCallback(); + + /** + * Returns the MQTT client that is responsible for processing the asynchronous + * action + * @return the client + */ + public MqttClientInterface getClient(); + + /** + * Returns the topic string(s) for the action being tracked by this + * token. If the action has not been initiated or the action has not + * topic associated with it such as connect then null will be returned. + * + * @return the topic string(s) for the subscribe being tracked by this token or null + */ + public String[] getTopics(); + + /** + * Store some context associated with an action. + *

Allows the caller of an action to store some context that can be + * accessed from within the ActionListener associated with the action. This + * can be useful when the same ActionListener is associated with multiple + * actions

+ * @param userContext to associate with an action + */ + public void setUserContext(Object userContext); + + /** + * Retrieve the context associated with an action. + *

Allows the ActionListener associated with an action to retrieve any context + * that was associated with the action when the action was invoked. If not + * context was provided null is returned.

+ + * @return Object context associated with an action or null if there is none. + */ + public Object getUserContext(); + + /** + * Returns the message ID of the message that is associated with the token. + * A message id of zero will be returned for tokens associated with + * connect, disconnect and ping operations as there can only ever + * be one of these outstanding at a time. For other operations + * the MQTT message id flowed over the network. + * @return the message ID of the message that is associated with the token + */ + public int getMessageId(); + + /** + * @return the granted QoS list from a suback + */ + public int[] getGrantedQos(); + + /** + * Returns a list of reason codes that were returned as a result of this token's action. + * You will receive reason codes from the following MQTT actions: + * + * @return the reason code(s) from the response for this token's action. + * + */ + public int[] getReasonCodes(); + + /** + * @return the session present flag from a connack + */ + public boolean getSessionPresent(); + + /** + * @return the response wire message + */ + public MqttWireMessage getResponse(); + + /** + * @return the response wire message properties + */ + public MqttProperties getResponseProperties(); + + /** + * Returns the message associated with this token. + *

Until the message has been delivered, the message being delivered will + * be returned. Once the message has been delivered null will be + * returned. + * @return the message associated with this token or null if already delivered. + * @throws MqttException if there was a problem completing retrieving the message + */ + public MqttMessage getMessage() throws MqttException; + + /** + * @return the request wire message + */ + public MqttWireMessage getRequestMessage(); + + /** + * @return the request wire message properties + */ + public MqttProperties getRequestProperties(); + +} diff --git a/src/main/java/org/eclipse/paho/mqttv5/client/MqttActionListener.java b/src/main/java/org/eclipse/paho/mqttv5/client/MqttActionListener.java new file mode 100644 index 0000000..7e14ac6 --- /dev/null +++ b/src/main/java/org/eclipse/paho/mqttv5/client/MqttActionListener.java @@ -0,0 +1,30 @@ +package org.eclipse.paho.mqttv5.client; + +/** + * Implementors of this interface will be notified when an asynchronous action completes. + * + *

A listener is registered on an MqttToken and a token is associated + * with an action like connect or publish. When used with tokens on the MqttAsyncClient + * the listener will be called back on the MQTT client's thread. The listener will be informed + * if the action succeeds or fails. It is important that the listener returns control quickly + * otherwise the operation of the MQTT client will be stalled. + *

+ */ +public interface MqttActionListener { + /** + * This method is invoked when an action has completed successfully. + * @param asyncActionToken associated with the action that has completed + */ + void onSuccess(IMqttToken asyncActionToken); + /** + * This method is invoked when an action fails. + * If a client is disconnected while an action is in progress + * onFailure will be called. For connections + * that use cleanStart set to false, any QoS 1 and 2 messages that + * are in the process of being delivered will be delivered to the requested + * quality of service next time the client connects. + * @param asyncActionToken associated with the action that has failed + * @param exception thrown by the action that has failed + */ + void onFailure(IMqttToken asyncActionToken, Throwable exception); +} diff --git a/src/main/java/org/eclipse/paho/mqttv5/client/MqttAsyncClient.java b/src/main/java/org/eclipse/paho/mqttv5/client/MqttAsyncClient.java new file mode 100644 index 0000000..ced752d --- /dev/null +++ b/src/main/java/org/eclipse/paho/mqttv5/client/MqttAsyncClient.java @@ -0,0 +1,1813 @@ +/******************************************************************************* + * Copyright (c) 2009, 2019 IBM Corp. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * and Eclipse Distribution License v1.0 which accompany this distribution. + * + * The Eclipse Public License is available at + * https://www.eclipse.org/legal/epl-2.0 + * and the Eclipse Distribution License is available at + * https://www.eclipse.org/org/documents/edl-v10.php + * + * Contributors: + * James Sutton - initial API and implementation and/or initial documentation + */ + +package org.eclipse.paho.mqttv5.client; + +import java.io.ByteArrayOutputStream; +import java.io.DataOutputStream; +import java.util.Hashtable; +import java.util.Properties; +import java.util.Timer; +import java.util.TimerTask; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; + +import javax.net.SocketFactory; +import org.eclipse.paho.mqttv5.client.internal.ClientComms; +import org.eclipse.paho.mqttv5.client.internal.ConnectActionListener; +import org.eclipse.paho.mqttv5.client.internal.DisconnectedMessageBuffer; +import org.eclipse.paho.mqttv5.client.internal.MqttConnectionState; +import org.eclipse.paho.mqttv5.client.internal.MqttSessionState; +import org.eclipse.paho.mqttv5.client.internal.NetworkModule; +import org.eclipse.paho.mqttv5.client.internal.NetworkModuleService; +import org.eclipse.paho.mqttv5.client.persist.MemoryPersistence; +import org.eclipse.paho.mqttv5.client.persist.MqttDefaultFilePersistence; +import org.eclipse.paho.mqttv5.client.util.Debug; +import org.eclipse.paho.mqttv5.client.logging.Logger; +import org.eclipse.paho.mqttv5.client.logging.LoggerFactory; +import org.eclipse.paho.mqttv5.common.ExceptionHelper; +import org.eclipse.paho.mqttv5.common.MqttException; +import org.eclipse.paho.mqttv5.common.MqttMessage; +import org.eclipse.paho.mqttv5.common.MqttPersistenceException; +import org.eclipse.paho.mqttv5.common.MqttSecurityException; +import org.eclipse.paho.mqttv5.common.MqttSubscription; +import org.eclipse.paho.mqttv5.common.packet.MqttAuth; +import org.eclipse.paho.mqttv5.common.packet.MqttDataTypes; +import org.eclipse.paho.mqttv5.common.packet.MqttDisconnect; +import org.eclipse.paho.mqttv5.common.packet.MqttProperties; +import org.eclipse.paho.mqttv5.common.packet.MqttPublish; +import org.eclipse.paho.mqttv5.common.packet.MqttReturnCode; +import org.eclipse.paho.mqttv5.common.packet.MqttSubscribe; +import org.eclipse.paho.mqttv5.common.packet.MqttUnsubscribe; +import org.eclipse.paho.mqttv5.common.util.MqttTopicValidator; + +/** + * Lightweight client for talking to an MQTT server using non-blocking methods + * that allow an operation to run in the background. + * + *

+ * This class implements the non-blocking {@link IMqttAsyncClient} client + * interface allowing applications to initiate MQTT actions and then carry on + * working while the MQTT action completes on a background thread. This + * implementation is compatible with all Java SE runtimes from 1.7 and up. + *

+ *

+ * An application can connect to an MQTT server using: + *

+ * + * + *

+ * To enable messages to be delivered even across network and client restarts + * messages need to be safely stored until the message has been delivered at the + * requested quality of service. A pluggable persistence mechanism is provided + * to store the messages. + *

+ *

+ * By default {@link MqttDefaultFilePersistence} is used to store messages to a + * file. If persistence is set to null then messages are stored in memory and + * hence can be lost if the client, Java runtime or device shuts down. + *

+ *

+ * If connecting with {@link MqttConnectionOptions#setCleanStart(boolean)} set + * to true it is safe to use memory persistence as all state is cleared when a + * client disconnects. If connecting with cleanStart set to false in order to + * provide reliable message delivery then a persistent message store such as the + * default one should be used. + *

+ *

+ * The message store interface is pluggable. Different stores can be used by + * implementing the {@link MqttClientPersistence} interface and passing it to + * the clients constructor. + *

+ * + * TODO - Class docs taken from IMqttAsyncClient, review for v5 Enables an + * application to communicate with an MQTT server using non-blocking methods. + *

+ * It provides applications a simple programming interface to all features of + * the MQTT version 3.1 specification including: + *

+ * + *

+ * There are two styles of MQTT client, this one and {@link IMqttClient}. + *

+ * + *

+ * An application is not restricted to using one style if an IMqttAsyncClient + * based client is used as both blocking and non-blocking methods can be used in + * the same application. If an IMqttClient based client is used then only + * blocking methods are available to the application. For more details on the + * blocking client see {@link IMqttClient} + *

+ * + *

+ * There are two forms of non-blocking method: + *

    + *
  1. + * + *
    + *     IMqttToken token = asyncClient.method(parms)
    + * 
    + *

    + * In this form the method returns a token that can be used to track the + * progress of the action (method). The method provides a waitForCompletion() + * method that once invoked will block until the action completes. Once + * completed there are method on the token that can be used to check if the + * action completed successfully or not. For example to wait until a connect + * completes: + *

    + * + *
    + *      IMqttToken conToken;
    + *   	conToken = asyncClient.client.connect(conToken);
    + *     ... do some work...
    + *   	conToken.waitForCompletion();
    + * 
    + *

    + * To turn a method into a blocking invocation the following form can be used: + *

    + * + *
    + * IMqttToken token;
    + * token = asyncClient.method(parms).waitForCompletion();
    + * 
    + * + *
  2. + * + *
  3. + * + *
    + *     IMqttToken token method(parms, Object userContext, IMqttActionListener callback)
    + * 
    + *

    + * In this form a callback is registered with the method. The callback will be + * notified when the action succeeds or fails. The callback is invoked on the + * thread managed by the MQTT client so it is important that processing is + * minimised in the callback. If not the operation of the MQTT client will be + * inhibited. For example to be notified (called back) when a connect completes: + *

    + * + *
    + *     	IMqttToken conToken;
    + *	    conToken = asyncClient.connect("some context",new new MqttAsyncActionListener() {
    + *			public void onSuccess(IMqttToken asyncActionToken) {
    + *				log("Connected");
    + *			}
    + *
    + *			public void onFailure(IMqttToken asyncActionToken, Throwable exception) {
    + *				log ("connect failed" +exception);
    + *			}
    + *		  });
    + * 
    + *

    + * An optional context object can be passed into the method which will then be + * made available in the callback. The context is stored by the MQTT client) in + * the token which is then returned to the invoker. The token is provided to the + * callback methods where the context can then be accessed. + *

    + *
  4. + *
+ *

+ * To understand when the delivery of a message is complete either of the two + * methods above can be used to either wait on or be notified when the publish + * completes. An alternative is to use the + * {@link MqttCallback#deliveryComplete(IMqttToken)} method which will + * also be notified when a message has been delivered to the requested quality + * of service. + *

+ * + * @see IMqttAsyncClient + */ +public class MqttAsyncClient implements MqttClientInterface, IMqttAsyncClient { + private static final String CLASS_NAME = MqttAsyncClient.class.getName(); + private Logger log = LoggerFactory.getLogger(LoggerFactory.MQTT_CLIENT_MSG_CAT, CLASS_NAME); + + private static final long QUIESCE_TIMEOUT = 30000; // ms + private static final long DISCONNECT_TIMEOUT = 10000; // ms + private static final char MIN_HIGH_SURROGATE = '\uD800'; + private static final char MAX_HIGH_SURROGATE = '\uDBFF'; + // private String clientId; + private String serverURI; + protected ClientComms comms; + private Hashtable topics; + private MqttClientPersistence persistence; + private MqttCallback mqttCallback; + private MqttConnectionOptions connOpts; + private Object userContext; + private Timer reconnectTimer; // Automatic reconnect timer + private static int reconnectDelay = 1000; // Reconnect delay, starts at 1 + // second + private boolean reconnecting = false; + private static final Object clientLock = new Object(); // Simple lock + + // Variables that exist within the life of an MQTT session + private MqttSessionState mqttSession = new MqttSessionState(); + + // Variables that exist within the life of an MQTT connection. + private MqttConnectionState mqttConnection; + + private ScheduledExecutorService executorService; + private MqttPingSender pingSender; + + /** + * Create an MqttAsyncClient that is used to communicate with an MQTT server. + *

+ * The address of a server can be specified on the constructor. Alternatively a + * list containing one or more servers can be specified using the + * {@link MqttConnectionOptions#setServerURIs(String[]) setServerURIs} method on + * MqttConnectOptions. + * + *

+ * The serverURI parameter is typically used with the the + * clientId parameter to form a key. The key is used to store and + * reference messages while they are being delivered. Hence the serverURI + * specified on the constructor must still be specified even if a list of + * servers is specified on an MqttConnectOptions object. The serverURI on the + * constructor must remain the same across restarts of the client for delivery + * of messages to be maintained from a given client to a given server or set of + * servers. + * + *

+ * The address of the server to connect to is specified as a URI. Two types of + * connection are supported tcp:// for a TCP connection and + * ssl:// for a TCP connection secured by SSL/TLS. For example: + *

+ * + *

+ * If the port is not specified, it will default to 1883 for + * tcp://" URIs, and 8883 for ssl:// URIs. + *

+ * + *

+ * A client identifier clientId must be specified and be less that + * 65535 characters. It must be unique across all clients connecting to the same + * server. The clientId is used by the server to store data related to the + * client, hence it is important that the clientId remain the same when + * connecting to a server if durable subscriptions or reliable messaging are + * required. + *

+ * As the client identifier is used by the server to identify a client when it + * reconnects, the client must use the same identifier between connections if + * durable subscriptions or reliable delivery of messages is required. + *

+ *

+ * In Java SE, SSL can be configured in one of several ways, which the client + * will use in the following order: + *

+ * + * + *

+ * In Java ME, the platform settings are used for SSL connections. + *

+ * + *

+ * An instance of the default persistence mechanism + * {@link MqttDefaultFilePersistence} is used by the client. To specify a + * different persistence mechanism or to turn off persistence, use the + * {@link #MqttAsyncClient(String, String, MqttClientPersistence)} constructor. + * + * @param serverURI + * the address of the server to connect to, specified as a URI. Can + * be overridden using + * {@link MqttConnectionOptions#setServerURIs(String[])} + * @param clientId + * a client identifier that is unique on the server being connected + * to + * @throws IllegalArgumentException + * if the URI does not start with "tcp://", "ssl://" or "local://". + * @throws IllegalArgumentException + * if the clientId is null or is greater than 65535 characters in + * length + * @throws MqttException + * if any other problem was encountered + */ + public MqttAsyncClient(String serverURI, String clientId) throws MqttException { + this(serverURI, clientId, new MqttDefaultFilePersistence()); + } + + /** + * Create an MqttAsyncClient that is used to communicate with an MQTT server. + *

+ * The address of a server can be specified on the constructor. Alternatively a + * list containing one or more servers can be specified using the + * {@link MqttConnectionOptions#setServerURIs(String[]) setServerURIs} method on + * MqttConnectOptions. + * + *

+ * The serverURI parameter is typically used with the the + * clientId parameter to form a key. The key is used to store and + * reference messages while they are being delivered. Hence the serverURI + * specified on the constructor must still be specified even if a list of + * servers is specified on an MqttConnectOptions object. The serverURI on the + * constructor must remain the same across restarts of the client for delivery + * of messages to be maintained from a given client to a given server or set of + * servers. + * + *

+ * The address of the server to connect to is specified as a URI. Two types of + * connection are supported tcp:// for a TCP connection and + * ssl:// for a TCP connection secured by SSL/TLS. For example: + *

+ * + *

+ * If the port is not specified, it will default to 1883 for + * tcp://" URIs, and 8883 for ssl:// URIs. + *

+ * + *

+ * A client identifier clientId must be specified and be less that + * 65535 characters. It must be unique across all clients connecting to the same + * server. The clientId is used by the server to store data related to the + * client, hence it is important that the clientId remain the same when + * connecting to a server if durable subscriptions or reliable messaging are + * required. + *

+ * As the client identifier is used by the server to identify a client when it + * reconnects, the client must use the same identifier between connections if + * durable subscriptions or reliable delivery of messages is required. + *

+ *

+ * In Java SE, SSL can be configured in one of several ways, which the client + * will use in the following order: + *

+ * + * + *

+ * In Java ME, the platform settings are used for SSL connections. + *

+ *

+ * A persistence mechanism is used to enable reliable messaging. For messages + * sent at qualities of service (QoS) 1 or 2 to be reliably delivered, messages + * must be stored (on both the client and server) until the delivery of the + * message is complete. If messages are not safely stored when being delivered + * then a failure in the client or server can result in lost messages. A + * pluggable persistence mechanism is supported via the + * {@link MqttClientPersistence} interface. An implementer of this interface + * that safely stores messages must be specified in order for delivery of + * messages to be reliable. In addition + * {@link MqttConnectionOptions#setCleanStart(boolean)} must be set to false. In + * the event that only QoS 0 messages are sent or received or cleanStart is set + * to true then a safe store is not needed. + *

+ *

+ * An implementation of file-based persistence is provided in class + * {@link MqttDefaultFilePersistence} which will work in all Java SE based + * systems. If no persistence is needed, the persistence parameter can be + * explicitly set to null. + *

+ * + * @param serverURI + * the address of the server to connect to, specified as a URI. Can + * be overridden using + * {@link MqttConnectionOptions#setServerURIs(String[])} + * @param clientId + * a client identifier that is unique on the server being connected + * to + * @param persistence + * the persistence class to use to store in-flight message. If null + * then the default persistence mechanism is used + * @throws MqttException + * if any other problem was encountered + */ + public MqttAsyncClient(String serverURI, String clientId, MqttClientPersistence persistence) throws MqttException { + this(serverURI, clientId, persistence, null, null); + } + + /** + * Create an MqttAsyncClient that is used to communicate with an MQTT server. + *

+ * The address of a server can be specified on the constructor. Alternatively a + * list containing one or more servers can be specified using the + * {@link MqttConnectionOptions#setServerURIs(String[]) setServerURIs} method on + * MqttConnectOptions. + * + *

+ * The serverURI parameter is typically used with the the + * clientId parameter to form a key. The key is used to store and + * reference messages while they are being delivered. Hence the serverURI + * specified on the constructor must still be specified even if a list of + * servers is specified on an MqttConnectOptions object. The serverURI on the + * constructor must remain the same across restarts of the client for delivery + * of messages to be maintained from a given client to a given server or set of + * servers. + * + *

+ * The address of the server to connect to is specified as a URI. Two types of + * connection are supported tcp:// for a TCP connection and + * ssl:// for a TCP connection secured by SSL/TLS. For example: + *

+ * + *

+ * If the port is not specified, it will default to 1883 for + * tcp://" URIs, and 8883 for ssl:// URIs. + *

+ * + *

+ * A client identifier clientId must be specified and be less that + * 65535 characters. It must be unique across all clients connecting to the same + * server. The clientId is used by the server to store data related to the + * client, hence it is important that the clientId remain the same when + * connecting to a server if durable subscriptions or reliable messaging are + * required. + *

+ * As the client identifier is used by the server to identify a client when it + * reconnects, the client must use the same identifier between connections if + * durable subscriptions or reliable delivery of messages is required. + *

+ *

+ * In Java SE, SSL can be configured in one of several ways, which the client + * will use in the following order: + *

+ * + * + *

+ * In Java ME, the platform settings are used for SSL connections. + *

+ *

+ * A persistence mechanism is used to enable reliable messaging. For messages + * sent at qualities of service (QoS) 1 or 2 to be reliably delivered, messages + * must be stored (on both the client and server) until the delivery of the + * message is complete. If messages are not safely stored when being delivered + * then a failure in the client or server can result in lost messages. A + * pluggable persistence mechanism is supported via the + * {@link MqttClientPersistence} interface. An implementer of this interface + * that safely stores messages must be specified in order for delivery of + * messages to be reliable. In addition + * {@link MqttConnectionOptions#setCleanStart(boolean)} must be set to false. In + * the event that only QoS 0 messages are sent or received or cleanStart is set + * to true then a safe store is not needed. + *

+ *

+ * An implementation of file-based persistence is provided in class + * {@link MqttDefaultFilePersistence} which will work in all Java SE based + * systems. If no persistence is needed, the persistence parameter can be + * explicitly set to null. + *

+ * + * @param serverURI + * the address of the server to connect to, specified as a URI. Can + * be overridden using + * {@link MqttConnectionOptions#setServerURIs(String[])} + * @param clientId + * a client identifier that is unique on the server being connected + * to + * @param persistence + * the persistence class to use to store in-flight message. If null + * then the default persistence mechanism is used + * @param pingSender + * the {@link MqttPingSender} Implementation to handle timing and + * sending Ping messages to the server. + * @param executorService + * used for managing threads. If null then a newScheduledThreadPool + * is used. + * @throws IllegalArgumentException + * if the URI does not start with "tcp://", "ssl://" or "local://" + * @throws IllegalArgumentException + * if the clientId is null or is greater than 65535 characters in + * length + * @throws MqttException + * if any other problem was encountered + */ + public MqttAsyncClient(String serverURI, String clientId, MqttClientPersistence persistence, + MqttPingSender pingSender, ScheduledExecutorService executorService) throws MqttException { + final String methodName = "MqttAsyncClient"; + + log.setResourceName(clientId); + + if (clientId != null) { + // Verify that the client ID is not too long + // Encode it ourselves to check + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + DataOutputStream dos = new DataOutputStream(baos); + MqttDataTypes.encodeUTF8(dos, clientId); + // Remove the two size bytes. + if (dos.size() - 2 > 65535) { + throw new IllegalArgumentException("ClientId longer than 65535 characters"); + } + + } else { + clientId = ""; + } + + mqttConnection = new MqttConnectionState(clientId); + + NetworkModuleService.validateURI(serverURI); + + this.serverURI = serverURI; + this.mqttSession.setClientId(clientId); + + this.persistence = persistence; + if (this.persistence == null) { + this.persistence = new MemoryPersistence(); + } + + this.executorService = executorService; + + this.pingSender = pingSender; + if (this.pingSender == null) { + this.pingSender = new TimerPingSender(this.executorService); + } + + // @TRACE 101= ClientID={0} ServerURI={1} PersistenceType={2} + log.fine(CLASS_NAME, methodName, "101", new Object[] { clientId, serverURI, persistence }); + + this.persistence.open(clientId); + this.comms = new ClientComms(this, this.persistence, this.pingSender, this.executorService, this.mqttSession, + this.mqttConnection); + this.persistence.close(); + this.topics = new Hashtable(); + + } + + /** + * @param ch + * the character to check. + * @return returns 'true' if the character is a high-surrogate code unit + */ + protected static boolean Character_isHighSurrogate(char ch) { + return (ch >= MIN_HIGH_SURROGATE) && (ch <= MAX_HIGH_SURROGATE); + } + + /** + * Factory method to create an array of network modules, one for each of the + * supplied URIs + * + * @param address + * the URI for the server. + * @param options + * the {@link MqttConnectionOptions} for the connection. + * @return a network module appropriate to the specified address. + * @throws MqttException + * if an exception occurs creating the network Modules + * @throws MqttSecurityException + * if an issue occurs creating an SSL / TLS Socket + */ + protected NetworkModule[] createNetworkModules(String address, MqttConnectionOptions options) + throws MqttException, MqttSecurityException { + final String methodName = "createNetworkModules"; + // @TRACE 116=URI={0} + log.fine(CLASS_NAME, methodName, "116", new Object[] { address }); + + NetworkModule[] networkModules = null; + String[] serverURIs = options.getServerURIs(); + String[] array = null; + if (serverURIs == null) { + array = new String[] { address }; + } else if (serverURIs.length == 0) { + array = new String[] { address }; + } else { + array = serverURIs; + } + + networkModules = new NetworkModule[array.length]; + for (int i = 0; i < array.length; i++) { + networkModules[i] = createNetworkModule(array[i], options); + } + + log.fine(CLASS_NAME, methodName, "108"); + return networkModules; + } + + /** + * Factory method to create the correct network module, based on the supplied + * address URI. + * + * @param address + * the URI for the server. + * @param options + * Connect options + * @return a network module appropriate to the specified address. + */ + private NetworkModule createNetworkModule(String address, MqttConnectionOptions options) + throws MqttException, MqttSecurityException { + final String methodName = "createNetworkModule"; + // @TRACE 115=URI={0} + log.fine(CLASS_NAME, methodName, "115", new Object[] { address }); + + + NetworkModule netModule = NetworkModuleService.createInstance(address, options, mqttSession.getClientId()); + + return netModule; + } + + private String getHostName(String uri) { + int portIndex = uri.indexOf(':'); + if (portIndex == -1) { + portIndex = uri.indexOf('/'); + } + if (portIndex == -1) { + portIndex = uri.length(); + } + return uri.substring(0, portIndex); + } + + /* + * (non-Javadoc) + * + * @see + * org.eclipse.paho.mqttv5.client.IMqttAsyncClient#connect(java.lang.Object, + * org.eclipse.paho.mqttv5.client.MqttActionListener) + */ + @Override + public IMqttToken connect(Object userContext, MqttActionListener callback) + throws MqttException, MqttSecurityException { + return this.connect(new MqttConnectionOptions(), userContext, callback); + } + + /* + * (non-Javadoc) + * + * @see org.eclipse.paho.mqttv5.client.IMqttAsyncClient#connect() + */ + @Override + public IMqttToken connect() throws MqttException, MqttSecurityException { + return this.connect(null, null); + } + + /* + * (non-Javadoc) + * + * @see + * org.eclipse.paho.mqttv5.client.IMqttAsyncClient#connect(org.eclipse.paho. + * mqttv5.client.MqttConnectionOptions) + */ + @Override + public IMqttToken connect(MqttConnectionOptions options) throws MqttException, MqttSecurityException { + return this.connect(options, null, null); + } + + /* + * (non-Javadoc) + * + * @see + * org.eclipse.paho.mqttv5.client.IMqttAsyncClient#connect(org.eclipse.paho. + * mqttv5.client.MqttConnectionOptions, java.lang.Object, + * org.eclipse.paho.mqttv5.client.MqttActionListener) + */ + @Override + public IMqttToken connect(MqttConnectionOptions options, Object userContext, MqttActionListener callback) + throws MqttException, MqttSecurityException { + final String methodName = "connect"; + if (comms.isConnected()) { + throw ExceptionHelper.createMqttException(MqttClientException.REASON_CODE_CLIENT_CONNECTED); + } + if (comms.isConnecting()) { + throw new MqttException(MqttClientException.REASON_CODE_CONNECT_IN_PROGRESS); + } + if (comms.isDisconnecting()) { + throw new MqttException(MqttClientException.REASON_CODE_CLIENT_DISCONNECTING); + } + if (comms.isClosed()) { + throw new MqttException(MqttClientException.REASON_CODE_CLIENT_CLOSED); + } + if (options == null) { + options = new MqttConnectionOptions(); + } + this.connOpts = options; + this.userContext = userContext; + final boolean automaticReconnect = options.isAutomaticReconnect(); + + // @TRACE 103=cleanStart={0} connectionTimeout={1} TimekeepAlive={2} + // userName={3} password={4} will={5} userContext={6} callback={7} + log.fine(CLASS_NAME, methodName, "103", + new Object[] { Boolean.valueOf(options.isCleanStart()), Integer.valueOf(options.getConnectionTimeout()), + Integer.valueOf(options.getKeepAliveInterval()), options.getUserName(), + ((null == options.getPassword()) ? "[null]" : "[notnull]"), + ((null == options.getWillMessage()) ? "[null]" : "[notnull]"), userContext, callback }); + comms.setNetworkModules(createNetworkModules(serverURI, options)); + comms.setReconnectCallback(new MqttReconnectCallback(automaticReconnect)); + + // Insert our own callback to iterate through the URIs till the connect + // succeeds + MqttToken userToken = new MqttToken(getClientId()); + ConnectActionListener connectActionListener = new ConnectActionListener(this, persistence, comms, options, + userToken, userContext, callback, reconnecting, mqttSession, this.mqttConnection); + userToken.setActionCallback(connectActionListener); + userToken.setUserContext(this); + + this.mqttConnection.setSendReasonMessages(this.connOpts.isSendReasonMessages()); + + // If we are using the MqttCallbackExtended, set it on the + // connectActionListener + if (this.mqttCallback instanceof MqttCallback) { + connectActionListener.setMqttCallbackExtended((MqttCallback) this.mqttCallback); + } + + if (this.connOpts.isCleanStart()) { + this.mqttSession.clearSessionState(); + } + this.mqttConnection.clearConnectionState(); + + this.mqttConnection.setIncomingTopicAliasMax(this.connOpts.getTopicAliasMaximum()); + + comms.setNetworkModuleIndex(0); + connectActionListener.connect(); + + return userToken; + } + + /* + * (non-Javadoc) + * + * @see + * org.eclipse.paho.mqttv5.client.IMqttAsyncClient#disconnect(java.lang.Object, + * org.eclipse.paho.mqttv5.client.MqttActionListener) + */ + @Override + public IMqttToken disconnect(Object userContext, MqttActionListener callback) throws MqttException { + return this.disconnect(QUIESCE_TIMEOUT, userContext, callback, MqttReturnCode.RETURN_CODE_SUCCESS, + new MqttProperties()); + } + + /* + * (non-Javadoc) + * + * @see org.eclipse.paho.mqttv5.client.IMqttAsyncClient#disconnect() + */ + @Override + public IMqttToken disconnect() throws MqttException { + return this.disconnect(null, null); + } + + /* + * (non-Javadoc) + * + * @see org.eclipse.paho.mqttv5.client.IMqttAsyncClient#disconnect(long) + */ + @Override + public IMqttToken disconnect(long quiesceTimeout) throws MqttException { + return this.disconnect(quiesceTimeout, null, null, MqttReturnCode.RETURN_CODE_SUCCESS, new MqttProperties()); + } + + /* + * (non-Javadoc) + * + * @see org.eclipse.paho.mqttv5.client.IMqttAsyncClient#disconnect(long, + * java.lang.Object, org.eclipse.paho.mqttv5.client.MqttActionListener, int, + * org.eclipse.paho.mqttv5.common.packet.MqttProperties) + */ + @Override + public IMqttToken disconnect(long quiesceTimeout, Object userContext, MqttActionListener callback, int reasonCode, + MqttProperties disconnectProperties) throws MqttException { + final String methodName = "disconnect"; + // @TRACE 104=> quiesceTimeout={0} userContext={1} callback={2} + log.fine(CLASS_NAME, methodName, "104", new Object[] { Long.valueOf(quiesceTimeout), userContext, callback }); + + MqttToken token = new MqttToken(getClientId()); + token.setActionCallback(callback); + token.setUserContext(userContext); + + MqttDisconnect disconnect = new MqttDisconnect(reasonCode, disconnectProperties); + + try { + comms.disconnect(disconnect, quiesceTimeout, token); + Thread.sleep(100); + } catch (MqttException ex) { + // @TRACE 105=< exception + log.fine(CLASS_NAME, methodName, "105", null, ex); + throw ex; + } catch (Exception tex) {} + // @TRACE 108=< + log.fine(CLASS_NAME, methodName, "108"); + + return token; + } + + /* + * (non-Javadoc) + * + * @see org.eclipse.paho.mqttv5.client.IMqttAsyncClient#disconnectForcibly() + */ + @Override + public void disconnectForcibly() throws MqttException { + disconnectForcibly(QUIESCE_TIMEOUT, DISCONNECT_TIMEOUT, MqttReturnCode.RETURN_CODE_SUCCESS, + new MqttProperties()); + } + + /* + * (non-Javadoc) + * + * @see org.eclipse.paho.mqttv5.client.IMqttAsyncClient#disconnectForcibly(long) + */ + @Override + public void disconnectForcibly(long disconnectTimeout) throws MqttException { + disconnectForcibly(QUIESCE_TIMEOUT, disconnectTimeout, MqttReturnCode.RETURN_CODE_SUCCESS, + new MqttProperties()); + } + + /* + * (non-Javadoc) + * + * @see org.eclipse.paho.mqttv5.client.IMqttAsyncClient#disconnectForcibly(long, + * long, int, org.eclipse.paho.mqttv5.common.packet.MqttProperties) + */ + @Override + public void disconnectForcibly(long quiesceTimeout, long disconnectTimeout, int reasonCode, + MqttProperties disconnectProperties) throws MqttException { + final String methodName = "disconnectForcibly"; + try { + comms.disconnectForcibly(quiesceTimeout, disconnectTimeout, reasonCode, disconnectProperties); + Thread.sleep(100); + } catch (MqttException ex) { + // @TRACE 105=< exception + log.fine(CLASS_NAME, methodName, "105", null, ex); + throw ex; + } catch (Exception tex) {} + // @TRACE 108=< + log.fine(CLASS_NAME, methodName, "108"); + } + + /* + * (non-Javadoc) + * + * @see org.eclipse.paho.mqttv5.client.IMqttAsyncClient#disconnectForcibly(long, + * long, boolean) + */ + @Override + public void disconnectForcibly(long quiesceTimeout, long disconnectTimeout, boolean sendDisconnectPacket) + throws MqttException { + comms.disconnectForcibly(quiesceTimeout, disconnectTimeout, sendDisconnectPacket, + MqttReturnCode.RETURN_CODE_SUCCESS, new MqttProperties()); + } + + /* + * (non-Javadoc) + * + * @see org.eclipse.paho.mqttv5.client.IMqttAsyncClient#isConnected() + */ + @Override + public boolean isConnected() { + return comms.isConnected(); + } + + /* + * (non-Javadoc) + * + * @see org.eclipse.paho.mqttv5.client.IMqttAsyncClient#getClientId() + */ + @Override + public String getClientId() { + return this.mqttSession.getClientId(); + } + + /* + * (non-Javadoc) + * + * @see + * org.eclipse.paho.mqttv5.client.IMqttAsyncClient#setClientId(java.lang.String) + */ + @Override + public void setClientId(String clientId) { + this.mqttSession.setClientId(clientId); + } + + /* + * (non-Javadoc) + * + * @see org.eclipse.paho.mqttv5.client.IMqttAsyncClient#getServerURI() + */ + @Override + public String getServerURI() { + return serverURI; + } + + /* + * (non-Javadoc) + * + * @see org.eclipse.paho.mqttv5.client.IMqttAsyncClient#getCurrentServerURI() + */ + @Override + public String getCurrentServerURI() { + return comms.getNetworkModules()[comms.getNetworkModuleIndex()].getServerURI(); + } + + /** + * Get a topic object which can be used to publish messages. + *

+ * There are two alternative methods that should be used in preference to this + * one when publishing a message: + *

+ *
    + *
  • {@link MqttAsyncClient#publish(String, MqttMessage)} to publish a message + * in a non-blocking manner or
  • + *
  • {@link MqttClient#publish(String, MqttMessage)} to publish + * a message in a blocking manner
  • + *
+ *

+ * When you build an application, the design of the topic tree should take into + * account the following principles of topic name syntax and semantics: + *

+ * + *
    + *
  • A topic must be at least one character long.
  • + *
  • Topic names are case sensitive. For example, ACCOUNTS and + * Accounts are two different topics.
  • + *
  • Topic names can include the space character. For example, Accounts + * payable is a valid topic.
  • + *
  • A leading "/" creates a distinct topic. For example, /finance is + * different from finance. /finance matches "+/+" and "/+", + * but not "+".
  • + *
  • Do not include the null character (Unicode \x0000) in any topic.
  • + *
+ * + *

+ * The following principles apply to the construction and content of a topic + * tree: + *

+ * + *
    + *
  • The length is limited to 64k but within that there are no limits to the + * number of levels in a topic tree.
  • + *
  • There can be any number of root nodes; that is, there can be any number + * of topic trees.
  • + *
+ * + * @param topic + * the topic to use, for example "finance/stock/ibm". + * @return an MqttTopic object, which can be used to publish messages to the + * topic. + * @throws IllegalArgumentException + * if the topic contains a '+' or '#' wildcard character. + */ + protected MqttTopic getTopic(String topic) { + MqttTopicValidator.validate(topic, false/* wildcards NOT allowed */, true); + + MqttTopic result = (MqttTopic) topics.get(topic); + if (result == null) { + result = new MqttTopic(topic, comms); + topics.put(topic, result); + } + return result; + } + + /* + * (non-Javadoc) Check and send a ping if needed.

By default, client sends + * PingReq to server to keep the connection to server. For some platforms which + * cannot use this mechanism, such as Android, developer needs to handle the + * ping request manually with this method.

+ * + * @throws MqttException for other errors encountered while publishing the + * message. + */ + /* + * (non-Javadoc) + * + * @see + * org.eclipse.paho.mqttv5.client.IMqttAsyncClient#checkPing(java.lang.Object, + * org.eclipse.paho.mqttv5.client.MqttActionListener) + */ + @Override + public IMqttToken checkPing(Object userContext, MqttActionListener callback) throws MqttException { + final String methodName = "ping"; + MqttToken token; + // @TRACE 117=> + log.fine(CLASS_NAME, methodName, "117"); + + token = comms.checkForActivity(callback); + // @TRACE 118=< + log.fine(CLASS_NAME, methodName, "118"); + + return token; + } + + /* + * (non-Javadoc) + * + * @see + * org.eclipse.paho.mqttv5.client.IMqttAsyncClient#subscribe(java.lang.String, + * int, java.lang.Object, org.eclipse.paho.mqttv5.client.MqttActionListener) + */ + @Override + public IMqttToken subscribe(String topicFilter, int qos, Object userContext, MqttActionListener callback) + throws MqttException { + return this.subscribe(new MqttSubscription[] { new MqttSubscription(topicFilter, qos) }, userContext, callback, + new MqttProperties()); + } + + @Override + public IMqttToken subscribe(String[] topicFilters, int[] qoss, Object userContext, MqttActionListener callback) + throws MqttException { + MqttSubscription[] subs = new MqttSubscription[topicFilters.length]; + for (int i = 0; i < topicFilters.length; ++ i) { + subs[i] = new MqttSubscription(topicFilters[i], qoss[i]); + } + return this.subscribe(subs, userContext, callback, new MqttProperties()); + } + + /* + * (non-Javadoc) + * + * @see + * org.eclipse.paho.mqttv5.client.IMqttAsyncClient#subscribe(java.lang.String, + * int) + */ + @Override + public IMqttToken subscribe(String topicFilter, int qos) throws MqttException { + return this.subscribe(new MqttSubscription[] { new MqttSubscription(topicFilter, qos) }, null, null, + new MqttProperties()); + } + + @Override + public IMqttToken subscribe(String[] topicFilters, int[] qoss) throws MqttException { + return this.subscribe(topicFilters, qoss, null, null); + } + + /* + * (non-Javadoc) + * + * @see + * org.eclipse.paho.mqttv5.client.IMqttAsyncClient#subscribe(java.lang.String, + * int) + */ + @Override + public IMqttToken subscribe(MqttSubscription subscription) throws MqttException { + return this.subscribe(new MqttSubscription[] { subscription }, null, null, new MqttProperties()); + } + + /* + * (non-Javadoc) + * + * @see + * org.eclipse.paho.mqttv5.client.IMqttAsyncClient#subscribe(org.eclipse.paho. + * mqttv5.common.MqttSubscription[]) + */ + @Override + public IMqttToken subscribe(MqttSubscription[] subscriptions) throws MqttException { + return this.subscribe(subscriptions, null, null, new MqttProperties()); + } + + /* + * (non-Javadoc) + * + * @see + * org.eclipse.paho.mqttv5.client.IMqttAsyncClient#subscribe(org.eclipse.paho. + * mqttv5.common.MqttSubscription[], java.lang.Object, + * org.eclipse.paho.mqttv5.client.MqttActionListener, + * org.eclipse.paho.mqttv5.common.packet.MqttProperties) + */ + @Override + public IMqttToken subscribe(MqttSubscription[] subscriptions, Object userContext, MqttActionListener callback, + MqttProperties subscriptionProperties) throws MqttException { + + // remove any message handlers for individual topics and validate Topics + for (MqttSubscription subscription : subscriptions) { + this.comms.removeMessageListener(subscription.getTopic()); + // Check if the topic filter is valid before subscribing + MqttTopicValidator.validate(subscription.getTopic(), + this.mqttConnection.isWildcardSubscriptionsAvailable(), + this.mqttConnection.isSharedSubscriptionsAvailable()); + } + + return this.subscribeBase(subscriptions, userContext, callback, subscriptionProperties); + } + + private IMqttToken subscribeBase(MqttSubscription[] subscriptions, Object userContext, MqttActionListener callback, + MqttProperties subscriptionProperties) throws MqttException { + final String methodName = "subscribe"; + + // Only Generate Log string if we are logging at FINE level + if (log.isLoggable(Logger.FINE)) { + StringBuffer subs = new StringBuffer(); + for (int i = 0; i < subscriptions.length; i++) { + if (i > 0) { + subs.append(", "); + } + subs.append(subscriptions[i].toString()); + } + // @TRACE 106=Subscribe topicFilter={0} userContext={1} callback={2} + log.fine(CLASS_NAME, methodName, "106", new Object[] { subs.toString(), userContext, callback }); + } + + MqttToken token = new MqttToken(getClientId()); + token.setActionCallback(callback); + token.setUserContext(userContext); + // TODO - Somehow refactor this.... + // token.internalTok.setTopics(topicFilters); + // TODO - Build up MQTT Subscriptions properly here + + MqttSubscribe register = new MqttSubscribe(subscriptions, subscriptionProperties); + token.setRequestMessage(register); + comms.sendNoWait(register, token); + // @TRACE 109=< + log.fine(CLASS_NAME, methodName, "109"); + + return token; + } + + /* + * (non-Javadoc) + * + * @see + * org.eclipse.paho.mqttv5.client.IMqttAsyncClient#subscribe(org.eclipse.paho. + * mqttv5.common.MqttSubscription, java.lang.Object, + * org.eclipse.paho.mqttv5.client.MqttActionListener, + * org.eclipse.paho.mqttv5.client.IMqttMessageListener, + * org.eclipse.paho.mqttv5.common.packet.MqttProperties) + */ + @Override + public IMqttToken subscribe(MqttSubscription mqttSubscription, Object userContext, MqttActionListener callback, + IMqttMessageListener messageListener, MqttProperties subscriptionProperties) throws MqttException { + + return this.subscribe(new MqttSubscription[] { mqttSubscription }, userContext, callback, messageListener, + subscriptionProperties); + } + + /* + * (non-Javadoc) + * + * @see + * org.eclipse.paho.mqttv5.client.IMqttAsyncClient#subscribe(org.eclipse.paho. + * mqttv5.common.MqttSubscription, + * org.eclipse.paho.mqttv5.client.IMqttMessageListener) + */ + @Override + public IMqttToken subscribe(MqttSubscription subscription, IMqttMessageListener messageListener) + throws MqttException { + return this.subscribe(new MqttSubscription[] { subscription }, null, null, messageListener, + new MqttProperties()); + } + + /* + * (non-Javadoc) + * + * @see + * org.eclipse.paho.mqttv5.client.IMqttAsyncClient#subscribe(org.eclipse.paho. + * mqttv5.common.MqttSubscription[], + * org.eclipse.paho.mqttv5.client.IMqttMessageListener) + */ + @Override + public IMqttToken subscribe(MqttSubscription[] subscriptions, IMqttMessageListener messageListener) + throws MqttException { + return this.subscribe(subscriptions, null, null, messageListener, new MqttProperties()); + } + + /* + * (non-Javadoc) + * + * @see + * org.eclipse.paho.mqttv5.client.IMqttAsyncClient#subscribe(org.eclipse.paho. + * mqttv5.common.MqttSubscription[], java.lang.Object, + * org.eclipse.paho.mqttv5.client.MqttActionListener, + * org.eclipse.paho.mqttv5.client.IMqttMessageListener[], + * org.eclipse.paho.mqttv5.common.packet.MqttProperties) + */ + @Override + public IMqttToken subscribe(MqttSubscription[] subscriptions, Object userContext, MqttActionListener callback, + IMqttMessageListener[] messageListeners, MqttProperties subscriptionProperties) throws MqttException { + + // add message handlers to the list for this client + for (int i = 0; i < subscriptions.length; ++i) { + MqttTopicValidator.validate(subscriptions[i].getTopic(), + this.mqttConnection.isWildcardSubscriptionsAvailable(), + this.mqttConnection.isSharedSubscriptionsAvailable()); + if (messageListeners == null || messageListeners[i] == null) { + this.comms.removeMessageListener(subscriptions[i].getTopic()); + } else { + this.comms.setMessageListener(null, subscriptions[i].getTopic(), messageListeners[i]); + } + } + + IMqttToken token = null; + try { + token = this.subscribeBase(subscriptions, userContext, callback, subscriptionProperties); + } catch(Exception e) { + // if the subscribe fails, then we have to remove the message handlers + for (MqttSubscription subscription : subscriptions) { + this.comms.removeMessageListener(subscription.getTopic()); + } + throw e; + } + return token; + } + + /* + * (non-Javadoc) + * + * @see + * org.eclipse.paho.mqttv5.client.IMqttAsyncClient#subscribe(org.eclipse.paho. + * mqttv5.common.MqttSubscription[], java.lang.Object, + * org.eclipse.paho.mqttv5.client.MqttActionListener, + * org.eclipse.paho.mqttv5.client.IMqttMessageListener, + * org.eclipse.paho.mqttv5.common.packet.MqttProperties) + */ + @Override + public IMqttToken subscribe(MqttSubscription[] subscriptions, Object userContext, MqttActionListener callback, + IMqttMessageListener messageListener, MqttProperties subscriptionProperties) throws MqttException { + + int subId = 0; + try { + subId = subscriptionProperties.getSubscriptionIdentifiers().get(0); + } catch (IndexOutOfBoundsException e) { + log.fine(CLASS_NAME, "subscribe", "No sub subscription property(s)"); + } + // Automatic Subscription Identifier Assignment is enabled + if (connOpts.useSubscriptionIdentifiers() && this.mqttConnection.isSubscriptionIdentifiersAvailable()) { + + // Application is overriding the subscription Identifier + if (subId != 0) { + // Check that we are not already using this ID, else throw Illegal Argument + // Exception + if (this.comms.doesSubscriptionIdentifierExist(subId)) { + throw new IllegalArgumentException( + String.format("The Subscription Identifier %s already exists.", subId)); + } + + } else { + // Automatically assign new ID and link to callback. + subId = this.mqttSession.getNextSubscriptionIdentifier(); + } + } + + // add message handlers to the list for this client + for (MqttSubscription subscription : subscriptions) { + MqttTopicValidator.validate(subscription.getTopic(), + this.mqttConnection.isWildcardSubscriptionsAvailable(), + this.mqttConnection.isSharedSubscriptionsAvailable()); + if (messageListener == null) { + this.comms.removeMessageListener(subscription.getTopic()); + } else { + this.comms.setMessageListener(subId, subscription.getTopic(), messageListener); + } + } + + IMqttToken token = null; + try { + token = this.subscribeBase(subscriptions, userContext, callback, subscriptionProperties); + } catch(Exception e) { + // if the subscribe fails, then we have to remove the message handlers + for (MqttSubscription subscription : subscriptions) { + this.comms.removeMessageListener(subscription.getTopic()); + } + throw e; + } + return token; + } + + /* + * (non-Javadoc) + * + * @see + * org.eclipse.paho.mqttv5.client.IMqttAsyncClient#unsubscribe(java.lang.String, + * java.lang.Object, org.eclipse.paho.mqttv5.client.MqttActionListener) + */ + @Override + public IMqttToken unsubscribe(String topicFilter, Object userContext, MqttActionListener callback) + throws MqttException { + return unsubscribe(new String[] { topicFilter }, userContext, callback, new MqttProperties()); + } + + /* + * (non-Javadoc) + * + * @see + * org.eclipse.paho.mqttv5.client.IMqttAsyncClient#unsubscribe(java.lang.String) + */ + @Override + public IMqttToken unsubscribe(String topicFilter) throws MqttException { + return unsubscribe(new String[] { topicFilter }, null, null, new MqttProperties()); + } + + /* + * (non-Javadoc) + * + * @see + * org.eclipse.paho.mqttv5.client.IMqttAsyncClient#unsubscribe(java.lang.String[ + * ]) + */ + @Override + public IMqttToken unsubscribe(String[] topicFilters) throws MqttException { + return unsubscribe(topicFilters, null, null, new MqttProperties()); + } + + /* + * (non-Javadoc) + * + * @see + * org.eclipse.paho.mqttv5.client.IMqttAsyncClient#unsubscribe(java.lang.String[ + * ], java.lang.Object, org.eclipse.paho.mqttv5.client.MqttActionListener, + * org.eclipse.paho.mqttv5.common.packet.MqttProperties) + */ + @Override + public IMqttToken unsubscribe(String[] topicFilters, Object userContext, MqttActionListener callback, + MqttProperties unsubscribeProperties) throws MqttException { + final String methodName = "unsubscribe"; + + // Only Generate Log string if we are logging at FINE level + if (log.isLoggable(Logger.FINE)) { + String subs = ""; + for (int i = 0; i < topicFilters.length; i++) { + if (i > 0) { + subs += ", "; + } + subs += topicFilters[i]; + } + + // @TRACE 107=Unsubscribe topic={0} userContext={1} callback={2} + log.fine(CLASS_NAME, methodName, "107", new Object[] { subs, userContext, callback }); + } + + for (String topicFilter : topicFilters) { + // Check if the topic filter is valid before unsubscribing + // Although we already checked when subscribing, but invalid + // topic filter is meanless for unsubscribing, just prohibit it + // to reduce unnecessary control packet send to broker. + MqttTopicValidator.validate(topicFilter, true/* allow wildcards */, this.mqttConnection.isSharedSubscriptionsAvailable()); + } + + // remove message handlers from the list for this client + for (String topicFilter : topicFilters) { + this.comms.removeMessageListener(topicFilter); + } + + MqttToken token = new MqttToken(getClientId()); + token.setActionCallback(callback); + token.setUserContext(userContext); + token.internalTok.setTopics(topicFilters); + + MqttUnsubscribe unregister = new MqttUnsubscribe(topicFilters, unsubscribeProperties); + token.setRequestMessage(unregister); + + comms.sendNoWait(unregister, token); + // @TRACE 110=< + log.fine(CLASS_NAME, methodName, "110"); + + return token; + } + + /* + * (non-Javadoc) + * + * @see + * org.eclipse.paho.mqttv5.client.IMqttAsyncClient#setCallback(org.eclipse.paho. + * mqttv5.client.MqttCallback) + */ + @Override + public void setCallback(MqttCallback callback) { + this.mqttCallback = callback; + comms.setCallback(callback); + } + + /* + * (non-Javadoc) + * + * @see org.eclipse.paho.mqttv5.client.IMqttAsyncClient#setManualAcks(boolean) + */ + @Override + public void setManualAcks(boolean manualAcks) { + comms.setManualAcks(manualAcks); + } + + /* + * (non-Javadoc) + * + * @see + * org.eclipse.paho.mqttv5.client.IMqttAsyncClient#messageArrivedComplete(int, + * int) + */ + @Override + public void messageArrivedComplete(int messageId, int qos) throws MqttException { + comms.messageArrivedComplete(messageId, qos); + } + + /* + * (non-Javadoc) + * + * @see + * org.eclipse.paho.mqttv5.client.IMqttAsyncClient#getPendingTokens() + */ + @Override + public IMqttToken[] getPendingTokens() { + return comms.getPendingTokens(); + } + + /* + * (non-Javadoc) + * + * @see + * org.eclipse.paho.mqttv5.client.IMqttAsyncClient#publish(java.lang.String, + * byte[], int, boolean, java.lang.Object, + * org.eclipse.paho.mqttv5.client.MqttActionListener) + */ + @Override + public IMqttToken publish(String topic, byte[] payload, int qos, boolean retained, Object userContext, + MqttActionListener callback) throws MqttException, MqttPersistenceException { + MqttMessage message = new MqttMessage(payload); + message.setProperties(new MqttProperties()); + message.setQos(qos); + message.setRetained(retained); + return this.publish(topic, message, userContext, callback); + } + + /* + * (non-Javadoc) + * + * @see + * org.eclipse.paho.mqttv5.client.IMqttAsyncClient#publish(java.lang.String, + * byte[], int, boolean) + */ + @Override + public IMqttToken publish(String topic, byte[] payload, int qos, boolean retained) + throws MqttException, MqttPersistenceException { + return this.publish(topic, payload, qos, retained, null, null); + } + + /* + * (non-Javadoc) + * + * @see + * org.eclipse.paho.mqttv5.client.IMqttAsyncClient#publish(java.lang.String, + * org.eclipse.paho.mqttv5.common.MqttMessage) + */ + @Override + public IMqttToken publish(String topic, MqttMessage message) + throws MqttException, MqttPersistenceException { + return this.publish(topic, message, null, null); + } + + /* + * (non-Javadoc) + * + * @see + * org.eclipse.paho.mqttv5.client.IMqttAsyncClient#publish(java.lang.String, + * org.eclipse.paho.mqttv5.common.MqttMessage, java.lang.Object, + * org.eclipse.paho.mqttv5.client.MqttActionListener, + * org.eclipse.paho.mqttv5.common.packet.MqttProperties) + */ + @Override + public IMqttToken publish(String topic, MqttMessage message, Object userContext, + MqttActionListener callback) throws MqttException, MqttPersistenceException { + final String methodName = "publish"; + // @TRACE 111=< topic={0} message={1}userContext={1} callback={2} + log.fine(CLASS_NAME, methodName, "111", new Object[] { topic, userContext, callback }); + + // Checks if a topic is valid when publishing a message. + MqttTopicValidator.validate(topic, false/* wildcards NOT allowed */, true); + + MqttToken token = new MqttToken(getClientId()); + token.internalTok.setDeliveryToken(true); + token.setActionCallback(callback); + token.setUserContext(userContext); + token.setMessage(message); + token.internalTok.setTopics(new String[] { topic }); + + MqttPublish pubMsg = new MqttPublish(topic, message, message.getProperties()); + token.setRequestMessage(pubMsg); + comms.sendNoWait(pubMsg, token); + + // @TRACE 112=< + log.fine(CLASS_NAME, methodName, "112"); + + return token; + } + + /* + * (non-Javadoc) + * + * @see org.eclipse.paho.mqttv5.client.IMqttAsyncClient#reconnect() + */ + @Override + public void reconnect() throws MqttException { + final String methodName = "reconnect"; + // @Trace 500=Attempting to reconnect client: {0} + log.fine(CLASS_NAME, methodName, "500", new Object[] { this.mqttSession.getClientId() }); + // Some checks to make sure that we're not attempting to reconnect an + // already connected client + if (comms.isConnected()) { + throw ExceptionHelper.createMqttException(MqttClientException.REASON_CODE_CLIENT_CONNECTED); + } + if (comms.isConnecting()) { + throw new MqttException(MqttClientException.REASON_CODE_CONNECT_IN_PROGRESS); + } + if (comms.isDisconnecting()) { + throw new MqttException(MqttClientException.REASON_CODE_CLIENT_DISCONNECTING); + } + if (comms.isClosed()) { + throw new MqttException(MqttClientException.REASON_CODE_CLIENT_CLOSED); + } + // We don't want to spam the server + stopReconnectCycle(); + + attemptReconnect(); + } + + /** + * Attempts to reconnect the client to the server. If successful it will make + * sure that there are no further reconnects scheduled. However if the connect + * fails, the delay will double up to 128 seconds and will re-schedule the + * reconnect for after the delay. + * + * Any thrown exceptions are logged but not acted upon as it is assumed that + * they are being thrown due to the server being offline and so reconnect + * attempts will continue. + */ + private void attemptReconnect() { + final String methodName = "attemptReconnect"; + // @Trace 500=Attempting to reconnect client: {0} + log.fine(CLASS_NAME, methodName, "500", new Object[] { this.mqttSession.getClientId() }); + try { + connect(this.connOpts, this.userContext, new MqttReconnectActionListener(methodName)); + } catch (MqttSecurityException ex) { + // @TRACE 804=exception + log.fine(CLASS_NAME, methodName, "804", null, ex); + } catch (MqttException ex) { + // @TRACE 804=exception + log.fine(CLASS_NAME, methodName, "804", null, ex); + } + } + + private void startReconnectCycle() { + String methodName = "startReconnectCycle"; + // @Trace 503=Start reconnect timer for client: {0}, delay: {1} + log.fine(CLASS_NAME, methodName, "503", + new Object[] { this.mqttSession.getClientId(), Long.valueOf(reconnectDelay) }); + reconnectTimer = new Timer("MQTT Reconnect: " + this.mqttSession.getClientId()); + reconnectTimer.schedule(new ReconnectTask(), reconnectDelay); + } + + private void stopReconnectCycle() { + String methodName = "stopReconnectCycle"; + // @Trace 504=Stop reconnect timer for client: {0} + log.fine(CLASS_NAME, methodName, "504", new Object[] { this.mqttSession.getClientId() }); + synchronized (clientLock) { + if (this.connOpts.isAutomaticReconnect()) { + if (reconnectTimer != null) { + reconnectTimer.cancel(); + reconnectTimer = null; + } + reconnectDelay = 1000; // Reset Delay Timer + } + } + } + + private class ReconnectTask extends TimerTask { + private static final String methodName = "ReconnectTask.run"; + + public void run() { + // @Trace 506=Triggering Automatic Reconnect attempt. + log.fine(CLASS_NAME, methodName, "506"); + attemptReconnect(); + } + } + + class MqttReconnectCallback implements MqttCallback { + + final boolean automaticReconnect; + + MqttReconnectCallback(boolean isAutomaticReconnect) { + automaticReconnect = isAutomaticReconnect; + } + + public void messageArrived(String topic, MqttMessage message) throws Exception { + } + + public void deliveryComplete(IMqttToken token) { + } + + public void connectComplete(boolean reconnect, String serverURI) { + } + + public void disconnected(MqttDisconnectResponse disconnectResponse) { + if (automaticReconnect) { + // Automatic reconnect is set so make sure comms is in resting + // state + comms.setRestingState(true); + reconnecting = true; + startReconnectCycle(); + } + } + + public void mqttErrorOccurred(MqttException exception) { + } + + public void authPacketArrived(int reasonCode, MqttProperties properties) { + + } + + } + + class MqttReconnectActionListener implements MqttActionListener { + + final String methodName; + + MqttReconnectActionListener(String methodName) { + this.methodName = methodName; + } + + public void onSuccess(IMqttToken asyncActionToken) { + // @Trace 501=Automatic Reconnect Successful: {0} + log.fine(CLASS_NAME, methodName, "501", new Object[] { asyncActionToken.getClient().getClientId() }); + comms.setRestingState(false); + stopReconnectCycle(); + } + + public void onFailure(IMqttToken asyncActionToken, Throwable exception) { + // @Trace 502=Automatic Reconnect failed, rescheduling: {0} + log.fine(CLASS_NAME, methodName, "502", new Object[] { asyncActionToken.getClient().getClientId() }); + if (reconnectDelay < connOpts.getMaxReconnectDelay()) { + reconnectDelay = reconnectDelay * 2; + } + rescheduleReconnectCycle(reconnectDelay); + } + + private void rescheduleReconnectCycle(int delay) { + String reschedulemethodName = methodName + ":rescheduleReconnectCycle"; + // @Trace 505=Rescheduling reconnect timer for client: {0}, delay: + // {1} + log.fine(CLASS_NAME, reschedulemethodName, "505", + new Object[] { MqttAsyncClient.this.mqttSession.getClientId(), String.valueOf(reconnectDelay) }); + synchronized (clientLock) { + if (MqttAsyncClient.this.connOpts.isAutomaticReconnect()) { + if (reconnectTimer != null) { + reconnectTimer.schedule(new ReconnectTask(), delay); + } else { + // The previous reconnect timer was cancelled + reconnectDelay = delay; + startReconnectCycle(); + } + } + } + } + + } + + /* + * (non-Javadoc) + * + * @see + * org.eclipse.paho.mqttv5.client.IMqttAsyncClient#setBufferOpts(org.eclipse. + * paho.mqttv5.client.DisconnectedBufferOptions) + */ + @Override + public void setBufferOpts(DisconnectedBufferOptions bufferOpts) { + this.comms.setDisconnectedMessageBuffer(new DisconnectedMessageBuffer(bufferOpts)); + } + + /* + * (non-Javadoc) + * + * @see + * org.eclipse.paho.mqttv5.client.IMqttAsyncClient#getBufferedMessageCount() + */ + @Override + public int getBufferedMessageCount() { + return this.comms.getBufferedMessageCount(); + } + + /* + * (non-Javadoc) + * + * @see org.eclipse.paho.mqttv5.client.IMqttAsyncClient#getBufferedMessage(int) + */ + @Override + public MqttMessage getBufferedMessage(int bufferIndex) { + return this.comms.getBufferedMessage(bufferIndex); + } + + /* + * (non-Javadoc) + * + * @see + * org.eclipse.paho.mqttv5.client.IMqttAsyncClient#deleteBufferedMessage(int) + */ + @Override + public void deleteBufferedMessage(int bufferIndex) { + this.comms.deleteBufferedMessage(bufferIndex); + } + + /* + * (non-Javadoc) + * + * @see + * org.eclipse.paho.mqttv5.client.IMqttAsyncClient#getInFlightMessageCount() + */ + @Override + public int getInFlightMessageCount() { + return this.comms.getActualInFlight(); + } + + /* + * (non-Javadoc) + * + * @see org.eclipse.paho.mqttv5.client.IMqttAsyncClient#close() + */ + @Override + public void close() throws MqttException { + close(false); + } + + /* + * (non-Javadoc) + * + * @see org.eclipse.paho.mqttv5.client.IMqttAsyncClient#close(boolean) + */ + @Override + public void close(boolean force) throws MqttException { + final String methodName = "close"; + // @TRACE 113=< + log.fine(CLASS_NAME, methodName, "113"); + comms.close(force); + // @TRACE 114=> + log.fine(CLASS_NAME, methodName, "114"); + + } + + /* + * (non-Javadoc) + * + * @see org.eclipse.paho.mqttv5.client.IMqttAsyncClient#getDebug() + */ + @Override + public Debug getDebug() { + return new Debug(this.mqttSession.getClientId(), comms); + } + + @Override + public IMqttToken authenticate(int reasonCode, Object userContext, MqttProperties properties) throws MqttException { + MqttToken token = new MqttToken(getClientId()); + token.setUserContext(userContext); + + MqttAuth auth = new MqttAuth(reasonCode, properties); + comms.sendNoWait(auth, token); + return null; + } + +} diff --git a/src/main/java/org/eclipse/paho/mqttv5/client/MqttCallback.java b/src/main/java/org/eclipse/paho/mqttv5/client/MqttCallback.java new file mode 100644 index 0000000..1c72f28 --- /dev/null +++ b/src/main/java/org/eclipse/paho/mqttv5/client/MqttCallback.java @@ -0,0 +1,132 @@ +/******************************************************************************* + * Copyright (c) 2009, 2018 IBM Corp. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * and Eclipse Distribution License v1.0 which accompany this distribution. + * + * The Eclipse Public License is available at + * https://www.eclipse.org/legal/epl-2.0 + * and the Eclipse Distribution License is available at + * https://www.eclipse.org/org/documents/edl-v10.php + * + * Contributors: + * Dave Locke - initial API and implementation and/or initial documentation + * James Sutton - MQTT V5 implementation + */ +package org.eclipse.paho.mqttv5.client; + +import org.eclipse.paho.mqttv5.common.MqttException; +import org.eclipse.paho.mqttv5.common.MqttMessage; +import org.eclipse.paho.mqttv5.common.packet.MqttProperties; + +/** + * Enables an application to be notified when asynchronous events related to the + * client occur. Classes implementing this interface can be registered on both + * types of client: {@link IMqttClient#setCallback(MqttCallback)} and + * {@link IMqttAsyncClient#setCallback(MqttCallback)} + */ +public interface MqttCallback { + + /** + * This method is called when the server gracefully disconnects from the client + * by sending a disconnect packet, or when the TCP connection is lost due to a + * network issue or if the client encounters an error. + * + * @param disconnectResponse + * a {@link MqttDisconnectResponse} containing relevant properties + * related to the cause of the disconnection. + */ + void disconnected(MqttDisconnectResponse disconnectResponse); + + /** + * This method is called when an exception is thrown within the MQTT client. The + * reasons for this may vary, from malformed packets, to protocol errors or even + * bugs within the MQTT client itself. This callback surfaces those errors to + * the application so that it may decide how best to deal with them. + * + * For example, The MQTT server may have sent a publish message with an invalid + * topic alias, the MQTTv5 specification suggests that the client should + * disconnect from the broker with the appropriate return code, however this is + * completely up to the application itself. + * + * @param exception + * - The exception thrown causing the error. + */ + void mqttErrorOccurred(MqttException exception); + + /** + * This method is called when a message arrives from the server. + * + *

+ * This method is invoked synchronously by the MQTT client. An acknowledgment is + * not sent back to the server until this method returns cleanly. + *

+ *

+ * If an implementation of this method throws an Exception, then + * the client will be shut down. When the client is next re-connected, any QoS 1 + * or 2 messages will be redelivered by the server. + *

+ *

+ * Any additional messages which arrive while an implementation of this method + * is running, will build up in memory, and will then back up on the network. + *

+ *

+ * If an application needs to persist data, then it should ensure the data is + * persisted prior to returning from this method, as after returning from this + * method, the message is considered to have been delivered, and will not be + * reproducible. + *

+ *

+ * It is possible to send a new message within an implementation of this + * callback (for example, a response to this message), but the implementation + * must not disconnect the client, as it will be impossible to send an + * acknowledgment for the message being processed, and a deadlock will occur. + *

+ * + * @param topic + * name of the topic on the message was published to + * @param message + * the actual message. + * @throws Exception + * if a terminal error has occurred, and the client should be shut + * down. + */ + void messageArrived(String topic, MqttMessage message) throws Exception; + + /** + * Called when delivery for a message has been completed, and all + * acknowledgments have been received. For QoS 0 messages it is called once the + * message has been handed to the network for delivery. For QoS 1 it is called + * when PUBACK is received and for QoS 2 when PUBCOMP is received. The token + * will be the same token as that returned when the message was published. + * + * @param token + * the delivery token associated with the message. + */ + void deliveryComplete(IMqttToken token); + + /** + * Called when the connection to the server is completed successfully. + * + * @param reconnect + * If true, the connection was the result of automatic reconnect. + * @param serverURI + * The server URI that the connection was made to. + */ + void connectComplete(boolean reconnect, String serverURI); + + /** + * Called when an AUTH packet is received by the client. + * + * @param reasonCode + * The Reason code, can be Success (0), Continue authentication (24) + * or Re-authenticate (25). + * @param properties + * The {@link MqttProperties} to be sent, containing the + * Authentication Method, Authentication Data and any required User + * Defined Properties. + */ + void authPacketArrived(int reasonCode, MqttProperties properties); + +} diff --git a/src/main/java/org/eclipse/paho/mqttv5/client/MqttClient.java b/src/main/java/org/eclipse/paho/mqttv5/client/MqttClient.java new file mode 100644 index 0000000..a4708f8 --- /dev/null +++ b/src/main/java/org/eclipse/paho/mqttv5/client/MqttClient.java @@ -0,0 +1,744 @@ +/******************************************************************************* + * Copyright (c) 2009, 2019 IBM Corp. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * and Eclipse Distribution License v1.0 which accompany this distribution. + * + * The Eclipse Public License is available at + * https://www.eclipse.org/legal/epl-2.0 + * and the Eclipse Distribution License is available at + * https://www.eclipse.org/org/documents/edl-v10.php + * + * Contributors: + * Dave Locke - initial API and implementation and/or initial documentation + * Ian Craggs - MQTT 3.1.1 support + * Ian Craggs - per subscription message handlers (bug 466579) + * Ian Craggs - ack control (bug 472172) + */ +package org.eclipse.paho.mqttv5.client; + +import java.util.Properties; +import java.util.concurrent.ScheduledExecutorService; + +import javax.net.SocketFactory; + +import org.eclipse.paho.mqttv5.client.persist.MqttDefaultFilePersistence; +import org.eclipse.paho.mqttv5.client.util.Debug; +import org.eclipse.paho.mqttv5.common.MqttException; +import org.eclipse.paho.mqttv5.common.MqttMessage; +import org.eclipse.paho.mqttv5.common.MqttPersistenceException; +import org.eclipse.paho.mqttv5.common.MqttSecurityException; +import org.eclipse.paho.mqttv5.common.MqttSubscription; +import org.eclipse.paho.mqttv5.common.packet.MqttProperties; +import org.eclipse.paho.mqttv5.common.packet.MqttReturnCode; + +/** + * Lightweight client for talking to an MQTT server using methods that block + * until an operation completes. + * + *

+ * This class implements the blocking {@link IMqttClient} client interface where + * all actions block until they have completed (or timed out). This + * implementation is compatible with all Java SE runtimes from 1.7 and up. + *

+ *

+ * An application can connect to an MQTT server using: + *

+ *
    + *
  • A plain TCP socket + *
  • An secure SSL/TLS socket + *
+ * + *

+ * To enable messages to be delivered even across network and client restarts + * messages need to be safely stored until the message has been delivered at the + * requested quality of service. A pluggable persistence mechanism is provided + * to store the messages. + *

+ *

+ * By default {@link MqttDefaultFilePersistence} is used to store messages to a + * file. If persistence is set to null then messages are stored in memory and + * hence can be lost if the client, Java runtime or device shuts down. + *

+ *

+ * If connecting with {@link MqttConnectionOptions#setCleanStart(boolean)} set + * to true it is safe to use memory persistence as all state it cleared when a + * client disconnects. If connecting with cleanStart set to false, to provide + * reliable message delivery then a persistent message store should be used such + * as the default one. + *

+ *

+ * The message store interface is pluggable. Different stores can be used by + * implementing the {@link MqttClientPersistence} interface and passing it to + * the clients constructor. + *

+ * + * @see IMqttClient + */ +public class MqttClient implements IMqttClient { + + protected MqttAsyncClient aClient = null; // Delegate implementation to MqttAsyncClient + protected long timeToWait = -1; // How long each method should wait for action to complete + + /** + * Create an MqttClient that can be used to communicate with an MQTT server. + *

+ * The address of a server can be specified on the constructor. Alternatively a + * list containing one or more servers can be specified using the + * {@link MqttConnectionOptions#setServerURIs(String[]) setServerURIs} method on + * MqttConnectOptions. + * + *

+ * The serverURI parameter is typically used with the the + * clientId parameter to form a key. The key is used to store and + * reference messages while they are being delivered. Hence the serverURI + * specified on the constructor must still be specified even if a list of + * servers is specified on an MqttConnectOptions object. The serverURI on the + * constructor must remain the same across restarts of the client for delivery + * of messages to be maintained from a given client to a given server or set of + * servers. + * + *

+ * The address of the server to connect to is specified as a URI. Two types of + * connection are supported tcp:// for a TCP connection and + * ssl:// for a TCP connection secured by SSL/TLS. For example: + *

+ *
    + *
  • tcp://localhost:1883
  • + *
  • ssl://localhost:8883
  • + *
+ *

+ * If the port is not specified, it will default to 1883 for + * tcp://" URIs, and 8883 for ssl:// URIs. + *

+ * + *

+ * A client identifier clientId must be specified and be less that + * 65535 characters. It must be unique across all clients connecting to the same + * server. The clientId is used by the server to store data related to the + * client, hence it is important that the clientId remain the same when + * connecting to a server if durable subscriptions or reliable messaging are + * required. + *

+ * As the client identifier + * is used by the server to identify a client when it reconnects, the client + * must use the same identifier between connections if durable subscriptions or + * reliable delivery of messages is required. + *

+ *

+ * In Java SE, SSL can be configured in one of several ways, which the client + * will use in the following order: + *

+ *
    + *
  • Supplying an SSLSocketFactory - + * applications can use + * {@link MqttConnectionOptions#setSocketFactory(SocketFactory)} to supply a + * factory with the appropriate SSL settings.
  • + *
  • SSL Properties - applications can supply SSL settings as + * a simple Java Properties using + * {@link MqttConnectionOptions#setSSLProperties(Properties)}.
  • + *
  • Use JVM settings - There are a number of standard Java + * system properties that can be used to configure key and trust stores.
  • + *
+ * + *

+ * In Java ME, the platform settings are used for SSL connections. + *

+ * + *

+ * An instance of the default persistence mechanism + * {@link MqttDefaultFilePersistence} is used by the client. To specify a + * different persistence mechanism or to turn off persistence, use the + * {@link #MqttClient(String, String, MqttClientPersistence)} + * constructor. + * + * @param serverURI + * the address of the server to connect to, specified as a URI. Can + * be overridden using + * {@link MqttConnectionOptions#setServerURIs(String[])} + * @param clientId + * a client identifier that is unique on the server being connected + * to + * @throws IllegalArgumentException + * if the URI does not start with "tcp://", "ssl://" or "local://". + * @throws IllegalArgumentException + * if the clientId is null or is greater than 65535 characters in + * length + * @throws MqttException + * if any other problem was encountered + */ + public MqttClient(String serverURI, String clientId) throws MqttException { + this(serverURI, clientId, new MqttDefaultFilePersistence()); + } + + /** + * Create an MqttClient that can be used to communicate with an MQTT server. + *

+ * The address of a server can be specified on the constructor. Alternatively a + * list containing one or more servers can be specified using the + * {@link MqttConnectionOptions#setServerURIs(String[]) setServerURIs} method on + * MqttConnectOptions. + * + *

+ * The serverURI parameter is typically used with the the + * clientId parameter to form a key. The key is used to store and + * reference messages while they are being delivered. Hence the serverURI + * specified on the constructor must still be specified even if a list of + * servers is specified on an MqttConnectOptions object. The serverURI on the + * constructor must remain the same across restarts of the client for delivery + * of messages to be maintained from a given client to a given server or set of + * servers. + * + *

+ * The address of the server to connect to is specified as a URI. Two types of + * connection are supported tcp:// for a TCP connection and + * ssl:// for a TCP connection secured by SSL/TLS. For example: + *

+ *
    + *
  • tcp://localhost:1883
  • + *
  • ssl://localhost:8883
  • + *
+ *

+ * If the port is not specified, it will default to 1883 for + * tcp://" URIs, and 8883 for ssl:// URIs. + *

+ * + *

+ * A client identifier clientId must be specified and be less that + * 65535 characters. It must be unique across all clients connecting to the same + * server. The clientId is used by the server to store data related to the + * client, hence it is important that the clientId remain the same when + * connecting to a server if durable subscriptions or reliable messaging are + * required. + *

+ * As the client identifier + * is used by the server to identify a client when it reconnects, the client + * must use the same identifier between connections if durable subscriptions or + * reliable delivery of messages is required. + *

+ *

+ * In Java SE, SSL can be configured in one of several ways, which the client + * will use in the following order: + *

+ *
    + *
  • Supplying an SSLSocketFactory - + * applications can use + * {@link MqttConnectionOptions#setSocketFactory(SocketFactory)} to supply a + * factory with the appropriate SSL settings.
  • + *
  • SSL Properties - applications can supply SSL settings as + * a simple Java Properties using + * {@link MqttConnectionOptions#setSSLProperties(Properties)}.
  • + *
  • Use JVM settings - There are a number of standard Java + * system properties that can be used to configure key and trust stores.
  • + *
+ * + *

+ * In Java ME, the platform settings are used for SSL connections. + *

+ *

+ * A persistence mechanism is used to enable reliable messaging. For messages + * sent at qualities of service (QoS) 1 or 2 to be reliably delivered, messages + * must be stored (on both the client and server) until the delivery of the + * message is complete. If messages are not safely stored when being delivered + * then a failure in the client or server can result in lost messages. A + * pluggable persistence mechanism is supported via the + * {@link MqttClientPersistence} interface. An implementer of this interface + * that safely stores messages must be specified in order for delivery of + * messages to be reliable. In addition + * {@link MqttConnectionOptions#setCleanStart(boolean)} must be set to false. + * In the event that only QoS 0 messages are sent or received or cleanStart is + * set to true then a safe store is not needed. + *

+ *

+ * An implementation of file-based persistence is provided in class + * {@link MqttDefaultFilePersistence} which will work in all Java SE based + * systems. If no persistence is needed, the persistence parameter can be + * explicitly set to null. + *

+ * + * @param serverURI + * the address of the server to connect to, specified as a URI. Can + * be overridden using + * {@link MqttConnectionOptions#setServerURIs(String[])} + * @param clientId + * a client identifier that is unique on the server being connected + * to + * @param persistence + * the persistence class to use to store in-flight message. If null + * then the default persistence mechanism is used + * @throws IllegalArgumentException + * if the URI does not start with "tcp://", "ssl://" or "local://" + * @throws IllegalArgumentException + * if the clientId is null or is greater than 65535 characters in + * length + * @throws MqttException + * if any other problem was encountered + */ + public MqttClient(String serverURI, String clientId, MqttClientPersistence persistence) + throws MqttException { + aClient = new MqttAsyncClient(serverURI, clientId, persistence); + } + + /** + * Create an MqttClient that can be used to communicate with an MQTT server. + *

+ * The address of a server can be specified on the constructor. Alternatively a + * list containing one or more servers can be specified using the + * {@link MqttConnectionOptions#setServerURIs(String[]) setServerURIs} method on + * MqttConnectOptions. + * + *

+ * The serverURI parameter is typically used with the the + * clientId parameter to form a key. The key is used to store and + * reference messages while they are being delivered. Hence the serverURI + * specified on the constructor must still be specified even if a list of + * servers is specified on an MqttConnectOptions object. The serverURI on the + * constructor must remain the same across restarts of the client for delivery + * of messages to be maintained from a given client to a given server or set of + * servers. + * + *

+ * The address of the server to connect to is specified as a URI. Two types of + * connection are supported tcp:// for a TCP connection and + * ssl:// for a TCP connection secured by SSL/TLS. For example: + *

+ *
    + *
  • tcp://localhost:1883
  • + *
  • ssl://localhost:8883
  • + *
+ *

+ * If the port is not specified, it will default to 1883 for + * tcp://" URIs, and 8883 for ssl:// URIs. + *

+ * + *

+ * A client identifier clientId must be specified and be less that + * 65535 characters. It must be unique across all clients connecting to the same + * server. The clientId is used by the server to store data related to the + * client, hence it is important that the clientId remain the same when + * connecting to a server if durable subscriptions or reliable messaging are + * required. + *

+ * As the client identifier + * is used by the server to identify a client when it reconnects, the client + * must use the same identifier between connections if durable subscriptions or + * reliable delivery of messages is required. + *

+ *

+ * In Java SE, SSL can be configured in one of several ways, which the client + * will use in the following order: + *

+ *
    + *
  • Supplying an SSLSocketFactory - + * applications can use + * {@link MqttConnectionOptions#setSocketFactory(SocketFactory)} to supply a + * factory with the appropriate SSL settings.
  • + *
  • SSL Properties - applications can supply SSL settings as + * a simple Java Properties using + * {@link MqttConnectionOptions#setSSLProperties(Properties)}.
  • + *
  • Use JVM settings - There are a number of standard Java + * system properties that can be used to configure key and trust stores.
  • + *
+ * + *

+ * In Java ME, the platform settings are used for SSL connections. + *

+ *

+ * A persistence mechanism is used to enable reliable messaging. For messages + * sent at qualities of service (QoS) 1 or 2 to be reliably delivered, messages + * must be stored (on both the client and server) until the delivery of the + * message is complete. If messages are not safely stored when being delivered + * then a failure in the client or server can result in lost messages. A + * pluggable persistence mechanism is supported via the + * {@link MqttClientPersistence} interface. An implementer of this interface + * that safely stores messages must be specified in order for delivery of + * messages to be reliable. In addition + * {@link MqttConnectionOptions#setCleanStart(boolean)} must be set to false. + * In the event that only QoS 0 messages are sent or received or cleanStart is + * set to true then a safe store is not needed. + *

+ *

+ * An implementation of file-based persistence is provided in class + * {@link MqttDefaultFilePersistence} which will work in all Java SE based + * systems. If no persistence is needed, the persistence parameter can be + * explicitly set to null. + *

+ * + * @param serverURI + * the address of the server to connect to, specified as a URI. Can + * be overridden using + * {@link MqttConnectionOptions#setServerURIs(String[])} + * @param clientId + * a client identifier that is unique on the server being connected + * to + * @param persistence + * the persistence class to use to store in-flight message. If null + * then the default persistence mechanism is used + * @param executorService + * used for managing threads. If null then a newScheduledThreadPool + * is used. + * @throws IllegalArgumentException + * if the URI does not start with "tcp://", "ssl://" or "local://" + * @throws IllegalArgumentException + * if the clientId is null or is greater than 65535 characters in + * length + * @throws MqttException + * if any other problem was encountered + */ + public MqttClient(String serverURI, String clientId, MqttClientPersistence persistence, + ScheduledExecutorService executorService) throws MqttException { + aClient = new MqttAsyncClient(serverURI, clientId, persistence, null, executorService); + } + + /* + * @see IMqttClient#connect() + */ + public void connect() throws MqttSecurityException, MqttException { + this.connect(new MqttConnectionOptions()); + } + + /* + * @see IMqttClient#connect(MqttConnectOptions) + */ + public void connect(MqttConnectionOptions options) throws MqttSecurityException, MqttException { + aClient.connect(options, null, null).waitForCompletion(getTimeToWait()); + } + + /* + * @see IMqttClient#connect(MqttConnectOptions) + */ + public IMqttToken connectWithResult(MqttConnectionOptions options) throws MqttSecurityException, MqttException { + IMqttToken tok = aClient.connect(options, null, null); + tok.waitForCompletion(getTimeToWait()); + return tok; + } + + /* + * @see IMqttClient#disconnect() + */ + public void disconnect() throws MqttException { + aClient.disconnect().waitForCompletion(); + } + + /* + * @see IMqttClient#disconnect(long) + */ + public void disconnect(long quiesceTimeout) throws MqttException { + aClient.disconnect(quiesceTimeout, null, null, MqttReturnCode.RETURN_CODE_SUCCESS, new MqttProperties()) + .waitForCompletion(); + } + + /* + * (non-Javadoc) + * + * @see org.eclipse.paho.mqttv5.client.IMqttAsyncClient#disconnectForcibly() + */ + public void disconnectForcibly() throws MqttException { + aClient.disconnectForcibly(); + } + + /* + * (non-Javadoc) + * + * @see org.eclipse.paho.mqttv5.client.IMqttAsyncClient#disconnectForcibly(long) + */ + public void disconnectForcibly(long disconnectTimeout) throws MqttException { + aClient.disconnectForcibly(disconnectTimeout); + } + + /* + * (non-Javadoc) + * + * @see org.eclipse.paho.mqttv5.client.IMqttAsyncClient#disconnectForcibly(long, + * long) + */ + public void disconnectForcibly(long quiesceTimeout, long disconnectTimeout) throws MqttException { + aClient.disconnectForcibly(quiesceTimeout, disconnectTimeout, MqttReturnCode.RETURN_CODE_SUCCESS, + new MqttProperties()); + } + + /** + * Disconnects from the server forcibly to reset all the states. Could be useful + * when disconnect attempt failed. + *

+ * Because the client is able to establish the TCP/IP connection to a none MQTT + * server and it will certainly fail to send the disconnect packet. + * + * @param quiesceTimeout + * the amount of time in milliseconds to allow for existing work to + * finish before disconnecting. A value of zero or less means the + * client will not quiesce. + * @param disconnectTimeout + * the amount of time in milliseconds to allow send disconnect packet + * to server. + * @param sendDisconnectPacket + * if true, will send the disconnect packet to the server + * @throws MqttException + * if any unexpected error + */ + public void disconnectForcibly(long quiesceTimeout, long disconnectTimeout, boolean sendDisconnectPacket) + throws MqttException { + aClient.disconnectForcibly(quiesceTimeout, disconnectTimeout, sendDisconnectPacket); + } + + /* + * @see IMqttClient#subscribe(String, int) + */ + public IMqttToken subscribe(String topicFilter, int qos) throws MqttException { + return this.subscribe(new String[] { topicFilter }, new int[] { qos }); + } + + @Override + public IMqttToken subscribe(String[] topicFilters, int[] qos) throws MqttException { + if (topicFilters.length != qos.length) { + throw new MqttException(MqttClientException.REASON_CODE_UNEXPECTED_ERROR); + } + + MqttSubscription[] subscriptions = new MqttSubscription[topicFilters.length]; + for (int i = 0; i < topicFilters.length; ++i) { + subscriptions[i] = new MqttSubscription(topicFilters[i], qos[i]); + } + + return this.subscribe(subscriptions); + } + + /* + * @see IMqttClient#subscribe(String[], int[]) + */ + public IMqttToken subscribe(MqttSubscription[] subscriptions) throws MqttException { + return this.subscribe(subscriptions, null); + } + + /* + * (non-Javadoc) + * + * @see org.eclipse.paho.mqttv5.client.IMqttClient#subscribe(java.lang.String, + * int) + */ + public IMqttToken subscribe(String topicFilter, int qos, IMqttMessageListener messageListener) + throws MqttException { + MqttSubscription subscription = new MqttSubscription(topicFilter); + subscription.setQos(qos); + IMqttToken token = aClient.subscribe(subscription, messageListener); + token.waitForCompletion(); + return token; + } + + public IMqttToken subscribe(String[] topicFilters, int[] qos, IMqttMessageListener[] messageListeners) + throws MqttException { + if (topicFilters.length != qos.length) { + throw new MqttException(MqttClientException.REASON_CODE_UNEXPECTED_ERROR); + } + + MqttSubscription[] subscriptions = new MqttSubscription[topicFilters.length]; + for (int i = 0; i < topicFilters.length; ++i) { + subscriptions[i] = new MqttSubscription(topicFilters[i], qos[i]); + } + + return this.subscribe(subscriptions, messageListeners); + } + + public IMqttToken subscribe(MqttSubscription[] subscriptions, IMqttMessageListener[] messageListeners) throws MqttException { + IMqttToken tok = aClient.subscribe(subscriptions, null, null, messageListeners, new MqttProperties()); + tok.waitForCompletion(getTimeToWait()); + return tok; + } + + /* + * @see IMqttClient#unsubscribe(String) + */ + public void unsubscribe(String topicFilter) throws MqttException { + unsubscribe(new String[] { topicFilter }); + } + + /* + * @see IMqttClient#unsubscribe(String[]) + */ + public void unsubscribe(String[] topicFilters) throws MqttException { + // message handlers removed in the async client unsubscribe below + aClient.unsubscribe(topicFilters, null, null, new MqttProperties()).waitForCompletion(getTimeToWait()); + } + + /* + * @see IMqttClient#publishBlock(String, byte[], int, boolean) + */ + public void publish(String topic, byte[] payload, int qos, boolean retained) + throws MqttException, MqttPersistenceException { + MqttMessage message = new MqttMessage(payload); + message.setQos(qos); + message.setRetained(retained); + this.publish(topic, message); + } + + /* + * @see IMqttClient#publishBlock(String, MqttMessage) + */ + public void publish(String topic, MqttMessage message) throws MqttException, MqttPersistenceException { + aClient.publish(topic, message, null, null).waitForCompletion(getTimeToWait()); + } + + /** + * Set the maximum time to wait for an action to complete. + *

+ * Set the maximum time to wait for an action to complete before returning + * control to the invoking application. Control is returned when: + *

+ *
    + *
  • the action completes
  • + *
  • or when the timeout if exceeded
  • + *
  • or when the client is disconnect/shutdown
  • + *
+ *

+ * The default value is -1 which means the action will not timeout. In the event + * of a timeout the action carries on running in the background until it + * completes. The timeout is used on methods that block while the action is in + * progress. + *

+ * + * @param timeToWaitInMillis + * before the action times out. A value or 0 or -1 will wait until + * the action finishes and not timeout. + * @throws IllegalArgumentException + * if timeToWaitInMillis is invalid + */ + public void setTimeToWait(long timeToWaitInMillis) throws IllegalArgumentException { + if (timeToWaitInMillis < -1) { + throw new IllegalArgumentException(); + } + this.timeToWait = timeToWaitInMillis; + } + + /** + * Return the maximum time to wait for an action to complete. + * + * @return the time to wait + * @see MqttClient#setTimeToWait(long) + */ + public long getTimeToWait() { + return this.timeToWait; + } + + /* + * (non-Javadoc) + * + * @see org.eclipse.paho.mqttv5.client.IMqttClient#close() + */ + public void close() throws MqttException { + aClient.close(false); + } + + /* + * (non-Javadoc) + * + * @see org.eclipse.paho.mqttv5.client.IMqttClient#close() + */ + public void close(boolean force) throws MqttException { + aClient.close(force); + } + + /* + * (non-Javadoc) + * + * @see org.eclipse.paho.mqttv5.client.IMqttClient#getClientId() + */ + public String getClientId() { + return aClient.getClientId(); + } + + /* + * (non-Javadoc) + * + * @see org.eclipse.paho.mqttv5.client.IMqttClient#getPendingTokens() + */ + public IMqttToken[] getPendingTokens() { + return aClient.getPendingTokens(); + } + + /* + * (non-Javadoc) + * + * @see org.eclipse.paho.mqttv5.client.IMqttClient#getServerURI() + */ + public String getServerURI() { + return aClient.getServerURI(); + } + + /** + * Returns the currently connected Server URI Implemented due to: + * https://bugs.eclipse.org/bugs/show_bug.cgi?id=481097 + * + * Where getServerURI only returns the URI that was provided in + * MqttAsyncClient's constructor, getCurrentServerURI returns the URI of the + * Server that the client is currently connected to. This would be different in + * scenarios where multiple server URIs have been provided to the + * MqttConnectOptions. + * + * @return the currently connected server URI + */ + public String getCurrentServerURI() { + return aClient.getCurrentServerURI(); + } + + /* + * (non-Javadoc) + * + * @see org.eclipse.paho.mqttv5.client.IMqttClient#getTopic(java.lang.String) + */ + public MqttTopic getTopic(String topic) { + return aClient.getTopic(topic); + } + + /* + * (non-Javadoc) + * + * @see org.eclipse.paho.mqttv5.client.IMqttClient#isConnected() + */ + public boolean isConnected() { + return aClient.isConnected(); + } + + /* + * (non-Javadoc) + * + * @see org.eclipse.paho.mqttv5.client.IMqttClient#setCallback(org.eclipse.paho. + * mqttv5.client.MqttCallback) + */ + public void setCallback(MqttCallback callback) { + aClient.setCallback(callback); + } + + /* + * (non-Javadoc) + * + * @see org.eclipse.paho.mqttv5.client.IMqttClient#setCallback(org.eclipse.paho. + * mqttv5.client.MqttCallback) + */ + public void setManualAcks(boolean manualAcks) { + aClient.setManualAcks(manualAcks); + } + + public void messageArrivedComplete(int messageId, int qos) throws MqttException { + aClient.messageArrivedComplete(messageId, qos); + } + + /** + * Will attempt to reconnect to the server after the client has lost connection. + * + * @throws MqttException + * if an error occurs attempting to reconnect + */ + public void reconnect() throws MqttException { + aClient.reconnect(); + } + + /** + * Return a debug object that can be used to help solve problems. + * + * @return the {@link Debug} Object. + */ + public Debug getDebug() { + return (aClient.getDebug()); + } + +} diff --git a/src/main/java/org/eclipse/paho/mqttv5/client/MqttClientException.java b/src/main/java/org/eclipse/paho/mqttv5/client/MqttClientException.java new file mode 100644 index 0000000..746d0b7 --- /dev/null +++ b/src/main/java/org/eclipse/paho/mqttv5/client/MqttClientException.java @@ -0,0 +1,164 @@ +package org.eclipse.paho.mqttv5.client; + +import org.eclipse.paho.mqttv5.common.MqttException; +import org.eclipse.paho.mqttv5.common.MqttMessage; + +public class MqttClientException { + + /** + * The Server sent a publish message with an invalid topic alias. + */ + public static final short REASON_CODE_INVALID_TOPIC_ALAS = 32301; + + /** + * The Server sent a publish message with an unknown topic alias and no topic + * string. + */ + public static final short REASON_CODE_UNKNOWN_TOPIC_ALIAS = 32302; + + /** + * Client timed out while waiting for a response from the server. The server is + * no longer responding to keep-alive messages. + */ + public static final short REASON_CODE_CLIENT_TIMEOUT = 32000; + + /** + * Internal error, caused by no new message IDs being available. + */ + public static final short REASON_CODE_NO_MESSAGE_IDS_AVAILABLE = 32001; + + /** + * Client timed out while waiting to write messages to the server. + */ + public static final short REASON_CODE_WRITE_TIMEOUT = 32002; + + /** + * The client is already connected. + */ + public static final short REASON_CODE_CLIENT_CONNECTED = 32100; + + /** + * The client is already disconnected. + */ + public static final short REASON_CODE_CLIENT_ALREADY_DISCONNECTED = 32101; + /** + * The client is currently disconnecting and cannot accept any new work. This + * can occur when waiting on a token, and then disconnecting the client. If the + * message delivery does not complete within the quiesce timeout period, then + * the waiting token will be notified with an exception. + */ + public static final short REASON_CODE_CLIENT_DISCONNECTING = 32102; + + /** Unable to connect to server */ + public static final short REASON_CODE_SERVER_CONNECT_ERROR = 32103; + + /** + * The client is not connected to the server. The {@link IMqttClient#connect()} + * or {@link IMqttClient#connect(MqttConnectionOptions)} method must be called + * first. It is also possible that the connection was lost - see + * {@link IMqttClient#setCallback(MqttCallback)} for a way to track lost + * connections. + */ + public static final short REASON_CODE_CLIENT_NOT_CONNECTED = 32104; + + /** + * Server URI and supplied SocketFactory do not match. URIs + * beginning tcp:// must use a + * javax.net.SocketFactory, and URIs beginning ssl:// + * must use a javax.net.ssl.SSLSocketFactory. + */ + public static final short REASON_CODE_SOCKET_FACTORY_MISMATCH = 32105; + + /** + * SSL configuration error. + */ + public static final short REASON_CODE_SSL_CONFIG_ERROR = 32106; + + /** + * Thrown when an attempt to call {@link IMqttClient#disconnect()} has been made + * from within a method on {@link MqttCallback}. These methods are invoked by + * the client's thread, and must not be used to control disconnection. + * + * @see MqttCallback#messageArrived(String, MqttMessage) + */ + public static final short REASON_CODE_CLIENT_DISCONNECT_PROHIBITED = 32107; + + /** + * Protocol error: the message was not recognized as a valid MQTT packet. + * Possible reasons for this include connecting to a non-MQTT server, or + * connecting to an SSL server port when the client isn't using SSL. + */ + public static final short REASON_CODE_INVALID_MESSAGE = 32108; + + /** + * The client has been unexpectedly disconnected from the server. The + * {@link MqttException#getCause() cause} will provide more details. + */ + public static final short REASON_CODE_CONNECTION_LOST = 32109; + + /** + * A connect operation in already in progress, only one connect can happen at a + * time. + */ + public static final short REASON_CODE_CONNECT_IN_PROGRESS = 32110; + + /** + * The client is closed - no operations are permitted on the client in this + * state. New up a new client to continue. + */ + public static final short REASON_CODE_CLIENT_CLOSED = 32111; + + /** + * A request has been made to use a token that is already associated with + * another action. If the action is complete the reset() can ve called on the + * token to allow it to be reused. + */ + public static final short REASON_CODE_TOKEN_INUSE = 32201; + + /** + * A request has been made to send a message but the maximum number of inflight + * messages has already been reached. Once one or more messages have been moved + * then new messages can be sent. + */ + public static final short REASON_CODE_MAX_INFLIGHT = 32202; + + /** + * The Client has attempted to publish a message whilst in the 'resting' / + * offline state with Disconnected Publishing enabled, however the buffer is + * full and deleteOldestMessages is disabled, therefore no more messages can be + * published until the client reconnects, or the application deletes buffered + * message manually. + */ + public static final short REASON_CODE_DISCONNECTED_BUFFER_FULL = 32203; + + public static final short REASON_CODE_SERVER_DISCONNECTED = 32204; + + /** + * The server has been sent an MQTT packet that was larger than the client + * defined value. + */ + public static final int REASON_CODE_INCOMING_PACKET_TOO_LARGE = 51001; + public static final int REASON_CODE_OUTGOING_PACKET_TOO_LARGE = 51002; + + // CONNACK return codes + /** The protocol version requested is not supported by the server. */ + public static final short REASON_CODE_INVALID_PROTOCOL_VERSION = 0x01; + /** The server has rejected the supplied client ID */ + public static final short REASON_CODE_INVALID_CLIENT_ID = 0x02; + /** The broker was not available to handle the request. */ + public static final short REASON_CODE_BROKER_UNAVAILABLE = 0x03; + /** + * Authentication with the server has failed, due to a bad user name or + * password. + */ + public static final short REASON_CODE_FAILED_AUTHENTICATION = 0x04; + /** Not authorized to perform the requested operation */ + public static final short REASON_CODE_NOT_AUTHORIZED = 0x05; + + /** An unexpected error has occurred. */ + public static final short REASON_CODE_UNEXPECTED_ERROR = 0x06; + + /** Error from subscribe - returned from the server. */ + public static final short REASON_CODE_SUBSCRIBE_FAILED = 0x80; + +} diff --git a/src/main/java/org/eclipse/paho/mqttv5/client/MqttClientInterface.java b/src/main/java/org/eclipse/paho/mqttv5/client/MqttClientInterface.java new file mode 100644 index 0000000..341e676 --- /dev/null +++ b/src/main/java/org/eclipse/paho/mqttv5/client/MqttClientInterface.java @@ -0,0 +1,9 @@ +package org.eclipse.paho.mqttv5.client; + +public interface MqttClientInterface { + + String getClientId(); + + String getServerURI(); + +} diff --git a/src/main/java/org/eclipse/paho/mqttv5/client/MqttClientPersistence.java b/src/main/java/org/eclipse/paho/mqttv5/client/MqttClientPersistence.java new file mode 100644 index 0000000..27c7bf1 --- /dev/null +++ b/src/main/java/org/eclipse/paho/mqttv5/client/MqttClientPersistence.java @@ -0,0 +1,104 @@ +/******************************************************************************* + * Copyright (c) 2009, 2019 IBM Corp. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * and Eclipse Distribution License v1.0 which accompany this distribution. + * + * The Eclipse Public License is available at + * https://www.eclipse.org/legal/epl-2.0 + * and the Eclipse Distribution License is available at + * https://www.eclipse.org/org/documents/edl-v10.php + * + * Contributors: + * Dave Locke - initial API and implementation and/or initial documentation + */ +package org.eclipse.paho.mqttv5.client; + +import java.util.Enumeration; + +import org.eclipse.paho.mqttv5.common.MqttPersistable; +import org.eclipse.paho.mqttv5.common.MqttPersistenceException; + +/** + * Represents a persistent data store, used to store outbound and inbound messages while they + * are in flight, enabling delivery to the QoS specified. You can specify an implementation + * of this interface using {@link MqttClient#MqttClient(String, String, MqttClientPersistence)}, + * which the {@link MqttClient} will use to persist QoS 1 and 2 messages. + *

+ * If the methods defined throw the MqttPersistenceException then the state of the data persisted + * should remain as prior to the method being called. For example, if {@link #put(String, MqttPersistable)} + * throws an exception at any point then the data will be assumed to not be in the persistent store. + * Similarly if {@link #remove(String)} throws an exception then the data will be + * assumed to still be held in the persistent store.

+ *

+ * It is up to the persistence interface to log any exceptions or error information + * which may be required when diagnosing a persistence failure.

+ */ +public interface MqttClientPersistence { + /** + * Initialise the persistent store. + * If a persistent store exists for this client ID then open it, otherwise + * create a new one. If the persistent store is already open then just return. + * An application may use the same client ID to connect to many different + * servers, so the client ID in conjunction with the + * connection will uniquely identify the persistence store required. + * + * @param clientId The client for which the persistent store should be opened. + * @throws MqttPersistenceException if there was a problem opening the persistent store. + */ + void open(String clientId) throws MqttPersistenceException; + + /** + * Close the persistent store that was previously opened. + * This will be called when a client application disconnects from the broker. + * @throws MqttPersistenceException if an error occurs closing the persistence store. + */ + void close() throws MqttPersistenceException; + + /** + * Puts the specified data into the persistent store. + * @param key the key for the data, which will be used later to retrieve it. + * @param persistable the data to persist + * @throws MqttPersistenceException if there was a problem putting the data + * into the persistent store. + */ + void put(String key, MqttPersistable persistable) throws MqttPersistenceException; + + /** + * Gets the specified data out of the persistent store. + * @param key the key for the data, which was used when originally saving it. + * @return the un-persisted data + * @throws MqttPersistenceException if there was a problem getting the data + * from the persistent store. + */ + MqttPersistable get(String key) throws MqttPersistenceException; + + /** + * Remove the data for the specified key. + * @param key The key for the data to remove + * @throws MqttPersistenceException if there was a problem removing the data. + */ + void remove(String key) throws MqttPersistenceException; + + /** + * Returns an Enumeration over the keys in this persistent data store. + * @return an enumeration of {@link String} objects. + * @throws MqttPersistenceException if there was a problem getting they keys + */ + Enumeration keys() throws MqttPersistenceException; + + /** + * Clears persistence, so that it no longer contains any persisted data. + * @throws MqttPersistenceException if there was a problem clearing all data from the persistence store + */ + void clear() throws MqttPersistenceException; + + /** + * Returns whether or not data is persisted using the specified key. + * @param key the key for data, which was used when originally saving it. + * @return True if the persistence store contains the key + * @throws MqttPersistenceException if there was a problem checking whether they key existed. + */ + boolean containsKey(String key) throws MqttPersistenceException; +} diff --git a/src/main/java/org/eclipse/paho/mqttv5/client/MqttConnectionOptions.java b/src/main/java/org/eclipse/paho/mqttv5/client/MqttConnectionOptions.java new file mode 100644 index 0000000..ff6283d --- /dev/null +++ b/src/main/java/org/eclipse/paho/mqttv5/client/MqttConnectionOptions.java @@ -0,0 +1,971 @@ +/******************************************************************************* + * Copyright (c) 2009, 2017 IBM Corp. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * and Eclipse Distribution License v1.0 which accompany this distribution. + * + * The Eclipse Public License is available at + * https://www.eclipse.org/legal/epl-2.0 + * and the Eclipse Distribution License is available at + * https://www.eclipse.org/org/documents/edl-v10.php + * + * Contributors: + * Dave Locke - initial API and implementation and/or initial documentation + * Ian Craggs - MQTT 3.1.1 support + * James Sutton - Automatic Reconnect & Offline Buffering + * James Sutton - MQTT v5 + */ +package org.eclipse.paho.mqttv5.client; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Properties; + +import javax.net.SocketFactory; +import javax.net.ssl.HostnameVerifier; + +import org.eclipse.paho.mqttv5.client.internal.NetworkModuleService; +import org.eclipse.paho.mqttv5.client.util.Debug; +import org.eclipse.paho.mqttv5.common.MqttMessage; +import org.eclipse.paho.mqttv5.common.packet.MqttProperties; +import org.eclipse.paho.mqttv5.common.packet.UserProperty; +import org.eclipse.paho.mqttv5.common.util.MqttTopicValidator; + +/** + * Holds the set of options that control how the client connects to a server. + * + * Constructs a new {@link MqttConnectionOptions} object using the default + * values. + * + * The defaults are: + *
    + *
  • The keep alive interval is 60 seconds
  • + *
  • Clean Session is true
  • + *
  • The message delivery retry interval is 15 seconds
  • + *
  • The connection timeout period is 30 seconds
  • + *
  • No Will message is set
  • + *
  • Automatic Reconnect is enabled, starting at 1 second and capped at two + * minutes
  • + *
  • A standard SocketFactory is used
  • + *
+ * More information about these values can be found in the setter methods. + */ +public class MqttConnectionOptions { + + private static final String CLIENT_ID_PREFIX = "paho"; + + // Connection Behaviour Properties + private String[] serverURIs = null; // List of Servers to connect to in order + private boolean automaticReconnect = false; // Automatic Reconnect + private int automaticReconnectMinDelay = 1; // Time to wait before first automatic reconnection attempt in seconds. + private int automaticReconnectMaxDelay = 120; // Max time to wait for automatic reconnection attempts in seconds. + private boolean useSubscriptionIdentifiers = true; // Whether to automatically assign subscription identifiers. + private int keepAliveInterval = 60; // Keep Alive Interval + private int connectionTimeout = 30; // Connection timeout in seconds + private boolean httpsHostnameVerificationEnabled = true; + private int maxReconnectDelay = 128000; + private boolean sendReasonMessages = false; + + public MqttProperties getConnectionProperties() { + MqttProperties connectionProperties = new MqttProperties(); + connectionProperties.setSessionExpiryInterval(sessionExpiryInterval); + connectionProperties.setReceiveMaximum(receiveMaximum); + connectionProperties.setMaximumPacketSize(maximumPacketSize); + connectionProperties.setTopicAliasMaximum(topicAliasMaximum); + connectionProperties.setRequestResponseInfo(requestResponseInfo); + connectionProperties.setRequestProblemInfo(requestProblemInfo); + connectionProperties.setUserProperties(userProperties); + connectionProperties.setAuthenticationMethod(authMethod); + connectionProperties.setAuthenticationData(authData); + return connectionProperties; + } + + public MqttProperties getWillMessageProperties() { + return willMessageProperties; + } + + public void setWillMessageProperties(MqttProperties willMessageProperties) { + this.willMessageProperties = willMessageProperties; + } + + MqttProperties willMessageProperties = new MqttProperties(); + + // Connection packet properties + private int mqttVersion = 5; // MQTT Version 5 + private boolean cleanStart = true; // Clean Session + private String willDestination = null; // Will Topic + private MqttMessage willMessage = null; // Will Message + private String userName; // Username + private byte[] password; // Password + private Long sessionExpiryInterval = null; // The Session expiry Interval in seconds, null is the default of + // never. + private Integer receiveMaximum = null; // The Receive Maximum, null defaults to 65,535, cannot be 0. + private Long maximumPacketSize = null; // The Maximum packet size, null defaults to no limit. + private Integer topicAliasMaximum = null; // The Topic Alias Maximum, null defaults to 0. + private Boolean requestResponseInfo = null; // Request Response Information, null defaults to false. + private Boolean requestProblemInfo = null; // Request Problem Information, null defaults to true. + private List userProperties = null; // User Defined Properties. + private String authMethod = null; // Authentication Method, If null, Extended Authentication is not performed. + private byte[] authData = null; // Authentication Data. + + // TLS Properties + private SocketFactory socketFactory; // SocketFactory to be used to connect + private Properties sslClientProps = null; // SSL Client Properties + private HostnameVerifier sslHostnameVerifier = null; // SSL Hostname Verifier + private Map customWebSocketHeaders; + + // Client Operation Parameters + private int executorServiceTimeout = 1; // How long to wait in seconds when terminating the executor service. + + /** + * Returns the MQTT version. + * + * @return the MQTT version. + */ + public int getMqttVersion() { + return mqttVersion; + } + + /** + * Returns the user name to use for the connection. + * + * @return the user name to use for the connection. + */ + public String getUserName() { + return userName; + } + + /** + * Sets the user name to use for the connection. + * + * @param userName + * The Username as a String + */ + public void setUserName(String userName) { + this.userName = userName; + } + + /** + * Returns the password to use for the connection. + * + * @return the password to use for the connection. + */ + public byte[] getPassword() { + return password; + } + + /** + * Sets the password to use for the connection. + * + * @param password + * A Char Array of the password + */ + public void setPassword(byte[] password) { + this.password = password; + } + + /** + * Returns the topic to be used for last will and testament (LWT). + * + * @return the MqttTopic to use, or null if LWT is not set. + * @see #setWill(String, MqttMessage) + */ + public String getWillDestination() { + return willDestination; + } + + /** + * Returns the message to be sent as last will and testament (LWT). The returned + * object is "read only". Calling any "setter" methods on the returned object + * will result in an IllegalStateException being thrown. + * + * @return the message to use, or null if LWT is not set. + */ + public MqttMessage getWillMessage() { + return willMessage; + } + + /** + * Sets the "Last Will and Testament" (LWT) for the connection. In the event + * that this client unexpectedly looses it's connection to the server, the + * server will publish a message to itself using the supplied details. + * + * @param topic + * the topic to publish to. + * @param message + * the {@link MqttMessage} to send + */ + public void setWill(String topic, MqttMessage message) { + if (topic == null || message == null || message.getPayload() == null) { + throw new IllegalArgumentException(); + } + MqttTopicValidator.validate(topic, false, true); // Wildcards are not allowed + this.willDestination = topic; + this.willMessage = message; + // Prevent any more changes to the will message + this.willMessage.setMutable(false); + } + + /** + * Returns whether the client and server should remember state for the client + * across reconnects. + * + * @return the clean session flag + */ + public boolean isCleanStart() { + return this.cleanStart; + } + + /** + * Sets whether the client and server should remember state across restarts and + * reconnects. If set to false, the server will retain the session state until + * either: + *
    + *
  • A new connection is made with the client and with the cleanStart flag set + * to true.
  • + *
  • The Session expiry interval is exceeded after the network connection is + * closed, see {@link MqttConnectionOptions#setSessionExpiryInterval}
  • + *
+ * + * If set to true, the server will immediately drop any existing session state + * for the given client and will initiate a new session. + * + * In order to implement QoS 1 and QoS 2 protocol flows the Client and Server + * need to associate state with the Client Identifier, this is referred to as + * the Session State. The Server also stores the subscriptions as part of the + * Session State. + * + * The session can continue across a sequence of Network Connections. It lasts + * as long as the latest Network Connection plus the Session Expiry Interval. + * + * The Session State in the Client consists of: + * + *
    + *
  • QoS 1 and QoS 2 messages which have been sent to the Server, but have not + * been completely acknowledged.
  • + *
  • QoS 2 messages which have been received from the Server, but have not + * been completely acknowledged.
  • + *
+ * + * The Session State in the Server consists of: + *
    + *
  • The existence of a Session, even if the rest of the Session State is + * empty.
  • + *
  • The Clients subscriptions, including any Subscription Identifiers.
  • + *
  • QoS 1 and QoS 2 messages which have been sent to the Client, but have not + * been completely acknowledged.
  • + *
  • QoS 1 and QoS 2 messages pending transmission to the Client and + * OPTIONALLY QoS 0 messages pending transmission to the Client.
  • + *
  • QoS 2 messages which have been received from the Client, but have not + * been completely acknowledged.The Will Message and the Will Delay + * Interval
  • + *
  • If the Session is currently not connected, the time at which the Session + * will end and Session State will be discarded.
  • + *
+ * + * Retained messages do not form part of the Session State in the Server, they + * are not deleted as a result of a Session ending. + * + * + * + * @param cleanStart + * Set to True to enable cleanSession + */ + public void setCleanStart(boolean cleanStart) { + this.cleanStart = cleanStart; + } + + /** + * Returns the "keep alive" interval. + * + * @see #setKeepAliveInterval(int) + * @return the keep alive interval. + */ + public int getKeepAliveInterval() { + return keepAliveInterval; + } + + /** + * Sets the "keep alive" interval. This value, measured in seconds, defines the + * maximum time interval between messages sent or received. It enables the + * client to detect if the server is no longer available, without having to wait + * for the TCP/IP timeout. The client will ensure that at least one message + * travels across the network within each keep alive period. In the absence of a + * data-related message during the time period, the client sends a very small + * "ping" message, which the server will acknowledge. A value of 0 disables + * keepalive processing in the client. + *

+ * The default value is 60 seconds + *

+ * + * @param keepAliveInterval + * the interval, measured in seconds, must be >= 0. + * @throws IllegalArgumentException + * if the keepAliveInterval was invalid + */ + public void setKeepAliveInterval(int keepAliveInterval) { + if (keepAliveInterval < 0) { + throw new IllegalArgumentException(); + } + this.keepAliveInterval = keepAliveInterval; + } + + /** + * Returns the connection timeout value. + * + * @see #setConnectionTimeout(int) + * @return the connection timeout value. + */ + public int getConnectionTimeout() { + return connectionTimeout; + } + + /** + * Sets the connection timeout value. This value, measured in seconds, defines + * the maximum time interval the client will wait for the network connection to + * the MQTT server to be established. The default timeout is 30 seconds. A value + * of 0 disables timeout processing meaning the client will wait until the + * network connection is made successfully or fails. + * + * @param connectionTimeout + * the timeout value, measured in seconds. It must be >0; + */ + public void setConnectionTimeout(int connectionTimeout) { + if (connectionTimeout < 0) { + throw new IllegalArgumentException(); + } + this.connectionTimeout = connectionTimeout; + } + + /** + * Get the maximum time (in millis) to wait between reconnects + * + * @return Get the maximum time (in millis) to wait between reconnects + */ + public int getMaxReconnectDelay() { + return maxReconnectDelay; + } + + /** + * Set the maximum time to wait between reconnects + * + * @param maxReconnectDelay + * the duration (in millis) + */ + public void setMaxReconnectDelay(int maxReconnectDelay) { + this.maxReconnectDelay = maxReconnectDelay; + } + + /** + * Return a list of serverURIs the client may connect to + * + * @return the serverURIs or null if not set + */ + public String[] getServerURIs() { + return serverURIs; + } + + /** + * Set a list of one or more serverURIs the client may connect to. + *

+ * Each serverURI specifies the address of a server that the client + * may connect to. Two types of connection are supported tcp:// for + * a TCP connection and ssl:// for a TCP connection secured by + * SSL/TLS. For example: + *

    + *
  • tcp://localhost:1883
  • + *
  • ssl://localhost:8883
  • + *
+ * If the port is not specified, it will default to 1883 for + * tcp://" URIs, and 8883 for ssl:// URIs. + *

+ * If serverURIs is set then it overrides the serverURI parameter passed in on + * the constructor of the MQTT client. + *

+ * When an attempt to connect is initiated the client will start with the first + * serverURI in the list and work through the list until a connection is + * established with a server. If a connection cannot be made to any of the + * servers then the connect attempt fails. + *

+ * Specifying a list of servers that a client may connect to has several uses: + *

    + *
  1. High Availability and reliable message delivery + *

    + * Some MQTT servers support a high availability feature where two or more + * "equal" MQTT servers share state. An MQTT client can connect to any of the + * "equal" servers and be assured that messages are reliably delivered and + * durable subscriptions are maintained no matter which server the client + * connects to. + *

    + *

    + * The cleanStart flag must be set to false if durable subscriptions and/or + * reliable message delivery is required. + *

    + *
  2. + *
  3. Hunt List + *

    + * A set of servers may be specified that are not "equal" (as in the high + * availability option). As no state is shared across the servers reliable + * message delivery and durable subscriptions are not valid. The cleanStart flag + * must be set to true if the hunt list mode is used + *

    + *
  4. + *
+ * + * @param serverURIs + * to be used by the client + */ + public void setServerURIs(String[] serverURIs) { + for (String serverURI : serverURIs) { + NetworkModuleService.validateURI(serverURI); + } + this.serverURIs = serverURIs.clone(); + } + + /** + * Returns whether the client will automatically attempt to reconnect to the + * server if the connection is lost + * + * @return the automatic reconnection flag. + */ + public boolean isAutomaticReconnect() { + return automaticReconnect; + } + + /** + * Sets whether the client will automatically attempt to reconnect to the server + * if the connection is lost. + *
    + *
  • If set to false, the client will not attempt to automatically reconnect + * to the server in the event that the connection is lost.
  • + *
  • If set to true, in the event that the connection is lost, the client will + * attempt to reconnect to the server. It will initially wait 1 second before it + * attempts to reconnect, for every failed reconnect attempt, the delay will + * double until it is at 2 minutes at which point the delay will stay at 2 + * minutes.
  • + *
+ * + * You can change the Minimum and Maximum delays by using + * {@link #setAutomaticReconnectDelay(int, int)} + * + * This Defaults to true + * + * @param automaticReconnect + * If set to True, Automatic Reconnect will be enabled + */ + public void setAutomaticReconnect(boolean automaticReconnect) { + this.automaticReconnect = automaticReconnect; + } + + /** + * Sets the Minimum and Maximum delays used when attempting to automatically + * reconnect. + * + * @param minDelay + * the minimum delay to wait before attempting to reconnect in + * seconds, defaults to 1 second. + * @param maxDelay + * the maximum delay to wait before attempting to reconnect in + * seconds, defaults to 120 seconds. + */ + public void setAutomaticReconnectDelay(int minDelay, int maxDelay) { + this.automaticReconnectMinDelay = minDelay; + this.automaticReconnectMaxDelay = maxDelay; + } + + /** + * Returns the minimum number of seconds to wait before attempting to + * automatically reconnect. + * + * @return the automatic reconnect minimum delay in seconds. + */ + public int getAutomaticReconnectMinDelay() { + return automaticReconnectMinDelay; + } + + /** + * Returns the maximum number of seconds to wait before attempting to + * automatically reconnect. + * + * @return the automatic reconnect maximum delay in seconds. + */ + public int getAutomaticReconnectMaxDelay() { + return automaticReconnectMaxDelay; + } + + /** + * Returns the Session Expiry Interval. If null, this means the + * session will not expire. + * + * @return the Session Expiry Interval in seconds. + */ + public Long getSessionExpiryInterval() { + return sessionExpiryInterval; + } + + /** + * Sets the Session Expiry Interval. This value, measured in seconds, defines + * the maximum time that the broker will maintain the session for once the + * client disconnects. Clients should only connect with a long Session Expiry + * interval if they intend to connect to the server at some later point in time. + * + *
    + *
  • By default this value is null and so will not be sent, in this case, the + * session will not expire.
  • + *
  • If a 0 is sent, the session will end immediately once the Network + * Connection is closed.
  • + *
+ * + * When the client has determined that it has no longer any use for the session, + * it should disconnect with a Session Expiry Interval set to 0. + * + * @param sessionExpiryInterval + * The Session Expiry Interval in seconds. + */ + public void setSessionExpiryInterval(Long sessionExpiryInterval) { + this.sessionExpiryInterval = sessionExpiryInterval; + } + + /** + * Returns the Receive Maximum value. If null, it will default to + * 65,535. + * + * @return the Receive Maximum + */ + public Integer getReceiveMaximum() { + return receiveMaximum; + } + + /** + * Sets the Receive Maximum. This value represents the limit of QoS 1 and QoS 2 + * publications that the client is willing to process concurrently. There is no + * mechanism to limit the number of QoS 0 publications that the Server might try + * to send. + *
    + *
  • If set to null then this value defaults to 65,535.
  • + *
  • If set, the minimum value for this property is 1.
  • + *
  • The maximum value for this property is 65,535.
  • + *
+ * + * @param receiveMaximum + * the Receive Maximum. + */ + public void setReceiveMaximum(Integer receiveMaximum) { + if (receiveMaximum != null && (receiveMaximum == 0 || receiveMaximum > 65535)) { + throw new IllegalArgumentException(); + } + this.receiveMaximum = receiveMaximum; + } + + /** + * Returns the Maximum Packet Size. If null, no limit is imposed. + * + * @return the Maximum Packet Size in bytes. + */ + public Long getMaximumPacketSize() { + return maximumPacketSize; + } + + /** + * Sets the Maximum Packet Size. This value represents the Maximum Packet Size + * the client is willing to accept. + * + *
    + *
  • If set to null then no limit is imposed beyond the + * limitations of the protocol.
  • + *
  • If set, the minimum value for this property is 1.
  • + *
  • The maximum value for this property is 2,684,354,656.
  • + *
+ * + * @param maximumPacketSize + * The Maximum packet size. + */ + public void setMaximumPacketSize(Long maximumPacketSize) { + this.maximumPacketSize = maximumPacketSize; + } + + /** + * Returns the Topic Alias Maximum. If null, the default value is + * 0. + * + * @return the Topic Alias Maximum. + */ + public Integer getTopicAliasMaximum() { + return topicAliasMaximum; + } + + /** + * Sets the Topic Alias Maximum. This value if present represents the highest + * value that the Client will accept as a Topic Alias sent by the Server. + * + *
    + *
  • If set to null, then it will default to to 0.
  • + *
  • If set to 0, the Client will not accept any Topic Aliases
  • + *
  • The Maximum value for this property is 65535.
  • + *
+ * + * @param topicAliasMaximum + * the Topic Alias Maximum + */ + public void setTopicAliasMaximum(Integer topicAliasMaximum) { + if (topicAliasMaximum != null && topicAliasMaximum > 65535) { + throw new IllegalArgumentException(); + } + this.topicAliasMaximum = topicAliasMaximum; + } + + /** + * Returns the Request Response Info flag. If null, the default + * value is false. + * + * @return The Request Response Info Flag. + */ + public Boolean getRequestResponseInfo() { + return requestResponseInfo; + } + + /** + * Sets the Request Response Info Flag. + *
    + *
  • If set to null, then it will default to false.
  • + *
  • If set to false, the server will not return any response information in + * the CONNACK.
  • + *
  • If set to true, the server MAY return response information in the + * CONNACK.
  • + *
+ * + * @param requestResponseInfo + * The Request Response Info Flag. + */ + public void setRequestResponseInfo(boolean requestResponseInfo) { + this.requestResponseInfo = requestResponseInfo; + } + + /** + * Returns the Request Problem Info flag. If null, the default + * value is true. + * + * @return the Request Problem Info flag. + */ + public Boolean getRequestProblemInfo() { + return requestProblemInfo; + } + + /** + * Sets the Request Problem Info flag. + *
    + *
  • If set to null, then it will default to true.
  • + *
  • If set to false, the server MAY return a Reason String or User Properties + * on a CONNACK or DISCONNECT, but must not send a Reason String or User + * Properties on any packet other than PUBLISH, CONNACK or DISCONNECT.
  • + *
  • If set to true, the server MAY return a Reason String or User Properties + * on any packet where it is allowed.
  • + *
+ * + * @param requestProblemInfo + * The Flag to request problem information. + */ + public void setRequestProblemInfo(boolean requestProblemInfo) { + this.requestProblemInfo = requestProblemInfo; + } + + /** + * Returns the User Properties. + * + * @return the User Properties. + */ + public List getUserProperties() { + return userProperties; + } + + /** + * Sets the User Properties. A User Property is a UTF-8 String Pair, the same + * name is allowed to appear more than once. + * + * @param userProperties + * User Properties + */ + public void setUserProperties(List userProperties) { + this.userProperties = userProperties; + } + + /** + * Returns the Authentication Method. If null, extended + * authentication is not performed. + * + * @return the Authentication Method. + */ + public String getAuthMethod() { + return authMethod; + } + + /** + * Sets the Authentication Method. If set, this value contains the name of the + * authentication method to be used for extended authentication. + * + * If null, extended authentication is not performed. + * + * @param authMethod + * The Authentication Method. + */ + public void setAuthMethod(String authMethod) { + this.authMethod = authMethod; + } + + /** + * Returns the Authentication Data. + * + * @return the Authentication Data. + */ + public byte[] getAuthData() { + return authData; + } + + /** + * Sets the Authentication Data. If set, this byte array contains the extended + * authentication data, defined by the Authenticated Method. It is a protocol + * error to include Authentication Data if there is no Authentication Method. + * + * @param authData + * The Authentication Data + */ + public void setAuthData(byte[] authData) { + this.authData = authData; + } + + /** + * Returns the socket factory that will be used when connecting, or + * null if one has not been set. + * + * @return The Socket Factory + */ + public SocketFactory getSocketFactory() { + return socketFactory; + } + + /** + * Sets the SocketFactory to use. This allows an application to + * apply its own policies around the creation of network sockets. If using an + * SSL connection, an SSLSocketFactory can be used to supply + * application-specific security settings. + * + * @param socketFactory + * the factory to use. + */ + public void setSocketFactory(SocketFactory socketFactory) { + this.socketFactory = socketFactory; + } + + /** + * Returns the SSL properties for the connection. + * + * @return the properties for the SSL connection + */ + public Properties getSSLProperties() { + return sslClientProps; + } + + /** + * Sets the SSL properties for the connection. + *

+ * Note that these properties are only valid if an implementation of the Java + * Secure Socket Extensions (JSSE) is available. These properties are + * not used if a SocketFactory has been set using + * {@link #setSocketFactory(SocketFactory)}. The following properties can be + * used: + *

+ *
+ *
com.ibm.ssl.protocol
+ *
One of: SSL, SSLv3, TLS, TLSv1, SSL_TLS.
+ *
com.ibm.ssl.contextProvider + *
Underlying JSSE provider. For example "IBMJSSE2" or "SunJSSE"
+ * + *
com.ibm.ssl.keyStore
+ *
The name of the file that contains the KeyStore object that you want the + * KeyManager to use. For example /mydir/etc/key.p12
+ * + *
com.ibm.ssl.keyStorePassword
+ *
The password for the KeyStore object that you want the KeyManager to use. + * The password can either be in plain-text, or may be obfuscated using the + * static method: + * com.ibm.micro.security.Password.obfuscate(char[] password). This + * obfuscates the password using a simple and insecure XOR and Base64 encoding + * mechanism. Note that this is only a simple scrambler to obfuscate clear-text + * passwords.
+ * + *
com.ibm.ssl.keyStoreType
+ *
Type of key store, for example "PKCS12", "JKS", or "JCEKS".
+ * + *
com.ibm.ssl.keyStoreProvider
+ *
Key store provider, for example "IBMJCE" or "IBMJCEFIPS".
+ * + *
com.ibm.ssl.trustStore
+ *
The name of the file that contains the KeyStore object that you want the + * TrustManager to use.
+ * + *
com.ibm.ssl.trustStorePassword
+ *
The password for the TrustStore object that you want the TrustManager to + * use. The password can either be in plain-text, or may be obfuscated using the + * static method: + * com.ibm.micro.security.Password.obfuscate(char[] password). This + * obfuscates the password using a simple and insecure XOR and Base64 encoding + * mechanism. Note that this is only a simple scrambler to obfuscate clear-text + * passwords.
+ * + *
com.ibm.ssl.trustStoreType
+ *
The type of KeyStore object that you want the default TrustManager to + * use. Same possible values as "keyStoreType".
+ * + *
com.ibm.ssl.trustStoreProvider
+ *
Trust store provider, for example "IBMJCE" or "IBMJCEFIPS".
+ * + *
com.ibm.ssl.enabledCipherSuites
+ *
A list of which ciphers are enabled. Values are dependent on the + * provider, for example: + * SSL_RSA_WITH_AES_128_CBC_SHA;SSL_RSA_WITH_3DES_EDE_CBC_SHA.
+ * + *
com.ibm.ssl.keyManager
+ *
Sets the algorithm that will be used to instantiate a KeyManagerFactory + * object instead of using the default algorithm available in the platform. + * Example values: "IbmX509" or "IBMJ9X509".
+ * + *
com.ibm.ssl.trustManager
+ *
Sets the algorithm that will be used to instantiate a TrustManagerFactory + * object instead of using the default algorithm available in the platform. + * Example values: "PKIX" or "IBMJ9X509".
+ *
+ * + * @param props + * The SSL {@link Properties} + */ + public void setSSLProperties(Properties props) { + this.sslClientProps = props; + } + + /** + * Returns the HostnameVerifier for the SSL connection. + * + * @return the HostnameVerifier for the SSL connection + */ + public HostnameVerifier getSSLHostnameVerifier() { + return sslHostnameVerifier; + } + + /** + * Sets the HostnameVerifier for the SSL connection. Note that it will be used + * after handshake on a connection and you should do actions by yourserlf when + * hostname is verified error. + *

+ * There is no default HostnameVerifier + *

+ * + * @param hostnameVerifier + * the {@link HostnameVerifier} + */ + public void setSSLHostnameVerifier(HostnameVerifier hostnameVerifier) { + this.sslHostnameVerifier = hostnameVerifier; + } + + /** + * Returns whether to automatically assign subscription identifiers when + * subscribing to a topic. + * + * @return if automatic assignment of subscription identifiers is enabled. + */ + public boolean useSubscriptionIdentifiers() { + return useSubscriptionIdentifiers; + } + + /** + * Sets whether to automatically assign subscription identifiers when + * subscribing to a topic. This will mean that if a subscription has a callback + * associated with it then it is guaranteed to be called when an incoming + * message has the correct subscription ID embedded. If disabled, then the + * client will do best effort topic matching with all callbacks, however this + * might result in an incorrect callback being called if there are multiple + * subscriptions to topics using a combination of wildcards. + * + * @param useSubscriptionIdentifiers + * Whether to enable automatic assignment of subscription + * identifiers. + */ + public void setUseSubscriptionIdentifiers(boolean useSubscriptionIdentifiers) { + this.useSubscriptionIdentifiers = useSubscriptionIdentifiers; + } + + public boolean isHttpsHostnameVerificationEnabled() { + return httpsHostnameVerificationEnabled; + } + + public void setHttpsHostnameVerificationEnabled(boolean httpsHostnameVerificationEnabled) { + this.httpsHostnameVerificationEnabled = httpsHostnameVerificationEnabled; + } + + /** + * @return The Debug Properties + */ + public Properties getDebug() { + final String strNull = "null"; + Properties p = new Properties(); + p.put("MqttVersion", getMqttVersion()); + p.put("CleanStart", Boolean.valueOf(isCleanStart())); + p.put("ConTimeout", getConnectionTimeout()); + p.put("KeepAliveInterval", getKeepAliveInterval()); + p.put("UserName", (getUserName() == null) ? strNull : getUserName()); + p.put("WillDestination", (getWillDestination() == null) ? strNull : getWillDestination()); + if (getSocketFactory() == null) { + p.put("SocketFactory", strNull); + } else { + p.put("SocketFactory", getSocketFactory()); + } + if (getSSLProperties() == null) { + p.put("SSLProperties", strNull); + } else { + p.put("SSLProperties", getSSLProperties()); + } + return p; + } + + /** + * Sets the Custom WebSocket Headers for the WebSocket Connection. + * + * @param headers + * The custom websocket headers {@link Properties} + */ + public void setCustomWebSocketHeaders(Map headers) { + this.customWebSocketHeaders = Collections.unmodifiableMap(headers); + } + + public Map getCustomWebSocketHeaders() { + return customWebSocketHeaders; + } + + public String toString() { + return Debug.dumpProperties(getDebug(), "Connection options"); + } + + public boolean isSendReasonMessages() { + return sendReasonMessages; + } + + public void setSendReasonMessages(boolean sendReasonMessages) { + this.sendReasonMessages = sendReasonMessages; + } + + public int getExecutorServiceTimeout() { + return executorServiceTimeout; + } + + /** + * Set the time in seconds that the executor service should wait when + * terminating before forcefully terminating. It is not recommended to change + * this value unless you are absolutely sure that you need to. + * + * @param executorServiceTimeout the time in seconds to wait when shutting down.Ï + */ + public void setExecutorServiceTimeout(int executorServiceTimeout) { + this.executorServiceTimeout = executorServiceTimeout; + } +} diff --git a/src/main/java/org/eclipse/paho/mqttv5/client/MqttConnectionOptionsBuilder.java b/src/main/java/org/eclipse/paho/mqttv5/client/MqttConnectionOptionsBuilder.java new file mode 100644 index 0000000..1de91fb --- /dev/null +++ b/src/main/java/org/eclipse/paho/mqttv5/client/MqttConnectionOptionsBuilder.java @@ -0,0 +1,109 @@ +package org.eclipse.paho.mqttv5.client; + +import java.util.ArrayList; + +import org.eclipse.paho.mqttv5.common.MqttMessage; +import org.eclipse.paho.mqttv5.common.packet.UserProperty; + +public class MqttConnectionOptionsBuilder { + private MqttConnectionOptions mqttConnectionOptions; + + public MqttConnectionOptionsBuilder() { + mqttConnectionOptions = new MqttConnectionOptions(); + } + + public MqttConnectionOptionsBuilder serverURI(String serverURI) { + mqttConnectionOptions.setServerURIs(new String[] { serverURI }); + return this; + } + + public MqttConnectionOptionsBuilder serverURIs(String[] serverURIs) { + mqttConnectionOptions.setServerURIs(serverURIs); + return this; + } + + public MqttConnectionOptionsBuilder automaticReconnect(boolean enabled) { + mqttConnectionOptions.setAutomaticReconnect(enabled); + return this; + } + + public MqttConnectionOptionsBuilder automaticReconnectDelay(int minimum, int maximum) { + mqttConnectionOptions.setAutomaticReconnectDelay(minimum, maximum); + return this; + } + + public MqttConnectionOptionsBuilder keepAliveInterval(int keepAlive) { + mqttConnectionOptions.setKeepAliveInterval(keepAlive); + return this; + } + + public MqttConnectionOptionsBuilder connectionTimeout(int connectionTimeout) { + mqttConnectionOptions.setConnectionTimeout(connectionTimeout); + return this; + } + + public MqttConnectionOptionsBuilder cleanStart(boolean cleanStart) { + mqttConnectionOptions.setCleanStart(cleanStart); + return this; + } + + public MqttConnectionOptionsBuilder username(String username) { + mqttConnectionOptions.setUserName(username); + return this; + } + + public MqttConnectionOptionsBuilder password(byte[] password) { + mqttConnectionOptions.setPassword(password); + return this; + } + + public MqttConnectionOptionsBuilder will(String topic, MqttMessage message) { + mqttConnectionOptions.setWill(topic, message); + return this; + } + + public MqttConnectionOptionsBuilder sessionExpiryInterval(Long sessionExpiryInterval) { + mqttConnectionOptions.setSessionExpiryInterval(sessionExpiryInterval); + return this; + } + + public MqttConnectionOptionsBuilder maximumPacketSize(Long maximumPacketSize) { + mqttConnectionOptions.setMaximumPacketSize(maximumPacketSize); + return this; + } + + public MqttConnectionOptionsBuilder topicAliasMaximum(Integer topicAliasMaximum) { + mqttConnectionOptions.setTopicAliasMaximum(topicAliasMaximum); + return this; + } + + public MqttConnectionOptionsBuilder requestReponseInfo(Boolean requestResponseInfo) { + mqttConnectionOptions.setRequestResponseInfo(requestResponseInfo); + return this; + } + + public MqttConnectionOptionsBuilder requestProblemInfo(Boolean requestProblemInfo) { + mqttConnectionOptions.setRequestProblemInfo(requestProblemInfo); + return this; + } + + public MqttConnectionOptionsBuilder userProperties(ArrayList properties) { + mqttConnectionOptions.setUserProperties(properties); + return this; + } + + public MqttConnectionOptionsBuilder authMethod(String authMethod) { + mqttConnectionOptions.setAuthMethod(authMethod); + return this; + } + + public MqttConnectionOptionsBuilder authData(byte[] authData) { + mqttConnectionOptions.setAuthData(authData); + return this; + } + + public MqttConnectionOptions build() { + return mqttConnectionOptions; + } + +} diff --git a/src/main/java/org/eclipse/paho/mqttv5/client/MqttDeliveryToken.java b/src/main/java/org/eclipse/paho/mqttv5/client/MqttDeliveryToken.java new file mode 100644 index 0000000..dc8aae7 --- /dev/null +++ b/src/main/java/org/eclipse/paho/mqttv5/client/MqttDeliveryToken.java @@ -0,0 +1,56 @@ +/******************************************************************************* + * Copyright (c) 2009, 2014 IBM Corp. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * and Eclipse Distribution License v1.0 which accompany this distribution. + * + * The Eclipse Public License is available at + * https://www.eclipse.org/legal/epl-2.0 + * and the Eclipse Distribution License is available at + * https://www.eclipse.org/org/documents/edl-v10.php + * + * Contributors: + * Dave Locke - initial API and implementation and/or initial documentation + */ +package org.eclipse.paho.mqttv5.client; + +import org.eclipse.paho.mqttv5.common.MqttException; +import org.eclipse.paho.mqttv5.common.MqttMessage; + +/** + * Provides a mechanism to track the delivery progress of a message. + * + *

+ * Used to track the the delivery progress of a message when a publish is + * executed in a non-blocking manner (run in the background)

+ * + * @see MqttToken + */ +public class MqttDeliveryToken extends MqttToken implements IMqttDeliveryToken { + + + public MqttDeliveryToken() { + super(); + } + + public MqttDeliveryToken(String logContext) { + super(logContext); + } + + /** + * Returns the message associated with this token. + *

Until the message has been delivered, the message being delivered will + * be returned. Once the message has been delivered null will be + * returned. + * @return the message associated with this token or null if already delivered. + * @throws MqttException if there was a problem completing retrieving the message + */ + public MqttMessage getMessage() throws MqttException { + return internalTok.getMessage(); + } + + protected void setMessage(MqttMessage msg) { + internalTok.setMessage(msg); + } +} diff --git a/src/main/java/org/eclipse/paho/mqttv5/client/MqttDisconnectResponse.java b/src/main/java/org/eclipse/paho/mqttv5/client/MqttDisconnectResponse.java new file mode 100644 index 0000000..7a09450 --- /dev/null +++ b/src/main/java/org/eclipse/paho/mqttv5/client/MqttDisconnectResponse.java @@ -0,0 +1,97 @@ +package org.eclipse.paho.mqttv5.client; + +import java.util.ArrayList; + +import org.eclipse.paho.mqttv5.common.MqttException; +import org.eclipse.paho.mqttv5.common.packet.MqttDisconnect; +import org.eclipse.paho.mqttv5.common.packet.UserProperty; + +/** + * This Object holds relevant properties for the client to use once the TCP + * connection has either been lost, or if the server has gracefully + * disconnected. + * + */ +public class MqttDisconnectResponse { + private int returnCode; + private String reasonString; + private ArrayList userProperties; + private String serverReference; + private MqttException exception; + + /** + * Initialises a new MqttDisconnectResponse with an exception. This will only be + * thrown in the event of a non-clean disconnect, i.e. connection lost. + * + * @param exception + * The exception thrown containing details about the lost connection. + */ + public MqttDisconnectResponse(MqttException exception) { + this.exception = exception; + } + + /** + * Initialise a new MqttDisconnectResponse with parameters provided in an + * {@link MqttDisconnect} packet. This will be used when a clean disconnect has + * Occurred. + * + * @param returnCode + * The Disconnect Return Code + * @param reasonString + * The Disconnect Reason String + * @param userProperties + * The Disconnect User Properties + * @param serverReference + * The Server Reference + */ + public MqttDisconnectResponse(int returnCode, String reasonString, ArrayList userProperties, + String serverReference) { + this.returnCode = returnCode; + this.reasonString = reasonString; + this.userProperties = userProperties; + this.serverReference = serverReference; + } + + /** + * @return The Return code + */ + public int getReturnCode() { + return returnCode; + } + + /** + * @return The Reason String + */ + public String getReasonString() { + return reasonString; + } + + /** + * @return The User Properties + */ + public ArrayList getUserProperties() { + return userProperties; + } + + /** + * @return The Server Reference, a new URI for a different server to connect to. + */ + public String getServerReference() { + return serverReference; + } + + /** + * @return The Exception thrown, most likely caused by a lost connection. + */ + public MqttException getException() { + return exception; + } + + @Override + public String toString() { + return "MqttDisconnectResponse [returnCode=" + returnCode + ", reasonString=" + reasonString + + ", userProperties=" + userProperties + ", serverReference=" + serverReference + ", exception=" + + exception + "]"; + } + +} diff --git a/src/main/java/org/eclipse/paho/mqttv5/client/MqttPingSender.java b/src/main/java/org/eclipse/paho/mqttv5/client/MqttPingSender.java new file mode 100644 index 0000000..c6763d8 --- /dev/null +++ b/src/main/java/org/eclipse/paho/mqttv5/client/MqttPingSender.java @@ -0,0 +1,52 @@ +/******************************************************************************* + * Copyright (c) 2014 IBM Corp. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * and Eclipse Distribution License v1.0 which accompany this distribution. + * + * The Eclipse Public License is available at + * https://www.eclipse.org/legal/epl-2.0 + * and the Eclipse Distribution License is available at + * https://www.eclipse.org/org/documents/edl-v10.php + */ + +package org.eclipse.paho.mqttv5.client; + +import org.eclipse.paho.mqttv5.client.internal.ClientComms; + +/** + * Represents an object used to send ping packet to MQTT broker every keep alive + * interval. + */ +public interface MqttPingSender { + + /** + * Initial method. Pass interal state of current client in. + * + * @param comms + * The core of the client, which holds the state information for + * pending and in-flight messages. + */ + void init(ClientComms comms); + + /** + * Start ping sender. It will be called after connection is success. + */ + void start(); + + /** + * Stop ping sender. It is called if there is any errors or connection + * shutdowns. + */ + void stop(); + + /** + * Schedule next ping in certain delay. + * + * @param delayInMilliseconds + * delay in milliseconds. + */ + void schedule(long delayInMilliseconds); + +} \ No newline at end of file diff --git a/src/main/java/org/eclipse/paho/mqttv5/client/MqttToken.java b/src/main/java/org/eclipse/paho/mqttv5/client/MqttToken.java new file mode 100644 index 0000000..b33aa69 --- /dev/null +++ b/src/main/java/org/eclipse/paho/mqttv5/client/MqttToken.java @@ -0,0 +1,148 @@ +/******************************************************************************* + * Copyright (c) 2014 IBM Corp. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * and Eclipse Distribution License v1.0 which accompany this distribution. + * + * The Eclipse Public License is available at + * https://www.eclipse.org/legal/epl-2.0 + * and the Eclipse Distribution License is available at + * https://www.eclipse.org/org/documents/edl-v10.php + * + * Contributions: + * Ian Craggs - MQTT 3.1.1 support + */ + +package org.eclipse.paho.mqttv5.client; + +import org.eclipse.paho.mqttv5.client.internal.Token; +import org.eclipse.paho.mqttv5.common.MqttException; +import org.eclipse.paho.mqttv5.common.MqttMessage; +import org.eclipse.paho.mqttv5.common.packet.MqttProperties; +import org.eclipse.paho.mqttv5.common.packet.MqttWireMessage; + +/** + * Provides a mechanism for tracking the completion of an asynchronous action. + *

+ * A token that implements the ImqttToken interface is returned from all + * non-blocking method with the exception of publish. + *

+ * + * @see IMqttToken + */ + +public class MqttToken implements IMqttToken { + private MqttAsyncClient client = null; + /** + * A reference to the the class that provides most of the implementation of the + * MqttToken. MQTT application programs must not use the internal class. + */ + public Token internalTok = null; + + public MqttToken() { + } + + public MqttToken(MqttAsyncClient client) { + this.client = client; + } + + public MqttToken(String logContext) { + internalTok = new Token(logContext); + } + + public MqttException getException() { + return internalTok.getException(); + } + + public boolean isComplete() { + return internalTok.isComplete(); + } + + public void setActionCallback(MqttActionListener listener) { + internalTok.setActionCallback(listener); + + } + + public MqttActionListener getActionCallback() { + return internalTok.getActionCallback(); + } + + public void waitForCompletion() throws MqttException { + internalTok.waitForCompletion(-1); + } + + public void waitForCompletion(long timeout) throws MqttException { + internalTok.waitForCompletion(timeout); + } + + public MqttClientInterface getClient() { + return internalTok.getClient(); + } + + public String[] getTopics() { + return internalTok.getTopics(); + } + + public Object getUserContext() { + return internalTok.getUserContext(); + } + + public void setUserContext(Object userContext) { + internalTok.setUserContext(userContext); + } + + public int getMessageId() { + return internalTok.getMessageID(); + } + + public int[] getGrantedQos() { + return internalTok.getGrantedQos(); + } + + public boolean getSessionPresent() { + return internalTok.getSessionPresent(); + } + + public MqttWireMessage getResponse() { + return internalTok.getResponse(); + } + + public MqttProperties getResponseProperties() { + return (internalTok.getWireMessage() == null) ? null : internalTok.getWireMessage().getProperties(); + } + + public MqttWireMessage getRequestMessage() { + return internalTok.getRequestMessage(); + } + + public void setRequestMessage(MqttWireMessage request) { + internalTok.setRequestMessage(request); + } + + public MqttProperties getRequestProperties() { + return (internalTok.getRequestMessage() == null) ? null : internalTok.getRequestMessage().getProperties(); + } + + @Override + public int[] getReasonCodes() { + return internalTok.getReasonCodes(); + } + + /** + * Returns the message associated with this token. + *

Until the message has been delivered, the message being delivered will + * be returned. Once the message has been delivered null will be + * returned. + * @return the message associated with this token or null if already delivered. + * @throws MqttException if there was a problem completing retrieving the message + */ + public MqttMessage getMessage() throws MqttException { + return internalTok.getMessage(); + } + + protected void setMessage(MqttMessage msg) { + internalTok.setMessage(msg); + } + +} diff --git a/src/main/java/org/eclipse/paho/mqttv5/client/MqttTopic.java b/src/main/java/org/eclipse/paho/mqttv5/client/MqttTopic.java new file mode 100644 index 0000000..5cccf86 --- /dev/null +++ b/src/main/java/org/eclipse/paho/mqttv5/client/MqttTopic.java @@ -0,0 +1,126 @@ +/******************************************************************************* + * Copyright (c) 2009, 2014 IBM Corp. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * and Eclipse Distribution License v1.0 which accompany this distribution. + * + * The Eclipse Public License is available at + * https://www.eclipse.org/legal/epl-2.0 + * and the Eclipse Distribution License is available at + * https://www.eclipse.org/org/documents/edl-v10.php + * + * Contributors: + * Dave Locke - initial API and implementation and/or initial documentation + */ +package org.eclipse.paho.mqttv5.client; + +import org.eclipse.paho.mqttv5.client.internal.ClientComms; +import org.eclipse.paho.mqttv5.common.MqttException; +import org.eclipse.paho.mqttv5.common.MqttMessage; +import org.eclipse.paho.mqttv5.common.MqttPersistenceException; +import org.eclipse.paho.mqttv5.common.packet.MqttProperties; +import org.eclipse.paho.mqttv5.common.packet.MqttPublish; + +/** + * Represents a topic destination, used for publish/subscribe messaging. + */ +public class MqttTopic { + + + private ClientComms comms; + private String name; + + /** + * @param name + * The Name of the topic + * @param comms + * The {@link ClientComms} + */ + public MqttTopic(String name, ClientComms comms) { + this.comms = comms; + this.name = name; + } + + /** + * Publishes a message on the topic. This is a convenience method, which will + * create a new {@link MqttMessage} object with a byte array payload and the + * specified QoS, and then publish it. All other values in the message will be + * set to the defaults. + * + * @param payload + * the byte array to use as the payload + * @param qos + * the Quality of Service. Valid values are 0, 1 or 2. + * @param retained + * whether or not this message should be retained by the server. + * @return {@link MqttToken} + * @throws MqttException + * If an error occurs publishing the message + * @throws MqttPersistenceException + * If an error occurs persisting the message + * @throws IllegalArgumentException + * if value of QoS is not 0, 1 or 2. + * @see #publish(MqttMessage) + * @see MqttMessage#setQos(int) + * @see MqttMessage#setRetained(boolean) + */ + public MqttToken publish(byte[] payload, int qos, boolean retained) + throws MqttException, MqttPersistenceException { + MqttMessage message = new MqttMessage(payload); + message.setQos(qos); + message.setRetained(retained); + return this.publish(message); + } + + /** + * Publishes the specified message to this topic, but does not wait for delivery + * of the message to complete. The returned {@link MqttToken token} can + * be used to track the delivery status of the message. Once this method has + * returned cleanly, the message has been accepted for publication by the + * client. Message delivery will be completed in the background when a + * connection is available. + * + * @param message + * the message to publish + * @return an MqttToken for tracking the delivery of the message + * @throws MqttException + * if an error occurs publishing the message + * @throws MqttPersistenceException + * if an error occurs persisting the message + */ + public MqttToken publish(MqttMessage message) throws MqttException, MqttPersistenceException { + MqttToken token = new MqttToken(comms.getClient().getClientId()); + token.internalTok.setDeliveryToken(true); + token.setMessage(message); + comms.sendNoWait(createPublish(message, new MqttProperties()), token); + token.internalTok.waitUntilSent(); + return token; + } + + /** + * Returns the name of the queue or topic. + * + * @return the name of this destination. + */ + public String getName() { + return name; + } + + /** + * Create a PUBLISH packet from the specified message. + */ + private MqttPublish createPublish(MqttMessage message, MqttProperties properties) { + return new MqttPublish(this.getName(), message, properties); + } + + /** + * Returns a string representation of this topic. + * + * @return a string representation of this topic. + */ + public String toString() { + return getName(); + } + +} diff --git a/src/main/java/org/eclipse/paho/mqttv5/client/TimerPingSender.java b/src/main/java/org/eclipse/paho/mqttv5/client/TimerPingSender.java new file mode 100644 index 0000000..85c4470 --- /dev/null +++ b/src/main/java/org/eclipse/paho/mqttv5/client/TimerPingSender.java @@ -0,0 +1,121 @@ +/******************************************************************************* + * Copyright (c) 2014, 2018 IBM Corp. + * Copyright (c) 2017 BMW Car IT GmbH. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * and Eclipse Distribution License v1.0 which accompany this distribution. + * + * The Eclipse Public License is available at + * https://www.eclipse.org/legal/epl-2.0 + * and the Eclipse Distribution License is available at + * https://www.eclipse.org/org/documents/edl-v10.php + */ + +package org.eclipse.paho.mqttv5.client; + +import java.util.Timer; +import java.util.TimerTask; + +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +import org.eclipse.paho.mqttv5.client.internal.ClientComms; +import org.eclipse.paho.mqttv5.client.logging.Logger; +import org.eclipse.paho.mqttv5.client.logging.LoggerFactory; + +/** + * Default ping sender implementation + * + *

This class implements the {@link MqttPingSender} pinger interface + * allowing applications to send ping packet to server every keep alive interval. + *

+ * + * @see MqttPingSender + */ +public class TimerPingSender implements MqttPingSender{ + private static final String CLASS_NAME = TimerPingSender.class.getName(); + private Logger log = LoggerFactory.getLogger(LoggerFactory.MQTT_CLIENT_MSG_CAT, CLASS_NAME); + + private ClientComms comms; + private Timer timer; + private ScheduledExecutorService executorService = null; + private ScheduledFuture scheduledFuture; + private String clientid; + + public TimerPingSender(ScheduledExecutorService executorService) { + this.executorService = executorService; + } + + public void init(ClientComms comms) { + if (comms == null) { + throw new IllegalArgumentException("ClientComms cannot be null."); + } + this.comms = comms; + clientid = comms.getClient().getClientId(); + log.setResourceName(clientid); + } + + public void start() { + final String methodName = "start"; + + //@Trace 659=start timer for client:{0} + log.fine(CLASS_NAME, methodName, "659", new Object[]{ clientid }); + if (executorService == null) { + timer = new Timer("MQTT Ping: " + clientid); + //Check ping after first keep alive interval. + timer.schedule(new PingTask(), comms.getKeepAlive()); + } else { + //Check ping after first keep alive interval. + schedule(comms.getKeepAlive()); + } + } + + public void stop() { + final String methodName = "stop"; + //@Trace 661=stop + log.fine(CLASS_NAME, methodName, "661", null); + if (executorService == null) { + if (timer != null){ + timer.cancel(); + } + } else { + if (scheduledFuture != null) { + scheduledFuture.cancel(true); + } + } + } + + public void schedule(long delayInMilliseconds) { + if (executorService == null) { + timer.schedule(new PingTask(), delayInMilliseconds); + } else { + scheduledFuture = executorService.schedule(new PingRunnable(), delayInMilliseconds, TimeUnit.MILLISECONDS); + } + } + + private class PingTask extends TimerTask { + private static final String methodName = "PingTask.run"; + + public void run() { + Thread.currentThread().setName("MQTT Ping: " + clientid); + //@Trace 660=Check schedule at {0} + log.fine(CLASS_NAME, methodName, "660", new Object[]{ Long.valueOf(System.nanoTime()) }); + comms.checkForActivity(); + } + } + + private class PingRunnable implements Runnable { + private static final String methodName = "PingTask.run"; + + public void run() { + String originalThreadName = Thread.currentThread().getName(); + Thread.currentThread().setName("MQTT Ping: " + clientid); + //@Trace 660=Check schedule at {0} + log.fine(CLASS_NAME, methodName, "660", new Object[]{ Long.valueOf(System.nanoTime()) }); + comms.checkForActivity(); + Thread.currentThread().setName(originalThreadName); + } + } +} diff --git a/src/main/java/org/eclipse/paho/mqttv5/client/internal/ClientComms.java b/src/main/java/org/eclipse/paho/mqttv5/client/internal/ClientComms.java new file mode 100644 index 0000000..7a84632 --- /dev/null +++ b/src/main/java/org/eclipse/paho/mqttv5/client/internal/ClientComms.java @@ -0,0 +1,978 @@ +package org.eclipse.paho.mqttv5.client.internal; + +/******************************************************************************* + * Copyright (c) 2009, 2018 IBM Corp. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * and Eclipse Distribution License v1.0 which accompany this distribution. + * + * The Eclipse Public License is available at + * https://www.eclipse.org/legal/epl-2.0 + * and the Eclipse Distribution License is available at + * https://www.eclipse.org/org/documents/edl-v10.php + * + * Contributors: + * Dave Locke - initial API and implementation and/or initial documentation + * Ian Craggs - per subscription message handlers (bug 466579) + * Ian Craggs - ack control (bug 472172) + * James Sutton - checkForActivity Token (bug 473928) + * James Sutton - Automatic Reconnect & Offline Buffering. + */ +import java.util.Enumeration; +import java.util.Properties; +import java.util.Vector; +import java.util.concurrent.ExecutorService; + +import org.eclipse.paho.mqttv5.client.BufferedMessage; +import org.eclipse.paho.mqttv5.client.IMqttMessageListener; +import org.eclipse.paho.mqttv5.client.MqttActionListener; +import org.eclipse.paho.mqttv5.client.MqttCallback; +import org.eclipse.paho.mqttv5.client.MqttClientException; +import org.eclipse.paho.mqttv5.client.MqttClientInterface; +import org.eclipse.paho.mqttv5.client.MqttClientPersistence; +import org.eclipse.paho.mqttv5.client.MqttConnectionOptions; +import org.eclipse.paho.mqttv5.client.MqttPingSender; +import org.eclipse.paho.mqttv5.client.MqttToken; +import org.eclipse.paho.mqttv5.client.MqttTopic; +import org.eclipse.paho.mqttv5.client.TimerPingSender; +import org.eclipse.paho.mqttv5.client.logging.Logger; +import org.eclipse.paho.mqttv5.client.logging.LoggerFactory; +import org.eclipse.paho.mqttv5.common.MqttException; +import org.eclipse.paho.mqttv5.common.MqttMessage; +import org.eclipse.paho.mqttv5.common.MqttPersistenceException; +import org.eclipse.paho.mqttv5.common.packet.MqttConnAck; +import org.eclipse.paho.mqttv5.common.packet.MqttConnect; +import org.eclipse.paho.mqttv5.common.packet.MqttDisconnect; +import org.eclipse.paho.mqttv5.common.packet.MqttProperties; +import org.eclipse.paho.mqttv5.common.packet.MqttPublish; +import org.eclipse.paho.mqttv5.common.packet.MqttWireMessage; + +/** + * Handles client communications with the server. Sends and receives MQTT V5 + * messages. + */ +public class ClientComms { + public static String VERSION = "${project.version}"; + public static String BUILD_LEVEL = "L${build.level}"; + private static final String CLASS_NAME = ClientComms.class.getName(); + private Logger log = LoggerFactory.getLogger(LoggerFactory.MQTT_CLIENT_MSG_CAT, CLASS_NAME); + + private static final byte CONNECTED = 0; + private static final byte CONNECTING = 1; + private static final byte DISCONNECTING = 2; + private static final byte DISCONNECTED = 3; + private static final byte CLOSED = 4; + + private MqttClientInterface client; + private int networkModuleIndex; + private NetworkModule[] networkModules; + private CommsReceiver receiver; + private CommsSender sender; + private CommsCallback callback; + private ClientState clientState; + private MqttConnectionOptions conOptions; + private MqttClientPersistence persistence; + private MqttPingSender pingSender; + private CommsTokenStore tokenStore; + private boolean stoppingComms = false; + + private byte conState = DISCONNECTED; + private final Object conLock = new Object(); // Used to synchronize connection state + private boolean closePending = false; + private boolean resting = false; + private DisconnectedMessageBuffer disconnectedMessageBuffer; + private ExecutorService executorService; + private MqttConnectionState mqttConnection; + + /** + * Creates a new ClientComms object, using the specified module to handle the + * network calls. + * + * @param client + * The {@link MqttClientInterface} + * @param persistence + * the {@link MqttClientPersistence} layer. + * @param pingSender + * the {@link TimerPingSender} + * @param executorService + * the {@link ExecutorService} + * @param mqttSession + * the {@link MqttSessionState} + * @param mqttConnection + * the {@link MqttConnectionState} + * @throws MqttException + * if an exception occurs whilst communicating with the server + */ + public ClientComms(MqttClientInterface client, MqttClientPersistence persistence, MqttPingSender pingSender, + ExecutorService executorService, MqttSessionState mqttSession, MqttConnectionState mqttConnection) throws MqttException { + this.conState = DISCONNECTED; + this.client = client; + this.persistence = persistence; + this.pingSender = pingSender; + this.pingSender.init(this); + this.executorService = executorService; + this.mqttConnection = mqttConnection; + + this.tokenStore = new CommsTokenStore(getClient().getClientId()); + this.callback = new CommsCallback(this); + this.clientState = new ClientState(persistence, tokenStore, this.callback, this, pingSender, this.mqttConnection); + + callback.setClientState(clientState); + log.setResourceName(getClient().getClientId()); + } + + CommsReceiver getReceiver() { + return receiver; + } + + /** + * Sends a message to the server. Does not check if connected this validation + * must be done by invoking routines. + * + * @param message + * @param token + * @throws MqttException + */ + void internalSend(MqttWireMessage message, MqttToken token) throws MqttException { + final String methodName = "internalSend"; + // @TRACE 200=internalSend key={0} message={1} token={2} + log.fine(CLASS_NAME, methodName, "200", new Object[] { message.getKey(), message, token }); + + if (token.getClient() == null) { + // Associate the client with the token - also marks it as in use. + token.internalTok.setClient(getClient()); + } else { + // Token is already in use - cannot reuse + // @TRACE 213=fail: token in use: key={0} message={1} token={2} + log.fine(CLASS_NAME, methodName, "213", new Object[] { message.getKey(), message, token }); + + throw new MqttException(MqttClientException.REASON_CODE_TOKEN_INUSE); + } + + try { + // Persist if needed and send the message + this.clientState.send(message, token); + } catch (MqttException e) { + token.internalTok.setClient(null); // undo client setting on error + if (message instanceof MqttPublish) { + this.clientState.undo((MqttPublish) message); + } + throw e; + } + } + + /** + * Sends a message to the broker if in connected state, but only waits for the + * message to be stored, before returning. + * + * @param message + * The {@link MqttWireMessage} to send + * @param token + * The {@link MqttToken} to send. + * @throws MqttException + * if an error occurs sending the message + */ + public void sendNoWait(MqttWireMessage message, MqttToken token) throws MqttException { + final String methodName = "sendNoWait"; + + if (isConnected() || (!isConnected() && message instanceof MqttConnect) + || (isDisconnecting() && message instanceof MqttDisconnect)) { + + if (disconnectedMessageBuffer != null && disconnectedMessageBuffer.getMessageCount() != 0) { + // @TRACE 507=Client Connected, Offline Buffer available, but not empty. Adding + // message to buffer. message={0} + log.fine(CLASS_NAME, methodName, "507", new Object[] { message.getKey() }); + // If the message is a publish, strip the topic alias: + if(message instanceof MqttPublish && message.getProperties().getTopicAlias()!= null) { + MqttProperties messageProps = message.getProperties(); + messageProps.setTopicAlias(null); + message.setProperties(messageProps); + } + if (disconnectedMessageBuffer.isPersistBuffer()) { + this.clientState.persistBufferedMessage(message); + } + disconnectedMessageBuffer.putMessage(message, token); + + } else { + + if (message instanceof MqttPublish) { + // Override the QoS if the server has set a maximum + if (this.mqttConnection.getMaximumQoS() != null + && ((MqttPublish) message).getMessage().getQos() > this.mqttConnection.getMaximumQoS()) { + MqttMessage mqttMessage = ((MqttPublish) message).getMessage(); + mqttMessage.setQos(this.mqttConnection.getMaximumQoS()); + ((MqttPublish) message).setMessage(mqttMessage); + } + + // Override the Retain flag if the server has disabled it + if (this.mqttConnection.isRetainAvailable() != null + && ((MqttPublish) message).getMessage().isRetained() + && (this.mqttConnection.isRetainAvailable() == false)) { + MqttMessage mqttMessage = ((MqttPublish) message).getMessage(); + mqttMessage.setRetained(false); + ((MqttPublish) message).setMessage(mqttMessage); + } + + } + this.internalSend(message, token); + } + } else if (disconnectedMessageBuffer != null && isResting()) { + // @TRACE 508=Client Resting, Offline Buffer available. Adding message to + // buffer. message={0} + log.fine(CLASS_NAME, methodName, "508", new Object[] { message.getKey() }); + if (disconnectedMessageBuffer.isPersistBuffer()) { + this.clientState.persistBufferedMessage(message); + } + disconnectedMessageBuffer.putMessage(message, token); + } else { + // @TRACE 208=failed: not connected + log.fine(CLASS_NAME, methodName, "208"); + throw ExceptionHelper.createMqttException(MqttClientException.REASON_CODE_CLIENT_NOT_CONNECTED); + } + } + + /** + * Close and tidy up. + * + * Call each main class and let it tidy up e.g. releasing the token store which + * normally survives a disconnect. + * + * @param force + * force disconnection + * @throws MqttException + * if not disconnected + */ + public void close(boolean force) throws MqttException { + final String methodName = "close"; + synchronized (conLock) { + if (!isClosed()) { + // Must be disconnected before close can take place or if we are being forced + if (!isDisconnected() || force) { + // @TRACE 224=failed: not disconnected + log.fine(CLASS_NAME, methodName, "224"); + + if (isConnecting()) { + throw new MqttException(MqttClientException.REASON_CODE_CONNECT_IN_PROGRESS); + } else if (isConnected()) { + throw ExceptionHelper.createMqttException(MqttClientException.REASON_CODE_CLIENT_CONNECTED); + } else if (isDisconnecting()) { + closePending = true; + return; + } + } + + conState = CLOSED; + // Don't shut down an externally supplied executor service + //shutdownExecutorService(); + // ShutdownConnection has already cleaned most things + clientState.close(); + clientState = null; + callback = null; + persistence = null; + sender = null; + pingSender = null; + receiver = null; + networkModules = null; + conOptions = null; + tokenStore = null; + } + } + } + + /** + * Sends a connect message and waits for an ACK or NACK. Connecting is a special + * case which will also start up the network connection, receive thread, and + * keep alive thread. + * + * @param options + * The {@link MqttConnectionOptions} for the connection + * @param token + * The {@link MqttToken} to track the connection + * @throws MqttException + * if an error occurs when connecting + */ + public void connect(MqttConnectionOptions options, MqttToken token) throws MqttException { + final String methodName = "connect"; + synchronized (conLock) { + if (isDisconnected() && !closePending) { + // @TRACE 214=state=CONNECTING + log.fine(CLASS_NAME, methodName, "214"); + + conState = CONNECTING; + + conOptions = options; + + MqttConnect connect = new MqttConnect(client.getClientId(), conOptions.getMqttVersion(), + conOptions.isCleanStart(), conOptions.getKeepAliveInterval(), + conOptions.getConnectionProperties(), conOptions.getWillMessageProperties()); + + if (conOptions.getWillDestination() != null) { + connect.setWillDestination(conOptions.getWillDestination()); + } + + if (conOptions.getWillMessage() != null) { + connect.setWillMessage(conOptions.getWillMessage()); + } + + if (conOptions.getUserName() != null) { + connect.setUserName(conOptions.getUserName()); + } + if (conOptions.getPassword() != null) { + connect.setPassword(conOptions.getPassword()); + } + + /* + * conOptions.getUserName(), conOptions.getPassword(), + * conOptions.getWillMessage(), conOptions.getWillDestination() + */ + this.mqttConnection.setKeepAliveSeconds(conOptions.getKeepAliveInterval()); + this.clientState.setCleanStart(conOptions.isCleanStart()); + + tokenStore.open(); + ConnectBG conbg = new ConnectBG(this, token, connect, executorService); + conbg.start(); + } else { + // @TRACE 207=connect failed: not disconnected {0} + log.fine(CLASS_NAME, methodName, "207", new Object[] { Byte.valueOf(conState) }); + if (isClosed() || closePending) { + throw new MqttException(MqttClientException.REASON_CODE_CLIENT_CLOSED); + } else if (isConnecting()) { + throw new MqttException(MqttClientException.REASON_CODE_CONNECT_IN_PROGRESS); + } else if (isDisconnecting()) { + throw new MqttException(MqttClientException.REASON_CODE_CLIENT_DISCONNECTING); + } else { + throw ExceptionHelper.createMqttException(MqttClientException.REASON_CODE_CLIENT_CONNECTED); + } + } + } + } + + public void connectComplete(MqttConnAck cack, MqttException mex) throws MqttException { + final String methodName = "connectComplete"; + int rc = cack.getReturnCode(); + synchronized (conLock) { + if (rc == 0) { + // We've successfully connected + // @TRACE 215=state=CONNECTED + log.fine(CLASS_NAME, methodName, "215"); + + conState = CONNECTED; + return; + } + } + + // @TRACE 204=connect failed: rc={0} + log.fine(CLASS_NAME, methodName, "204", new Object[] { Integer.valueOf(rc) }); + throw mex; + } + + /** + * Shuts down the connection to the server. This may have been invoked as a + * result of a user calling disconnect or an abnormal disconnection. The method + * may be invoked multiple times in parallel as each thread when it receives an + * error uses this method to ensure that shutdown completes successfully. + * + * @param token + * the {@link MqttToken} To track closing the connection + * @param reason + * the {@link MqttException} thrown requiring the connection to be + * shut down. + * @param message + * the {@link MqttDisconnect} that triggered the connection to be + * shut down. + */ + public void shutdownConnection(MqttToken token, MqttException reason, MqttDisconnect message) { + final String methodName = "shutdownConnection"; + + boolean wasConnected; + MqttToken endToken = null; // Token to notify after disconnect completes + + // This method could concurrently be invoked from many places only allow it + // to run once. + synchronized (conLock) { + if (stoppingComms || closePending || isClosed()) { + return; + } + stoppingComms = true; + + // @TRACE 216=state=DISCONNECTING + log.fine(CLASS_NAME, methodName, "216"); + + wasConnected = (isConnected() || isDisconnecting()); + conState = DISCONNECTING; + } + + // Update the token with the reason for shutdown if it + // is not already complete. + if (token != null && !token.isComplete()) { + token.internalTok.setException(reason); + } + + // Stop the thread that is used to call the user back + // when actions complete + if (callback != null) { + callback.stop(); + } + + // Stop the thread that handles inbound work from the network + if (receiver != null) { + receiver.stop(); + } + + // Stop the network module, send and receive now not possible + try { + if (networkModules != null) { + NetworkModule networkModule = networkModules[networkModuleIndex]; + if (networkModule != null) { + networkModule.stop(); + } + } + } catch (Exception ioe) { + // Ignore as we are shutting down + } + + // Stop any new tokens being saved by app and throwing an exception if they do + tokenStore.quiesce(new MqttException(MqttClientException.REASON_CODE_CLIENT_DISCONNECTING)); + + // Notify any outstanding tokens with the exception of + // con or discon which may be returned and will be notified at + // the end + endToken = handleOldTokens(token, reason); + + try { + // Clean session handling and tidy up + clientState.disconnected(reason); + if (clientState.getCleanStart()) + callback.removeMessageListeners(); + } catch (Exception ex) { + // Ignore as we are shutting down + } + + if (sender != null) { + sender.stop(); + } + + if (pingSender != null) { + pingSender.stop(); + } + + try { + if (disconnectedMessageBuffer == null && persistence != null) { + persistence.close(); + } + + } catch (Exception ex) { + // Ignore as we are shutting down + } + // All disconnect logic has been completed allowing the + // client to be marked as disconnected. + synchronized (conLock) { + // @TRACE 217=state=DISCONNECTED + log.fine(CLASS_NAME, methodName, "217"); + + conState = DISCONNECTED; + stoppingComms = false; + } + + // Internal disconnect processing has completed. If there + // is a disconnect token or a connect in error notify + // it now. This is done at the end to allow a new connect + // to be processed and now throw a currently disconnecting error. + // any outstanding tokens and unblock any waiters + if (endToken != null && callback != null) { + callback.asyncOperationComplete(endToken); + } + if (wasConnected && callback != null) { + // Let the user know client has disconnected either normally or abnormally + callback.connectionLost(reason, message); + } + + // While disconnecting, close may have been requested - try it now + synchronized (conLock) { + if (closePending) { + try { + close(true); + } catch (Exception e) { // ignore any errors as closing + } + } + } + } + + // Tidy up. There may be tokens outstanding as the client was + // not disconnected/quiseced cleanly! Work out what tokens still + // need to be notified and waiters unblocked. Store the + // disconnect or connect token to notify after disconnect is + // complete. + private MqttToken handleOldTokens(MqttToken token, MqttException reason) { + final String methodName = "handleOldTokens"; + // @TRACE 222=> + log.fine(CLASS_NAME, methodName, "222"); + + MqttToken tokToNotifyLater = null; + try { + // First the token that was related to the disconnect / shutdown may + // not be in the token table - temporarily add it if not + if (token != null) { + if (tokenStore.getToken(token.internalTok.getKey()) == null) { + tokenStore.saveToken(token, token.internalTok.getKey()); + } + } + + Vector toksToNot = clientState.resolveOldTokens(reason); + Enumeration toksToNotE = toksToNot.elements(); + while (toksToNotE.hasMoreElements()) { + MqttToken tok = (MqttToken) toksToNotE.nextElement(); + + if (tok.internalTok.getKey().equals(MqttDisconnect.KEY) + || tok.internalTok.getKey().equals(MqttConnect.KEY)) { + // Its con or discon so remember and notify @ end of disc routine + tokToNotifyLater = tok; + } else { + // notify waiters and callbacks of outstanding tokens + // that a problem has occurred and disconnect is in + // progress + callback.asyncOperationComplete(tok); + } + } + } catch (Exception ex) { + // Ignore as we are shutting down + } + return tokToNotifyLater; + } + + public void disconnect(MqttDisconnect disconnect, long quiesceTimeout, MqttToken token) throws MqttException { + final String methodName = "disconnect"; + synchronized (conLock) { + if (isClosed()) { + // @TRACE 223=failed: in closed state + log.fine(CLASS_NAME, methodName, "223"); + throw ExceptionHelper.createMqttException(MqttClientException.REASON_CODE_CLIENT_CLOSED); + } else if (isDisconnected()) { + // @TRACE 211=failed: already disconnected + log.fine(CLASS_NAME, methodName, "211"); + throw ExceptionHelper.createMqttException(MqttClientException.REASON_CODE_CLIENT_ALREADY_DISCONNECTED); + } else if (isDisconnecting()) { + // @TRACE 219=failed: already disconnecting + log.fine(CLASS_NAME, methodName, "219"); + throw ExceptionHelper.createMqttException(MqttClientException.REASON_CODE_CLIENT_DISCONNECTING); + } else if (Thread.currentThread() == callback.getThread()) { + // @TRACE 210=failed: called on callback thread + log.fine(CLASS_NAME, methodName, "210"); + // Not allowed to call disconnect() from the callback, as it will deadlock. + throw ExceptionHelper.createMqttException(MqttClientException.REASON_CODE_CLIENT_DISCONNECT_PROHIBITED); + } + + // @TRACE 218=state=DISCONNECTING + log.fine(CLASS_NAME, methodName, "218"); + conState = DISCONNECTING; + DisconnectBG discbg = new DisconnectBG(disconnect, quiesceTimeout, token, executorService); + discbg.start(); + } + } + + public void disconnectForcibly(long quiesceTimeout, long disconnectTimeout, int reasonCode, + MqttProperties disconnectProperties) throws MqttException { + disconnectForcibly(quiesceTimeout, disconnectTimeout, true, reasonCode, disconnectProperties); + } + + /** + * Disconnect the connection and reset all the states. + * + * @param quiesceTimeout + * How long to wait whilst quiesing before messages are deleted. + * @param disconnectTimeout + * How long to wait whilst disconnecting + * @param sendDisconnectPacket + * If true, will send a disconnect packet + * @param reasonCode + * the disconnection reason code. + * @param disconnectProperties + * the {@link MqttProperties} to send in the Disconnect packet. + * @throws MqttException + * if an error occurs whilst disconnecting + */ + public void disconnectForcibly(long quiesceTimeout, long disconnectTimeout, boolean sendDisconnectPacket, + int reasonCode, MqttProperties disconnectProperties) throws MqttException { + conState = DISCONNECTING; + // Allow current inbound and outbound work to complete + if (clientState != null) { + clientState.quiesce(quiesceTimeout); + } + MqttToken token = new MqttToken(client.getClientId()); + try { + // Send disconnect packet + if (sendDisconnectPacket) { + internalSend(new MqttDisconnect(reasonCode, disconnectProperties), token); + + // Wait util the disconnect packet sent with timeout + token.waitForCompletion(disconnectTimeout); + } + } catch (Exception ex) { + // ignore, probably means we failed to send the disconnect packet. + } finally { + token.internalTok.markComplete(null, null); + shutdownConnection(token, null, null); + } + } + + public boolean isConnected() { + synchronized (conLock) { + return conState == CONNECTED; + } + } + + public boolean isConnecting() { + synchronized (conLock) { + return conState == CONNECTING; + } + } + + public boolean isDisconnected() { + synchronized (conLock) { + return conState == DISCONNECTED; + } + } + + public boolean isDisconnecting() { + synchronized (conLock) { + return conState == DISCONNECTING; + } + } + + public boolean isClosed() { + synchronized (conLock) { + return conState == CLOSED; + } + } + + public boolean isResting() { + synchronized (conLock) { + return resting; + } + } + + public void setCallback(MqttCallback mqttCallback) { + this.callback.setCallback(mqttCallback); + } + + public void setReconnectCallback(MqttCallback callback) { + this.callback.setReconnectCallback(callback); + } + + public void setManualAcks(boolean manualAcks) { + this.callback.setManualAcks(manualAcks); + } + + public void messageArrivedComplete(int messageId, int qos) throws MqttException { + this.callback.messageArrivedComplete(messageId, qos); + } + + public void setMessageListener(Integer subscriptionId, String topicFilter, IMqttMessageListener messageListener) { + this.callback.setMessageListener(subscriptionId, topicFilter, messageListener); + } + + public void removeMessageListener(String topicFilter) { + this.callback.removeMessageListener(topicFilter); + } + + protected MqttTopic getTopic(String topic) { + return new MqttTopic(topic, this); + } + + public void setNetworkModuleIndex(int index) { + this.networkModuleIndex = index; + } + + public int getNetworkModuleIndex() { + return networkModuleIndex; + } + + public NetworkModule[] getNetworkModules() { + return networkModules; + } + + public void setNetworkModules(NetworkModule[] networkModules) { + this.networkModules = networkModules; + } + + public MqttToken[] getPendingTokens() { + return tokenStore.getOutstandingDelTokens(); + } + + protected void deliveryComplete(MqttPublish msg) throws MqttPersistenceException { + this.clientState.deliveryComplete(msg); + } + + protected void deliveryComplete(int messageId) throws MqttPersistenceException { + this.clientState.deliveryComplete(messageId); + } + + public MqttClientInterface getClient() { + return client; + } + + public long getKeepAlive() { + return this.mqttConnection.getKeepAlive(); + } + + public MqttState getClientState() { + return clientState; + } + + public MqttConnectionOptions getConOptions() { + return conOptions; + } + + public Properties getDebug() { + Properties props = new Properties(); + props.put("conState", Integer.valueOf(conState)); + props.put("serverURI", getClient().getServerURI()); + props.put("callback", callback); + props.put("stoppingComms", Boolean.valueOf(stoppingComms)); + return props; + } + + // Kick off the connect processing in the background so that it does not block. + // For instance + // the socket could take time to create. + private class ConnectBG implements Runnable { + ClientComms clientComms = null; + MqttToken conToken; + MqttConnect conPacket; + private String threadName; + + ConnectBG(ClientComms cc, MqttToken cToken, MqttConnect cPacket, ExecutorService executorService) { + clientComms = cc; + conToken = cToken; + conPacket = cPacket; + threadName = "MQTT Con: " + getClient().getClientId(); + } + + void start() { + if (executorService == null) { + new Thread(this).start(); + } else { + executorService.execute(this); + } + } + + public void run() { + Thread.currentThread().setName(threadName); + final String methodName = "connectBG:run"; + MqttException mqttEx = null; + // @TRACE 220=> + log.fine(CLASS_NAME, methodName, "220"); + + try { + // Reset an exception on existing delivery tokens. + // This will have been set if disconnect occurred before delivery was + // fully processed. + MqttToken[] toks = tokenStore.getOutstandingDelTokens(); + for (MqttToken tok : toks) { + tok.internalTok.setException(null); + } + + // Save the connect token in tokenStore as failure can occur before send + tokenStore.saveToken(conToken, conPacket); + + // Connect to the server at the network level e.g. TCP socket and then + // start the background processing threads before sending the connect + // packet. + NetworkModule networkModule = networkModules[networkModuleIndex]; + networkModule.start(); + receiver = new CommsReceiver(clientComms, clientState, tokenStore, networkModule.getInputStream()); + receiver.start("MQTT Rec: " + getClient().getClientId(), executorService); + sender = new CommsSender(clientComms, clientState, tokenStore, networkModule.getOutputStream()); + sender.start("MQTT Snd: " + getClient().getClientId(), executorService); + callback.start("MQTT Call: " + getClient().getClientId(), executorService); + internalSend(conPacket, conToken); + } catch (MqttException ex) { + // @TRACE 212=connect failed: unexpected exception + log.fine(CLASS_NAME, methodName, "212", null, ex); + mqttEx = ex; + } catch (Exception ex) { + // @TRACE 209=connect failed: unexpected exception + log.fine(CLASS_NAME, methodName, "209", null, ex); + mqttEx = ExceptionHelper.createMqttException(ex); + } + + if (mqttEx != null) { + shutdownConnection(conToken, mqttEx, null); + } + } + } + + // Kick off the disconnect processing in the background so that it does not + // block. For instance + // the quiesce + private class DisconnectBG implements Runnable { + MqttDisconnect disconnect; + long quiesceTimeout; + MqttToken token; + private String threadName; + + DisconnectBG(MqttDisconnect disconnect, long quiesceTimeout, MqttToken token, ExecutorService executorService) { + this.disconnect = disconnect; + this.quiesceTimeout = quiesceTimeout; + this.token = token; + } + + void start() { + threadName = "MQTT Disc: "+getClient().getClientId(); + if (executorService == null) { + new Thread(this).start(); + } else { + executorService.execute(this); + } + } + + public void run() { + Thread.currentThread().setName(threadName); + final String methodName = "disconnectBG:run"; + // @TRACE 221=> + log.fine(CLASS_NAME, methodName, "221"); + + // Allow current inbound and outbound work to complete + clientState.quiesce(quiesceTimeout); + try { + internalSend(disconnect, token); + // do not wait if the sender process is not running + if (sender != null && sender.isRunning()) { + token.internalTok.waitUntilSent(); + } + } + catch (MqttException ex) { + } + finally { + token.internalTok.markComplete(null, null); + if (sender == null || !sender.isRunning()) { + // if the sender process is not running + token.internalTok.notifyComplete(); + } + shutdownConnection(token, null, null); + } + } + } + + /* + * Check and send a ping if needed and check for ping timeout. Need to send a + * ping if nothing has been sent or received in the last keepalive interval. + */ + public MqttToken checkForActivity() { + return this.checkForActivity(null); + } + + /* + * Check and send a ping if needed and check for ping timeout. Need to send a + * ping if nothing has been sent or received in the last keepalive interval. + * Passes an IMqttActionListener to ClientState.checkForActivity so that the + * callbacks are attached as soon as the token is created (Bug 473928) + */ + public MqttToken checkForActivity(MqttActionListener pingCallback) { + MqttToken token = null; + try { + token = clientState.checkForActivity(pingCallback); + } catch (MqttException e) { + handleRunException(e); + } catch (Exception e) { + handleRunException(e); + } + return token; + } + + private void handleRunException(Exception ex) { + final String methodName = "handleRunException"; + // @TRACE 804=exception + log.fine(CLASS_NAME, methodName, "804", null, ex); + MqttException mex; + if (!(ex instanceof MqttException)) { + mex = new MqttException(MqttClientException.REASON_CODE_CONNECTION_LOST, ex); + } else { + mex = (MqttException) ex; + } + + shutdownConnection(null, mex, null); + } + + /** + * When Automatic reconnect is enabled, we want ClientComs to enter the + * 'resting' state if disconnected. This will allow us to publish messages + * + * @param resting + * if true, resting is enabled + */ + public void setRestingState(boolean resting) { + this.resting = resting; + } + + public void setDisconnectedMessageBuffer(DisconnectedMessageBuffer disconnectedMessageBuffer) { + this.disconnectedMessageBuffer = disconnectedMessageBuffer; + } + + public int getBufferedMessageCount() { + return this.disconnectedMessageBuffer.getMessageCount(); + } + + public MqttMessage getBufferedMessage(int bufferIndex) { + MqttPublish send = (MqttPublish) this.disconnectedMessageBuffer.getMessage(bufferIndex).getMessage(); + return send.getMessage(); + } + + public void deleteBufferedMessage(int bufferIndex) { + this.disconnectedMessageBuffer.deleteMessage(bufferIndex); + } + + /** + * When the client automatically reconnects, we want to send all messages from + * the buffer first before allowing the user to send any messages + */ + public void notifyReconnect() { + final String methodName = "notifyReconnect"; + if (disconnectedMessageBuffer != null) { + // @TRACE 509=Client Reconnected, Offline Buffer Available. Sending Buffered + // Messages. + log.fine(CLASS_NAME, methodName, "509"); + + disconnectedMessageBuffer.setPublishCallback(new ReconnectDisconnectedBufferCallback(methodName)); + if (executorService == null) { + new Thread(disconnectedMessageBuffer).start(); + } else { + executorService.execute(disconnectedMessageBuffer); + } + } + } + + class ReconnectDisconnectedBufferCallback implements IDisconnectedBufferCallback { + + final String methodName; + + ReconnectDisconnectedBufferCallback(String methodName) { + this.methodName = methodName; + } + + public void publishBufferedMessage(BufferedMessage bufferedMessage) throws MqttException { + if (isConnected()) { + // @TRACE 510=Publising Buffered message message={0} + log.fine(CLASS_NAME, methodName, "510", new Object[] { bufferedMessage.getMessage().getKey() }); + internalSend(bufferedMessage.getMessage(), bufferedMessage.getToken()); + // Delete from persistence if in there + clientState.unPersistBufferedMessage(bufferedMessage.getMessage()); + } else { + // @TRACE 208=failed: not connected + log.fine(CLASS_NAME, methodName, "208"); + throw ExceptionHelper.createMqttException(MqttClientException.REASON_CODE_CLIENT_NOT_CONNECTED); + } + } + } + + public int getActualInFlight() { + return this.clientState.getActualInFlight(); + } + + public boolean doesSubscriptionIdentifierExist(int subscriptionIdentifier) { + return this.callback.doesSubscriptionIdentifierExist(subscriptionIdentifier); + + } + +} diff --git a/src/main/java/org/eclipse/paho/mqttv5/client/internal/ClientState.java b/src/main/java/org/eclipse/paho/mqttv5/client/internal/ClientState.java new file mode 100644 index 0000000..216d62c --- /dev/null +++ b/src/main/java/org/eclipse/paho/mqttv5/client/internal/ClientState.java @@ -0,0 +1,1625 @@ +/******************************************************************************* + * Copyright (c) 2009, 2018 IBM Corp and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * and Eclipse Distribution License v1.0 which accompany this distribution. + * + * The Eclipse Public License is available at + * https://www.eclipse.org/legal/epl-2.0 + * and the Eclipse Distribution License is available at + * https://www.eclipse.org/org/documents/edl-v10.php + * + * Contributors: + * Dave Locke - initial API and implementation and/or initial documentation + * Ian Craggs - fix duplicate message id (Bug 466853) + * Ian Craggs - ack control (bug 472172) + * James Sutton - Ping Callback (bug 473928) + * Ian Craggs - fix for NPE bug 470718 + * James Sutton - Automatic Reconnect & Offline Buffering + * Jens Reimann - Fix issue #370 + * James Sutton - Mqttv5 - Outgoing Topic Aliases + */ +package org.eclipse.paho.mqttv5.client.internal; + +import java.io.EOFException; +import java.util.Enumeration; +import java.util.Hashtable; +import java.util.Properties; +import java.util.Vector; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; + +import org.eclipse.paho.mqttv5.client.MqttActionListener; +import org.eclipse.paho.mqttv5.client.MqttClientException; +import org.eclipse.paho.mqttv5.client.MqttClientPersistence; +import org.eclipse.paho.mqttv5.client.MqttPingSender; +import org.eclipse.paho.mqttv5.client.MqttToken; +import org.eclipse.paho.mqttv5.client.logging.Logger; +import org.eclipse.paho.mqttv5.client.logging.LoggerFactory; +import org.eclipse.paho.mqttv5.common.MqttException; +import org.eclipse.paho.mqttv5.common.MqttMessage; +import org.eclipse.paho.mqttv5.common.MqttPersistable; +import org.eclipse.paho.mqttv5.common.MqttPersistenceException; +import org.eclipse.paho.mqttv5.common.packet.MqttAck; +import org.eclipse.paho.mqttv5.common.packet.MqttAuth; +import org.eclipse.paho.mqttv5.common.packet.MqttConnAck; +import org.eclipse.paho.mqttv5.common.packet.MqttConnect; +import org.eclipse.paho.mqttv5.common.packet.MqttPingReq; +import org.eclipse.paho.mqttv5.common.packet.MqttPingResp; +import org.eclipse.paho.mqttv5.common.packet.MqttProperties; +import org.eclipse.paho.mqttv5.common.packet.MqttPubAck; +import org.eclipse.paho.mqttv5.common.packet.MqttPubComp; +import org.eclipse.paho.mqttv5.common.packet.MqttPubRec; +import org.eclipse.paho.mqttv5.common.packet.MqttPubRel; +import org.eclipse.paho.mqttv5.common.packet.MqttPublish; +import org.eclipse.paho.mqttv5.common.packet.MqttReturnCode; +import org.eclipse.paho.mqttv5.common.packet.MqttWireMessage; + +/** + * The core of the client, which holds the state information for pending and + * in-flight messages. + * + * Messages that have been accepted for delivery are moved between several + * objects while being delivered. + * + * 1) When the client is not running messages are stored in a persistent store + * that implements the MqttClientPersistent Interface. The default is + * MqttDefaultFilePersistencew which stores messages safely across failures and + * system restarts. If no persistence is specified there is a fall back to + * MemoryPersistence which will maintain the messages while the Mqtt client is + * instantiated. + * + * 2) When the client or specifically ClientState is instantiated the messages + * are read from the persistent store into: - outboundqos2 hashtable if a QoS 2 + * PUBLISH or PUBREL - outboundqos1 hashtable if a QoS 1 PUBLISH (see + * restoreState) + * + * 3) On Connect, copy messages from the outbound hashtables to the + * pendingMessages or pendingFlows vector in messageid order. - Initial message + * publish goes onto the pendingmessages buffer. - PUBREL goes onto the + * pendingflows buffer (see restoreInflightMessages) + * + * 4) Sender thread reads messages from the pendingflows and pendingmessages + * buffer one at a time. The message is removed from the pendingbuffer but + * remains on the outbound* hashtable. The hashtable is the place where the full + * set of outstanding messages are stored in memory. (Persistence is only used + * at start up) + * + * 5) Receiver thread - receives wire messages: - if QoS 1 then remove from + * persistence and outboundqos1 - if QoS 2 PUBREC send PUBREL. Updating the + * outboundqos2 entry with the PUBREL and update persistence. - if QoS 2 PUBCOMP + * remove from persistence and outboundqos2 + * + * Notes: because of the multithreaded nature of the client it is vital that any + * changes to this class take concurrency into account. For instance as soon as + * a flow / message is put on the wire it is possible for the receiving thread + * to receive the ack and to be processing the response before the sending side + * has finished processing. For instance a connect may be sent, the conack + * received before the connect notify send has been processed! + * + */ +public class ClientState implements MqttState { + private static final String CLASS_NAME = ClientState.class.getName(); + private Logger log = LoggerFactory.getLogger(LoggerFactory.MQTT_CLIENT_MSG_CAT, CLASS_NAME); + private static final String PERSISTENCE_SENT_PREFIX = "s-"; + private static final String PERSISTENCE_SENT_BUFFERED_PREFIX = "sb-"; + private static final String PERSISTENCE_CONFIRMED_PREFIX = "sc-"; + private static final String PERSISTENCE_RECEIVED_PREFIX = "r-"; + + private static final int MIN_MSG_ID = 1; // Lowest possible MQTT message ID to use + private static final int MAX_MSG_ID = 65535; // Highest possible MQTT message ID to use + private int nextMsgId = MIN_MSG_ID - 1; // The next available message ID to use + private ConcurrentHashMap inUseMsgIds; // Used to store a set of in-use message IDs + + volatile private Vector pendingMessages; + volatile private Vector pendingFlows; + + private CommsTokenStore tokenStore; + private ClientComms clientComms = null; + private CommsCallback callback = null; + //private long keepAlive; + private boolean cleanStart; + private MqttClientPersistence persistence; + + private int actualInFlight = 0; + private int inFlightPubRels = 0; + + private final Object queueLock = new Object(); + private final Object quiesceLock = new Object(); + private boolean quiescing = false; + + private long lastOutboundActivity = 0; + private long lastInboundActivity = 0; + private long lastPing = 0; + private MqttWireMessage pingCommand; + private final Object pingOutstandingLock = new Object(); + private int pingOutstanding = 0; + + private boolean connected = false; + + private ConcurrentHashMap outboundQoS2 = null; + private ConcurrentHashMap outboundQoS1 = null; + private ConcurrentHashMap outboundQoS0 = null; + private ConcurrentHashMap inboundQoS2 = null; + + private MqttPingSender pingSender = null; + + // Topic Alias Maps + private Hashtable outgoingTopicAliases; + private Hashtable incomingTopicAliases; + + private MqttConnectionState mqttConnection; + + protected ClientState(MqttClientPersistence persistence, CommsTokenStore tokenStore, CommsCallback callback, + ClientComms clientComms, MqttPingSender pingSender, MqttConnectionState mqttConnection) + throws MqttException { + + log.setResourceName(clientComms.getClient().getClientId()); + log.finer(CLASS_NAME, "", ""); + + inUseMsgIds = new ConcurrentHashMap<>(); + pendingFlows = new Vector(); + pendingMessages = new Vector(mqttConnection.getReceiveMaximum()); + outboundQoS2 = new ConcurrentHashMap<>(); + outboundQoS1 = new ConcurrentHashMap<>(); + outboundQoS0 = new ConcurrentHashMap<>(); + inboundQoS2 = new ConcurrentHashMap<>(); + pingCommand = new MqttPingReq(); + inFlightPubRels = 0; + actualInFlight = 0; + this.outgoingTopicAliases = new Hashtable(); + this.incomingTopicAliases = new Hashtable(); + + this.persistence = persistence; + this.callback = callback; + this.tokenStore = tokenStore; + this.clientComms = clientComms; + this.pingSender = pingSender; + this.mqttConnection = mqttConnection; + + restoreState(); + } + + protected void setCleanStart(boolean cleanStart) { + this.cleanStart = cleanStart; + } + + protected boolean getCleanStart() { + return this.cleanStart; + } + + private String getSendPersistenceKey(MqttWireMessage message) { + return PERSISTENCE_SENT_PREFIX + message.getMessageId(); + } + + private String getSendConfirmPersistenceKey(MqttWireMessage message) { + return PERSISTENCE_CONFIRMED_PREFIX + message.getMessageId(); + } + + private String getReceivedPersistenceKey(MqttWireMessage message) { + return PERSISTENCE_RECEIVED_PREFIX + message.getMessageId(); + } + + private String getReceivedPersistenceKey(int messageId) { + return PERSISTENCE_RECEIVED_PREFIX + messageId; + } + + private String getSendBufferedPersistenceKey(MqttWireMessage message) { + return PERSISTENCE_SENT_BUFFERED_PREFIX + message.getMessageId(); + } + + protected void clearState() throws MqttException { + final String methodName = "clearState"; + // @TRACE 603=clearState + log.fine(CLASS_NAME, methodName, ">"); + + persistence.clear(); + inUseMsgIds.clear(); + pendingMessages.clear(); + pendingFlows.clear(); + outboundQoS2.clear(); + outboundQoS1.clear(); + outboundQoS0.clear(); + inboundQoS2.clear(); + tokenStore.clear(); + outgoingTopicAliases.clear(); + incomingTopicAliases.clear(); + } + + protected void clearConnectionState() throws MqttException { + final String methodName = "clearConnectionState"; + // @TRACE=665=Clearing Connection State (Topic Aliases) + log.fine(CLASS_NAME, methodName, "665"); + outgoingTopicAliases.clear(); + incomingTopicAliases.clear(); + + } + + private MqttWireMessage restoreMessage(String key, MqttPersistable persistable) throws MqttException { + final String methodName = "restoreMessage"; + MqttWireMessage message = null; + + try { + message = MqttWireMessage.createWireMessage(persistable); + } catch (MqttException ex) { + // @TRACE 602=key={0} exception + log.fine(CLASS_NAME, methodName, "602", new Object[] { key }, ex); + if (ex.getCause() instanceof EOFException) { + // Premature end-of-file means that the message is corrupted + if (key != null) { + persistence.remove(key); + } + } else { + throw ex; + } + } + // @TRACE 601=key={0} message={1} + log.fine(CLASS_NAME, methodName, "601", new Object[] { key, message }); + return message; + } + + /** + * Inserts a new message to the list, ensuring that list is ordered from lowest + * to highest in terms of the message id's. + * + * @param list + * the list to insert the message into + * @param newMsg + * the message to insert into the list + */ + private void insertInOrder(Vector list, MqttWireMessage newMsg) { + int newMsgId = newMsg.getMessageId(); + for (int i = 0; i < list.size(); i++) { + MqttWireMessage otherMsg = (MqttWireMessage) list.elementAt(i); + int otherMsgId = otherMsg.getMessageId(); + if (otherMsgId > newMsgId) { + list.insertElementAt(newMsg, i); + return; + } + } + list.addElement(newMsg); + } + + /** + * Produces a new list with the messages properly ordered according to their + * message id's. + * + * @param list + * the list containing the messages to produce a new reordered list + * for - this will not be modified or replaced, i.e., be read-only to + * this method + * @return a new reordered list + */ + private Vector reOrder(Vector list) { + + // here up the new list + Vector newList = new Vector(); + + if (list.size() == 0) { + return newList; // nothing to reorder + } + + int previousMsgId = 0; + int largestGap = 0; + int largestGapMsgIdPosInList = 0; + for (int i = 0; i < list.size(); i++) { + int currentMsgId = ((MqttWireMessage) list.elementAt(i)).getMessageId(); + if (currentMsgId - previousMsgId > largestGap) { + largestGap = currentMsgId - previousMsgId; + largestGapMsgIdPosInList = i; + } + previousMsgId = currentMsgId; + } + int lowestMsgId = ((MqttWireMessage) list.elementAt(0)).getMessageId(); + int highestMsgId = previousMsgId; // last in the sorted list + + // we need to check that the gap after highest msg id to the lowest msg id is + // not beaten + if (MAX_MSG_ID - highestMsgId + lowestMsgId > largestGap) { + largestGapMsgIdPosInList = 0; + } + + // starting message has been located, let's start from this point on + for (int i = largestGapMsgIdPosInList; i < list.size(); i++) { + newList.addElement(list.elementAt(i)); + } + + // and any wrapping back to the beginning + for (int i = 0; i < largestGapMsgIdPosInList; i++) { + newList.addElement(list.elementAt(i)); + } + + return newList; + } + + /** + * Restores the state information from persistence. + * + * @throws MqttException + * if an exception occurs whilst restoring state + */ + protected void restoreState() throws MqttException { + final String methodName = "restoreState"; + Enumeration messageKeys = persistence.keys(); + MqttPersistable persistable; + String key; + int highestMsgId = nextMsgId; + Vector orphanedPubRels = new Vector(); + // @TRACE 600=> + log.fine(CLASS_NAME, methodName, "600"); + + while (messageKeys.hasMoreElements()) { + key = (String) messageKeys.nextElement(); + persistable = persistence.get(key); + MqttWireMessage message = restoreMessage(key, persistable); + if (message != null) { + if (key.startsWith(PERSISTENCE_RECEIVED_PREFIX)) { + // @TRACE 604=inbound QoS 2 publish key={0} message={1} + log.fine(CLASS_NAME, methodName, "604", new Object[] { key, message }); + + // The inbound messages that we have persisted will be QoS 2 + inboundQoS2.put(Integer.valueOf(message.getMessageId()), message); + } else if (key.startsWith(PERSISTENCE_SENT_PREFIX)) { + MqttPublish sendMessage = (MqttPublish) message; + highestMsgId = Math.max(sendMessage.getMessageId(), highestMsgId); + if (persistence.containsKey(getSendConfirmPersistenceKey(sendMessage))) { + MqttPersistable persistedConfirm = persistence.get(getSendConfirmPersistenceKey(sendMessage)); + // QoS 2, and CONFIRM has already been sent... + // NO DUP flag is allowed for 3.1.1 spec while it's not clear for 3.1 spec + // So we just remove DUP + MqttPubRel confirmMessage = (MqttPubRel) restoreMessage(key, persistedConfirm); + if (confirmMessage != null) { + // confirmMessage.setDuplicate(true); // REMOVED + // @TRACE 605=outbound QoS 2 pubrel key={0} message={1} + log.fine(CLASS_NAME, methodName, "605", new Object[] { key, message }); + + outboundQoS2.put(Integer.valueOf(confirmMessage.getMessageId()), confirmMessage); + } else { + // @TRACE 606=outbound QoS 2 completed key={0} message={1} + log.fine(CLASS_NAME, methodName, "606", new Object[] { key, message }); + } + } else { + // QoS 1 or 2, with no CONFIRM sent... + // Put the SEND to the list of pending messages, ensuring message ID ordering... + sendMessage.setDuplicate(true); + if (sendMessage.getMessage().getQos() == 2) { + // @TRACE 607=outbound QoS 2 publish key={0} message={1} + log.fine(CLASS_NAME, methodName, "607", new Object[] { key, message }); + + outboundQoS2.put(Integer.valueOf(sendMessage.getMessageId()), sendMessage); + } else { + // @TRACE 608=outbound QoS 1 publish key={0} message={1} + log.fine(CLASS_NAME, methodName, "608", new Object[] { key, message }); + + outboundQoS1.put(Integer.valueOf(sendMessage.getMessageId()), sendMessage); + } + } + MqttToken tok = tokenStore.restoreToken(sendMessage); + tok.internalTok.setClient(clientComms.getClient()); + inUseMsgIds.put(Integer.valueOf(sendMessage.getMessageId()), + Integer.valueOf(sendMessage.getMessageId())); + } else if (key.startsWith(PERSISTENCE_SENT_BUFFERED_PREFIX)) { + + // Buffered outgoing messages that have not yet been sent at all + MqttPublish sendMessage = (MqttPublish) message; + highestMsgId = Math.max(sendMessage.getMessageId(), highestMsgId); + if (sendMessage.getMessage().getQos() == 2) { + // @TRACE 607=outbound QoS 2 publish key={0} message={1} + log.fine(CLASS_NAME, methodName, "607", new Object[] { key, message }); + outboundQoS2.put(Integer.valueOf(sendMessage.getMessageId()), sendMessage); + } else if (sendMessage.getMessage().getQos() == 1) { + // @TRACE 608=outbound QoS 1 publish key={0} message={1} + log.fine(CLASS_NAME, methodName, "608", new Object[] { key, message }); + + outboundQoS1.put(Integer.valueOf(sendMessage.getMessageId()), sendMessage); + + } else { + // @TRACE 511=outbound QoS 0 publish key={0} message={1} + log.fine(CLASS_NAME, methodName, "511", new Object[] { key, message }); + outboundQoS0.put(Integer.valueOf(sendMessage.getMessageId()), sendMessage); + // Because there is no Puback, we have to trust that this is enough to send the + // message + persistence.remove(key); + + } + + MqttToken tok = tokenStore.restoreToken(sendMessage); + tok.internalTok.setClient(clientComms.getClient()); + inUseMsgIds.put(Integer.valueOf(sendMessage.getMessageId()), + Integer.valueOf(sendMessage.getMessageId())); + + } else if (key.startsWith(PERSISTENCE_CONFIRMED_PREFIX)) { + MqttPubRel pubRelMessage = (MqttPubRel) message; + if (!persistence.containsKey(getSendPersistenceKey(pubRelMessage))) { + orphanedPubRels.addElement(key); + } + } + } + } + + messageKeys = orphanedPubRels.elements(); + while (messageKeys.hasMoreElements()) { + key = (String) messageKeys.nextElement(); + // @TRACE 609=removing orphaned pubrel key={0} + log.fine(CLASS_NAME, methodName, "609", new Object[] { key }); + + persistence.remove(key); + } + + nextMsgId = highestMsgId; + } + + private void restoreInflightMessages() { + final String methodName = "restoreInflightMessages"; + pendingMessages = new Vector(this.mqttConnection.getReceiveMaximum()); + pendingFlows = new Vector(); + + Enumeration keys = outboundQoS2.keys(); + while (keys.hasMoreElements()) { + Integer key = keys.nextElement(); + MqttWireMessage msg = (MqttWireMessage) outboundQoS2.get(key); + if (msg instanceof MqttPublish) { + // @TRACE 610=QoS 2 publish key={0} + log.fine(CLASS_NAME, methodName, "610", new Object[] { key }); + // set DUP flag only for PUBLISH, but NOT for PUBREL (spec 3.1.1) + msg.setDuplicate(true); + insertInOrder(pendingMessages, (MqttPublish) msg); + } else if (msg instanceof MqttPubRel) { + // @TRACE 611=QoS 2 pubrel key={0} + log.fine(CLASS_NAME, methodName, "611", new Object[] { key }); + + insertInOrder(pendingFlows, (MqttPubRel) msg); + } + } + keys = outboundQoS1.keys(); + while (keys.hasMoreElements()) { + Object key = keys.nextElement(); + MqttPublish msg = (MqttPublish) outboundQoS1.get(key); + msg.setDuplicate(true); + // @TRACE 612=QoS 1 publish key={0} + log.fine(CLASS_NAME, methodName, "612", new Object[] { key }); + + insertInOrder(pendingMessages, msg); + } + keys = outboundQoS0.keys(); + while (keys.hasMoreElements()) { + Object key = keys.nextElement(); + MqttPublish msg = (MqttPublish) outboundQoS0.get(key); + // @TRACE 512=QoS 0 publish key={0} + log.fine(CLASS_NAME, methodName, "512", new Object[] { key }); + insertInOrder(pendingMessages, msg); + + } + + this.pendingFlows = reOrder(pendingFlows); + this.pendingMessages = reOrder(pendingMessages); + } + + /* + * (non-Javadoc) + * + * @see org.eclipse.paho.mqttv5.client.internal.MqttState#send(org.eclipse.paho. + * client.mqttv3.internal.wire.MqttWireMessage, + * org.eclipse.paho.mqttv5.client.MqttToken) + */ + @Override + public void send(MqttWireMessage message, MqttToken token) throws MqttException { + final String methodName = "send"; + // Set Message ID if required + if (message.isMessageIdRequired() && (message.getMessageId() == 0)) { + message.setMessageId(getNextMessageId()); + } + // Set Topic Alias if required + if (message instanceof MqttPublish && ((MqttPublish) message).getTopicName() != null + && this.mqttConnection != null && this.mqttConnection.getOutgoingTopicAliasMaximum() > 0) { + String topic = ((MqttPublish) message).getTopicName(); + if (outgoingTopicAliases.containsKey(topic)) { + // Existing Topic Alias, Assign it and remove the topic string + ((MqttPublish) message).getProperties().setTopicAlias(outgoingTopicAliases.get(topic)); + ((MqttPublish) message).setTopicName(null); + } else { + int nextOutgoingTopicAlias = this.mqttConnection.getNextOutgoingTopicAlias(); + if (nextOutgoingTopicAlias <= this.mqttConnection.getOutgoingTopicAliasMaximum()) { + // Create a new Topic Alias and increment the counter + ((MqttPublish) message).getProperties().setTopicAlias(nextOutgoingTopicAlias); + outgoingTopicAliases.put(((MqttPublish) message).getTopicName(), nextOutgoingTopicAlias); + } + } + } + + if (token != null) { + try { + token.internalTok.setMessageID(message.getMessageId()); + } catch (Exception e) { + } + } + + if (message instanceof MqttPublish) { + synchronized (queueLock) { + if (actualInFlight >= this.mqttConnection.getReceiveMaximum()) { + // @TRACE 613= sending {0} msgs at max inflight window + log.fine(CLASS_NAME, methodName, "613", new Object[] { Integer.valueOf(actualInFlight) }); + + throw new MqttException(MqttClientException.REASON_CODE_MAX_INFLIGHT); + } + + MqttMessage innerMessage = ((MqttPublish) message).getMessage(); + // @TRACE 628=pending publish key={0} qos={1} message={2} + log.fine(CLASS_NAME, methodName, "628", new Object[] { Integer.valueOf(message.getMessageId()), + Integer.valueOf(innerMessage.getQos()), message }); + + switch (innerMessage.getQos()) { + case 2: + outboundQoS2.put(Integer.valueOf(message.getMessageId()), message); + persistence.put(getSendPersistenceKey(message), (MqttPublish) message); + break; + case 1: + outboundQoS1.put(Integer.valueOf(message.getMessageId()), message); + persistence.put(getSendPersistenceKey(message), (MqttPublish) message); + break; + } + tokenStore.saveToken(token, message); + pendingMessages.addElement(message); + queueLock.notifyAll(); + } + } else { + // @TRACE 615=pending send key={0} message {1} + log.fine(CLASS_NAME, methodName, "615", new Object[] { Integer.valueOf(message.getMessageId()), message }); + + if (message instanceof MqttConnect) { + synchronized (queueLock) { + // Add the connect action at the head of the pending queue ensuring it jumps + // ahead of any of other pending actions. + tokenStore.saveToken(token, message); + pendingFlows.insertElementAt(message, 0); + queueLock.notifyAll(); + } + } else { + if (message instanceof MqttPingReq) { + this.pingCommand = message; + } else if (message instanceof MqttPubRel) { + outboundQoS2.put(Integer.valueOf(message.getMessageId()), message); + persistence.put(getSendConfirmPersistenceKey(message), (MqttPubRel) message); + } else if (message instanceof MqttPubComp) { + persistence.remove(getReceivedPersistenceKey(message)); + } + + synchronized (queueLock) { + if (!(message instanceof MqttAck)) { + tokenStore.saveToken(token, message); + } + pendingFlows.addElement(message); + queueLock.notifyAll(); + } + } + } + } + + /* + * (non-Javadoc) + * + * @see + * org.eclipse.paho.mqttv5.client.internal.MqttState#persistBufferedMessage(org. + * eclipse.paho.client.mqttv3.internal.wire.MqttWireMessage) + */ + @Override + public void persistBufferedMessage(MqttWireMessage message) { + final String methodName = "persistBufferedMessage"; + String key = getSendBufferedPersistenceKey(message); + + // Because the client will have disconnected, we will want to re-open + // persistence + try { + message.setMessageId(getNextMessageId()); + key = getSendBufferedPersistenceKey(message); + try { + persistence.put(key, (MqttPublish) message); + } catch (MqttPersistenceException mpe) { + // @TRACE 515=Could not Persist, attempting to Re-Open Persistence Store + log.fine(CLASS_NAME, methodName, "515"); + persistence.open(this.clientComms.getClient().getClientId()); + persistence.put(key, (MqttPublish) message); + } + // @TRACE 513=Persisted Buffered Message key={0} + log.fine(CLASS_NAME, methodName, "513", new Object[] { key }); + } catch (MqttException ex) { + // @TRACE 514=Failed to persist buffered message key={0} + log.warning(CLASS_NAME, methodName, "513", new Object[] { key }); + } + } + + /* + * (non-Javadoc) + * + * @see + * org.eclipse.paho.mqttv5.client.internal.MqttState#unPersistBufferedMessage( + * org.eclipse.paho.mqttv5.client.internal.wire.MqttWireMessage) + */ + @Override + public void unPersistBufferedMessage(MqttWireMessage message) { + final String methodName = "unPersistBufferedMessage"; + try { + // @TRACE 517=Un-Persisting Buffered message key={0} + log.fine(CLASS_NAME, methodName, "517", new Object[] { message.getKey() }); + persistence.remove(getSendBufferedPersistenceKey(message)); + } catch (MqttPersistenceException mpe) { + // @TRACE 518=Failed to Un-Persist Buffered message key={0} + log.fine(CLASS_NAME, methodName, "518", new Object[] { message.getKey() }); + } + + } + + /** + * This removes the MqttSend message from the outbound queue and persistence. + * + * @param message + * the {@link MqttPublish} message to be removed + * @throws MqttPersistenceException + * if an exception occurs whilst removing the message + */ + protected void undo(MqttPublish message) throws MqttPersistenceException { + final String methodName = "undo"; + synchronized (queueLock) { + // @TRACE 618=key={0} QoS={1} + log.fine(CLASS_NAME, methodName, "618", new Object[] { Integer.valueOf(message.getMessageId()), + Integer.valueOf(message.getMessage().getQos()) }); + + if (message.getMessage().getQos() == 1) { + outboundQoS1.remove(Integer.valueOf(message.getMessageId())); + } else { + outboundQoS2.remove(Integer.valueOf(message.getMessageId())); + } + pendingMessages.removeElement(message); + persistence.remove(getSendPersistenceKey(message)); + tokenStore.removeToken(message); + if (message.getMessage().getQos() > 0) { + // Free this message Id so it can be used again + releaseMessageId(message.getMessageId()); + // Set the messageId to 0 so if it's ever retried, it will get a new messageId + message.setMessageId(0); + } + + checkQuiesceLock(); + } + } + + /* + * (non-Javadoc) + * + * @see org.eclipse.paho.mqttv5.client.internal.MqttState#checkForActivity(org. + * eclipse.paho.client.mqttv3.IMqttActionListener) + */ + @Override + public MqttToken checkForActivity(MqttActionListener pingCallback) throws MqttException { + final String methodName = "checkForActivity"; + // @TRACE 616=checkForActivity entered + log.fine(CLASS_NAME, methodName, "616", new Object[] {}); + + synchronized (quiesceLock) { + // ref bug: https://bugs.eclipse.org/bugs/show_bug.cgi?id=440698 + // No ping while quiescing + if (quiescing) { + return null; + } + } + + MqttToken token = null; + + long nextPingTime, keepAlive = TimeUnit.MILLISECONDS.toNanos(this.mqttConnection.getKeepAlive()); + + if (connected && this.mqttConnection.getKeepAlive() > 0) { + long time = System.nanoTime(); + // Reduce schedule frequency since System.currentTimeMillis is no accurate, add + // a buffer (This might not be needed since we moved to nanoTime) + // It is 1/10 in minimum keepalive unit. + int delta = 100000; + + // ref bug: https://bugs.eclipse.org/bugs/show_bug.cgi?id=446663 + synchronized (pingOutstandingLock) { + + // Is the broker connection lost because the broker did not reply to my ping? + if (pingOutstanding > 0 && (time - lastInboundActivity >= keepAlive + delta)) { + // lastInboundActivity will be updated once receiving is done. + // Add a delta, since the timer and System.currentTimeMillis() is not accurate. + // (This might not be needed since we moved to nanoTime) + // A ping is outstanding but no packet has been received in KA so connection is + // deemed broken + // @TRACE 619=Timed out as no activity, keepAlive={0} lastOutboundActivity={1} + // lastInboundActivity={2} time={3} lastPing={4} + log.severe(CLASS_NAME, methodName, "619", + new Object[] { Long.valueOf(keepAlive), Long.valueOf(lastOutboundActivity), + Long.valueOf(lastInboundActivity), Long.valueOf(time), Long.valueOf(lastPing) }); + + // A ping has already been sent. At this point, assume that the + // broker has hung and the TCP layer hasn't noticed. + throw ExceptionHelper.createMqttException(MqttClientException.REASON_CODE_CLIENT_TIMEOUT); + } + + // Is the broker connection lost because I could not get any successful write + // for 2 keepAlive intervals? + if (pingOutstanding == 0 && (time - lastOutboundActivity >= 2 * keepAlive)) { + + // I am probably blocked on a write operations as I should have been able to + // write at least a ping message + log.severe(CLASS_NAME, methodName, "642", + new Object[] { Long.valueOf(keepAlive), Long.valueOf(lastOutboundActivity), + Long.valueOf(lastInboundActivity), Long.valueOf(time), Long.valueOf(lastPing) }); + + // A ping has not been sent but I am not progressing on the current write + // operation. + // At this point, assume that the broker has hung and the TCP layer hasn't + // noticed. + throw ExceptionHelper.createMqttException(MqttClientException.REASON_CODE_WRITE_TIMEOUT); + } + + // 1. Is a ping required by the client to verify whether the broker is down? + // Condition: ((pingOutstanding == 0 && (time - lastInboundActivity >= keepAlive + // + delta))) + // In this case only one ping is sent. If not confirmed, client will assume a + // lost connection to the broker. + // 2. Is a ping required by the broker to keep the client alive? + // Condition: (time - lastOutboundActivity >= keepAlive - delta) + // In this case more than one ping outstanding may be necessary. + // This would be the case when receiving a large message; + // the broker needs to keep receiving a regular ping even if the ping response + // are queued after the long message + // If lacking to do so, the broker will consider my connection lost and cut my + // socket. + if ((pingOutstanding == 0 && (time - lastInboundActivity >= keepAlive - delta)) + || (time - lastOutboundActivity >= keepAlive - delta)) { + + // @TRACE 620=ping needed. keepAlive={0} lastOutboundActivity={1} + // lastInboundActivity={2} + log.fine(CLASS_NAME, methodName, "620", new Object[] { Long.valueOf(keepAlive), + Long.valueOf(lastOutboundActivity), Long.valueOf(lastInboundActivity) }); + + // pingOutstanding++; // it will be set after the ping has been written on the + // wire + // lastPing = time; // it will be set after the ping has been written on the + // wire + token = new MqttToken(clientComms.getClient().getClientId()); + if (pingCallback != null) { + token.setActionCallback(pingCallback); + } + tokenStore.saveToken(token, pingCommand); + pendingFlows.insertElementAt(pingCommand, 0); + + nextPingTime = keepAlive; + + // Wake sender thread since it may be in wait state (in ClientState.get()) + notifyQueueLock(); + } else { + log.fine(CLASS_NAME, methodName, "634", null); + nextPingTime = Math.max(1, keepAlive - (time - lastOutboundActivity)); + } + } + // @TRACE 624=Schedule next ping at {0} + log.fine(CLASS_NAME, methodName, "624", new Object[] { Long.valueOf(nextPingTime) }); + pingSender.schedule(TimeUnit.NANOSECONDS.toMillis(nextPingTime)); + } + + return token; + } + + /** + * This returns the next piece of work, ie message, for the CommsSender to send + * over the network. Calls to this method block until either: - there is a + * message to be sent - the keepAlive interval is exceeded, which triggers a + * ping message to be returned - {@link ClientState#disconnected(MqttException)} + * is called + * + * @return the next message to send, or null if the client is disconnected + * @throws MqttException + * if an exception occurs whilst returning the next piece of work + */ + protected MqttWireMessage get() throws MqttException { + final String methodName = "get"; + MqttWireMessage result = null; + + synchronized (queueLock) { + while (result == null) { + + // If there is no work wait until there is work. + // If the inflight window is full and no flows are pending wait until space is + // freed. + // In both cases queueLock will be notified. + if ((pendingMessages.isEmpty() && pendingFlows.isEmpty()) + || (pendingFlows.isEmpty() && actualInFlight >= this.mqttConnection.getReceiveMaximum())) { + try { + // @TRACE 644=wait for new work or for space in the inflight window + log.fine(CLASS_NAME, methodName, "644"); + + queueLock.wait(); + + // @TRACE 647=new work or ping arrived + log.fine(CLASS_NAME, methodName, "647"); + } catch (InterruptedException e) { + } + } + + // Handle the case where not connected. This should only be the case if: + // - in the process of disconnecting / shutting down + // - in the process of connecting + if (pendingFlows == null || (!connected && (pendingFlows.isEmpty() + || !((MqttWireMessage) pendingFlows.elementAt(0) instanceof MqttConnect)))) { + // @TRACE 621=no outstanding flows and not connected + log.fine(CLASS_NAME, methodName, "621"); + + return null; + } + + // Check if there is a need to send a ping to keep the session alive. + // Note this check is done before processing messages. If not done first + // an app that only publishes QoS 0 messages will prevent keepalive processing + // from functioning. + // checkForActivity(); //Use pinger, don't check here + + // Now process any queued flows or messages + if (!pendingFlows.isEmpty()) { + // Process the first "flow" in the queue + result = (MqttWireMessage) pendingFlows.remove(0); + if (result instanceof MqttPubRel) { + inFlightPubRels++; + + // @TRACE 617=+1 inflightpubrels={0} + log.fine(CLASS_NAME, methodName, "617", new Object[] { Integer.valueOf(inFlightPubRels) }); + } + + checkQuiesceLock(); + } else if (!pendingMessages.isEmpty()) { + + // If the inflight window is full then messages are not + // processed until the inflight window has space. + if (actualInFlight < this.mqttConnection.getReceiveMaximum()) { + // The in flight window is not full so process the + // first message in the queue + result = (MqttWireMessage) pendingMessages.elementAt(0); + pendingMessages.removeElementAt(0); + actualInFlight++; + + // @TRACE 623=+1 actualInFlight={0} + log.fine(CLASS_NAME, methodName, "623", new Object[] { Integer.valueOf(actualInFlight) }); + } else { + // @TRACE 622=inflight window full + log.fine(CLASS_NAME, methodName, "622"); + } + } + } // end while + } // synchronized + return result; + } + + /* + * (non-Javadoc) + * + * @see org.eclipse.paho.mqttv5.client.internal.MqttState#notifySentBytes(int) + */ + @Override + public void notifySentBytes(int sentBytesCount) { + final String methodName = "notifySentBytes"; + if (sentBytesCount > 0) { + this.lastOutboundActivity = System.nanoTime(); + } + // @TRACE 643=sent bytes count={0} + log.fine(CLASS_NAME, methodName, "643", new Object[] { Integer.valueOf(sentBytesCount) }); + } + + /** + * Called by the CommsSender when a message has been sent + * + * @param message + * the {@link MqttWireMessage} to notify + */ + protected void notifySent(MqttWireMessage message) { + final String methodName = "notifySent"; + + this.lastOutboundActivity = System.nanoTime(); + // @TRACE 625=key={0} + log.fine(CLASS_NAME, methodName, "625", new Object[] { message.getKey() }); + + MqttToken token = tokenStore.getToken(message); + if (token == null) return; + token.internalTok.notifySent(); + if (message instanceof MqttPingReq) { + synchronized (pingOutstandingLock) { + long time = System.nanoTime(); + synchronized (pingOutstandingLock) { + lastPing = time; + pingOutstanding++; + } + // @TRACE 635=ping sent. pingOutstanding: {0} + log.fine(CLASS_NAME, methodName, "635", new Object[] { Integer.valueOf(pingOutstanding) }); + } + } else if (message instanceof MqttPublish) { + if (((MqttPublish) message).getMessage().getQos() == 0) { + // once a QoS 0 message is sent we can clean up its records straight away as + // we won't be hearing about it again + token.internalTok.markComplete(null, null); + callback.asyncOperationComplete(token); + decrementInFlight(); + releaseMessageId(message.getMessageId()); + tokenStore.removeToken(message); + checkQuiesceLock(); + } + } + } + + private void decrementInFlight() { + final String methodName = "decrementInFlight"; + synchronized (queueLock) { + actualInFlight--; + // @TRACE 646=-1 actualInFlight={0} + log.fine(CLASS_NAME, methodName, "646", new Object[] { Integer.valueOf(actualInFlight) }); + + if (!checkQuiesceLock()) { + queueLock.notifyAll(); + } + } + } + + protected boolean checkQuiesceLock() { + final String methodName = "checkQuiesceLock"; + // if (quiescing && actualInFlight == 0 && pendingFlows.size() == 0 && + // inFlightPubRels == 0 && callback.isQuiesced()) { + int tokC = tokenStore.count(); + if (quiescing && tokC == 0 && pendingFlows.size() == 0 && callback.isQuiesced()) { + // @TRACE 626=quiescing={0} actualInFlight={1} pendingFlows={2} + // inFlightPubRels={3} callbackQuiesce={4} tokens={5} + log.fine(CLASS_NAME, methodName, "626", + new Object[] { Boolean.valueOf(quiescing), Integer.valueOf(actualInFlight), + Integer.valueOf(pendingFlows.size()), Integer.valueOf(inFlightPubRels), + Boolean.valueOf(callback.isQuiesced()), Integer.valueOf(tokC) }); + synchronized (quiesceLock) { + quiesceLock.notifyAll(); + } + return true; + } + return false; + } + + /* + * (non-Javadoc) + * + * @see + * org.eclipse.paho.mqttv5.client.internal.MqttState#notifyReceivedBytes(int) + */ + @Override + public void notifyReceivedBytes(int receivedBytesCount) { + final String methodName = "notifyReceivedBytes"; + if (receivedBytesCount > 0) { + this.lastInboundActivity = System.nanoTime(); + } + // @TRACE 630=received bytes count={0} + log.fine(CLASS_NAME, methodName, "630", new Object[] { Integer.valueOf(receivedBytesCount) }); + } + + /** + * Called by the CommsReceiver when an ack has arrived. + * + * @param ack + * The {@link MqttAck} that has arrived + * @throws MqttException + * if an exception occurs when sending / notifying + */ + protected void notifyReceivedAck(MqttAck ack) throws MqttException { + final String methodName = "notifyReceivedAck"; + this.lastInboundActivity = System.nanoTime(); + + // @TRACE 627=received key={0} message={1} + log.fine(CLASS_NAME, methodName, "627", new Object[] { Integer.valueOf(ack.getMessageId()), ack }); + + MqttToken token = tokenStore.getToken(ack); + MqttException mex = null; + + if (token == null) { + // @TRACE 662=no message found for ack id={0} + log.fine(CLASS_NAME, methodName, "662", new Object[] { Integer.valueOf(ack.getMessageId()) }); + } else if (ack instanceof MqttPubRec) { + if (((MqttPubRec) ack).getReasonCodes()[0] > MqttReturnCode.RETURN_CODE_UNSPECIFIED_ERROR) { + // @TRACE 664=[MQTT-4.3.3-4] - A Reason code greater than 0x80 (128) was + // received in an incoming PUBREC id={0} rc={1} message={2}, halting QoS 2 flow. + log.severe(CLASS_NAME, methodName, "664", + new Object[] { ack.getMessageId(), ack.getReasonCodes()[0], ack.toString() }); + throw new MqttException(((MqttPubRec) ack).getReasonCodes()[0]); + } + + // Update the token with the reason codes + updateResult(ack, token, mex); + + // Complete the QoS 2 flow. Unlike all other + // flows, QoS is a 2 phase flow. The second phase sends a + // PUBREL - the operation is not complete until a PUBCOMP + // is received + // Currently this client has no need of the properties, so this is left empty. + MqttPubRel rel = new MqttPubRel(MqttReturnCode.RETURN_CODE_SUCCESS, ack.getMessageId(), + new MqttProperties()); + this.send(rel, token); + } else if (ack instanceof MqttPubAck || ack instanceof MqttPubComp) { + + // QoS 1 & 2 notify users of result before removing from + // persistence + notifyResult(ack, token, mex); + // Do not remove publish / delivery token at this stage + // do this when the persistence is removed later + } else if (ack instanceof MqttPingResp) { + + synchronized (pingOutstandingLock) { + pingOutstanding = Math.max(0, pingOutstanding - 1); + notifyResult(ack, token, mex); + if (pingOutstanding == 0) { + tokenStore.removeToken(ack); + } + } + // @TRACE 636=ping response received. pingOutstanding: {0} + log.fine(CLASS_NAME, methodName, "636", new Object[] { Integer.valueOf(pingOutstanding) }); + } else if (ack instanceof MqttConnAck) { + + int rc = ((MqttConnAck) ack).getReturnCode(); + if (rc == 0) { + synchronized (queueLock) { + if (cleanStart) { + clearState(); + // Add the connect token back in so that users can be + // notified when connect completes. + tokenStore.saveToken(token, ack); + } + inFlightPubRels = 0; + actualInFlight = 0; + restoreInflightMessages(); + connected(); + } + } else { + + mex = ExceptionHelper.createMqttException(rc); + throw mex; + } + + clientComms.connectComplete((MqttConnAck) ack, mex); + notifyResult(ack, token, mex); + tokenStore.removeToken(ack); + + // Notify the sender thread that there maybe work for it to do now + synchronized (queueLock) { + queueLock.notifyAll(); + } + } else { + notifyResult(ack, token, mex); + releaseMessageId(ack.getMessageId()); + tokenStore.removeToken(ack); + } + + checkQuiesceLock(); + } + + /** + * Called by the CommsReceiver when an Ack has been received but cannot be + * matched to a token. This method will generate the appropriate response with + * an error code. + * + * @param ack + * - The Orphaned Ack + * @throws MqttException + * if an exception occurs whilst handling orphaned Acks + */ + protected void handleOrphanedAcks(MqttAck ack) throws MqttException { + final String methodName = "handleOrphanedAcks"; + // @TRACE 666=Orphaned Ack key={0} message={1} + log.fine(CLASS_NAME, methodName, "666", new Object[] { Integer.valueOf(ack.getMessageId()), ack }); + + if (ack instanceof MqttPubAck) { + // MqttPubAck - This would be the end of a QoS 1 flow, so message can be ignored + } else if (ack instanceof MqttPubRec) { + // MqttPubRec - Send an MqttPubRel with the appropriate Reason Code + MqttProperties pubRelProperties = new MqttProperties(); + if (this.mqttConnection.isSendReasonMessages()) { + String reasonString = String.format("Message identifier [%d] was not found. Discontinuing QoS 2 flow.", + ack.getMessageId()); + pubRelProperties.setReasonString(reasonString); + } + MqttPubRel rel = new MqttPubRel(MqttReturnCode.RETURN_CODE_PACKET_ID_NOT_FOUND, ack.getMessageId(), + new MqttProperties()); + this.send(rel, null); + } else if (ack instanceof MqttPubComp) { + // MqttPubComp + } + } + + /** + * Called by {@link ClientState#notifyReceivedMsg} when a PUBREL message is + * received. + * + * @param pubRel + * The in-bound PUBREL + * @throws MqttException + * When an exception occurs whilst handling the PUBREL + */ + protected void handleInboundPubRel(MqttPubRel pubRel) throws MqttException { + final String methodName = "handleInboundPubRel"; + if (pubRel.getReasonCodes()[0] > MqttReturnCode.RETURN_CODE_UNSPECIFIED_ERROR) { + // @TRACE 667=MqttPubRel was received with an error code: key={0} message={1}, + // Reason Code={2} + log.severe(CLASS_NAME, methodName, "667", + new Object[] { pubRel.getMessageId(), pubRel.toString(), pubRel.getReasonCodes()[0] }); + throw new MqttException(pubRel.getReasonCodes()[0]); + } else { + // Currently this client has no need of the properties, so this is left empty. + MqttPubComp pubComp = new MqttPubComp(MqttReturnCode.RETURN_CODE_SUCCESS, pubRel.getMessageId(), + new MqttProperties()); + // @TRACE 668=Creating MqttPubComp: {0} + log.info(CLASS_NAME, methodName, "668", new Object[] { pubComp.toString() }); + this.send(pubComp, null); + } + } + + /** + * Called by the CommsReceiver when a message has been received. Handles inbound + * messages and other flows such as PUBREL. + * + * @param message + * The {@link MqttWireMessage} that has been received + * @throws MqttException + * when an exception occurs whilst notifying + */ + protected void notifyReceivedMsg(MqttWireMessage message) throws MqttException { + final String methodName = "notifyReceivedMsg"; + this.lastInboundActivity = System.nanoTime(); + + // @TRACE 651=received key={0} message={1} + log.fine(CLASS_NAME, methodName, "651", new Object[] { Integer.valueOf(message.getMessageId()), message }); + + if (!quiescing) { + if (message instanceof MqttPublish) { + MqttPublish send = (MqttPublish) message; + + // Do we have an incoming topic Alias? + if (send.getProperties().getTopicAlias() != null) { + int incomingTopicAlias = send.getProperties().getTopicAlias(); + + // Are incoming Topic Aliases enabled / is it a valid Alias? + if (incomingTopicAlias > this.mqttConnection.getIncomingTopicAliasMax() + || incomingTopicAlias == 0) { + // @TRACE 653=Invalid Topic Alias: topicAliasMax={0}, publishTopicAlias={1} + log.severe(CLASS_NAME, methodName, "653", + new Object[] { Integer.valueOf(this.mqttConnection.getIncomingTopicAliasMax()), + Integer.valueOf(incomingTopicAlias) }); + if (callback != null) { + callback.mqttErrorOccurred(new MqttException(MqttException.REASON_CODE_INVALID_TOPIC_ALAS)); + } + throw new MqttException(MqttClientException.REASON_CODE_INVALID_TOPIC_ALAS); + + } + + // Is this alias being sent with a topic string? + if (send.getTopicName() != null) { + // @TRACE 652=Setting Incoming New Topic Alias alias={0}, topicName={1} + log.fine(CLASS_NAME, methodName, "652", new Object[] { + Integer.valueOf(send.getProperties().getTopicAlias()), send.getTopicName() }); + incomingTopicAliases.put(send.getProperties().getTopicAlias(), send.getTopicName()); + } else { + // No Topic String, so must be in incomingTopicAliases. + if (incomingTopicAliases.contains(incomingTopicAlias)) { + send.setTopicName(incomingTopicAliases.get(incomingTopicAlias)); + } else { + // @TRACE 654=Unknown Topic Alias: Incoming Alias={1} + log.severe(CLASS_NAME, methodName, "654", + new Object[] { Integer.valueOf(send.getProperties().getTopicAlias()) }); + throw new MqttException(MqttClientException.REASON_CODE_UNKNOWN_TOPIC_ALIAS); + } + } + } + + switch (send.getMessage().getQos()) { + case 0: + case 1: + if (callback != null) { + callback.messageArrived(send); + } + break; + case 2: + persistence.put(getReceivedPersistenceKey(message), (MqttPublish) message); + inboundQoS2.put(Integer.valueOf(send.getMessageId()), send); + if (callback != null) { + callback.messageArrived(send); + } + // Currently this client has no need of the properties, so this is left empty. + this.send(new MqttPubRec(MqttReturnCode.RETURN_CODE_SUCCESS, send.getMessageId(), + new MqttProperties()), null); + break; + + default: + // should NOT reach here + } + } else if (message instanceof MqttPubRel) { + handleInboundPubRel((MqttPubRel) message); + } else if (message instanceof MqttAuth) { + MqttAuth authMsg = (MqttAuth) message; + callback.authMessageReceived(authMsg); + } + } + } + + /** + * Called when waiters and callbacks have processed the message. For messages + * where delivery is complete the message can be removed from persistence and + * counters adjusted accordingly. Also tidy up by removing token from store... + * + * @param token + * The {@link MqttToken} that will be used to notify + * @throws MqttException + * if an exception occurs during notification + */ + protected void notifyComplete(MqttToken token) throws MqttException { + + final String methodName = "notifyComplete"; + + MqttWireMessage message = token.internalTok.getWireMessage(); + + if (message != null && message instanceof MqttAck) { + + // @TRACE 629=received key={0} token={1} message={2} + log.fine(CLASS_NAME, methodName, "629", + new Object[] { Integer.valueOf(message.getMessageId()), token, message }); + + MqttAck ack = (MqttAck) message; + + if (ack instanceof MqttPubAck) { + + // QoS 1 - user notified now remove from persistence... + persistence.remove(getSendPersistenceKey(message)); + persistence.remove(getSendBufferedPersistenceKey(message)); + outboundQoS1.remove(Integer.valueOf(ack.getMessageId())); + decrementInFlight(); + releaseMessageId(message.getMessageId()); + tokenStore.removeToken(message); + // @TRACE 650=removed Qos 1 publish. key={0} + log.fine(CLASS_NAME, methodName, "650", new Object[] { Integer.valueOf(ack.getMessageId()) }); + } else if (ack instanceof MqttPubComp) { + // QoS 2 - user notified now remove from persistence... + persistence.remove(getSendPersistenceKey(message)); + persistence.remove(getSendConfirmPersistenceKey(message)); + persistence.remove(getSendBufferedPersistenceKey(message)); + outboundQoS2.remove(Integer.valueOf(ack.getMessageId())); + + inFlightPubRels--; + decrementInFlight(); + releaseMessageId(message.getMessageId()); + tokenStore.removeToken(message); + + // @TRACE 645=removed QoS 2 publish/pubrel. key={0}, -1 inFlightPubRels={1} + log.fine(CLASS_NAME, methodName, "645", + new Object[] { Integer.valueOf(ack.getMessageId()), Integer.valueOf(inFlightPubRels) }); + } + + checkQuiesceLock(); + } + } + + /** + * Updates a token with the latest reason codes, currently only used for PubRec + * messages. + * + * @param ack + * - The message that we are using for the update + * @param token + * - The Token we are updating + * @param ex + * - if there was a problem store the exception in the token. + */ + protected void updateResult(MqttWireMessage ack, MqttToken token, MqttException ex) { + token.internalTok.update(ack, ex); + } + + protected void notifyResult(MqttWireMessage ack, MqttToken token, MqttException ex) { + final String methodName = "notifyResult"; + // unblock any threads waiting on the token + token.internalTok.markComplete(ack, ex); + token.internalTok.notifyComplete(); + + // Let the user know an async operation has completed and then remove the token + if (ack != null && ack instanceof MqttAck && !(ack instanceof MqttPubRec)) { + // @TRACE 648=key{0}, msg={1}, excep={2} + log.fine(CLASS_NAME, methodName, "648", new Object[] { token.internalTok.getKey(), ack, ex }); + callback.asyncOperationComplete(token); + } + // There are cases where there is no ack as the operation failed before + // an ack was received + if (ack == null) { + // @TRACE 649=key={0},excep={1} + log.fine(CLASS_NAME, methodName, "649", new Object[] { token.internalTok.getKey(), ex }); + callback.asyncOperationComplete(token); + } + } + + /* + * (non-Javadoc) + * + * @see org.eclipse.paho.mqttv5.client.internal.MqttState#connected() + */ + @Override + public void connected() { + final String methodName = "connected"; + // @TRACE 631=connected + log.fine(CLASS_NAME, methodName, "631"); + this.connected = true; + + pingSender.start(); // Start ping thread when client connected to server. + } + + /* + * (non-Javadoc) + * + * @see org.eclipse.paho.mqttv5.client.internal.MqttState#resolveOldTokens(org. + * eclipse.paho.client.mqttv3.MqttException) + */ + @Override + public Vector resolveOldTokens(MqttException reason) { + final String methodName = "resolveOldTokens"; + // @TRACE 632=reason {0} + log.fine(CLASS_NAME, methodName, "632", new Object[] { reason }); + + // If any outstanding let the user know the reason why it is still + // outstanding by putting the reason shutdown is occurring into the + // token. + MqttException shutReason = reason; + if (reason == null) { + shutReason = new MqttException(MqttClientException.REASON_CODE_CLIENT_DISCONNECTING); + } + + // Set the token up so it is ready to be notified after disconnect + // processing has completed. Do not + // remove the token from the store if it is a delivery token, it is + // valid after a reconnect. + Vector outT = tokenStore.getOutstandingTokens(); + Enumeration outTE = outT.elements(); + while (outTE.hasMoreElements()) { + MqttToken tok = (MqttToken) outTE.nextElement(); + synchronized (tok) { + if (!tok.isComplete() && !tok.internalTok.isCompletePending() && tok.getException() == null) { + tok.internalTok.setException(shutReason); + } + } + if (!(tok.internalTok.isDeliveryToken())) { + // If not a delivery token it is not valid on + // restart so remove + tokenStore.removeToken(tok.internalTok.getKey()); + } + } + return outT; + } + + /* + * (non-Javadoc) + * + * @see + * org.eclipse.paho.mqttv5.client.internal.MqttState#disconnected(org.eclipse. + * paho.client.mqttv3.MqttException) + */ + @Override + public void disconnected(MqttException reason) { + final String methodName = "disconnected"; + // @TRACE 633=disconnected + log.fine(CLASS_NAME, methodName, "633", new Object[] { reason }); + + this.connected = false; + + try { + if (cleanStart) { + clearState(); + } + + clearConnectionState(); + + pendingMessages.clear(); + pendingFlows.clear(); + synchronized (pingOutstandingLock) { + // Reset pingOutstanding to allow reconnects to assume no previous ping. + pingOutstanding = 0; + } + } catch (MqttException e) { + // Ignore as we have disconnected at this point + } + } + + /** + * Releases a message ID back into the pool of available message IDs. If the + * supplied message ID is not in use, then nothing will happen. + * + * @param msgId + * A message ID that can be freed up for re-use. + */ + private synchronized void releaseMessageId(int msgId) { + inUseMsgIds.remove(Integer.valueOf(msgId)); + } + + /** + * Get the next MQTT message ID that is not already in use, and marks it as now + * being in use. + * + * @return the next MQTT message ID to use + */ + private synchronized int getNextMessageId() throws MqttException { + int startingMessageId = nextMsgId; + // Allow two complete passes of the message ID range. This gives + // any asynchronous releases a chance to occur + int loopCount = 0; + do { + nextMsgId++; + if (nextMsgId > MAX_MSG_ID) { + nextMsgId = MIN_MSG_ID; + } + if (nextMsgId == startingMessageId) { + loopCount++; + if (loopCount == 2) { + throw ExceptionHelper.createMqttException(MqttClientException.REASON_CODE_NO_MESSAGE_IDS_AVAILABLE); + } + } + } while (inUseMsgIds.containsKey(Integer.valueOf(nextMsgId))); + Integer id = Integer.valueOf(nextMsgId); + inUseMsgIds.put(id, id); + return nextMsgId; + } + + /* + * (non-Javadoc) + * + * @see org.eclipse.paho.mqttv5.client.internal.MqttState#quiesce(long) + */ + @Override + public void quiesce(long timeout) { + final String methodName = "quiesce"; + // If the timeout is greater than zero t + if (timeout > 0) { + // @TRACE 637=timeout={0} + log.fine(CLASS_NAME, methodName, "637", new Object[] { Long.valueOf(timeout) }); + synchronized (queueLock) { + this.quiescing = true; + } + // We don't want to handle any new inbound messages + callback.quiesce(); + notifyQueueLock(); + + synchronized (quiesceLock) { + try { + // If token count is not zero there is outbound work to process and + // if pending flows is not zero there is outstanding work to complete and + // if call back is not quiseced there it needs to complete. + int tokc = tokenStore.count(); + if (tokc > 0 || pendingFlows.size() > 0 || !callback.isQuiesced()) { + // @TRACE 639=wait for outstanding: actualInFlight={0} pendingFlows={1} + // inFlightPubRels={2} tokens={3} + log.fine(CLASS_NAME, methodName, "639", + new Object[] { Integer.valueOf(actualInFlight), Integer.valueOf(pendingFlows.size()), + Integer.valueOf(inFlightPubRels), Integer.valueOf(tokc) }); + + // wait for outstanding in flight messages to complete and + // any pending flows to complete + quiesceLock.wait(timeout); + } + } catch (InterruptedException ex) { + // Don't care, as we're shutting down anyway + } + } + + // Quiesce time up or inflight messages delivered. Ensure pending delivery + // vectors are cleared ready for disconnect to be sent as the final flow. + synchronized (queueLock) { + pendingMessages.clear(); + pendingFlows.clear(); + quiescing = false; + actualInFlight = 0; + } + // @TRACE 640=finished + log.fine(CLASS_NAME, methodName, "640"); + } + } + + /* + * (non-Javadoc) + * + * @see org.eclipse.paho.mqttv5.client.internal.MqttState#notifyQueueLock() + */ + @Override + public void notifyQueueLock() { + final String methodName = "notifyQueueLock"; + synchronized (queueLock) { + // @TRACE 638=notifying queueLock holders + log.fine(CLASS_NAME, methodName, "638"); + queueLock.notifyAll(); + } + } + + protected void deliveryComplete(MqttPublish message) throws MqttPersistenceException { + final String methodName = "deliveryComplete"; + + // @TRACE 641=remove publish from persistence. key={0} + log.fine(CLASS_NAME, methodName, "641", new Object[] { Integer.valueOf(message.getMessageId()) }); + + persistence.remove(getReceivedPersistenceKey(message)); + inboundQoS2.remove(Integer.valueOf(message.getMessageId())); + } + + protected void deliveryComplete(int messageId) throws MqttPersistenceException { + final String methodName = "deliveryComplete"; + + // @TRACE 641=remove publish from persistence. key={0} + log.fine(CLASS_NAME, methodName, "641", new Object[] { Integer.valueOf(messageId) }); + + persistence.remove(getReceivedPersistenceKey(messageId)); + inboundQoS2.remove(Integer.valueOf(messageId)); + } + + /* + * (non-Javadoc) + * + * @see org.eclipse.paho.mqttv5.client.internal.MqttState#getActualInFlight() + */ + @Override + public int getActualInFlight() { + return actualInFlight; + } + + public Long getOutgoingMaximumPacketSize() { + return this.mqttConnection.getIncomingMaximumPacketSize(); + } + + public Long getIncomingMaximumPacketSize() { + return this.mqttConnection.getOutgoingMaximumPacketSize(); + } + + /** + * Tidy up - ensure that tokens are released as they are maintained over a + * disconnect / connect cycle. + */ + protected void close() { + inUseMsgIds.clear(); + if (pendingMessages != null) { + pendingMessages.clear(); + } + pendingFlows.clear(); + outboundQoS2.clear(); + outboundQoS1.clear(); + outboundQoS0.clear(); + inboundQoS2.clear(); + tokenStore.clear(); + inUseMsgIds = null; + pendingMessages = null; + pendingFlows = null; + outboundQoS2 = null; + outboundQoS1 = null; + outboundQoS0 = null; + inboundQoS2 = null; + tokenStore = null; + callback = null; + clientComms = null; + persistence = null; + pingCommand = null; + } + + /* + * (non-Javadoc) + * + * @see org.eclipse.paho.mqttv5.client.internal.MqttState#getDebug() + */ + @Override + public Properties getDebug() { + Properties props = new Properties(); + props.put("In use msgids", inUseMsgIds); + props.put("pendingMessages", pendingMessages); + props.put("pendingFlows", pendingFlows); + props.put("serverReceiveMaximum", Integer.valueOf(this.mqttConnection.getReceiveMaximum())); + props.put("nextMsgID", Integer.valueOf(nextMsgId)); + props.put("actualInFlight", Integer.valueOf(actualInFlight)); + props.put("inFlightPubRels", Integer.valueOf(inFlightPubRels)); + props.put("quiescing", Boolean.valueOf(quiescing)); + props.put("pingoutstanding", Integer.valueOf(pingOutstanding)); + props.put("lastOutboundActivity", Long.valueOf(lastOutboundActivity)); + props.put("lastInboundActivity", Long.valueOf(lastInboundActivity)); + props.put("outboundQoS2", outboundQoS2); + props.put("outboundQoS1", outboundQoS1); + props.put("outboundQoS0", outboundQoS0); + props.put("inboundQoS2", inboundQoS2); + props.put("tokens", tokenStore); + return props; + } +} diff --git a/src/main/java/org/eclipse/paho/mqttv5/client/internal/CommsCallback.java b/src/main/java/org/eclipse/paho/mqttv5/client/internal/CommsCallback.java new file mode 100644 index 0000000..7e7cdd7 --- /dev/null +++ b/src/main/java/org/eclipse/paho/mqttv5/client/internal/CommsCallback.java @@ -0,0 +1,670 @@ +/******************************************************************************* + * Copyright (c) 2009, 2019 IBM Corp. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * and Eclipse Distribution License v1.0 which accompany this distribution. + * + * The Eclipse Public License is available at + * https://www.eclipse.org/legal/epl-2.0 + * and the Eclipse Distribution License is available at + * https://www.eclipse.org/org/documents/edl-v10.php + * + * Contributors: + * Dave Locke - initial API and implementation and/or initial documentation + * Ian Craggs - per subscription message handlers (bug 466579) + * Ian Craggs - ack control (bug 472172) + * James Sutton - Automatic Reconnect & Offline Buffering + */ +package org.eclipse.paho.mqttv5.client.internal; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Future; +import java.util.concurrent.atomic.AtomicInteger; + +import org.eclipse.paho.mqttv5.client.IMqttMessageListener; +import org.eclipse.paho.mqttv5.client.MqttActionListener; +import org.eclipse.paho.mqttv5.client.MqttCallback; +import org.eclipse.paho.mqttv5.client.MqttDisconnectResponse; +import org.eclipse.paho.mqttv5.client.MqttToken; +import org.eclipse.paho.mqttv5.client.logging.Logger; +import org.eclipse.paho.mqttv5.client.logging.LoggerFactory; +import org.eclipse.paho.mqttv5.common.MqttException; +import org.eclipse.paho.mqttv5.common.MqttMessage; +import org.eclipse.paho.mqttv5.common.packet.MqttAuth; +import org.eclipse.paho.mqttv5.common.packet.MqttDisconnect; +import org.eclipse.paho.mqttv5.common.packet.MqttProperties; +import org.eclipse.paho.mqttv5.common.packet.MqttPubAck; +import org.eclipse.paho.mqttv5.common.packet.MqttPubComp; +import org.eclipse.paho.mqttv5.common.packet.MqttPublish; +import org.eclipse.paho.mqttv5.common.packet.MqttReturnCode; +import org.eclipse.paho.mqttv5.common.packet.UserProperty; +import org.eclipse.paho.mqttv5.common.util.MqttTopicValidator; + +/** + * Bridge between Receiver and the external API. This class gets called by + * Receiver, and then converts the comms-centric MQTT message objects into ones + * understood by the external API. + */ +public class CommsCallback implements Runnable { + private static final String CLASS_NAME = CommsCallback.class.getName(); + private Logger log = LoggerFactory.getLogger(LoggerFactory.MQTT_CLIENT_MSG_CAT, CLASS_NAME); + + private static final int INBOUND_QUEUE_SIZE = 10; + private MqttCallback mqttCallback; + private MqttCallback reconnectInternalCallback; + private HashMap callbackMap; // Map of message handler callbacks to internal IDs + private HashMap callbackTopicMap; // Map of Topic Strings to internal callback Ids + private HashMap subscriptionIdMap; // Map of Subscription Ids to callback Ids + private AtomicInteger messageHandlerId = new AtomicInteger(0); + private ClientComms clientComms; + private ArrayList messageQueue; + private ArrayList completeQueue; + + private enum State {STOPPED, RUNNING, QUIESCING} + + private State current_state = State.STOPPED; + private State target_state = State.STOPPED; + private final Object lifecycle = new Object(); + private Thread callbackThread; + private String threadName; + private Future callbackFuture; + + private final Object workAvailable = new Object(); + private final Object spaceAvailable = new Object(); + private ClientState clientState; + private boolean manualAcks = false; + + + CommsCallback(ClientComms clientComms) { + this.clientComms = clientComms; + this.messageQueue = new ArrayList<>(INBOUND_QUEUE_SIZE); + this.completeQueue = new ArrayList<>(INBOUND_QUEUE_SIZE); + this.callbackMap = new HashMap<>(); + this.callbackTopicMap = new HashMap<>(); + this.subscriptionIdMap = new HashMap<>(); + log.setResourceName(clientComms.getClient().getClientId()); + } + + public void setClientState(ClientState clientState) { + this.clientState = clientState; + } + + /** + * Starts up the Callback thread. + * + * @param threadName + * The name of the thread + * @param executorService + * the {@link ExecutorService} + */ + public void start(String threadName, ExecutorService executorService) { + this.threadName = threadName; + synchronized (lifecycle) { + if (current_state == State.STOPPED) { + // Preparatory work before starting the background thread. + // For safety ensure any old events are cleared. + synchronized (workAvailable) { + messageQueue.clear(); + completeQueue.clear(); + } + target_state = State.RUNNING; + if (executorService == null) { + new Thread(this).start(); + } else { + callbackFuture = executorService.submit(this); + } + } + } + while (!isRunning()) { + try { Thread.sleep(100); } catch (Exception e) { } + } + } + + /** + * Stops the callback thread. This call will block until stop has completed. + */ + public void stop() { + final String methodName = "stop"; + synchronized (lifecycle) { + if (callbackFuture != null) { + callbackFuture.cancel(true); + } + } + if (isRunning()) { + // @TRACE 700=stopping + log.fine(CLASS_NAME, methodName, "700"); + synchronized (lifecycle) { + target_state = State.STOPPED; + } + if (!Thread.currentThread().equals(callbackThread)) { + synchronized (workAvailable) { + // @TRACE 701=notify workAvailable and wait for run + // to finish + log.fine(CLASS_NAME, methodName, "701"); + workAvailable.notifyAll(); + } + // Wait for the thread to finish. + while (isRunning()) { + try { Thread.sleep(100); } catch (Exception e) { } + clientState.notifyQueueLock(); + } + } + callbackThread = null; + // @TRACE 703=stopped + log.fine(CLASS_NAME, methodName, "703"); + } + } + + public void setCallback(MqttCallback mqttCallback) { + this.mqttCallback = mqttCallback; + } + + public void setReconnectCallback(MqttCallback callback) { + this.reconnectInternalCallback = callback; + } + + public void setManualAcks(boolean manualAcks) { + this.manualAcks = manualAcks; + } + + public void run() { + final String methodName = "run"; + callbackThread = Thread.currentThread(); + callbackThread.setName(threadName); + + synchronized (lifecycle) { + current_state = State.RUNNING; + } + + while (isRunning()) { + try { + // If no work is currently available, then wait until there is some... + try { + synchronized (workAvailable) { + if (isRunning() && messageQueue.isEmpty() + && completeQueue.isEmpty()) { + // @TRACE 704=wait for workAvailable + log.fine(CLASS_NAME, methodName, "704"); + workAvailable.wait(); + } + } + } catch (InterruptedException e) { + } + + if (isRunning()) { + // Check for deliveryComplete callbacks... + MqttToken token = null; + synchronized (workAvailable) { + if (!completeQueue.isEmpty()) { + // First call the delivery arrived callback if needed + token = completeQueue.get(0); + completeQueue.remove(0); + } + } + if (null != token) { + handleActionComplete(token); + } + + // Check for messageArrived callbacks... + MqttPublish message = null; + synchronized (workAvailable) { + if (!messageQueue.isEmpty()) { + // Note, there is a window on connect where a publish + // could arrive before we've + // finished the connect logic. + message = messageQueue.get(0); + messageQueue.remove(0); + } + } + if (null != message) { + handleMessage(message); + } + } + + if (isQuiescing()) { + clientState.checkQuiesceLock(); + } + + } catch (Throwable ex) { + // Users code could throw an Error or Exception e.g. in the case + // of class NoClassDefFoundError + // @TRACE 714=callback threw exception + log.fine(CLASS_NAME, methodName, "714", null, ex); + + clientComms.shutdownConnection(null, new MqttException(ex), null); + } finally { + + synchronized (spaceAvailable) { + // Notify the spaceAvailable lock, to say that there's now + // some space on the queue... + + // @TRACE 706=notify spaceAvailable + log.fine(CLASS_NAME, methodName, "706"); + spaceAvailable.notifyAll(); + } + } + } + synchronized (lifecycle) { + current_state = State.STOPPED; + } + callbackThread = null; + } + + private void handleActionComplete(MqttToken token) throws MqttException { + final String methodName = "handleActionComplete"; + synchronized (token) { + // @TRACE 705=callback and notify for key={0} + log.fine(CLASS_NAME, methodName, "705", new Object[] { token.internalTok.getKey() }); + if (token.isComplete()) { + // Finish by doing any post processing such as delete + // from persistent store but only do so if the action + // is complete + clientState.notifyComplete(token); + } + + // Unblock any waiters and if pending complete now set completed + token.internalTok.notifyComplete(); + + if (!token.internalTok.isNotified()) { + // If a callback is registered and delivery has finished + // call delivery complete callback. + if (mqttCallback != null && token.internalTok.isDeliveryToken() == true && token.isComplete()) { + try { + mqttCallback.deliveryComplete(token); + } catch (Throwable ex) { + // Just log the fact that an exception was thrown + // @TRACE 726=Ignoring Exception thrown from deliveryComplete {0} + log.fine(CLASS_NAME, methodName, "726", new Object[] { ex }); + } + } + // Now call async action completion callbacks + fireActionEvent(token); + } + + // Set notified so we don't tell the user again about this action. + if (token.isComplete()) { + if (token.internalTok.isDeliveryToken() == true || token.getActionCallback() instanceof MqttActionListener) { + token.internalTok.setNotified(true); + } + } + + } + } + + /** + * This method is called when the connection to the server is lost. If there is + * no cause then it was a clean disconnect. The connectionLost callback will be + * invoked if registered and run on the thread that requested shutdown e.g. + * receiver or sender thread. If the request was a user initiated disconnect + * then the disconnect token will be notified. + * + * @param cause + * the reason behind the loss of connection. + * @param message + * The {@link MqttDisconnect} packet sent by the server + */ + public void connectionLost(MqttException cause, MqttDisconnect message) { + final String methodName = "connectionLost"; + // If there was a problem and a client callback has been set inform + // the connection lost listener of the problem. + try { + if (mqttCallback != null && message != null) { + + // @TRACE 722=Server initiated disconnect, connection closed. Disconnect={0} + log.fine(CLASS_NAME, methodName, "722", new Object[] { message.toString() }); + MqttDisconnectResponse disconnectResponse = new MqttDisconnectResponse(message.getReturnCode(), + message.getProperties().getReasonString(), + (ArrayList) message.getProperties().getUserProperties(), + message.getProperties().getServerReference()); + mqttCallback.disconnected(disconnectResponse); + } else if (mqttCallback != null && cause != null) { + // @TRACE 708=call connectionLost + log.fine(CLASS_NAME, methodName, "708", new Object[] { cause }); + MqttDisconnectResponse disconnectResponse = new MqttDisconnectResponse(cause); + mqttCallback.disconnected(disconnectResponse); + } + if (reconnectInternalCallback != null && cause != null) { + MqttDisconnectResponse disconnectResponse = new MqttDisconnectResponse(cause); + + reconnectInternalCallback.disconnected(disconnectResponse); + } + } catch (Throwable t) { + // Just log the fact that an exception was thrown + // @TRACE 720=Ignoring Exception thrown from connectionLost {0} + log.fine(CLASS_NAME, methodName, "720", new Object[] { t }); + } + } + + /** + * An action has completed - if a completion listener has been set on the token + * then invoke it with the outcome of the action. + * + * @param token + * The {@link MqttToken} that has completed + */ + public void fireActionEvent(MqttToken token) { + final String methodName = "fireActionEvent"; + + if (token != null) { + MqttActionListener asyncCB = token.getActionCallback(); + if (asyncCB != null) { + if (token.getException() == null) { + // @TRACE 716=call onSuccess key={0} + log.fine(CLASS_NAME, methodName, "716", new Object[] { token.internalTok.getKey() }); + asyncCB.onSuccess(token); + } else { + // @TRACE 717=call onFailure key {0} + log.fine(CLASS_NAME, methodName, "716", new Object[] { token.internalTok.getKey() }); + asyncCB.onFailure(token, token.getException()); + } + } + } + } + + /** + * This method is called when a message arrives on a topic. Messages are only + * added to the queue for inbound messages if the client is not quiescing. + * + * @param sendMessage + * the MQTT SEND message. + */ + public void messageArrived(MqttPublish sendMessage) { + final String methodName = "messageArrived"; + if (mqttCallback != null || callbackMap.size() > 0) { + // If we already have enough messages queued up in memory, wait + // until some more queue space becomes available. This helps + // the client protect itself from getting flooded by messages + // from the server. + synchronized (spaceAvailable) { + while (isRunning() && !isQuiescing() && messageQueue.size() >= INBOUND_QUEUE_SIZE) { + try { + // @TRACE 709=wait for spaceAvailable + log.fine(CLASS_NAME, methodName, "709"); + spaceAvailable.wait(200); + } catch (InterruptedException ex) { + } + } + } + if (!isQuiescing()) { + // Notify the CommsCallback thread that there's work to do... + synchronized (workAvailable) { + messageQueue.add(sendMessage); + // @TRACE 710=new msg avail, notify workAvailable + log.fine(CLASS_NAME, methodName, "710"); + workAvailable.notifyAll(); + } + } + } + } + + /** + * This method is called when an Auth Message is received. + * + * @param authMessage + * The {@link MqttAuth} message. + */ + public void authMessageReceived(MqttAuth authMessage) { + String methodName = "authMessageReceived"; + if (mqttCallback != null) { + try { + mqttCallback.authPacketArrived(authMessage.getReturnCode(), authMessage.getProperties()); + } catch (Throwable ex) { + // Just log the fact that an exception was thrown + // @TRACE 727=Ignoring Exception thrown from authPacketArrived {0} + log.fine(CLASS_NAME, methodName, "727", new Object[] { ex }); + } + } + } + + /** + * This method is called when a non-critical MQTT error has occurred in the + * client that the application should choose how to deal with. + * + * @param exception + * The exception that was thrown containing the cause for + * disconnection. + */ + public void mqttErrorOccurred(MqttException exception) { + final String methodName = "mqttErrorOccurred"; + log.warning(CLASS_NAME, methodName, "721", new Object[] { exception.getMessage() }); + if (mqttCallback != null) { + try { + mqttCallback.mqttErrorOccurred(exception); + } catch (Exception ex) { + // Just log the fact that an exception was thrown + // @TRACE 724=Ignoring Exception thrown from mqttErrorOccurred: {0} + log.fine(CLASS_NAME, methodName, "724", new Object[] { ex }); + } + } + } + + /** + * Let the call back thread quiesce. Prevent new inbound messages being added to + * the process queue and let existing work quiesce. (until the thread is told to + * shutdown). + */ + public void quiesce() { + final String methodName = "quiesce"; + synchronized (lifecycle) { + if (current_state == State.RUNNING) + current_state = State.QUIESCING; + } + synchronized (spaceAvailable) { + // @TRACE 711=quiesce notify spaceAvailable + log.fine(CLASS_NAME, methodName, "711"); + // Unblock anything waiting for space... + spaceAvailable.notifyAll(); + } + } + + boolean areQueuesEmpty() { + synchronized (workAvailable) { + return completeQueue.isEmpty() && messageQueue.isEmpty(); + } + } + + public boolean isQuiesced() { + return (isQuiescing() && areQueuesEmpty()); + } + + private void handleMessage(MqttPublish publishMessage) throws Exception { + final String methodName = "handleMessage"; + // If quisecing process any pending messages. + String destName = publishMessage.getTopicName(); + + // @TRACE 713=call messageArrived key={0} topic={1} + log.fine(CLASS_NAME, methodName, "713", new Object[] { Integer.valueOf(publishMessage.getMessageId()), destName }); + deliverMessage(destName, publishMessage.getMessageId(), publishMessage.getMessage()); + + // If we are not in manual ACK mode: + if (!this.manualAcks && publishMessage.getMessage().getQos() == 1) { + this.clientComms.internalSend(new MqttPubAck(MqttReturnCode.RETURN_CODE_SUCCESS, + publishMessage.getMessageId(), new MqttProperties()), + new MqttToken(clientComms.getClient().getClientId())); + } + } + + public void messageArrivedComplete(int messageId, int qos) throws MqttException { + if (qos == 1) { + this.clientComms.internalSend( + new MqttPubAck(MqttReturnCode.RETURN_CODE_SUCCESS, messageId, new MqttProperties()), + new MqttToken(clientComms.getClient().getClientId())); + } else if (qos == 2) { + this.clientComms.deliveryComplete(messageId); + MqttPubComp pubComp = new MqttPubComp(MqttReturnCode.RETURN_CODE_SUCCESS, messageId, new MqttProperties()); + // @TRACE 723=Creating MqttPubComp due to manual ACK: {0} + log.info(CLASS_NAME, "messageArrivedComplete", "723", new Object[] { pubComp.toString() }); + + this.clientComms.internalSend(pubComp, new MqttToken(clientComms.getClient().getClientId())); + } + } + + public void asyncOperationComplete(MqttToken token) { + final String methodName = "asyncOperationComplete"; + + if (isRunning()) { + // invoke callbacks on callback thread + synchronized (workAvailable) { + completeQueue.add(token); + // @TRACE 715=new workAvailable. key={0} + log.fine(CLASS_NAME, methodName, "715", new Object[] { token.internalTok.getKey() }); + workAvailable.notifyAll(); + } + } else { + // invoke async callback on invokers thread + try { + handleActionComplete(token); + } catch (MqttException ex) { + // Users code could throw an Error or Exception e.g. in the case + // of class NoClassDefFoundError + // @TRACE 719=callback threw ex: + log.fine(CLASS_NAME, methodName, "719", null, ex); + + // Shutdown likely already in progress but no harm to confirm + clientComms.shutdownConnection(null, new MqttException(ex), null); + } + + } + } + + /** + * Returns the thread used by this callback. + * + * @return The {@link Thread} + */ + protected Thread getThread() { + return callbackThread; + } + + public void setMessageListener(Integer subscriptionId, String topicFilter, IMqttMessageListener messageListener) { + int internalId = messageHandlerId.incrementAndGet(); + this.callbackMap.put(internalId, messageListener); + this.callbackTopicMap.put(topicFilter, internalId); + + if (subscriptionId != null) { + this.subscriptionIdMap.put(subscriptionId, internalId); + } + } + + /** + * Removes a Message Listener by Topic. If the Topic is null or incorrect, this + * function will return without making any changes. It will also attempt to find + * any subscription IDs linked to the same message listener and will remove them + * too. + * + * @param topicFilter + * the topic filter that identifies the Message listener to remove. + */ + public void removeMessageListener(String topicFilter) { + Integer callbackId = this.callbackTopicMap.get(topicFilter); + this.callbackMap.remove(callbackId); + this.callbackTopicMap.remove(topicFilter); + + // Reverse lookup the subscription ID if it exists to remove that as well + for (Map.Entry entry : this.subscriptionIdMap.entrySet()) { + if (entry.getValue().equals(callbackId)) { + this.subscriptionIdMap.remove(entry.getKey()); + } + } + } + + /** + * Removes a Message Listener by subscription ID. If the Subscription Identifier + * is null or incorrect, this function will return without making any changes. + * It will also attempt to find any Topic Strings linked to the same message + * listener and will remove them too. + * + * @param subscriptionId + * the subscription ID that identifies the Message listener to + * remove. + */ + public void removeMessageListener(Integer subscriptionId) { + Integer callbackId = this.subscriptionIdMap.get(subscriptionId); + this.subscriptionIdMap.remove(callbackId); + this.callbackMap.remove(callbackId); + + // Reverse lookup the topic if it exists to remove that as well + for (Map.Entry entry : this.callbackTopicMap.entrySet()) { + if (entry.getValue().equals(callbackId)) { + this.callbackTopicMap.remove(entry.getKey()); + } + } + } + + public void removeMessageListeners() { + this.callbackMap.clear(); + this.subscriptionIdMap.clear(); + this.callbackTopicMap.clear(); + } + + protected boolean deliverMessage(String topicName, int messageId, MqttMessage aMessage) throws Exception { + boolean delivered = false; + String methodName = "deliverMessage"; + + if (aMessage.getProperties().getSubscriptionIdentifiers().isEmpty()) { + // No Subscription IDs, use topic filter matching + for (Map.Entry entry : this.callbackTopicMap.entrySet()) { + if (MqttTopicValidator.isMatched(entry.getKey(), topicName)) { + aMessage.setId(messageId); + this.callbackMap.get(entry.getValue()).messageArrived(topicName, aMessage); + delivered = true; + } + } + + } else { + // We have Subscription IDs + for (Integer subId : aMessage.getProperties().getSubscriptionIdentifiers()) { + if (this.subscriptionIdMap.containsKey(subId)) { + Integer callbackId = this.subscriptionIdMap.get(subId); + aMessage.setId(messageId); + this.callbackMap.get(callbackId).messageArrived(topicName, aMessage); + delivered = true; + } + } + } + + /* + * if the message hasn't been delivered to a per subscription handler, give it + * to the default handler + */ + if (mqttCallback != null && !delivered) { + aMessage.setId(messageId); + try { + mqttCallback.messageArrived(topicName, aMessage); + } catch (Exception ex) { + // Just log the fact that an exception was thrown + // @TRACE 725=Ignoring Exception thrown from messageArrived: {0} + log.fine(CLASS_NAME, methodName, "725", new Object[] { ex }); + } + delivered = true; + } + + return delivered; + } + + public boolean doesSubscriptionIdentifierExist(int subscriptionIdentifier) { + return (this.subscriptionIdMap.containsKey(subscriptionIdentifier)); + } + + public boolean isRunning() { + boolean result; + synchronized (lifecycle) { + result = ((current_state == State.RUNNING || current_state == State.QUIESCING) + && target_state == State.RUNNING); + } + return result; + } + + public boolean isQuiescing() { + boolean result; + synchronized (lifecycle) { + result = (current_state == State.QUIESCING); + } + return result; + } + +} diff --git a/src/main/java/org/eclipse/paho/mqttv5/client/internal/CommsReceiver.java b/src/main/java/org/eclipse/paho/mqttv5/client/internal/CommsReceiver.java new file mode 100644 index 0000000..5ab52b4 --- /dev/null +++ b/src/main/java/org/eclipse/paho/mqttv5/client/internal/CommsReceiver.java @@ -0,0 +1,241 @@ +/******************************************************************************* + * Copyright (c) 2009, 2019 IBM Corp. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * and Eclipse Distribution License v1.0 which accompany this distribution. + * + * The Eclipse Public License is available at + * https://www.eclipse.org/legal/epl-2.0 + * and the Eclipse Distribution License is available at + * https://www.eclipse.org/org/documents/edl-v10.php + * + * Contributors: + * Dave Locke - initial API and implementation and/or initial documentation + */ +package org.eclipse.paho.mqttv5.client.internal; + +import java.io.IOException; +import java.io.InputStream; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Future; + +import org.eclipse.paho.mqttv5.client.MqttClientException; +import org.eclipse.paho.mqttv5.client.MqttToken; +import org.eclipse.paho.mqttv5.client.logging.Logger; +import org.eclipse.paho.mqttv5.client.logging.LoggerFactory; +import org.eclipse.paho.mqttv5.client.wire.MqttInputStream; +import org.eclipse.paho.mqttv5.common.MqttException; +import org.eclipse.paho.mqttv5.common.packet.MqttAck; +import org.eclipse.paho.mqttv5.common.packet.MqttDisconnect; +import org.eclipse.paho.mqttv5.common.packet.MqttWireMessage; + +/** + * Receives MQTT packets from the server. + */ +public class CommsReceiver implements Runnable { + private static final String CLASS_NAME = CommsReceiver.class.getName(); + private Logger log = LoggerFactory.getLogger(LoggerFactory.MQTT_CLIENT_MSG_CAT, CLASS_NAME); + + private enum State {STOPPED, RUNNING, STARTING, RECEIVING} + + private State current_state = State.STOPPED; + private State target_state = State.STOPPED; + private final Object lifecycle = new Object(); + private String threadName; + private Future receiverFuture; + + private ClientState clientState = null; + private ClientComms clientComms = null; + private MqttInputStream in; + private CommsTokenStore tokenStore = null; + private Thread recThread = null; + + public CommsReceiver(ClientComms clientComms, ClientState clientState, CommsTokenStore tokenStore, InputStream in) { + this.in = new MqttInputStream(clientState, in, clientComms.getClient().getClientId()); + this.clientComms = clientComms; + this.clientState = clientState; + this.tokenStore = tokenStore; + log.setResourceName(clientComms.getClient().getClientId()); + } + + /** + * Starts up the Receiver's thread. + * + * @param threadName + * the thread name. + * @param executorService + * used to execute the thread + */ + public void start(String threadName, ExecutorService executorService) { + this.threadName = threadName; + final String methodName = "start"; + // @TRACE 855=starting + log.fine(CLASS_NAME, methodName, "855"); + synchronized (lifecycle) { + if (current_state == State.STOPPED && target_state == State.STOPPED) { + target_state = State.RUNNING; + if (executorService == null) { + new Thread(this).start(); + } else { + receiverFuture = executorService.submit(this); + } + } + } + while (!isRunning()) { + try { Thread.sleep(100); } catch (Exception e) { } + } + } + + /** + * Stops the Receiver's thread. This call will block. + */ + public void stop() { + final String methodName = "stop"; + synchronized (lifecycle) { + if (receiverFuture != null) { + receiverFuture.cancel(true); + } + //@TRACE 850=stopping + log.fine(CLASS_NAME,methodName, "850"); + if (isRunning()) { + target_state = State.STOPPED; + } + } + while (isRunning()) { + try { Thread.sleep(100); } catch (Exception e) { } + } + //@TRACE 851=stopped + log.fine(CLASS_NAME,methodName,"851"); + } + + /** + * Run loop to receive messages from the server. + */ + public void run() { + recThread = Thread.currentThread(); + recThread.setName(threadName); + final String methodName = "run"; + MqttToken token = null; + + synchronized (lifecycle) { + current_state = State.RUNNING; + } + + try { + State my_target; + synchronized (lifecycle) { + my_target = target_state; + } + while (my_target == State.RUNNING && (in != null)) { + try { + //@TRACE 852=network read message + log.fine(CLASS_NAME,methodName,"852"); + if (in.available() > 0) { + synchronized (lifecycle) { + current_state = State.RECEIVING; + } + } + MqttWireMessage message = in.readMqttWireMessage(); + synchronized (lifecycle) { + current_state = State.RUNNING; + } + + // instanceof checks if message is null + if (message instanceof MqttAck) { + token = tokenStore.getToken(message); + if (token != null) { + synchronized (token) { + // Ensure the notify processing is done under a lock on the token + // This ensures that the send processing can complete before the + // receive processing starts! ( request and ack and ack processing + // can occur before request processing is complete if not! + clientState.notifyReceivedAck((MqttAck) message); + } + } else { + // This is an ack for a message we no longer have a ticket for. + log.fine(CLASS_NAME, methodName, "857"); + clientState.handleOrphanedAcks((MqttAck) message); + } + } else if (message != null && message instanceof MqttDisconnect) { + // This is a Disconnect Message + clientComms.shutdownConnection(null, new MqttException(MqttClientException.REASON_CODE_SERVER_DISCONNECTED, (MqttDisconnect) message), (MqttDisconnect) message); + } else { + if (message != null) { + // A new message has arrived + clientState.notifyReceivedMsg(message); + } + else { + if (!clientComms.isConnected() && !clientComms.isConnecting()) { + throw new IOException("Connection is lost."); + } + } + } + } + catch (MqttException ex) { + // @TRACE 856=Stopping, MQttException + log.fine(CLASS_NAME, methodName, "856", null, ex); + synchronized (lifecycle) { + target_state = State.STOPPED; + } + // Token maybe null but that is handled in shutdown + clientComms.shutdownConnection(token, ex, null); + } + catch (IOException ioe) { + // @TRACE 853=Stopping due to IOException + log.fine(CLASS_NAME, methodName, "853"); + if (target_state != State.STOPPED) { + synchronized (lifecycle) { + target_state = State.STOPPED; + } + // An EOFException could be raised if the broker processes the + // DISCONNECT and ends the socket before we complete. As such, + // only shutdown the connection if we're not already shutting down. + if (!clientComms.isDisconnecting()) { + clientComms.shutdownConnection(token, + new MqttException(MqttClientException.REASON_CODE_CONNECTION_LOST, ioe), null); + } + } + } + finally { + synchronized (lifecycle) { + current_state = State.RUNNING; + } + } + synchronized (lifecycle) { + my_target = target_state; + } + } // end while + } finally { + synchronized (lifecycle) { + current_state = State.STOPPED; + } + } // end try + + recThread = null; + //@TRACE 854=< + log.fine(CLASS_NAME,methodName,"854"); + } + + public boolean isRunning() { + boolean result; + synchronized (lifecycle) { + result = ((current_state == State.RUNNING || current_state == State.RECEIVING) + && target_state == State.RUNNING); + } + return result; + } + + /** + * Returns the receiving state. + * + * @return true if the receiver is receiving data, false otherwise. + */ + public boolean isReceiving() { + boolean result; + synchronized (lifecycle) { + result = (current_state == State.RECEIVING); + } + return result; + } +} diff --git a/src/main/java/org/eclipse/paho/mqttv5/client/internal/CommsSender.java b/src/main/java/org/eclipse/paho/mqttv5/client/internal/CommsSender.java new file mode 100644 index 0000000..6c8146c --- /dev/null +++ b/src/main/java/org/eclipse/paho/mqttv5/client/internal/CommsSender.java @@ -0,0 +1,210 @@ +/******************************************************************************* + * Copyright (c) 2009, 2019 IBM Corp. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * and Eclipse Distribution License v1.0 which accompany this distribution. + * + * The Eclipse Public License is available at + * https://www.eclipse.org/legal/epl-2.0 + * and the Eclipse Distribution License is available at + * https://www.eclipse.org/org/documents/edl-v10.php + * + * Contributors: + * Dave Locke - initial API and implementation and/or initial documentation + */ +package org.eclipse.paho.mqttv5.client.internal; + +import java.io.IOException; +import java.io.OutputStream; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Future; + +import org.eclipse.paho.mqttv5.client.MqttClientException; +import org.eclipse.paho.mqttv5.client.MqttToken; +import org.eclipse.paho.mqttv5.client.logging.Logger; +import org.eclipse.paho.mqttv5.client.logging.LoggerFactory; +import org.eclipse.paho.mqttv5.client.wire.MqttOutputStream; +import org.eclipse.paho.mqttv5.common.MqttException; +import org.eclipse.paho.mqttv5.common.packet.MqttAck; +import org.eclipse.paho.mqttv5.common.packet.MqttDisconnect; +import org.eclipse.paho.mqttv5.common.packet.MqttWireMessage; + + +public class CommsSender implements Runnable { + private static final String CLASS_NAME = CommsSender.class.getName(); + private Logger log = LoggerFactory.getLogger(LoggerFactory.MQTT_CLIENT_MSG_CAT, CLASS_NAME); + + //Sends MQTT packets to the server on its own thread + private enum State {STOPPED, RUNNING, STARTING} + + private State current_state = State.STOPPED; + private State target_state = State.STOPPED; + private final Object lifecycle = new Object(); + private Thread sendThread = null; + private String threadName; + private Future senderFuture; + + private ClientState clientState = null; + private MqttOutputStream out; + private ClientComms clientComms = null; + private CommsTokenStore tokenStore = null; + + + public CommsSender(ClientComms clientComms, ClientState clientState, CommsTokenStore tokenStore, OutputStream out) { + this.out = new MqttOutputStream(clientState, out, clientComms.getClient().getClientId()); + this.clientComms = clientComms; + this.clientState = clientState; + this.tokenStore = tokenStore; + log.setResourceName(clientComms.getClient().getClientId()); + } + + /** + * Starts up the Sender thread. + * @param threadName the threadname + * @param executorService used to execute the thread + */ + public void start(String threadName, ExecutorService executorService) { + this.threadName = threadName; + synchronized (lifecycle) { + if (current_state == State.STOPPED && target_state == State.STOPPED) { + target_state = State.RUNNING; + if (executorService == null) { + new Thread(this).start(); + } else { + senderFuture = executorService.submit(this); + } + } + } + while (!isRunning()) { + try { Thread.sleep(100); } catch (Exception e) { } + } + } + + /** + * Stops the Sender's thread. This call will block. + */ + public void stop() { + final String methodName = "stop"; + + if (!isRunning()) { + return; + } + + synchronized (lifecycle) { + if (senderFuture != null) { + senderFuture.cancel(true); + } + //@TRACE 800=stopping sender + log.fine(CLASS_NAME,methodName,"800"); + if (isRunning()) { + target_state = State.STOPPED; + clientState.notifyQueueLock(); + } + } + while (isRunning()) { + try { Thread.sleep(100); } catch (Exception e) { } + clientState.notifyQueueLock(); + } + //@TRACE 801=stopped + log.fine(CLASS_NAME,methodName,"801"); + } + + public void run() { + sendThread = Thread.currentThread(); + sendThread.setName(threadName); + final String methodName = "run"; + MqttWireMessage message = null; + + synchronized (lifecycle) { + current_state = State.RUNNING; + } + + try { + State my_target; + synchronized (lifecycle) { + my_target = target_state; + } + while (my_target == State.RUNNING && (out != null)) { + try { + message = clientState.get(); + if (message != null) { + //@TRACE 802=network send key={0} msg={1} + log.fine(CLASS_NAME,methodName,"802", new Object[] {message.getKey(),message}); + + if (message instanceof MqttAck) { + out.write(message); + out.flush(); + } else { + MqttToken token = tokenStore.getToken(message); + // While quiescing the tokenstore can be cleared so need + // to check for null for the case where clear occurs + // while trying to send a message. + if (token != null) { + synchronized (token) { + out.write(message); + try { + out.flush(); + } catch (IOException ex) { + // The flush has been seen to fail on disconnect of a SSL socket + // as disconnect is in progress this should not be treated as an error + if (!(message instanceof MqttDisconnect)) { + throw ex; + } + } + clientState.notifySent(message); + } + } + } + } else { // null message + //@TRACE 803=get message returned null, stopping} + log.fine(CLASS_NAME,methodName,"803"); + synchronized (lifecycle) { + target_state = State.STOPPED; + } + } + } catch (MqttException me) { + handleRunException(message, me); + } catch (Exception ex) { + handleRunException(message, ex); + } + synchronized (lifecycle) { + my_target = target_state; + } + } // end while + } finally { + synchronized (lifecycle) { + current_state = State.STOPPED; + sendThread = null; + } + } + + //@TRACE 805=< + log.fine(CLASS_NAME, methodName,"805"); + + } + + private void handleRunException(MqttWireMessage message, Exception ex) { + final String methodName = "handleRunException"; + //@TRACE 804=exception + log.severe(CLASS_NAME,methodName,"804",null, ex); + MqttException mex; + if ( !(ex instanceof MqttException)) { + mex = new MqttException(MqttClientException.REASON_CODE_CONNECTION_LOST, ex); + } else { + mex = (MqttException)ex; + } + synchronized (lifecycle) { + target_state = State.STOPPED; + } + clientComms.shutdownConnection(null, mex, null); + } + + public boolean isRunning() { + boolean result; + synchronized (lifecycle) { + result = (current_state == State.RUNNING && target_state == State.RUNNING); + } + return result; + } +} diff --git a/src/main/java/org/eclipse/paho/mqttv5/client/internal/CommsTokenStore.java b/src/main/java/org/eclipse/paho/mqttv5/client/internal/CommsTokenStore.java new file mode 100644 index 0000000..5ccecd6 --- /dev/null +++ b/src/main/java/org/eclipse/paho/mqttv5/client/internal/CommsTokenStore.java @@ -0,0 +1,254 @@ +/******************************************************************************* + * Copyright (c) 2009, 2018 IBM Corp. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * and Eclipse Distribution License v1.0 which accompany this distribution. + * + * The Eclipse Public License is available at + * https://www.eclipse.org/legal/epl-2.0 + * and the Eclipse Distribution License is available at + * https://www.eclipse.org/org/documents/edl-v10.php + * + * Contributors: + * Dave Locke - initial API and implementation and/or initial documentation + */ +package org.eclipse.paho.mqttv5.client.internal; + +import java.util.Enumeration; +import java.util.Hashtable; +import java.util.Vector; + +import org.eclipse.paho.mqttv5.client.MqttToken; +import org.eclipse.paho.mqttv5.client.logging.Logger; +import org.eclipse.paho.mqttv5.client.logging.LoggerFactory; +import org.eclipse.paho.mqttv5.common.MqttException; +import org.eclipse.paho.mqttv5.common.packet.MqttPublish; +import org.eclipse.paho.mqttv5.common.packet.MqttWireMessage; + + +/** + * Provides a "token" based system for storing and tracking actions across + * multiple threads. + * When a message is sent, a token is associated with the message + * and saved using the {@link CommsTokenStore#saveToken(MqttToken, MqttWireMessage)} method. Anyone interested + * in tacking the state can call one of the wait methods on the token or using + * the asynchronous listener callback method on the operation. + * The {@link CommsReceiver} class, on another thread, reads responses back from + * the network. It uses the response to find the relevant token, which it can then + * notify. + * + * Note: + * Ping, connect and disconnect do not have a unique message id as + * only one outstanding request of each type is allowed to be outstanding + */ +public class CommsTokenStore { + private static final String CLASS_NAME = CommsTokenStore.class.getName(); + private Logger log = LoggerFactory.getLogger(LoggerFactory.MQTT_CLIENT_MSG_CAT, CLASS_NAME); + + // Maps message-specific data (usually message IDs) to tokens + private final Hashtable tokens; + private String logContext; + private MqttException closedResponse = null; + + public CommsTokenStore(String logContext) { + final String methodName = ""; + + log.setResourceName(logContext); + this.tokens = new Hashtable(); + this.logContext = logContext; + //@TRACE 308=<> + log.fine(CLASS_NAME,methodName,"308");//,new Object[]{message}); + + } + + /** + * Based on the message type that has just been received return the associated + * token from the token store or null if one does not exist. + * @param message whose token is to be returned + * @return token for the requested message + */ + public MqttToken getToken(MqttWireMessage message) { + String key = message.getKey(); + return (MqttToken)tokens.get(key); + } + + public MqttToken getToken(String key) { + return (MqttToken)tokens.get(key); + } + + + public MqttToken removeToken(MqttWireMessage message) { + if (message != null) { + return removeToken(message.getKey()); + } + return null; + } + + public MqttToken removeToken(String key) { + final String methodName = "removeToken"; + //@TRACE 306=key={0} + log.fine(CLASS_NAME,methodName,"306",new Object[]{key}); + + if ( null != key ){ + return (MqttToken) tokens.remove(key); + } + + return null; + } + + /** + * Restores a token after a client restart. This method could be called + * for a SEND of CONFIRM, but either way, the original SEND is what's + * needed to re-build the token. + * @param message The {@link MqttPublish} message to restore + * @return {@link MqttToken} + */ + protected MqttToken restoreToken(MqttPublish message) { + final String methodName = "restoreToken"; + MqttToken token; + synchronized(tokens) { + String key = Integer.valueOf(message.getMessageId()).toString(); + if (this.tokens.containsKey(key)) { + token = this.tokens.get(key); + //@TRACE 302=existing key={0} message={1} token={2} + log.fine(CLASS_NAME,methodName, "302",new Object[]{key, message,token}); + } else { + token = new MqttToken(logContext); + token.internalTok.setDeliveryToken(true); + token.internalTok.setKey(key); + this.tokens.put(key, token); + //@TRACE 303=creating new token key={0} message={1} token={2} + log.fine(CLASS_NAME,methodName,"303",new Object[]{key, message, token}); + } + } + return token; + } + + // For outbound messages store the token in the token store + // For pubrel use the existing publish token + protected void saveToken(MqttToken token, MqttWireMessage message) throws MqttException { + final String methodName = "saveToken"; + + synchronized(tokens) { + if (closedResponse == null) { + String key = message.getKey(); + //@TRACE 300=key={0} message={1} + log.fine(CLASS_NAME,methodName,"300",new Object[]{key, message}); + + saveToken(token,key); + } else { + throw closedResponse; + } + } + } + + protected void saveToken(MqttToken token, String key) { + final String methodName = "saveToken"; + + synchronized(tokens) { + //@TRACE 307=key={0} token={1} + log.fine(CLASS_NAME,methodName,"307",new Object[]{key,token.toString()}); + token.internalTok.setKey(key); + this.tokens.put(key, token); + } + } + + protected void quiesce(MqttException quiesceResponse) { + final String methodName = "quiesce"; + + synchronized(tokens) { + //@TRACE 309=resp={0} + log.fine(CLASS_NAME,methodName,"309",new Object[]{quiesceResponse}); + + closedResponse = quiesceResponse; + } + } + + public void open() { + final String methodName = "open"; + + synchronized(tokens) { + //@TRACE 310=> + log.fine(CLASS_NAME,methodName,"310"); + + closedResponse = null; + } + } + + public MqttToken[] getOutstandingDelTokens() { + final String methodName = "getOutstandingDelTokens"; + + synchronized(tokens) { + //@TRACE 311=> + log.fine(CLASS_NAME,methodName,"311"); + + Vector list = new Vector(); + Enumeration enumeration = tokens.elements(); + MqttToken token; + while(enumeration.hasMoreElements()) { + token = (MqttToken)enumeration.nextElement(); + if (token != null + && token.internalTok.isDeliveryToken() == true + && !token.internalTok.isNotified()) { + + list.addElement(token); + } + } + + MqttToken[] result = new MqttToken[list.size()]; + return (MqttToken[]) list.toArray(result); + } + } + + public Vector getOutstandingTokens() { + final String methodName = "getOutstandingTokens"; + + synchronized(tokens) { + //@TRACE 312=> + log.fine(CLASS_NAME,methodName,"312"); + + Vector list = new Vector(); + Enumeration enumeration = tokens.elements(); + MqttToken token; + while(enumeration.hasMoreElements()) { + token = (MqttToken)enumeration.nextElement(); + if (token != null) { + list.addElement(token); + } + } + return list; + } + } + + /** + * Empties the token store without notifying any of the tokens. + */ + public void clear() { + final String methodName = "clear"; + //@TRACE 305=> {0} tokens + log.fine(CLASS_NAME, methodName, "305", new Object[] { Integer.valueOf(tokens.size())}); + synchronized(tokens) { + tokens.clear(); + } + } + + public int count() { + synchronized(tokens) { + return tokens.size(); + } + } + public String toString() { + String lineSep = System.getProperty("line.separator","\n"); + StringBuffer toks = new StringBuffer(); + synchronized(tokens) { + Enumeration enumeration = tokens.elements(); + MqttToken token; + while(enumeration.hasMoreElements()) { + token = (MqttToken)enumeration.nextElement(); + toks.append("{"+token.internalTok+"}"+lineSep); + } + return toks.toString(); + } + } +} diff --git a/src/main/java/org/eclipse/paho/mqttv5/client/internal/ConnectActionListener.java b/src/main/java/org/eclipse/paho/mqttv5/client/internal/ConnectActionListener.java new file mode 100644 index 0000000..bc1c3f3 --- /dev/null +++ b/src/main/java/org/eclipse/paho/mqttv5/client/internal/ConnectActionListener.java @@ -0,0 +1,263 @@ +/******************************************************************************* + * Copyright (c) 2009, 2019 IBM Corp. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * and Eclipse Distribution License v1.0 which accompany this distribution. + * + * The Eclipse Public License is available at + * https://www.eclipse.org/legal/epl-2.0 + * and the Eclipse Distribution License is available at + * https://www.eclipse.org/org/documents/edl-v10.php + * + * Contributors: + * Ian Craggs - MQTT 3.1.1 support + * Ian Craggs - fix bug 469527 + * James Sutton - Automatic Reconnect & Offline Buffering + */ +package org.eclipse.paho.mqttv5.client.internal; + +import org.eclipse.paho.mqttv5.client.IMqttToken; +import org.eclipse.paho.mqttv5.client.MqttActionListener; +import org.eclipse.paho.mqttv5.client.MqttAsyncClient; +import org.eclipse.paho.mqttv5.client.MqttCallback; +import org.eclipse.paho.mqttv5.client.MqttClientPersistence; +import org.eclipse.paho.mqttv5.client.MqttConnectionOptions; +import org.eclipse.paho.mqttv5.client.MqttToken; +import org.eclipse.paho.mqttv5.common.MqttException; +import org.eclipse.paho.mqttv5.common.MqttPersistenceException; + +/** + *

+ * This class handles the connection of the AsyncClient to one of the available + * URLs. + *

+ *

+ * The URLs are supplied as either the singleton when the client is created, or + * as a list in the connect options. + *

+ *

+ * This class uses its own onSuccess and onFailure callbacks in preference to + * the user supplied callbacks. + *

+ *

+ * An attempt is made to connect to each URL in the list until either a + * connection attempt succeeds or all the URLs have been tried + *

+ *

+ * If a connection succeeds then the users token is notified and the users + * onSuccess callback is called. + *

+ *

+ * If a connection fails then another URL in the list is attempted, otherwise + * the users token is notified and the users onFailure callback is called + *

+ */ +public class ConnectActionListener implements MqttActionListener { + + private MqttClientPersistence persistence; + private MqttAsyncClient client; + private ClientComms comms; + private MqttConnectionOptions options; + private MqttToken userToken; + private Object userContext; + private MqttActionListener userCallback; + private MqttCallback mqttCallback; + private MqttSessionState mqttSession; + private MqttConnectionState mqttConnection; + private boolean reconnect; + + /** + * @param persistence + * The {@link MqttClientPersistence} layer + * @param client + * the {@link MqttAsyncClient} + * @param comms + * {@link ClientComms} + * @param options + * the {@link MqttConnectionOptions} + * @param userToken + * the {@link MqttToken} + * @param userContext + * the User Context Object + * @param userCallback + * the {@link MqttActionListener} as the callback for the user + * @param mqttSession + * the {@link MqttSessionState} + * @param mqttConnection + * the {@link MqttConnectionState} + * @param reconnect + * If true, this is a reconnect attempt + */ + public ConnectActionListener(MqttAsyncClient client, MqttClientPersistence persistence, ClientComms comms, + MqttConnectionOptions options, MqttToken userToken, Object userContext, MqttActionListener userCallback, + boolean reconnect, MqttSessionState mqttSession, MqttConnectionState mqttConnection) { + this.persistence = persistence; + this.client = client; + this.comms = comms; + this.options = options; + this.userToken = userToken; + this.userContext = userContext; + this.userCallback = userCallback; + this.reconnect = reconnect; + this.mqttSession = mqttSession; + this.mqttConnection = mqttConnection; + + } + + /** + * If the connect succeeded then call the users onSuccess callback + * + * @param token + * the {@link IMqttToken} from the successful connection + */ + public void onSuccess(IMqttToken token) { + // Set properties imposed on us by the Server + MqttToken myToken = (MqttToken) token; + if (myToken.getResponseProperties() != null) { + mqttConnection.setReceiveMaximum(myToken.getResponseProperties().getReceiveMaximum()); + mqttConnection.setMaximumQoS(myToken.getResponseProperties().getMaximumQoS()); + mqttConnection.setRetainAvailable(myToken.getResponseProperties().isRetainAvailable()); + mqttConnection.setOutgoingMaximumPacketSize(myToken.getResponseProperties().getMaximumPacketSize()); + mqttConnection.setIncomingMaximumPacketSize(options.getMaximumPacketSize()); + mqttConnection.setOutgoingTopicAliasMaximum(myToken.getResponseProperties().getTopicAliasMaximum()); + mqttConnection + .setWildcardSubscriptionsAvailable(myToken.getResponseProperties().isWildcardSubscriptionsAvailable()); + mqttConnection.setSubscriptionIdentifiersAvailable( + myToken.getResponseProperties().isSubscriptionIdentifiersAvailable()); + mqttConnection.setSharedSubscriptionsAvailable(myToken.getResponseProperties().isSharedSubscriptionAvailable()); + + // If provided, set the server keep alive value. + if(myToken.getResponseProperties().getServerKeepAlive() != null) { + mqttConnection.setKeepAliveSeconds(myToken.getResponseProperties().getServerKeepAlive()); + } + + // If we are assigning the client ID post connect, then we need to re-initialise + // our persistence layer. + if (myToken.getResponseProperties().getAssignedClientIdentifier() != null) { + mqttSession.setClientId(myToken.getResponseProperties().getAssignedClientIdentifier()); + try { + persistence.open(myToken.getResponseProperties().getAssignedClientIdentifier()); + + if (options.isCleanStart()) { + persistence.clear(); + } + } catch (MqttPersistenceException exception) { + + // If we fail to open persistence at this point, our best bet is to immediately + // close the connection. + try { + client.disconnect(); + } catch (MqttException ex) { + } + onFailure(token, exception); + return; + } + } + } + + userToken.internalTok.markComplete(token.getResponse(), null); + userToken.internalTok.notifyComplete(); + userToken.internalTok.setClient(this.client); // fix bug 469527 - maybe should be set elsewhere? + + if (reconnect) { + comms.notifyReconnect(); + } + + if (userCallback != null) { + userToken.setUserContext(userContext); + userCallback.onSuccess(userToken); + } + + if (mqttCallback != null) { + String serverURI = comms.getNetworkModules()[comms.getNetworkModuleIndex()].getServerURI(); + try { + mqttCallback.connectComplete(reconnect, serverURI); + } catch (Throwable ex) { + // Just catch any exceptions thrown here and ignore. + } + + } + + } + + /** + * The connect failed, so try the next URI on the list. If there are no more + * URIs, then fail the overall connect. + * + * @param token + * the {@link IMqttToken} from the failed connection attempt + * @param exception + * the {@link Throwable} exception from the failed connection attempt + */ + public void onFailure(IMqttToken token, Throwable exception) { + + int numberOfURIs = comms.getNetworkModules().length; + int index = comms.getNetworkModuleIndex(); + + if ((index + 1) < numberOfURIs) { + + comms.setNetworkModuleIndex(index + 1); + + try { + connect(); + } catch (MqttPersistenceException e) { + onFailure(token, e); // try the next URI in the list + } + } else { + + MqttException ex; + if (exception instanceof MqttException) { + ex = (MqttException) exception; + } else { + ex = new MqttException(exception); + } + userToken.internalTok.markComplete(null, ex); + userToken.internalTok.notifyComplete(); + userToken.internalTok.setClient(this.client); // fix bug 469527 - maybe should be set elsewhere? + + if (userCallback != null) { + userToken.setUserContext(userContext); + userCallback.onFailure(userToken, exception); + } + } + } + + /** + * Start the connect processing + * + * @throws MqttPersistenceException + * if an error is thrown whilst setting up persistence + */ + public void connect() throws MqttPersistenceException { + MqttToken token = new MqttToken(client.getClientId()); + token.setActionCallback(this); + token.setUserContext(this); + + if (!client.getClientId().equals("")) { + persistence.open(client.getClientId()); + + if (options.isCleanStart()) { + persistence.clear(); + } + } + + try { + comms.connect(options, token); + } catch (MqttException e) { + onFailure(token, e); + } + } + + /** + * Set the MqttCallbackExtened callback to receive connectComplete callbacks + * + * @param mqttCallback + * the {@link MqttCallback} to be called when the connection + * completes + */ + public void setMqttCallbackExtended(MqttCallback mqttCallback) { + this.mqttCallback = mqttCallback; + } + +} diff --git a/src/main/java/org/eclipse/paho/mqttv5/client/internal/DestinationProvider.java b/src/main/java/org/eclipse/paho/mqttv5/client/internal/DestinationProvider.java new file mode 100644 index 0000000..7fb7510 --- /dev/null +++ b/src/main/java/org/eclipse/paho/mqttv5/client/internal/DestinationProvider.java @@ -0,0 +1,31 @@ +/******************************************************************************* + * Copyright (c) 2009, 2014 IBM Corp. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * and Eclipse Distribution License v1.0 which accompany this distribution. + * + * The Eclipse Public License is available at + * https://www.eclipse.org/legal/epl-2.0 + * and the Eclipse Distribution License is available at + * https://www.eclipse.org/org/documents/edl-v10.php + * + * Contributors: + * Dave Locke - initial API and implementation and/or initial documentation + */ +package org.eclipse.paho.mqttv5.client.internal; + +import org.eclipse.paho.mqttv5.client.MqttTopic; + +/** + * This interface exists to act as a common type for + * MqttClient and MqttMIDPClient so they can be passed to + * ClientComms without either client class need to know + * about the other. + * Specifically, this allows the MIDP client to work + * without the non-MIDP MqttClient/MqttConnectOptions + * classes being present. + */ +public interface DestinationProvider { + MqttTopic getTopic(String topic); +} diff --git a/src/main/java/org/eclipse/paho/mqttv5/client/internal/DisconnectedMessageBuffer.java b/src/main/java/org/eclipse/paho/mqttv5/client/internal/DisconnectedMessageBuffer.java new file mode 100644 index 0000000..8d7a529 --- /dev/null +++ b/src/main/java/org/eclipse/paho/mqttv5/client/internal/DisconnectedMessageBuffer.java @@ -0,0 +1,135 @@ +/******************************************************************************* + * Copyright (c) 2016, 2018 IBM Corp. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * and Eclipse Distribution License v1.0 which accompany this distribution. + * + * The Eclipse Public License is available at + * https://www.eclipse.org/legal/epl-2.0 + * and the Eclipse Distribution License is available at + * https://www.eclipse.org/org/documents/edl-v10.php + * + * Contributors: + * James Sutton - Initial Contribution for Automatic Reconnect & Offline Buffering + */ +package org.eclipse.paho.mqttv5.client.internal; + +import java.util.ArrayList; + +import org.eclipse.paho.mqttv5.client.BufferedMessage; +import org.eclipse.paho.mqttv5.client.DisconnectedBufferOptions; +import org.eclipse.paho.mqttv5.client.MqttClientException; +import org.eclipse.paho.mqttv5.client.MqttToken; +import org.eclipse.paho.mqttv5.client.logging.Logger; +import org.eclipse.paho.mqttv5.client.logging.LoggerFactory; +import org.eclipse.paho.mqttv5.common.MqttException; +import org.eclipse.paho.mqttv5.common.packet.MqttWireMessage; + +public class DisconnectedMessageBuffer implements Runnable { + + private static final String CLASS_NAME = DisconnectedMessageBuffer.class.getName(); + private Logger log = LoggerFactory.getLogger(LoggerFactory.MQTT_CLIENT_MSG_CAT, CLASS_NAME); + private DisconnectedBufferOptions bufferOpts; + private ArrayList buffer; + private final Object bufLock = new Object(); // Used to synchronise the buffer + private IDisconnectedBufferCallback callback; + + public DisconnectedMessageBuffer(DisconnectedBufferOptions options){ + this.bufferOpts = options; + buffer = new ArrayList(); + } + + /** + * This will add a new message to the offline buffer, + * if the buffer is full and deleteOldestMessages is enabled + * then the 0th item in the buffer will be deleted and the + * new message will be added. If it is not enabled then an + * MqttException will be thrown. + * @param message the {@link MqttWireMessage} that will be buffered + * @param token the associated {@link MqttToken} + * @throws MqttException if the Buffer is full + */ + public void putMessage(MqttWireMessage message, MqttToken token) throws MqttException{ + BufferedMessage bufferedMessage = new BufferedMessage(message, token); + synchronized (bufLock) { + if(buffer.size() < bufferOpts.getBufferSize()){ + buffer.add(bufferedMessage); + } else if(bufferOpts.isDeleteOldestMessages() == true){ + buffer.remove(0); + buffer.add(bufferedMessage); + }else { + throw new MqttException(MqttClientException.REASON_CODE_DISCONNECTED_BUFFER_FULL); + } + } + } + + /** + * Retrieves a message from the buffer at the given index. + * @param messageIndex the index of the message to be retrieved in the buffer + * @return the {@link BufferedMessage} + */ + public BufferedMessage getMessage(int messageIndex){ + synchronized (bufLock) { + return((BufferedMessage) buffer.get(messageIndex)); + } + } + + + /** + * Removes a message from the buffer + * @param messageIndex the index of the message to be deleted in the buffer + */ + public void deleteMessage(int messageIndex){ + synchronized (bufLock) { + buffer.remove(messageIndex); + } + } + + /** + * Returns the number of messages currently in the buffer + * @return The count of messages in the buffer + */ + public int getMessageCount() { + synchronized (bufLock) { + return buffer.size(); + } + } + + /** + * Flushes the buffer of messages into an open connection + */ + public void run() { + final String methodName = "run"; + // @TRACE 516=Restoring all buffered messages. + log.fine(CLASS_NAME, methodName, "516"); + while(getMessageCount() > 0){ + try { + BufferedMessage bufferedMessage = getMessage(0); + callback.publishBufferedMessage(bufferedMessage); + // Publish was successful, remove message from buffer. + deleteMessage(0); + } catch (MqttException ex) { + if (ex.getReasonCode() == MqttClientException.REASON_CODE_MAX_INFLIGHT) { + // If we get the max_inflight condition, try again after a short + // interval to allow more messages to be completely sent. + try { Thread.sleep(100); } catch (Exception e) {} + } else { + // Error occurred attempting to publish buffered message likely because the client is not connected + // @TRACE 519=Error occurred attempting to publish buffered message due to disconnect. Exception: {0}. + log.warning(CLASS_NAME, methodName, "519", new Object[]{ex.getMessage()}); + break; + } + } + } + } + + public void setPublishCallback(IDisconnectedBufferCallback callback) { + this.callback = callback; + } + + public boolean isPersistBuffer(){ + return bufferOpts.isPersistBuffer(); + } + +} diff --git a/src/main/java/org/eclipse/paho/mqttv5/client/internal/ExceptionHelper.java b/src/main/java/org/eclipse/paho/mqttv5/client/internal/ExceptionHelper.java new file mode 100644 index 0000000..924d19b --- /dev/null +++ b/src/main/java/org/eclipse/paho/mqttv5/client/internal/ExceptionHelper.java @@ -0,0 +1,63 @@ +/******************************************************************************* + * Copyright (c) 2009, 2014 IBM Corp. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * and Eclipse Distribution License v1.0 which accompany this distribution. + * + * The Eclipse Public License is available at + * https://www.eclipse.org/legal/epl-2.0 + * and the Eclipse Distribution License is available at + * https://www.eclipse.org/org/documents/edl-v10.php + * + * Contributors: + * Dave Locke - initial API and implementation and/or initial documentation + */ +package org.eclipse.paho.mqttv5.client.internal; + +import org.eclipse.paho.mqttv5.client.MqttClientException; +import org.eclipse.paho.mqttv5.common.MqttException; +import org.eclipse.paho.mqttv5.common.MqttSecurityException; + +/** + * Utility class to help create exceptions of the correct type. + */ +public class ExceptionHelper { + public static MqttException createMqttException(int reasonCode) { + if ((reasonCode == MqttClientException.REASON_CODE_FAILED_AUTHENTICATION) || + (reasonCode == MqttClientException.REASON_CODE_NOT_AUTHORIZED)) { + return new MqttSecurityException(reasonCode); + } + + return new MqttException(reasonCode); + } + + public static MqttException createMqttException(Throwable cause) { + if (cause.getClass().getName().equals("java.security.GeneralSecurityException")) { + return new MqttSecurityException(cause); + } + return new MqttException(cause); + } + + /** + * Returns whether or not the specified class is available to the current + * class loader. This is used to protect the code against using Java SE + * APIs on Java ME. + * @param className The name of the class + * @return If true, the class is available + */ + public static boolean isClassAvailable(String className) { + boolean result = false; + try { + Class.forName(className); + result = true; + } + catch (ClassNotFoundException ex) { + } + return result; + } + + // Utility classes should not have a public or default constructor. + private ExceptionHelper() { + } +} diff --git a/src/main/java/org/eclipse/paho/mqttv5/client/internal/FileLock.java b/src/main/java/org/eclipse/paho/mqttv5/client/internal/FileLock.java new file mode 100644 index 0000000..52aaca2 --- /dev/null +++ b/src/main/java/org/eclipse/paho/mqttv5/client/internal/FileLock.java @@ -0,0 +1,96 @@ +/******************************************************************************* + * Copyright (c) 2009, 2014 IBM Corp. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * and Eclipse Distribution License v1.0 which accompany this distribution. + * + * The Eclipse Public License is available at + * https://www.eclipse.org/legal/epl-2.0 + * and the Eclipse Distribution License is available at + * https://www.eclipse.org/org/documents/edl-v10.php + * + * Contributors: + * Dave Locke - initial API and implementation and/or initial documentation + */ +package org.eclipse.paho.mqttv5.client.internal; +/** + * FileLock - used to obtain a lock that can be used to prevent other MQTT clients + * using the same persistent store. If the lock is already held then an exception + * is thrown. + * + * Some Java runtimes such as JME MIDP do not support file locking or even + * the Java classes that support locking. The class is coded to both compile + * and work on all Java runtimes. In Java runtimes that do not support + * locking it will look as though a lock has been obtained but in reality + * no lock has been obtained. + */ +import java.io.File; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.lang.reflect.Method; + +public class FileLock { + private File lockFile; + private RandomAccessFile file; + private Object fileLock; + + /** + * Creates an NIO FileLock on the specified file if on a suitable Java runtime. + * @param clientDir the a File of the directory to contain the lock file. + * @param lockFilename name of the the file to lock + * @throws Exception if the lock could not be obtained for any reason + */ + public FileLock(File clientDir, String lockFilename) throws Exception { + // Create a file to obtain a lock on. + lockFile = new File(clientDir,lockFilename); + if (ExceptionHelper.isClassAvailable("java.nio.channels.FileLock")) { + try { + this.file = new RandomAccessFile(lockFile,"rw"); + Method m = file.getClass().getMethod("getChannel",new Class[]{}); + Object channel = m.invoke(file,new Object[]{}); + m = channel.getClass().getMethod("tryLock",new Class[]{}); + this.fileLock = m.invoke(channel, new Object[]{}); + } catch(NoSuchMethodException nsme) { + this.fileLock = null; + } catch(IllegalArgumentException iae) { + this.fileLock = null; + } catch(IllegalAccessException iae) { + this.fileLock = null; + } + if (fileLock == null) { + // Lock not obtained + release(); + throw new Exception("Problem obtaining file lock"); + } + } + } + + /** + * Releases the lock. + */ + public void release() { + try { + if (fileLock != null) { + Method m = fileLock.getClass().getMethod("release",new Class[]{}); + m.invoke(fileLock, new Object[]{}); + fileLock = null; + } + } catch (Exception e) { + // Ignore exceptions + } + if (file != null) { + try { + file.close(); + } catch (IOException e) { + } + file = null; + } + + if (lockFile != null && lockFile.exists()) { + lockFile.delete(); + } + lockFile = null; + } + +} diff --git a/src/main/java/org/eclipse/paho/mqttv5/client/internal/IDisconnectedBufferCallback.java b/src/main/java/org/eclipse/paho/mqttv5/client/internal/IDisconnectedBufferCallback.java new file mode 100644 index 0000000..8a4717c --- /dev/null +++ b/src/main/java/org/eclipse/paho/mqttv5/client/internal/IDisconnectedBufferCallback.java @@ -0,0 +1,25 @@ +/******************************************************************************* + * Copyright (c) 2016 IBM Corp. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * and Eclipse Distribution License v1.0 which accompany this distribution. + * + * The Eclipse Public License is available at + * https://www.eclipse.org/legal/epl-2.0 + * and the Eclipse Distribution License is available at + * https://www.eclipse.org/org/documents/edl-v10.php + * + * Contributors: + * James Sutton - Initial Contribution for Automatic Reconnect & Offline Buffering + */ +package org.eclipse.paho.mqttv5.client.internal; + +import org.eclipse.paho.mqttv5.client.BufferedMessage; +import org.eclipse.paho.mqttv5.common.MqttException; + +public interface IDisconnectedBufferCallback { + + void publishBufferedMessage(BufferedMessage bufferedMessage) throws MqttException; + +} diff --git a/src/main/java/org/eclipse/paho/mqttv5/client/internal/MessageCatalog.java b/src/main/java/org/eclipse/paho/mqttv5/client/internal/MessageCatalog.java new file mode 100644 index 0000000..fa4de45 --- /dev/null +++ b/src/main/java/org/eclipse/paho/mqttv5/client/internal/MessageCatalog.java @@ -0,0 +1,46 @@ +/******************************************************************************* + * Copyright (c) 2009, 2014 IBM Corp. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * and Eclipse Distribution License v1.0 which accompany this distribution. + * + * The Eclipse Public License is available at + * https://www.eclipse.org/legal/epl-2.0 + * and the Eclipse Distribution License is available at + * https://www.eclipse.org/org/documents/edl-v10.php + * + * Contributors: + * Dave Locke - initial API and implementation and/or initial documentation + */ +package org.eclipse.paho.mqttv5.client.internal; + +/** + * Catalog of human readable error messages. + */ +public abstract class MessageCatalog { + private static MessageCatalog INSTANCE = null; + + public static final String getMessage(int id) { + if (INSTANCE == null) { + if (ExceptionHelper.isClassAvailable("java.util.ResourceBundle")) { + try { + // Hide this class reference behind reflection so that the class does not need to + // be present when compiled on midp + INSTANCE = (MessageCatalog)Class.forName("org.eclipse.paho.mqttv5.client.internal.ResourceBundleCatalog").newInstance(); + } catch (Exception e) { + return ""; + } + } else if (ExceptionHelper.isClassAvailable("org.eclipse.paho.mqttv5.client.internal.MIDPCatalog")){ + try { + INSTANCE = (MessageCatalog)Class.forName("org.eclipse.paho.mqttv5.client.internal.MIDPCatalog").newInstance(); + } catch (Exception e) { + return ""; + } + } + } + return INSTANCE.getLocalizedMessage(id); + } + + protected abstract String getLocalizedMessage(int id); +} diff --git a/src/main/java/org/eclipse/paho/mqttv5/client/internal/MqttConnectionState.java b/src/main/java/org/eclipse/paho/mqttv5/client/internal/MqttConnectionState.java new file mode 100644 index 0000000..e5c1495 --- /dev/null +++ b/src/main/java/org/eclipse/paho/mqttv5/client/internal/MqttConnectionState.java @@ -0,0 +1,185 @@ +package org.eclipse.paho.mqttv5.client.internal; + +import java.util.concurrent.atomic.AtomicInteger; + +/** + * This class is used as a store for client information that should be preserved + * for a single connection. + * Properties returned in subsequent connect packets will override existing properties + * here as well. + * + * Connection variables that this class holds: + * + *
    + *
  • Receive Maximum
  • + *
  • Maximum QoS
  • + *
  • Retain Available
  • + *
  • Maximum Packet Size
  • + *
  • Outgoing Topic Alias Maximum
  • + *
  • Incoming Topic Alias Maximum
  • + *
  • Wildcard Subscriptions Available
  • + *
  • Subscription Identifiers Available
  • + *
  • Shared Subscriptions Available
  • + *
  • Send Reason Messages
  • + *
+ */ +public class MqttConnectionState { + + // ******* Connection properties ******// + private Integer receiveMaximum = 65535; + private Integer maximumQoS = 2; + private Boolean retainAvailable = true; + private Long outgoingMaximumPacketSize = null; + private Long incomingMaximumPacketSize = null; + private Integer outgoingTopicAliasMaximum = 0; + private Integer incomingTopicAliasMax = 0; + private Boolean wildcardSubscriptionsAvailable = true; + private Boolean subscriptionIdentifiersAvailable = true; + private Boolean sharedSubscriptionsAvailable = true; + private boolean sendReasonMessages = false; + private long keepAlive = 60; + private String clientId = ""; + + // ******* Counters ******// + private AtomicInteger nextOutgoingTopicAlias = new AtomicInteger(1); + + public MqttConnectionState(String clientId) { + this.clientId = clientId; + } + + public String getClientId() { + return clientId; + } + + /** + * Clears the session and resets. This would be called when the connection has + * been lost and cleanStart = True. + */ + public void clearConnectionState() { + nextOutgoingTopicAlias.set(1); + } + + public Integer getReceiveMaximum() { + if (receiveMaximum == null) { + return 65535; + } + return receiveMaximum; + } + + public void setReceiveMaximum(Integer receiveMaximum) { + if (receiveMaximum != null) + this.receiveMaximum = receiveMaximum; + } + + public Integer getMaximumQoS() { + return maximumQoS; + } + + public void setMaximumQoS(Integer maximumQoS) { + if (maximumQoS != null) + this.maximumQoS = maximumQoS; + } + + public Boolean isRetainAvailable() { + return retainAvailable; + } + + public void setRetainAvailable(Boolean retainAvailable) { + if (retainAvailable != null) + this.retainAvailable = retainAvailable; + } + + public Long getOutgoingMaximumPacketSize() { + return outgoingMaximumPacketSize; + } + + public void setOutgoingMaximumPacketSize(Long maximumPacketSize) { + if (maximumPacketSize != null) + this.outgoingMaximumPacketSize = maximumPacketSize; + } + + public Long getIncomingMaximumPacketSize() { + return incomingMaximumPacketSize; + } + + + public void setIncomingMaximumPacketSize(Long incomingMaximumPacketSize) { + if (incomingMaximumPacketSize != null) + this.incomingMaximumPacketSize = incomingMaximumPacketSize; + } + + + public Integer getOutgoingTopicAliasMaximum() { + return outgoingTopicAliasMaximum; + } + + public void setOutgoingTopicAliasMaximum(Integer topicAliasMaximum) { + if (topicAliasMaximum != null) + this.outgoingTopicAliasMaximum = topicAliasMaximum; + } + + public Boolean isWildcardSubscriptionsAvailable() { + return wildcardSubscriptionsAvailable; + } + + public void setWildcardSubscriptionsAvailable(Boolean wildcardSubscriptionsAvailable) { + if (wildcardSubscriptionsAvailable != null) + this.wildcardSubscriptionsAvailable = wildcardSubscriptionsAvailable; + } + + public Boolean isSubscriptionIdentifiersAvailable() { + return subscriptionIdentifiersAvailable; + } + + public void setSubscriptionIdentifiersAvailable(Boolean subscriptionIdentifiersAvailable) { + if (subscriptionIdentifiersAvailable != null) + this.subscriptionIdentifiersAvailable = subscriptionIdentifiersAvailable; + } + + public Boolean isSharedSubscriptionsAvailable() { + return sharedSubscriptionsAvailable; + } + + public void setSharedSubscriptionsAvailable(Boolean sharedSubscriptionsAvailable) { + if (sharedSubscriptionsAvailable != null) + this.sharedSubscriptionsAvailable = sharedSubscriptionsAvailable; + } + + public Integer getNextOutgoingTopicAlias() { + return nextOutgoingTopicAlias.getAndIncrement(); + } + + + public Integer getIncomingTopicAliasMax() { + return incomingTopicAliasMax; + } + + + public void setIncomingTopicAliasMax(Integer incomingTopicAliasMax) { + if (incomingTopicAliasMax != null) + this.incomingTopicAliasMax = incomingTopicAliasMax; + } + + + public boolean isSendReasonMessages() { + return sendReasonMessages; + } + + + public void setSendReasonMessages(boolean enableReasonMessages) { + this.sendReasonMessages = enableReasonMessages; + } + + + public long getKeepAlive() { + return keepAlive; + } + + + public void setKeepAliveSeconds(long keepAlive) { + this.keepAlive = keepAlive * 1000; + } + + + +} diff --git a/src/main/java/org/eclipse/paho/mqttv5/client/internal/MqttPersistentData.java b/src/main/java/org/eclipse/paho/mqttv5/client/internal/MqttPersistentData.java new file mode 100644 index 0000000..21835b4 --- /dev/null +++ b/src/main/java/org/eclipse/paho/mqttv5/client/internal/MqttPersistentData.java @@ -0,0 +1,98 @@ +/******************************************************************************* + * Copyright (c) 2009, 2014 IBM Corp. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * and Eclipse Distribution License v1.0 which accompany this distribution. + * + * The Eclipse Public License is available at + * https://www.eclipse.org/legal/epl-2.0 + * and the Eclipse Distribution License is available at + * https://www.eclipse.org/org/documents/edl-v10.php + * + * Contributors: + * Dave Locke - initial API and implementation and/or initial documentation + */ +package org.eclipse.paho.mqttv5.client.internal; + +import org.eclipse.paho.mqttv5.common.MqttPersistable; + +public class MqttPersistentData implements MqttPersistable { + // Message key + private String key = null; + + // Message header + private byte[] header = null; + private int hOffset = 0; + private int hLength = 0; + + // Message payload + private byte[] payload = null; + private int pOffset = 0; + private int pLength = 0; + + /** + * Construct a data object to pass across the MQTT client persistence interface. + * + * When this Object is passed to the persistence implementation the key is + * used by the client to identify the persisted data to which further + * update or deletion requests are targeted.
+ * When this Object is created for returning to the client when it is + * recovering its state from persistence the key is not required to be set. + * The client can determine the key from the data. + * @param key The key which identifies this data + * @param header The message header + * @param hOffset The start offset of the header bytes in header. + * @param hLength The length of the header in the header bytes array. + * @param payload The message payload + * @param pOffset The start offset of the payload bytes in payload. + * @param pLength The length of the payload in the payload bytes array + * when persisting the message. + */ + public MqttPersistentData( String key, + byte[] header, + int hOffset, + int hLength, + byte[] payload, + int pOffset, + int pLength) { + this.key = key; + this.header = header; + this.hOffset = hOffset; + this.hLength = hLength; + this.payload = payload; + this.pOffset = pOffset; + this.pLength = pLength; + } + + public String getKey() { + return key; + } + + public byte[] getHeaderBytes() { + return header; + } + + public int getHeaderLength() { + return hLength; + } + + public int getHeaderOffset() { + return hOffset; + } + + public byte[] getPayloadBytes() { + return payload; + } + + public int getPayloadLength() { + if ( payload == null ) { + return 0; + } + return pLength; + } + + public int getPayloadOffset() { + return pOffset; + } +} diff --git a/src/main/java/org/eclipse/paho/mqttv5/client/internal/MqttSessionState.java b/src/main/java/org/eclipse/paho/mqttv5/client/internal/MqttSessionState.java new file mode 100644 index 0000000..63eb7f6 --- /dev/null +++ b/src/main/java/org/eclipse/paho/mqttv5/client/internal/MqttSessionState.java @@ -0,0 +1,39 @@ +package org.eclipse.paho.mqttv5.client.internal; + +import java.util.concurrent.atomic.AtomicInteger; + +/** + * This class is used as a store for client information that should be preserved + * for a single MQTT Session. If the client is disconnected and reconnects with + * clean start = true, then this object will be reset to it's initial state. + * + * Connection variables that this class holds: + * + *
    + *
  • Client ID
  • + *
  • Next Subscription Identifier - The next subscription Identifier available + * to use.
  • + *
+ */ +public class MqttSessionState { + + // ******* Session Specific Properties and counters ******// + private AtomicInteger nextSubscriptionIdentifier = new AtomicInteger(1); + private String clientId; + + public void clearSessionState() { + nextSubscriptionIdentifier.set(1); + } + + public Integer getNextSubscriptionIdentifier() { + return nextSubscriptionIdentifier.getAndIncrement(); + } + + public String getClientId() { + return clientId; + } + + public void setClientId(String clientId) { + this.clientId = clientId; + } +} diff --git a/src/main/java/org/eclipse/paho/mqttv5/client/internal/MqttState.java b/src/main/java/org/eclipse/paho/mqttv5/client/internal/MqttState.java new file mode 100644 index 0000000..bd4a344 --- /dev/null +++ b/src/main/java/org/eclipse/paho/mqttv5/client/internal/MqttState.java @@ -0,0 +1,114 @@ +/******************************************************************************* + * Copyright (c) 2016 IBM Corp. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * and Eclipse Distribution License v1.0 which accompany this distribution. + * + * The Eclipse Public License is available at + * https://www.eclipse.org/legal/epl-2.0 + * and the Eclipse Distribution License is available at + * https://www.eclipse.org/org/documents/edl-v10.php + * + * Contributors: + * Dave Locke - Original MQTTv3 implementation + * James Sutton - Initial MQTTv5 implementation + */ +package org.eclipse.paho.mqttv5.client.internal; + +import java.util.Properties; +import java.util.Vector; + +import org.eclipse.paho.mqttv5.client.MqttActionListener; +import org.eclipse.paho.mqttv5.client.MqttToken; +import org.eclipse.paho.mqttv5.common.MqttException; +import org.eclipse.paho.mqttv5.common.packet.MqttWireMessage; + +public interface MqttState { + + /** + * Submits a message for delivery. This method will block until there is + * room in the inFlightWindow for the message. The message is put into + * persistence before returning. + * + * @param message the message to send + * @param token the token that can be used to track delivery of the message + * @throws MqttException if an exception occurs whilst sending the message + */ + void send(MqttWireMessage message, MqttToken token) throws MqttException; + + /** + * Persists a buffered message to the persistence layer + * + * @param message The {@link MqttWireMessage} to persist + */ + void persistBufferedMessage(MqttWireMessage message); + + /** + * @param message The {@link MqttWireMessage} to un-persist + */ + void unPersistBufferedMessage(MqttWireMessage message); + + /** + * Check and send a ping if needed and check for ping timeout. + * Need to send a ping if nothing has been sent or received + * in the last keepalive interval. It is important to check for + * both sent and received packets in order to catch the case where an + * app is solely sending QoS 0 messages or receiving QoS 0 messages. + * QoS 0 message are not good enough for checking a connection is + * alive as they are one way messages. + * + * If a ping has been sent but no data has been received in the + * last keepalive interval then the connection is deamed to be broken. + * @param pingCallback The {@link MqttActionListener} to be called + * @return token of ping command, null if no ping command has been sent. + * @throws MqttException if an exception occurs during the Ping + */ + MqttToken checkForActivity(MqttActionListener pingCallback) throws MqttException; + + void notifySentBytes(int sentBytesCount); + + void notifyReceivedBytes(int receivedBytesCount); + + /** + * Called when the client has successfully connected to the broker + */ + void connected(); + + /** + * Called during shutdown to work out if there are any tokens still + * to be notified and waiters to be unblocked. Notifying and unblocking + * takes place after most shutdown processing has completed. The tokenstore + * is tidied up so it only contains outstanding delivery tokens which are + * valid after reconnect (if clean session is false) + * @param reason The root cause of the disconnection, or null if it is a clean disconnect + * @return {@link Vector} + */ + Vector resolveOldTokens(MqttException reason); + + /** + * Called when the client has been disconnected from the broker. + * @param reason The root cause of the disconnection, or null if it is a clean disconnect + */ + void disconnected(MqttException reason); + + /** + * Quiesce the client state, preventing any new messages getting sent, + * and preventing the callback on any newly received messages. + * After the timeout expires, delete any pending messages except for + * outbound ACKs, and wait for those ACKs to complete. + * @param timeout How long to wait during Quiescing + */ + void quiesce(long timeout); + + void notifyQueueLock(); + + int getActualInFlight(); + + Properties getDebug(); + + Long getOutgoingMaximumPacketSize(); + + Long getIncomingMaximumPacketSize(); + +} \ No newline at end of file diff --git a/src/main/java/org/eclipse/paho/mqttv5/client/internal/NetworkModule.java b/src/main/java/org/eclipse/paho/mqttv5/client/internal/NetworkModule.java new file mode 100644 index 0000000..b5f2e6f --- /dev/null +++ b/src/main/java/org/eclipse/paho/mqttv5/client/internal/NetworkModule.java @@ -0,0 +1,35 @@ +/******************************************************************************* + * Copyright (c) 2009, 2014 IBM Corp. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * and Eclipse Distribution License v1.0 which accompany this distribution. + * + * The Eclipse Public License is available at + * https://www.eclipse.org/legal/epl-2.0 + * and the Eclipse Distribution License is available at + * https://www.eclipse.org/org/documents/edl-v10.php + * + * Contributors: + * Dave Locke - initial API and implementation and/or initial documentation + */ +package org.eclipse.paho.mqttv5.client.internal; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +import org.eclipse.paho.mqttv5.common.MqttException; + + +public interface NetworkModule { + void start() throws IOException, MqttException; + + InputStream getInputStream() throws IOException; + + OutputStream getOutputStream() throws IOException; + + void stop() throws IOException; + + String getServerURI(); +} diff --git a/src/main/java/org/eclipse/paho/mqttv5/client/internal/NetworkModuleService.java b/src/main/java/org/eclipse/paho/mqttv5/client/internal/NetworkModuleService.java new file mode 100644 index 0000000..124b394 --- /dev/null +++ b/src/main/java/org/eclipse/paho/mqttv5/client/internal/NetworkModuleService.java @@ -0,0 +1,158 @@ +/* + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * and Eclipse Distribution License v1.0 which accompany this distribution. + * + * The Eclipse Public License is available at + * https://www.eclipse.org/legal/epl-2.0 + * and the Eclipse Distribution License is available at + * https://www.eclipse.org/org/documents/edl-v10.php + */ +package org.eclipse.paho.mqttv5.client.internal; + +import java.lang.reflect.Field; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.ServiceLoader; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.eclipse.paho.mqttv5.client.MqttConnectionOptions; +import org.eclipse.paho.mqttv5.client.logging.Logger; +import org.eclipse.paho.mqttv5.client.logging.LoggerFactory; +import org.eclipse.paho.mqttv5.client.spi.NetworkModuleFactory; +import org.eclipse.paho.mqttv5.common.MqttException; + +/** + * The NetworkModuleService uses the installed {@link NetworkModuleFactory}s to create {@link NetworkModule} instances. + *

+ * The selection of the appropriate NetworkModuleFactory is based on the URI scheme. + * + * @author Maik Scheibler + */ +public class NetworkModuleService { + private static Logger LOG = LoggerFactory.getLogger(LoggerFactory.MQTT_CLIENT_MSG_CAT, + NetworkModuleService.class.getSimpleName()); + private static final ServiceLoader FACTORY_SERVICE_LOADER = ServiceLoader.load( + NetworkModuleFactory.class, NetworkModuleService.class.getClassLoader()); + + /** Pattern to match URI authority parts: {@code authority = [userinfo"@"]host[":"port]} */ + private static final Pattern AUTHORITY_PATTERN = Pattern.compile("((.+)@)?([^:]*)(:(\\d+))?"); + private static final int AUTH_GROUP_USERINFO = 2; + private static final int AUTH_GROUP_HOST = 3; + private static final int AUTH_GROUP_PORT = 5; + + private NetworkModuleService() { + // no instances + } + + /** + * Validates the provided URI to be valid and that a NetworkModule is installed to serve it. + * + * @param brokerUri to be validated + * @throws IllegalArgumentException is case the URI is invalid or there is no {@link NetworkModule} installed for + * the URI scheme + */ + public static void validateURI(String brokerUri) throws IllegalArgumentException { + try { + URI uri = new URI(brokerUri); + String scheme = uri.getScheme(); + if (scheme == null || scheme.isEmpty()) { + throw new IllegalArgumentException("missing scheme in broker URI: " + brokerUri); + } + scheme = scheme.toLowerCase(); + synchronized (FACTORY_SERVICE_LOADER) { + for (NetworkModuleFactory factory : FACTORY_SERVICE_LOADER) { + if (factory.getSupportedUriSchemes().contains(scheme)) { + factory.validateURI(uri); + return; + } + } + } + throw new IllegalArgumentException("no NetworkModule installed for scheme \"" + scheme + + "\" of URI \"" + brokerUri + "\""); + } catch (URISyntaxException e) { + throw new IllegalArgumentException("Can't parse string to URI \"" + brokerUri + "\"", e); + } + } + + /** + * Creates a {@link NetworkModule} instance for the provided address, using the given options. + * + * @param address must be a valid URI + * @param options used to initialize the NetworkModule + * @param clientId a client identifier that is unique on the server being connected to + * @return a new NetworkModule instance + * @throws MqttException if the initialization fails + * @throws IllegalArgumentException if the provided {@code address} is invalid + */ + public static NetworkModule createInstance(String address, MqttConnectionOptions options, String clientId) + throws MqttException, IllegalArgumentException + { + try { + URI brokerUri = new URI(address); + applyRFC3986AuthorityPatch(brokerUri); + String scheme = brokerUri.getScheme().toLowerCase(); + synchronized (FACTORY_SERVICE_LOADER) { + for (NetworkModuleFactory factory : FACTORY_SERVICE_LOADER) { + if (factory.getSupportedUriSchemes().contains(scheme)) { + return factory.createNetworkModule(brokerUri, options, clientId); + } + } + } + /* + * To throw an IllegalArgumentException exception matches the previous behavior of + * MqttConnectOptions.validateURI(String), but it would be nice to provide something more meaningful. + */ + throw new IllegalArgumentException(brokerUri.toString()); + } catch (URISyntaxException e) { + throw new IllegalArgumentException(address, e); + } + } + + /** + * Java does URI parsing according to RFC2396 and thus hostnames are limited to alphanumeric characters and '-'. + * But the current "Uniform Resource Identifier (URI): Generic Syntax" (RFC3986) allows for a much wider + * range of valid characters. This causes Java to fail parsing the authority part and thus the user-info, host and + * port will not be set on an URI which does not conform to RFC2396. + *

+ * This workaround tries to detect such a parsing failure and does tokenize the authority parts according to + * RFC3986, but does not enforce any character restrictions (for sake of simplicity). + * + * @param toPatch + * @see https://tools.ietf.org/html/rfc3986#section-3.2 + */ + static void applyRFC3986AuthorityPatch(URI toPatch) { + if (toPatch == null + || toPatch.getHost() != null // already successfully parsed + || toPatch.getAuthority() == null + || toPatch.getAuthority().isEmpty()) + { + return; + } + Matcher matcher = AUTHORITY_PATTERN.matcher(toPatch.getAuthority()); + if (matcher.find()) { + setURIField(toPatch, "userInfo", matcher.group(AUTH_GROUP_USERINFO)); + setURIField(toPatch, "host", matcher.group(AUTH_GROUP_HOST)); + String portString = matcher.group(AUTH_GROUP_PORT); + setURIField(toPatch, "port", portString != null ? Integer.parseInt(portString) : -1); + } + } + + /** + * Reflective manipulation of a URI field to work around the URI parser, because all fields are validated even on + * the full qualified URI constructor. + * + * @see URI#URI(String, String, String, int, String, String, String) + */ + private static void setURIField(URI toManipulate, String fieldName, Object newValue) { + try { + Field field = URI.class.getDeclaredField(fieldName); + field.setAccessible(true); + field.set(toManipulate, newValue); + } catch (NoSuchFieldException | SecurityException | IllegalArgumentException | IllegalAccessException e) { + LOG.warning(NetworkModuleService.class.getName(), "setURIField", "115", new Object[] { + toManipulate.toString() }, e); + } + } +} diff --git a/src/main/java/org/eclipse/paho/mqttv5/client/internal/ResourceBundleCatalog.java b/src/main/java/org/eclipse/paho/mqttv5/client/internal/ResourceBundleCatalog.java new file mode 100644 index 0000000..bc942d0 --- /dev/null +++ b/src/main/java/org/eclipse/paho/mqttv5/client/internal/ResourceBundleCatalog.java @@ -0,0 +1,37 @@ +/******************************************************************************* + * Copyright (c) 2009, 2014 IBM Corp. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * and Eclipse Distribution License v1.0 which accompany this distribution. + * + * The Eclipse Public License is available at + * https://www.eclipse.org/legal/epl-2.0 + * and the Eclipse Distribution License is available at + * https://www.eclipse.org/org/documents/edl-v10.php + * + * Contributors: + * Dave Locke - initial API and implementation and/or initial documentation + */ +package org.eclipse.paho.mqttv5.client.internal; + +import java.util.MissingResourceException; +import java.util.ResourceBundle; + +public class ResourceBundleCatalog extends MessageCatalog { + + private ResourceBundle bundle; + + public ResourceBundleCatalog() { + //MAY throws MissingResourceException + bundle = ResourceBundle.getBundle("org.eclipse.paho.mqttv5.client.internal.nls.messages"); + } + + protected String getLocalizedMessage(int id) { + try { + return bundle.getString(Integer.toString(id)); + } catch(MissingResourceException mre) { + return "MqttException"; + } + } +} diff --git a/src/main/java/org/eclipse/paho/mqttv5/client/internal/SSLNetworkModule.java b/src/main/java/org/eclipse/paho/mqttv5/client/internal/SSLNetworkModule.java new file mode 100644 index 0000000..f588dec --- /dev/null +++ b/src/main/java/org/eclipse/paho/mqttv5/client/internal/SSLNetworkModule.java @@ -0,0 +1,161 @@ +/******************************************************************************* + * Copyright (c) 2009, 2019 IBM Corp. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * and Eclipse Distribution License v1.0 which accompany this distribution. + * + * The Eclipse Public License is available at + * https://www.eclipse.org/legal/epl-2.0 + * and the Eclipse Distribution License is available at + * https://www.eclipse.org/org/documents/edl-v10.php + * + * Contributors: + * Dave Locke - initial API and implementation and/or initial documentation + */ +package org.eclipse.paho.mqttv5.client.internal; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.SNIHostName; +import javax.net.ssl.SNIServerName; +import javax.net.ssl.SSLParameters; +import javax.net.ssl.SSLPeerUnverifiedException; +import javax.net.ssl.SSLSession; +import javax.net.ssl.SSLSocket; +import javax.net.ssl.SSLSocketFactory; + +import org.eclipse.paho.mqttv5.client.logging.Logger; +import org.eclipse.paho.mqttv5.client.logging.LoggerFactory; +import org.eclipse.paho.mqttv5.common.MqttException; + +/** + * A network module for connecting over SSL. + */ +public class SSLNetworkModule extends TCPNetworkModule { + private static final String CLASS_NAME = SSLNetworkModule.class.getName(); + private Logger log = LoggerFactory.getLogger(LoggerFactory.MQTT_CLIENT_MSG_CAT, CLASS_NAME); + + private String[] enabledCiphers; + private int handshakeTimeoutSecs; + private HostnameVerifier hostnameVerifier; + private boolean httpsHostnameVerificationEnabled = true; + + private String host; + private int port; + + /** + * Constructs a new SSLNetworkModule using the specified host and port. The + * supplied SSLSocketFactory is used to supply the network socket. + * + * @param factory + * the {@link SSLSocketFactory} to be used in this SSLNetworkModule + * @param host + * the Hostname of the Server + * @param port + * the Port of the Server + * @param resourceContext + * Resource Context + */ + public SSLNetworkModule(SSLSocketFactory factory, String host, int port, String resourceContext) { + super(factory, host, port, resourceContext); + this.host = host; + this.port = port; + log.setResourceName(resourceContext); + } + + /** + * Returns the enabled cipher suites. + * + * @return a string array of enabled Cipher suites + */ + public String[] getEnabledCiphers() { + return enabledCiphers; + } + + /** + * Sets the enabled cipher suites on the underlying network socket. + * + * @param enabledCiphers + * a String array of cipher suites to enable + */ + public void setEnabledCiphers(String[] enabledCiphers) { + final String methodName = "setEnabledCiphers"; + this.enabledCiphers = enabledCiphers; + if ((socket != null) && (enabledCiphers != null)) { + if (log.isLoggable(Logger.FINE)) { + String ciphers = ""; + for (int i = 0; i < enabledCiphers.length; i++) { + if (i > 0) { + ciphers += ","; + } + ciphers += enabledCiphers[i]; + } + // @TRACE 260=setEnabledCiphers ciphers={0} + log.fine(CLASS_NAME, methodName, "260", new Object[] { ciphers }); + } + ((SSLSocket) socket).setEnabledCipherSuites(enabledCiphers); + } + } + + public void setSSLhandshakeTimeout(int timeout) { + super.setConnectTimeout(timeout); + this.handshakeTimeoutSecs = timeout; + } + + public HostnameVerifier getSSLHostnameVerifier() { + return hostnameVerifier; + } + + public void setSSLHostnameVerifier(HostnameVerifier hostnameVerifier) { + this.hostnameVerifier = hostnameVerifier; + } + + public boolean isHttpsHostnameVerificationEnabled() { + return httpsHostnameVerificationEnabled; + } + + public void setHttpsHostnameVerificationEnabled(boolean httpsHostnameVerificationEnabled) { + this.httpsHostnameVerificationEnabled = httpsHostnameVerificationEnabled; + } + + public void start() throws IOException, MqttException { + super.start(); + setEnabledCiphers(enabledCiphers); + int soTimeout = socket.getSoTimeout(); + // RTC 765: Set a timeout to avoid the SSL handshake being blocked indefinitely + socket.setSoTimeout(this.handshakeTimeoutSecs * 1000); + + // SNI support. Should be automatic under some circumstances - not all, apparently + SSLParameters sslParameters = ((SSLSocket)socket).getSSLParameters(); + List sniHostNames = new ArrayList(1); + sniHostNames.add(new SNIHostName(host)); + sslParameters.setServerNames(sniHostNames); + + // If default Hostname verification is enabled, use the same method that is used with HTTPS + if (this.httpsHostnameVerificationEnabled) { + sslParameters.setEndpointIdentificationAlgorithm("HTTPS"); + } + + ((SSLSocket) socket).setSSLParameters(sslParameters); + + ((SSLSocket) socket).startHandshake(); + if (hostnameVerifier != null) { + SSLSession session = ((SSLSocket) socket).getSession(); + if(!hostnameVerifier.verify(host, session)) { + session.invalidate(); + socket.close(); + throw new SSLPeerUnverifiedException("Host: " + host + ", Peer Host: " + session.getPeerHost()); + } + } + // reset timeout to default value + socket.setSoTimeout(soTimeout); + } + + public String getServerURI() { + return "ssl://" + host + ":" + port; + } +} diff --git a/src/main/java/org/eclipse/paho/mqttv5/client/internal/SSLNetworkModuleFactory.java b/src/main/java/org/eclipse/paho/mqttv5/client/internal/SSLNetworkModuleFactory.java new file mode 100644 index 0000000..4f12260 --- /dev/null +++ b/src/main/java/org/eclipse/paho/mqttv5/client/internal/SSLNetworkModuleFactory.java @@ -0,0 +1,88 @@ +/* + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * and Eclipse Distribution License v1.0 which accompany this distribution. + * + * The Eclipse Public License is available at + * https://www.eclipse.org/legal/epl-2.0 + * and the Eclipse Distribution License is available at + * https://www.eclipse.org/org/documents/edl-v10.php + */ +package org.eclipse.paho.mqttv5.client.internal; + +import java.net.URI; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Properties; +import java.util.Set; + +import javax.net.SocketFactory; +import javax.net.ssl.SSLSocketFactory; +import org.eclipse.paho.mqttv5.client.MqttClientException; +import org.eclipse.paho.mqttv5.client.MqttConnectionOptions; +import org.eclipse.paho.mqttv5.client.security.SSLSocketFactoryFactory; +import org.eclipse.paho.mqttv5.client.spi.NetworkModuleFactory; +import org.eclipse.paho.mqttv5.common.MqttException; + +public class SSLNetworkModuleFactory implements NetworkModuleFactory { + + @Override + public Set getSupportedUriSchemes() { + return Collections.unmodifiableSet(new HashSet<>(Arrays.asList("ssl"))); + } + + @Override + public void validateURI(URI brokerUri) throws IllegalArgumentException { + String path = brokerUri.getPath(); + if (path != null && !path.isEmpty()) { + throw new IllegalArgumentException(brokerUri.toString()); + } + } + + @Override + public NetworkModule createNetworkModule(URI brokerUri, MqttConnectionOptions options, String clientId) + throws MqttException + { + String host = brokerUri.getHost(); + int port = brokerUri.getPort(); // -1 if not defined + if (port == -1) { + port = 8883; + } + String path = brokerUri.getPath(); + if (path != null && !path.isEmpty()) { + throw new IllegalArgumentException(brokerUri.toString()); + } + SocketFactory factory = options.getSocketFactory(); + SSLSocketFactoryFactory factoryFactory = null; + if (factory == null) { +// try { + factoryFactory = new SSLSocketFactoryFactory(); + Properties sslClientProps = options.getSSLProperties(); + if (null != sslClientProps) { + factoryFactory.initialize(sslClientProps, null); + } + factory = factoryFactory.createSocketFactory(null); +// } +// catch (MqttDirectException ex) { +// throw ExceptionHelper.createMqttException(ex.getCause()); +// } + } else if ((factory instanceof SSLSocketFactory) == false) { + throw ExceptionHelper.createMqttException(MqttClientException.REASON_CODE_SOCKET_FACTORY_MISMATCH); + } + + // Create the network module... + SSLNetworkModule netModule = new SSLNetworkModule((SSLSocketFactory) factory, host, port, clientId); + netModule.setSSLhandshakeTimeout(options.getConnectionTimeout()); + netModule.setSSLHostnameVerifier(options.getSSLHostnameVerifier()); + netModule.setHttpsHostnameVerificationEnabled(options.isHttpsHostnameVerificationEnabled()); + // Ciphers suites need to be set, if they are available + if (factoryFactory != null) { + String[] enabledCiphers = factoryFactory.getEnabledCipherSuites(null); + if (enabledCiphers != null) { + netModule.setEnabledCiphers(enabledCiphers); + } + } + return netModule; + } +} diff --git a/src/main/java/org/eclipse/paho/mqttv5/client/internal/TCPNetworkModule.java b/src/main/java/org/eclipse/paho/mqttv5/client/internal/TCPNetworkModule.java new file mode 100644 index 0000000..48a785b --- /dev/null +++ b/src/main/java/org/eclipse/paho/mqttv5/client/internal/TCPNetworkModule.java @@ -0,0 +1,133 @@ +/******************************************************************************* + * Copyright (c) 2009, 2018 IBM Corp. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * and Eclipse Distribution License v1.0 which accompany this distribution. + * + * The Eclipse Public License is available at + * https://www.eclipse.org/legal/epl-2.0 + * and the Eclipse Distribution License is available at + * https://www.eclipse.org/org/documents/edl-v10.php + * + * Contributors: + * Dave Locke - initial API and implementation and/or initial documentation + */ +package org.eclipse.paho.mqttv5.client.internal; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.ConnectException; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.net.SocketAddress; + +import javax.net.SocketFactory; + +import org.eclipse.paho.mqttv5.client.MqttClientException; +import org.eclipse.paho.mqttv5.client.logging.Logger; +import org.eclipse.paho.mqttv5.client.logging.LoggerFactory; +import org.eclipse.paho.mqttv5.common.MqttException; + +/** + * A network module for connecting over TCP. + */ +public class TCPNetworkModule implements NetworkModule { + private static final String CLASS_NAME = TCPNetworkModule.class.getName(); + private Logger log = LoggerFactory.getLogger(LoggerFactory.MQTT_CLIENT_MSG_CAT,CLASS_NAME); + + protected Socket socket; + private SocketFactory factory; + private String host; + private int port; + private int conTimeout; + + /** + * Constructs a new TCPNetworkModule using the specified host and + * port. The supplied SocketFactory is used to supply the network + * socket. + * @param factory the {@link SocketFactory} to be used to set up this connection + * @param host The server hostname + * @param port The server port + * @param resourceContext The Resource Context + */ + public TCPNetworkModule(SocketFactory factory, String host, int port, String resourceContext) { + log.setResourceName(resourceContext); + this.factory = factory; + this.host = host; + this.port = port; + + } + + /** + * Starts the module, by creating a TCP socket to the server. + * @throws IOException if there is an error creating the socket + * @throws MqttException if there is an error connecting to the server + */ + public void start() throws IOException, MqttException { + final String methodName = "start"; + try { + // @TRACE 252=connect to host {0} port {1} timeout {2} + log.fine(CLASS_NAME,methodName, "252", new Object[] {host, Integer.valueOf(port), Long.valueOf(conTimeout*1000)}); + SocketAddress sockaddr = new InetSocketAddress(host, port); + socket = factory.createSocket(); + socket.connect(sockaddr, conTimeout*1000); + socket.setSoTimeout(1000); + } + catch (ConnectException ex) { + //@TRACE 250=Failed to create TCP socket + log.fine(CLASS_NAME,methodName,"250",null,ex); + throw new MqttException(MqttClientException.REASON_CODE_SERVER_CONNECT_ERROR, ex); + } + } + + public InputStream getInputStream() throws IOException { + return socket.getInputStream(); + } + + public OutputStream getOutputStream() throws IOException { + return socket.getOutputStream(); + } + + /** + * Stops the module, by closing the TCP socket. + * @throws IOException if there is an error closing the socket + */ + public void stop() throws IOException { + if (socket != null) { + // CDA: an attempt is made to stop the receiver cleanly before closing the socket. + // If the socket is forcibly closed too early, the blocking socket read in + // the receiver thread throws a SocketException. + // While this causes the receiver thread to exit, it also invalidates the + // SSL session preventing to perform an accelerated SSL handshake in the + // next connection. + // + // Also note that due to the blocking socket reads in the receiver thread, + // it's not possible to interrupt the thread. Using non blocking reads in + // combination with a socket timeout (see setSoTimeout()) would be a better approach. + // + // Please note that the Javadoc only says that an EOF is returned on + // subsequent reads of the socket stream. + // Anyway, at least with Oracle Java SE 7 on Linux systems, this causes a blocked read + // to return EOF immediately. + // This workaround should not cause any harm in general but you might + // want to move it in SSLNetworkModule. + + socket.shutdownInput(); + socket.close(); + } + } + + /** + * Set the maximum time to wait for a socket to be established + * @param timeout The connection timeout + */ + public void setConnectTimeout(int timeout) { + this.conTimeout = timeout; + } + + public String getServerURI() { + return "tcp://" + host + ":" + port; + } +} diff --git a/src/main/java/org/eclipse/paho/mqttv5/client/internal/TCPNetworkModuleFactory.java b/src/main/java/org/eclipse/paho/mqttv5/client/internal/TCPNetworkModuleFactory.java new file mode 100644 index 0000000..4c88601 --- /dev/null +++ b/src/main/java/org/eclipse/paho/mqttv5/client/internal/TCPNetworkModuleFactory.java @@ -0,0 +1,64 @@ +/* + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * and Eclipse Distribution License v1.0 which accompany this distribution. + * + * The Eclipse Public License is available at + * https://www.eclipse.org/legal/epl-2.0 + * and the Eclipse Distribution License is available at + * https://www.eclipse.org/org/documents/edl-v10.php + */ +package org.eclipse.paho.mqttv5.client.internal; + +import java.net.URI; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +import javax.net.SocketFactory; +import javax.net.ssl.SSLSocketFactory; +import org.eclipse.paho.mqttv5.client.MqttClientException; +import org.eclipse.paho.mqttv5.client.MqttConnectionOptions; +import org.eclipse.paho.mqttv5.client.spi.NetworkModuleFactory; +import org.eclipse.paho.mqttv5.common.MqttException; + +public class TCPNetworkModuleFactory implements NetworkModuleFactory { + + @Override + public Set getSupportedUriSchemes() { + return Collections.unmodifiableSet(new HashSet<>(Arrays.asList("tcp"))); + } + + @Override + public void validateURI(URI brokerUri) throws IllegalArgumentException { + String path = brokerUri.getPath(); + if (path != null && !path.isEmpty()) { + throw new IllegalArgumentException("URI path must be empty \"" + brokerUri.toString() + "\""); + } + } + + @Override + public NetworkModule createNetworkModule(URI brokerUri, MqttConnectionOptions options, String clientId) + throws MqttException + { + String host = brokerUri.getHost(); + int port = brokerUri.getPort(); // -1 if not defined + if (port == -1) { + port = 1883; + } + String path = brokerUri.getPath(); + if (path != null && !path.isEmpty()) { + throw new IllegalArgumentException(brokerUri.toString()); + } + SocketFactory factory = options.getSocketFactory(); + if (factory == null) { + factory = SocketFactory.getDefault(); + } else if (factory instanceof SSLSocketFactory) { + throw ExceptionHelper.createMqttException(MqttClientException.REASON_CODE_SOCKET_FACTORY_MISMATCH); + } + TCPNetworkModule networkModule = new TCPNetworkModule(factory, host, port, clientId); + networkModule.setConnectTimeout(options.getConnectionTimeout()); + return networkModule; + } +} diff --git a/src/main/java/org/eclipse/paho/mqttv5/client/internal/Token.java b/src/main/java/org/eclipse/paho/mqttv5/client/internal/Token.java new file mode 100644 index 0000000..4648d22 --- /dev/null +++ b/src/main/java/org/eclipse/paho/mqttv5/client/internal/Token.java @@ -0,0 +1,516 @@ +/******************************************************************************* + * Copyright (c) 2014, 2019 IBM Corp. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * and Eclipse Distribution License v1.0 which accompany this distribution. + * + * The Eclipse Public License is available at + * https://www.eclipse.org/legal/epl-2.0 + * and the Eclipse Distribution License is available at + * https://www.eclipse.org/org/documents/edl-v10.php + * + * Contributors: + * Ian Craggs - MQTT 3.1.1 support + */ + +package org.eclipse.paho.mqttv5.client.internal; + +import org.eclipse.paho.mqttv5.client.MqttActionListener; +import org.eclipse.paho.mqttv5.client.MqttClientException; +import org.eclipse.paho.mqttv5.client.MqttClientInterface; +import org.eclipse.paho.mqttv5.client.logging.Logger; +import org.eclipse.paho.mqttv5.client.logging.LoggerFactory; +import org.eclipse.paho.mqttv5.common.MqttException; +import org.eclipse.paho.mqttv5.common.MqttMessage; +import org.eclipse.paho.mqttv5.common.packet.MqttAck; +import org.eclipse.paho.mqttv5.common.packet.MqttConnAck; +import org.eclipse.paho.mqttv5.common.packet.MqttPubAck; +import org.eclipse.paho.mqttv5.common.packet.MqttPubComp; +import org.eclipse.paho.mqttv5.common.packet.MqttPubRec; +import org.eclipse.paho.mqttv5.common.packet.MqttPubRel; +import org.eclipse.paho.mqttv5.common.packet.MqttSubAck; +import org.eclipse.paho.mqttv5.common.packet.MqttUnsubAck; +import org.eclipse.paho.mqttv5.common.packet.MqttWireMessage; + +public class Token { + private static final String CLASS_NAME = Token.class.getName(); + private Logger log = LoggerFactory.getLogger(LoggerFactory.MQTT_CLIENT_MSG_CAT, CLASS_NAME); + + private volatile boolean completed = false; + private boolean pendingComplete = false; + private boolean sent = false; + + private final Object responseLock = new Object(); + private final Object sentLock = new Object(); + + protected MqttMessage message = null; + private MqttWireMessage response = null; + private MqttWireMessage request = null; + private MqttException exception = null; + private String[] topics = null; + + private String key; + + private MqttClientInterface client = null; + private MqttActionListener callback = null; + + private Object userContext = null; + + private int messageID = 0; + private boolean notified = false; + + private int[] reasonCodes = null; + + private boolean deliveryToken = false; + + public Token(String logContext) { + log.setResourceName(logContext); + } + + public int getMessageID() { + return messageID; + } + + public void setMessageID(int messageID) { + this.messageID = messageID; + } + + public void setDeliveryToken(boolean deliveryToken) { + this.deliveryToken = deliveryToken; + } + + public boolean isDeliveryToken() { + return this.deliveryToken; + } + + public boolean checkResult() throws MqttException { + if (getException() != null) { + throw getException(); + } + return true; + } + + public MqttException getException() { + return exception; + } + + public boolean isComplete() { + return completed; + } + + protected boolean isCompletePending() { + return pendingComplete; + } + + protected boolean isInUse() { + return (getClient() != null && !isComplete()); + } + + public void setActionCallback(MqttActionListener listener) { + this.callback = listener; + + } + + public MqttActionListener getActionCallback() { + return callback; + } + + public void waitForCompletion() throws MqttException { + waitForCompletion(-1); + } + + public void waitForCompletion(long timeout) throws MqttException { + final String methodName = "waitForCompletion"; + // @TRACE 407=key={0} wait max={1} token={2} + log.fine(CLASS_NAME, methodName, "407", new Object[] { getKey(), Long.valueOf(timeout), this }); + + MqttWireMessage resp = null; + try { + resp = waitForResponse(timeout); + } catch (MqttException e) { + if (e.getReasonCode() != MqttClientException.REASON_CODE_CLIENT_DISCONNECTING /*|| + message.getId() != MqttWireMessage.MESSAGE_TYPE_DISCONNECT*/) { + throw e; + } + } + if (resp == null && !completed) { + // @TRACE 406=key={0} timed out token={1} + log.fine(CLASS_NAME, methodName, "406", new Object[] { getKey(), this }); + exception = new MqttException(MqttClientException.REASON_CODE_CLIENT_TIMEOUT); + throw exception; + } + checkResult(); + } + + /** + * Waits for the message delivery to complete, but doesn't throw an exception in + * the case of a NACK. It does still throw an exception if something else goes + * wrong (e.g. an IOException). This is used for packets like CONNECT, which + * have useful information in the ACK that needs to be accessed. + * + * @return the {@link MqttWireMessage} + * @throws MqttException + * if there is an error whilst waiting for the response + */ + protected MqttWireMessage waitForResponse() throws MqttException { + return waitForResponse(-1); + } + + protected MqttWireMessage waitForResponse(long timeout) throws MqttException { + final String methodName = "waitForResponse"; + synchronized (responseLock) { + // @TRACE 400=>key={0} timeout={1} sent={2} completed={3} hasException={4} + // response={5} token={6} + log.fine( + CLASS_NAME, methodName, "400", new Object[] { getKey(), Long.valueOf(timeout), Boolean.valueOf(sent), + Boolean.valueOf(completed), (exception == null) ? "false" : "true", response, this }, + exception); + + while (!this.completed) { + if (this.exception == null) { + try { + // @TRACE 408=key={0} wait max={1} + log.fine(CLASS_NAME, methodName, "408", new Object[] { getKey(), Long.valueOf(timeout) }); + + if (timeout <= 0) { + responseLock.wait(); + } else { + responseLock.wait(timeout); + } + } catch (InterruptedException e) { + exception = new MqttException(e); + } + } + if (!this.completed) { + if (this.exception != null) { + // @TRACE 401=failed with exception + log.fine(CLASS_NAME, methodName, "401", null, exception); + throw exception; + } + + if (timeout > 0) { + // time up and still not completed + break; + } + } + } + } + // @TRACE 402=key={0} response={1} + log.fine(CLASS_NAME, methodName, "402", new Object[] { getKey(), this.response }); + return this.response; + } + + /** + * Update the token with any new reason codes, currently only used for PubRec + * + * @param msg + * response message. + * @param ex + * if there was a problem store the exception in the token. + */ + protected void update(MqttWireMessage msg, MqttException ex) { + final String methodName = "markComplete"; + // @TRACE 411=>key={0} response={1} excep={2} + log.fine(CLASS_NAME, methodName, "411", new Object[] { getKey(), msg, ex }); + + synchronized (responseLock){ + if(msg instanceof MqttPubRec) { + if(msg.getReasonCodes() != null) { + updateReasonCodes(msg.getReasonCodes()); + } + } + } + + } + + /** + * Updates the reasonCodes Array and extends it with any new reason codes. + * This allows message flows like Qos 2 to combine reason codes together across multiple messages e.g. PubRec and PubComp + * @param newReasonCodes - The additional Reason Codes. + */ + protected void updateReasonCodes(int[] newReasonCodes) { + if(this.reasonCodes == null) { + this.reasonCodes = newReasonCodes; + } else { + int[] updatedReasonCodes = new int[this.reasonCodes.length + newReasonCodes.length]; + System.arraycopy(this.reasonCodes, 0, updatedReasonCodes, 0, this.reasonCodes.length); + System.arraycopy(newReasonCodes, 0, updatedReasonCodes, this.reasonCodes.length, newReasonCodes.length); + this.reasonCodes = updatedReasonCodes; + } + } + + /** + * Mark the token as complete and ready for users to be notified. + * + * @param msg + * response message. Optional - there are no response messages for + * some flows + * @param ex + * if there was a problem store the exception in the token. + */ + protected void markComplete(MqttWireMessage msg, MqttException ex) { + final String methodName = "markComplete"; + // @TRACE 404=>key={0} response={1} excep={2} + log.fine(CLASS_NAME, methodName, "404", new Object[] { getKey(), msg, ex }); + + synchronized (responseLock) { + // If reason codes are available, store them here. + if (msg instanceof MqttPubAck || msg instanceof MqttPubComp || msg instanceof MqttPubRec + || msg instanceof MqttPubRel || msg instanceof MqttSubAck || msg instanceof MqttUnsubAck) { + if (msg.getReasonCodes() != null) { + updateReasonCodes(msg.getReasonCodes()); + } + } + + // ACK means that everything was OK, so mark the message for garbage collection. + if (msg instanceof MqttAck) { + this.message = null; + } + + this.pendingComplete = true; + this.response = msg; + this.exception = ex; + } + } + + /** + * Notifies this token that a response message (an ACK or NACK) has been + * received. + */ + protected void notifyComplete() { + final String methodName = "notifyComplete"; + // @TRACE 411=>key={0} response={1} excep={2} + log.fine(CLASS_NAME, methodName, "404", new Object[] { getKey(), this.response, this.exception }); + + synchronized (responseLock) { + // If pending complete is set then normally the token can be marked + // as complete and users notified. An abnormal error may have + // caused the client to shutdown beween pending complete being set + // and notifying the user. In this case - the action must be failed. + if (exception == null && pendingComplete) { + completed = true; + pendingComplete = false; + } else { + pendingComplete = false; + } + + responseLock.notifyAll(); + } + synchronized (sentLock) { + sent = true; + sentLock.notifyAll(); + } + } + + // /** + // * Notifies this token that an exception has occurred. This is only + // * used for things like IOException, and not for MQTT NACKs. + // */ + // protected void notifyException() { + // final String methodName = "notifyException"; + // //@TRACE 405=token={0} excep={1} + // log.fine(CLASS_NAME,methodName, "405",new Object[]{this,this.exception}); + // synchronized (responseLock) { + // responseLock.notifyAll(); + // } + // synchronized (sentLock) { + // sentLock.notifyAll(); + // } + // } + + public void waitUntilSent() throws MqttException { + final String methodName = "waitUntilSent"; + synchronized (sentLock) { + synchronized (responseLock) { + if (this.exception != null) { + throw this.exception; + } + } + while (!sent) { + try { + // @TRACE 409=wait key={0} + log.fine(CLASS_NAME, methodName, "409", new Object[] { getKey() }); + + sentLock.wait(); + } catch (InterruptedException e) { + } + } + + while (!sent) { + if (this.exception == null) { + throw ExceptionHelper.createMqttException(MqttClientException.REASON_CODE_UNEXPECTED_ERROR); + } + throw this.exception; + } + } + } + + /** + * Notifies this token that the associated message has been sent (i.e. written + * to the TCP/IP socket). + */ + protected void notifySent() { + final String methodName = "notifySent"; + // @TRACE 403=> key={0} + log.fine(CLASS_NAME, methodName, "403", new Object[] { getKey() }); + synchronized (responseLock) { + this.response = null; + this.completed = false; + } + synchronized (sentLock) { + sent = true; + sentLock.notifyAll(); + } + } + + public MqttClientInterface getClient() { + return client; + } + + protected void setClient(MqttClientInterface client) { + this.client = client; + } + + public void reset() throws MqttException { + final String methodName = "reset"; + if (isInUse()) { + // Token is already in use - cannot reset + throw new MqttException(MqttClientException.REASON_CODE_TOKEN_INUSE); + } + // @TRACE 410=> key={0} + log.fine(CLASS_NAME, methodName, "410", new Object[] { getKey() }); + + client = null; + completed = false; + response = null; + sent = false; + exception = null; + userContext = null; + } + + public MqttMessage getMessage() { + return message; + } + + public MqttWireMessage getWireMessage() { + return response; + } + + public void setMessage(MqttMessage msg) { + this.message = msg; + } + + public String[] getTopics() { + return topics; + } + + public void setTopics(String[] topics) { + this.topics = topics; + } + + public Object getUserContext() { + return userContext; + } + + public void setUserContext(Object userContext) { + this.userContext = userContext; + } + + public void setKey(String key) { + this.key = key; + } + + public String getKey() { + return key; + } + + public void setException(MqttException exception) { + synchronized (responseLock) { + this.exception = exception; + } + } + + public boolean isNotified() { + return notified; + } + + public void setNotified(boolean notified) { + this.notified = notified; + } + + public String toString() { + StringBuffer tok = new StringBuffer(); + tok.append("key=").append(getKey()); + tok.append(" ,topics="); + if (getTopics() != null) { + for (int i = 0; i < getTopics().length; i++) { + tok.append(getTopics()[i]).append(", "); + } + } + tok.append(" ,usercontext=").append(getUserContext()); + tok.append(" ,isComplete=").append(isComplete()); + tok.append(" ,isNotified=").append(isNotified()); + tok.append(" ,exception=").append(getException()); + tok.append(" ,actioncallback=").append(getActionCallback()); + + return tok.toString(); + } + + public int[] getGrantedQos() { + int[] val = new int[0]; + if (response instanceof MqttSubAck) { + // TODO - Work out how to map multiple returncodes + if (response != null) { + val = ((MqttSubAck) response).getReturnCodes(); + } + } + return val; + } + + public boolean getSessionPresent() { + boolean val = false; + if (response instanceof MqttConnAck) { + if (response != null) { + val = ((MqttConnAck) response).getSessionPresent(); + } + } + return val; + } + + public MqttWireMessage getResponse() { + return response; + } + + /** + * Returns the reason codes from the MqttWireMessage. + * These will be present if the messages is of the following types: + *

    + *
  • CONNACK - 1 Reason Code Max.
  • + *
  • PUBACK - 1 Reason Code Max.
  • + *
  • PUBREC - 1 Reason Code Max.
  • + *
  • PUBCOMP - 1 Reason Code Max.
  • + *
  • PUBREL - 1 Reason Code Max.
  • + *
  • SUBACK - 1 or more Reason Codes.
  • + *
  • UNSUBACK - 1 or more Reason Codes.
  • + *
  • AUTH - 1 Reason Code Max.
  • + *
+ * + * Warning: This method may be removed in favour of MqttWireMessage.getReasonCodes() + * + * May be null if this message does not contain any Reason Codes. + * @return An array of return codes, or null. + */ + public int[] getReasonCodes() { + return reasonCodes; + } + + public void setRequestMessage(MqttWireMessage requestMessage) { + this.request = requestMessage; + } + + public MqttWireMessage getRequestMessage() { + return this.request; + } + +} diff --git a/src/main/java/org/eclipse/paho/mqttv5/client/logging/JSR47Logger.java b/src/main/java/org/eclipse/paho/mqttv5/client/logging/JSR47Logger.java new file mode 100644 index 0000000..8071d7b --- /dev/null +++ b/src/main/java/org/eclipse/paho/mqttv5/client/logging/JSR47Logger.java @@ -0,0 +1,278 @@ +/******************************************************************************* + * Copyright (c) 2009, 2014 IBM Corp. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * and Eclipse Distribution License v1.0 which accompany this distribution. + * + * The Eclipse Public License is available at + * https://www.eclipse.org/legal/epl-2.0 + * and the Eclipse Distribution License is available at + * https://www.eclipse.org/org/documents/edl-v10.php + * + * Contributors: + * Dave Locke - initial API and implementation and/or initial documentation + */ +package org.eclipse.paho.mqttv5.client.logging; +import java.text.MessageFormat; +import java.util.MissingResourceException; +import java.util.ResourceBundle; +import java.util.logging.Handler; +import java.util.logging.LogRecord; +import java.util.logging.MemoryHandler; + +/** + * Implementation of the the logger interface that uses java.uti.logging + * + * A Logger that utilises Java's built in logging facility - java.util.logging. + *

A sample java.util.logging properties file - jsr47min.properties is provided that demonstrates + * how to run with a memory based trace facility that runs with minimal performance + * overhead. The memory buffer can be dumped when a log/trace record is written matching + * the MemoryHandlers trigger level or when the push method is invoked on the MemoryHandler. + * {@link org.eclipse.paho.mqttv5.client.util.Debug Debug} provides method to make it easy + * to dump the memory buffer as well as other useful debug info. + */ +public class JSR47Logger implements Logger { + private java.util.logging.Logger julLogger = null; + private ResourceBundle logMessageCatalog = null; + private ResourceBundle traceMessageCatalog = null; + private String catalogID = null; + private String resourceName = null; + private String loggerName = null; + + /** + * + * @param logMsgCatalog The resource bundle associated with this logger + * @param loggerID The suffix for the loggerName (will be appeneded to org.eclipse.paho.mqttv5.client + * @param resourceContext A context for the logger e.g. clientID or appName... + */ + public void initialise(ResourceBundle logMsgCatalog, String loggerID, String resourceContext ) { + this.traceMessageCatalog = logMessageCatalog; + this.resourceName = resourceContext; +// loggerName = "org.eclipse.paho.mqttv5.client." + ((null == loggerID || 0 == loggerID.length()) ? "internal" : loggerID); + loggerName = loggerID; + this.julLogger = java.util.logging.Logger.getLogger(loggerName); + this.logMessageCatalog = logMsgCatalog; + this.traceMessageCatalog = logMsgCatalog; + this.catalogID = logMessageCatalog.getString("0"); + + } + + public void setResourceName(String logContext) { + this.resourceName = logContext; + } + + public boolean isLoggable(int level) { + return julLogger.isLoggable(mapJULLevel(level)); // || InternalTracer.isLoggable(level); + } + + public void severe(String sourceClass, String sourceMethod, String msg) { + log(SEVERE, sourceClass, sourceMethod, msg, null, null); + } + + public void severe(String sourceClass, String sourceMethod, String msg, Object[] inserts) { + log(SEVERE, sourceClass, sourceMethod, msg, inserts, null); + } + + public void severe(String sourceClass, String sourceMethod, String msg, Object[] inserts, Throwable thrown) { + log(SEVERE, sourceClass, sourceMethod, msg, inserts, thrown); + } + + public void warning(String sourceClass, String sourceMethod, String msg) { + log(WARNING, sourceClass, sourceMethod, msg, null, null); + } + + public void warning(String sourceClass, String sourceMethod, String msg, Object[] inserts) { + log(WARNING, sourceClass, sourceMethod, msg, inserts, null); + } + + public void warning(String sourceClass, String sourceMethod, String msg, Object[] inserts, Throwable thrown) { + log(WARNING, sourceClass, sourceMethod, msg, inserts, thrown); + } + + public void info(String sourceClass, String sourceMethod, String msg) { + log(INFO, sourceClass, sourceMethod, msg, null, null); + } + + public void info(String sourceClass, String sourceMethod, String msg, Object[] inserts) { + log(INFO, sourceClass, sourceMethod, msg, inserts, null); + } + + public void info(String sourceClass, String sourceMethod, String msg, Object[] inserts, Throwable thrown) { + log(INFO, sourceClass, sourceMethod, msg, inserts, thrown); + } + + public void config(String sourceClass, String sourceMethod, String msg) { + log(CONFIG, sourceClass, sourceMethod, msg, null, null); + } + + public void config(String sourceClass, String sourceMethod, String msg, Object[] inserts) { + log(CONFIG, sourceClass, sourceMethod, msg, inserts, null); + } + + public void config(String sourceClass, String sourceMethod, String msg, Object[] inserts, Throwable thrown) { + log(CONFIG, sourceClass, sourceMethod, msg, inserts, thrown); + } + + public void log(int level, String sourceClass, String sourceMethod, String msg, Object[] inserts, Throwable thrown) { +// InternalTracer.log(this.catalogID, level, sourceClass, sourceMethod, msg, inserts, thrown); + java.util.logging.Level julLevel = mapJULLevel(level); + if (julLogger.isLoggable(julLevel)) { + logToJsr47(julLevel, sourceClass, sourceMethod, this.catalogID, this.logMessageCatalog, msg, inserts, thrown); + } + } + +// public void setTrace(Trace trace) { +// InternalTracer.setTrace(trace); +// } + + public void fine(String sourceClass, String sourceMethod, String msg) { + trace(FINE, sourceClass, sourceMethod, msg, null, null); + } + + public void fine(String sourceClass, String sourceMethod, String msg, Object[] inserts) { + trace(FINE, sourceClass, sourceMethod, msg, inserts, null); + } + + public void fine(String sourceClass, String sourceMethod, String msg, Object[] inserts, Throwable ex) { + trace(FINE, sourceClass, sourceMethod, msg, inserts, ex); + } + + public void finer(String sourceClass, String sourceMethod, String msg) { + trace(FINER, sourceClass, sourceMethod, msg, null, null); + } + + public void finer(String sourceClass, String sourceMethod, String msg, Object[] inserts) { + trace(FINER, sourceClass, sourceMethod, msg, inserts, null); + } + + public void finer(String sourceClass, String sourceMethod, String msg, Object[] inserts, Throwable ex) { + trace(FINER, sourceClass, sourceMethod, msg, inserts, ex); + } + + public void finest(String sourceClass, String sourceMethod, String msg) { + trace(FINEST, sourceClass, sourceMethod, msg, null, null); + } + + public void finest(String sourceClass, String sourceMethod, String msg, Object[] inserts) { + trace(FINEST, sourceClass, sourceMethod, msg, inserts, null); + } + + public void finest(String sourceClass, String sourceMethod, String msg, Object[] inserts, Throwable ex) { + trace(FINEST, sourceClass, sourceMethod, msg, inserts, ex); + } + + + public void trace(int level, String sourceClass, String sourceMethod, String msg, Object[] inserts, Throwable ex) { + java.util.logging.Level julLevel = mapJULLevel(level); + boolean isJULLoggable = julLogger.isLoggable(julLevel); +// if (FINE == level || isJULLoggable || InternalTracer.isLoggable(level)) { +// InternalTracer.traceForced(level, sourceClass, sourceMethod, msg, inserts); +// } + if (isJULLoggable) { + logToJsr47(julLevel, sourceClass, sourceMethod, this.catalogID, this.traceMessageCatalog, msg, inserts, ex); + } + } + + + private String getResourceMessage(ResourceBundle messageCatalog, String msg) { + String message; + try { + message = messageCatalog.getString(msg); + } catch (MissingResourceException e) { + // This is acceptable, simply return the given msg string. + message = msg; + } + return message; + } + + private void logToJsr47(java.util.logging.Level julLevel, String sourceClass, String sourceMethod, String catalogName, + ResourceBundle messageCatalog, String msg, Object[] inserts, Throwable thrown) { +// LogRecord logRecord = new LogRecord(julLevel, msg); + String formattedWithArgs = msg; + if (!msg.contains("=====")) { + formattedWithArgs = MessageFormat.format(getResourceMessage(messageCatalog, msg), inserts); + } + LogRecord logRecord = new LogRecord(julLevel, resourceName + ": " +formattedWithArgs); + + logRecord.setSourceClassName(sourceClass); + logRecord.setSourceMethodName(sourceMethod); + logRecord.setLoggerName(loggerName); +// logRecord.setResourceBundleName(catalogName); +// logRecord.setResourceBundle(messageCatalog); +// if (null != inserts) { +// logRecord.setParameters(inserts); +// } + if (null != thrown) { + logRecord.setThrown(thrown); + } + + julLogger.log(logRecord); + } + + private java.util.logging.Level mapJULLevel(int level) { + java.util.logging.Level julLevel = null; + + switch (level) { + case SEVERE: + julLevel = java.util.logging.Level.SEVERE; + break; + case WARNING: + julLevel = java.util.logging.Level.WARNING; + break; + case INFO: + julLevel = java.util.logging.Level.INFO; + break; + case CONFIG: + julLevel = java.util.logging.Level.CONFIG; + break; + case FINE: + julLevel = java.util.logging.Level.FINE; + break; + case FINER: + julLevel = java.util.logging.Level.FINER; + break; + case FINEST: + julLevel = java.util.logging.Level.FINEST; + break; + + default: + } + + return julLevel; + } + + public String formatMessage(String msg, Object[] inserts) { + String formatString; + try { + formatString = logMessageCatalog.getString(msg); + } catch (MissingResourceException e) { + formatString = msg; + } + return formatString; + } + + public void dumpTrace() { + dumpMemoryTrace47(julLogger); + } + + protected static void dumpMemoryTrace47(java.util.logging.Logger logger) { + MemoryHandler mHand = null; + + if (logger!= null) { + Handler[] handlers = logger.getHandlers(); + + for (Handler handler : handlers) { + if (handler instanceof MemoryHandler) { + synchronized (handler) { + mHand = ((MemoryHandler) handler); + mHand.push(); + return; + } // synchronized (handler). + } + } // for handlers... + dumpMemoryTrace47(logger.getParent()); + } + } + +} diff --git a/src/main/java/org/eclipse/paho/mqttv5/client/logging/Logger.java b/src/main/java/org/eclipse/paho/mqttv5/client/logging/Logger.java new file mode 100644 index 0000000..4a5fcf7 --- /dev/null +++ b/src/main/java/org/eclipse/paho/mqttv5/client/logging/Logger.java @@ -0,0 +1,579 @@ +/******************************************************************************* + * Copyright (c) 2009, 2014 IBM Corp. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * and Eclipse Distribution License v1.0 which accompany this distribution. + * + * The Eclipse Public License is available at + * https://www.eclipse.org/legal/epl-2.0 + * and the Eclipse Distribution License is available at + * https://www.eclipse.org/org/documents/edl-v10.php + * + * Contributors: + * Dave Locke - initial API and implementation and/or initial documentation + */ +package org.eclipse.paho.mqttv5.client.logging; + +import java.util.ResourceBundle; + +/** + * A Logger object is used to send log and trace messages to a platform + * specific logging implementation. Loggers are named, using a hierarchical + * dot-separated name-space. + * Logger names can be arbitrary strings, but they should normally be based on + * the component or the package name of the logged component + * + * Logger objects may be obtained by calls on one of the getLogger factory + * methods. These will either create a new Logger or return a suitable existing + * Logger. + * + *

+ * The int levels define a set of standard logging levels that can be used to + * control logging output. The logging levels are ordered and are specified by + * ordered integers. Enabling logging at a given level also enables logging at + * all higher levels. + *

+ * Clients should use the the convenience methods such as severe() and fine() or + * one of the predefined level constants such as Logger.SEVERE and Logger.FINE + * with the appropriate log(int level...) or trace(int level...) methods. + *

+ * The levels in descending order are:

+ *
    + *
  • SEVERE (log - highest value)
  • + *
  • WARNING (log)
  • + *
  • INFO (log)
  • + *
  • CONFIG (log)
  • + *
  • FINE (trace)
  • + *
  • FINER (trace)
  • + *
  • FINEST (trace - lowest value)
  • + *
+ * + */ +public interface Logger { + /** + * SEVERE is a message level indicating a serious failure. + *

+ * In general SEVERE messages should describe events that are of + * considerable importance and which will prevent normal program execution. + * They should be reasonably intelligible to end users and to system + * administrators. + */ + int SEVERE = 1; + /** + * WARNING is a message level indicating a potential problem. + *

+ * In general WARNING messages should describe events that will be of + * interest to end users or system managers, or which indicate potential + * problems. + */ + int WARNING = 2; + /** + * INFO is a message level for informational messages. + *

+ * Typically INFO messages will be written to the console or its equivalent. + * So the INFO level should only be used for reasonably significant messages + * that will make sense to end users and system admins. + */ + int INFO = 3; + /** + * CONFIG is a message level for static configuration messages. + *

+ * CONFIG messages are intended to provide a variety of static configuration + * information, to assist in debugging problems that may be associated with + * particular configurations. For example, CONFIG message might include the + * CPU type, the graphics depth, the GUI look-and-feel, etc. + */ + int CONFIG = 4; + /** + * FINE is a message level providing tracing information. + *

+ * All of FINE, FINER, and FINEST are intended for relatively detailed + * tracing. The exact meaning of the three levels will vary between + * subsystems, but in general, FINEST should be used for the most voluminous + * detailed output, FINER for somewhat less detailed output, and FINE for + * the lowest volume (and most important) messages. + *

+ * In general the FINE level should be used for information that will be + * broadly interesting to developers who do not have a specialized interest + * in the specific subsystem. + *

+ * FINE messages might include things like minor (recoverable) failures. + * Issues indicating potential performance problems are also worth logging + * as FINE. + */ + int FINE = 5; + /** + * FINER indicates a fairly detailed tracing message. By default logging + * calls for entering, returning, or throwing an exception are traced at + * this level. + */ + int FINER = 6; + /** + * FINEST indicates a highly detailed tracing message. + */ + int FINEST = 7; + + void initialise(ResourceBundle messageCatalog, String loggerID, String resourceName); + + /** + * Set a name that can be used to provide context with each log record. + * This overrides the value passed in on initialise + * @param logContext The Log context name + */ + void setResourceName(String logContext); + + /** + * Check if a message of the given level would actually be logged by this + * logger. This check is based on the Loggers effective level, which may be + * inherited from its parent. + * + * @param level + * a message logging level. + * @return true if the given message level is currently being logged. + */ + boolean isLoggable(int level); + + /** + * Log a message, specifying source class and method, if the logger is + * currently enabled for the given message level. + * + * @param sourceClass + * Name of class that issued the logging request. + * @param sourceMethod + * Name of method that issued the logging request. + * @param msg + * The key in the message localization catalog for the message or + * the actual message itself. During formatting, if the logger + * has a mapping for the msg string, then the msg string is + * replaced by the localized value. Otherwise the original msg + * string is used. + */ + void severe(String sourceClass, String sourceMethod, String msg); + + /** + * Log a message, specifying source class and method, with an array of + * object arguments, if the logger is currently enabled for the given + * message level. + * + * @param sourceClass + * Name of class that issued the logging request. + * @param sourceMethod + * Name of method that issued the logging request. + * @param msg + * The key in the message localization catalog for the message or + * the actual message itself. During formatting, if the logger + * has a mapping for the msg string, then the msg string is + * replaced by the localized value. Otherwise the original msg + * string is used. The formatter uses java.text.MessageFormat + * style formatting to format parameters, so for example a format + * string "{0} {1}" would format two inserts into the message. + * @param inserts + * Array of parameters to the message. + */ + void severe(String sourceClass, String sourceMethod, String msg, Object[] inserts); + + /** + * Log a message, specifying source class and method, with an array of + * object arguments and a throwable, if the logger is currently enabled for + * the given message level. + * + * @param sourceClass + * Name of class that issued the logging request. + * @param sourceMethod + * Name of method that issued the logging request. + * @param msg + * The key in the message localization catalog for the message or + * the actual message itself. During formatting, if the logger + * has a mapping for the msg string, then the msg string is + * replaced by the localized value. Otherwise the original msg + * string is used. The formatter uses java.text.MessageFormat + * style formatting to format parameters, so for example a format + * string "{0} {1}" would format two inserts into the message. + * @param inserts + * Array of parameters to the message. + * @param thrown + * Throwable associated with log message. + */ + void severe(String sourceClass, String sourceMethod, String msg, Object[] inserts, Throwable thrown); + + /** + * Log a message, specifying source class and method, if the logger is + * currently enabled for the given message level. + * + * @param sourceClass + * Name of class that issued the logging request. + * @param sourceMethod + * Name of method that issued the logging request. + * @param msg + * The key in the message localization catalog for the message or + * the actual message itself. During formatting, if the logger + * has a mapping for the msg string, then the msg string is + * replaced by the localized value. Otherwise the original msg + * string is used. + */ + void warning(String sourceClass, String sourceMethod, String msg); + + /** + * Log a message, specifying source class and method, with an array of + * object arguments, if the logger is currently enabled for the given + * message level. + * + * @param sourceClass + * Name of class that issued the logging request. + * @param sourceMethod + * Name of method that issued the logging request. + * @param msg + * The key in the message localization catalog for the message or + * the actual message itself. During formatting, if the logger + * has a mapping for the msg string, then the msg string is + * replaced by the localized value. Otherwise the original msg + * string is used. The formatter uses java.text.MessageFormat + * style formatting to format parameters, so for example a format + * string "{0} {1}" would format two inserts into the message. + * @param inserts + * Array of parameters to the message. + */ + void warning(String sourceClass, String sourceMethod, String msg, Object[] inserts); + + /** + * Log a message, specifying source class and method, with an array of + * object arguments and a throwable, if the logger is currently enabled for + * the given message level. + * + * @param sourceClass + * Name of class that issued the logging request. + * @param sourceMethod + * Name of method that issued the logging request. + * @param msg + * The key in the message localization catalog for the message or + * the actual message itself. During formatting, if the logger + * has a mapping for the msg string, then the msg string is + * replaced by the localized value. Otherwise the original msg + * string is used. The formatter uses java.text.MessageFormat + * style formatting to format parameters, so for example a format + * string "{0} {1}" would format two inserts into the message. + * @param inserts + * Array of parameters to the message. + * @param thrown + * Throwable associated with log message. + */ + void warning(String sourceClass, String sourceMethod, String msg, Object[] inserts, Throwable thrown); + + /** + * Log a message, specifying source class and method, if the logger is + * currently enabled for the given message level. + * + * @param sourceClass + * Name of class that issued the logging request. + * @param sourceMethod + * Name of method that issued the logging request. + * @param msg + * The key in the message localization catalog for the message or + * the actual message itself. During formatting, if the logger + * has a mapping for the msg string, then the msg string is + * replaced by the localized value. Otherwise the original msg + * string is used. + */ + void info(String sourceClass, String sourceMethod, String msg); + + /** + * Log a message, specifying source class and method, with an array of + * object arguments, if the logger is currently enabled for the given + * message level. + * + * @param sourceClass + * Name of class that issued the logging request. + * @param sourceMethod + * Name of method that issued the logging request. + * @param msg + * The key in the message localization catalog for the message or + * the actual message itself. During formatting, if the logger + * has a mapping for the msg string, then the msg string is + * replaced by the localized value. Otherwise the original msg + * string is used. The formatter uses java.text.MessageFormat + * style formatting to format parameters, so for example a format + * string "{0} {1}" would format two inserts into the message. + * @param inserts + * Array of parameters to the message. + */ + void info(String sourceClass, String sourceMethod, String msg, Object[] inserts); + + /** + * Log a message, specifying source class and method, with an array of + * object arguments and a throwable, if the logger is currently enabled for + * the given message level. + * + * @param sourceClass + * Name of class that issued the logging request. + * @param sourceMethod + * Name of method that issued the logging request. + * @param msg + * The key in the message localization catalog for the message or + * the actual message itself. During formatting, if the logger + * has a mapping for the msg string, then the msg string is + * replaced by the localized value. Otherwise the original msg + * string is used. The formatter uses java.text.MessageFormat + * style formatting to format parameters, so for example a format + * string "{0} {1}" would format two inserts into the message. + * @param inserts + * Array of parameters to the message. + * @param thrown + * Throwable associated with log message. + */ + void info(String sourceClass, String sourceMethod, String msg, Object[] inserts, Throwable thrown); + + /** + * Log a message, specifying source class and method, if the logger is + * currently enabled for the given message level. + * + * @param sourceClass + * Name of class that issued the logging request. + * @param sourceMethod + * Name of method that issued the logging request. + * @param msg + * The key in the message localization catalog for the message or + * the actual message itself. During formatting, if the logger + * has a mapping for the msg string, then the msg string is + * replaced by the localized value. Otherwise the original msg + * string is used. + */ + void config(String sourceClass, String sourceMethod, String msg); + + /** + * Log a message, specifying source class and method, with an array of + * object arguments, if the logger is currently enabled for the given + * message level. + * + * @param sourceClass + * Name of class that issued the logging request. + * @param sourceMethod + * Name of method that issued the logging request. + * @param msg + * The key in the message localization catalog for the message or + * the actual message itself. During formatting, if the logger + * has a mapping for the msg string, then the msg string is + * replaced by the localized value. Otherwise the original msg + * string is used. The formatter uses java.text.MessageFormat + * style formatting to format parameters, so for example a format + * string "{0} {1}" would format two inserts into the message. + * @param inserts + * Array of parameters to the message. + */ + void config(String sourceClass, String sourceMethod, String msg, Object[] inserts); + + /** + * Log a message, specifying source class and method, with an array of + * object arguments and a throwable, if the logger is currently enabled for + * the given message level. + * + * @param sourceClass + * Name of class that issued the logging request. + * @param sourceMethod + * Name of method that issued the logging request. + * @param msg + * The key in the message localization catalog for the message or + * the actual message itself. During formatting, if the logger + * has a mapping for the msg string, then the msg string is + * replaced by the localized value. Otherwise the original msg + * string is used. The formatter uses java.text.MessageFormat + * style formatting to format parameters, so for example a format + * string "{0} {1}" would format two inserts into the message. + * @param inserts + * Array of parameters to the message. + * @param thrown + * Throwable associated with log message. + */ + void config(String sourceClass, String sourceMethod, String msg, Object[] inserts, Throwable thrown); + + /** + * Trace a message, specifying source class and method, if the logger is + * currently enabled for the given message level. + * + * @param sourceClass + * Name of class that issued the logging request. + * @param sourceMethod + * Name of method that issued the logging request. + * @param msg + * The key in the message catalog for the message or the actual + * message itself. During formatting, if the logger has a mapping + * for the msg string, then the msg string is replaced by the + * value. Otherwise the original msg string is used. + */ + void fine(String sourceClass, String sourceMethod, String msg); + + /** + * Trace a message, specifying source class and method, with an array of + * object arguments, if the logger is currently enabled for the given + * message level. + * + * @param sourceClass + * Name of class that issued the logging request. + * @param sourceMethod + * Name of method that issued the logging request. + * @param msg + * The key in the message catalog for the message or the actual + * message itself. During formatting, if the logger has a mapping + * for the msg string, then the msg string is replaced by the + * value. Otherwise the original msg string is used. The + * formatter uses java.text.MessageFormat style formatting to + * format parameters, so for example a format string "{0} {1}" + * would format two inserts into the message. + * @param inserts + * Array of parameters to the message. + */ + void fine(String sourceClass, String sourceMethod, String msg, Object[] inserts); + + void fine(String sourceClass, String sourceMethod, String msg, Object[] inserts, Throwable ex); + + /** + * Trace a message, specifying source class and method, if the logger is + * currently enabled for the given message level. + * + * @param sourceClass + * Name of class that issued the logging request. + * @param sourceMethod + * Name of method that issued the logging request. + * @param msg + * The key in the message catalog for the message or the actual + * message itself. During formatting, if the logger has a mapping + * for the msg string, then the msg string is replaced by the + * value. Otherwise the original msg string is used. + */ + void finer(String sourceClass, String sourceMethod, String msg); + + /** + * Trace a message, specifying source class and method, with an array of + * object arguments, if the logger is currently enabled for the given + * message level. + * + * @param sourceClass + * Name of class that issued the logging request. + * @param sourceMethod + * Name of method that issued the logging request. + * @param msg + * The key in the message catalog for the message or the actual + * message itself. During formatting, if the logger has a mapping + * for the msg string, then the msg string is replaced by the + * value. Otherwise the original msg string is used. The + * formatter uses java.text.MessageFormat style formatting to + * format parameters, so for example a format string "{0} {1}" + * would format two inserts into the message. + * @param inserts + * Array of parameters to the message. + */ + void finer(String sourceClass, String sourceMethod, String msg, Object[] inserts); + + void finer(String sourceClass, String sourceMethod, String msg, Object[] inserts, Throwable ex); + + /** + * Trace a message, specifying source class and method, if the logger is + * currently enabled for the given message level. + * + * @param sourceClass + * Name of class that issued the logging request. + * @param sourceMethod + * Name of method that issued the logging request. + * @param msg + * The key in the message catalog for the message or the actual + * message itself. During formatting, if the logger has a mapping + * for the msg string, then the msg string is replaced by the + * value. Otherwise the original msg string is used. + */ + void finest(String sourceClass, String sourceMethod, String msg); + + /** + * Trace a message, specifying source class and method, with an array of + * object arguments, if the logger is currently enabled for the given + * message level. + * + * @param sourceClass + * Name of class that issued the logging request. + * @param sourceMethod + * Name of method that issued the logging request. + * @param msg + * The key in the message catalog for the message or the actual + * message itself. During formatting, if the logger has a mapping + * for the msg string, then the msg string is replaced by the + * value. Otherwise the original msg string is used. The + * formatter uses java.text.MessageFormat style formatting to + * format parameters, so for example a format string "{0} {1}" + * would format two inserts into the message. + * @param inserts + * Array of parameters to the message. + */ + void finest(String sourceClass, String sourceMethod, String msg, Object[] inserts); + + void finest(String sourceClass, String sourceMethod, String msg, Object[] inserts, Throwable ex); + + /** + * Log a message, specifying source class and method, with an array of + * object arguments and a throwable, if the logger is currently enabled for + * the given message level. + * + * @param level + * One of the message level identifiers, e.g. SEVERE. + * @param sourceClass + * Name of class that issued the logging request. + * @param sourceMethod + * Name of method that issued the logging request. + * @param msg + * The key in the message localization catalog for the message or + * the actual message itself. During formatting, if the logger + * has a mapping for the msg string, then the msg string is + * replaced by the localized value. Otherwise the original msg + * string is used. The formatter uses java.text.MessageFormat + * style formatting to format parameters, so for example a format + * string "{0} {1}" would format two inserts into the message. + * @param inserts + * Array of parameters to the message, may be null. + * @param thrown + * Throwable associated with log message. + */ + void log(int level, String sourceClass, String sourceMethod, String msg, Object[] inserts, Throwable thrown); + + /** + * Log a trace message, specifying source class and method, with an array of + * object arguments and a throwable, if the logger is currently enabled for + * the given message level. + * + * @param level + * One of the message level identifiers, e.g. SEVERE. + * @param sourceClass + * Name of class that issued the logging request. + * @param sourceMethod + * Name of method that issued the logging request. + * @param msg + * The key in the message catalog for the message or the actual + * message itself. During formatting, if the logger has a mapping + * for the msg string, then the msg string is replaced by the + * value. Otherwise the original msg string is used. The + * formatter uses java.text.MessageFormat style formatting to + * format parameters, so for example a format string "{0} {1}" + * would format two inserts into the message. + * @param inserts + * Array of parameters to the message, may be null. + * @param ex + * Throwable associated with log message. + */ + void trace(int level, String sourceClass, String sourceMethod, String msg, Object[] inserts, Throwable ex); + + /** + * Format a log message without causing it to be written to the log. + * + * @param msg + * The key in the message localization catalog for the message or + * the actual message itself. During formatting, if the logger + * has a mapping for the msg string, then the msg string is + * replaced by the localized value. Otherwise the original msg + * string is used. The formatter uses java.text.MessageFormat + * style formatting to format parameters, so for example a format + * string "{0} {1}" would format two inserts into the message. + * @param inserts + * Array of parameters to the message. + * @return The formatted message for the current locale. + */ + String formatMessage(String msg, Object[] inserts); + + void dumpTrace(); +} \ No newline at end of file diff --git a/src/main/java/org/eclipse/paho/mqttv5/client/logging/LoggerFactory.java b/src/main/java/org/eclipse/paho/mqttv5/client/logging/LoggerFactory.java new file mode 100644 index 0000000..4ee11d3 --- /dev/null +++ b/src/main/java/org/eclipse/paho/mqttv5/client/logging/LoggerFactory.java @@ -0,0 +1,155 @@ +/******************************************************************************* + * Copyright (c) 2009, 2014 IBM Corp. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * and Eclipse Distribution License v1.0 which accompany this distribution. + * + * The Eclipse Public License is available at + * https://www.eclipse.org/legal/epl-2.0 + * and the Eclipse Distribution License is available at + * https://www.eclipse.org/org/documents/edl-v10.php + * + * Contributors: + * Dave Locke - initial API and implementation and/or initial documentation + */ +package org.eclipse.paho.mqttv5.client.logging; + +import java.lang.reflect.Method; + +/** + * LoggerFactory will create a logger instance ready for use by the caller. + * + * The default is to create a logger that utilises the Java's built in + * logging facility java.util.logging (JSR47). It is possible to override + * this for systems where JSR47 is not available or an alternative logging + * facility is needed by using setLogger and passing the the class name of + * a logger that implements {@link Logger} + */ +import java.util.MissingResourceException; +import java.util.ResourceBundle; +/** + * A factory that returns a logger for use by the MQTT client. + * + * The default log and trace facility uses Java's build in log facility:- + * java.util.logging. For systems where this is not available or where + * an alternative logging framework is required the logging facility can be + * replaced using {@link org.eclipse.paho.mqttv5.client.logging.LoggerFactory#setLogger(String)} + * which takes an implementation of the {@link org.eclipse.paho.mqttv5.client.logging.Logger} + * interface. + */ +public class LoggerFactory { + /** + * Default message catalog. + */ + public final static String MQTT_CLIENT_MSG_CAT = "org.eclipse.paho.mqttv5.client.internal.nls.logcat"; + private static final String CLASS_NAME = LoggerFactory.class.getName(); + + private static String overrideloggerClassName = null; + /** + * Default logger that uses java.util.logging. + */ + private static String jsr47LoggerClassName = JSR47Logger.class.getName(); + + /** + * Find or create a logger for a named package/class. + * If a logger has already been created with the given name + * it is returned. Otherwise a new logger is created. By default a logger + * that uses java.util.logging will be returned. + * + * @param messageCatalogName the resource bundle containing the logging messages. + * @param loggerID unique name to identify this logger. + * @return a suitable Logger. + */ + public static Logger getLogger(String messageCatalogName, String loggerID) { + String loggerClassName = overrideloggerClassName; + Logger logger = null; + + if (loggerClassName == null) { + loggerClassName = jsr47LoggerClassName; + } +// logger = getJSR47Logger(ResourceBundle.getBundle(messageCatalogName), loggerID, null) ; + logger = getLogger(loggerClassName, ResourceBundle.getBundle(messageCatalogName), loggerID, null) ; +// } + + if (null == logger) { + throw new MissingResourceException("Error locating the logging class", CLASS_NAME, loggerID); + } + + return logger; + } + + + /** + * Return an instance of a logger + * + * @param the class name of the load to load + * @param messageCatalog the resource bundle containing messages + * @param loggerID an identifier for the logger + * @param resourceName a name or context to associate with this logger instance. + * @return a ready for use logger + */ + private static Logger getLogger(String loggerClassName, ResourceBundle messageCatalog, String loggerID, String resourceName) { //, FFDC ffdc) { + Logger logger = null; + Class logClass = null; + + try { + logClass = Class.forName(loggerClassName); + } catch (NoClassDefFoundError ncdfe) { + return null; + } catch (ClassNotFoundException cnfe) { + return null; + } + if (null != logClass) { + // Now instantiate the log + try { + logger = (Logger)logClass.newInstance(); + } catch (IllegalAccessException e) { + return null; + } catch (InstantiationException e) { + return null; + } catch (ExceptionInInitializerError e) { + return null; + } catch (SecurityException e) { + return null; + } + logger.initialise(messageCatalog, loggerID, resourceName); + } + + return logger; + } + + /** + * When run in JSR47, this allows access to the properties in the logging.properties + * file. + * If not run in JSR47, or the property isn't set, returns null. + * @param name the property to return + * @return the property value, or null if it isn't set or JSR47 isn't being used + */ + public static String getLoggingProperty(String name) { + String result = null; + try { + // Hide behind reflection as java.util.logging is guaranteed to be + // available. + Class logManagerClass = Class.forName("java.util.logging.LogManager"); + Method m1 = logManagerClass.getMethod("getLogManager", new Class[]{}); + Object logManagerInstance = m1.invoke(null, null); + Method m2 = logManagerClass.getMethod("getProperty", new Class[]{String.class}); + result = (String)m2.invoke(logManagerInstance,new Object[]{name}); + } catch(Exception e) { + // Any error, assume JSR47 isn't available and return null + result = null; + } + return result; + } + + /** + * Set the class name of the logger that the LoggerFactory will load + * If not set getLogger will attempt to create a logger + * appropriate for the platform. + * @param loggerClassName - Logger implementation class name to use. + */ + public static void setLogger(String loggerClassName) { + LoggerFactory.overrideloggerClassName = loggerClassName; + } +} \ No newline at end of file diff --git a/src/main/java/org/eclipse/paho/mqttv5/client/logging/SimpleLogFormatter.java b/src/main/java/org/eclipse/paho/mqttv5/client/logging/SimpleLogFormatter.java new file mode 100644 index 0000000..6266f3f --- /dev/null +++ b/src/main/java/org/eclipse/paho/mqttv5/client/logging/SimpleLogFormatter.java @@ -0,0 +1,104 @@ +/******************************************************************************* + * Copyright (c) 2014 IBM Corp. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * and Eclipse Distribution License v1.0 which accompany this distribution. + * + * The Eclipse Public License is available at + * https://www.eclipse.org/legal/epl-2.0 + * and the Eclipse Distribution License is available at + * https://www.eclipse.org/org/documents/edl-v10.php + */ + +package org.eclipse.paho.mqttv5.client.logging; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.text.MessageFormat; +import java.util.Date; +import java.util.logging.Formatter; +import java.util.logging.LogRecord; + +/** + * SimpleLogFormatter prints a single line + * log record in human readable form. + */ +public class SimpleLogFormatter extends Formatter { + + private static final String LS = System.getProperty("line.separator"); + /** + * Constructs a SimpleFormatter object. + */ + public SimpleLogFormatter() { + super(); + } + + /** + * Format the logrecord as a single line with well defined columns. + */ + public String format(LogRecord r) { + StringBuffer sb = new StringBuffer(); + sb.append(r.getLevel().getName()).append("\t"); + sb.append(MessageFormat.format("{0, date, yy-MM-dd} {0, time, kk:mm:ss.SSSS} ", + new Object[] { new Date(r.getMillis()) })+"\t"); + String cnm = r.getSourceClassName(); + String cn=""; + if (cnm != null) { + int cnl = cnm.length(); + if (cnl>20) { + cn = r.getSourceClassName().substring(cnl-19); + } else { + char[] sp = {' '}; + StringBuffer sb1= new StringBuffer().append(cnm); + cn = sb1.append(sp,0, 1).toString(); + } + } + sb.append(cn).append("\t").append(" "); + sb.append(left(r.getSourceMethodName(),23,' ')).append("\t"); + sb.append(r.getThreadID()).append("\t"); + sb.append(formatMessage(r)).append(LS); + if (null != r.getThrown()) { + sb.append("Throwable occurred: "); + Throwable t = r.getThrown(); + PrintWriter pw = null; + try { + StringWriter sw = new StringWriter(); + pw = new PrintWriter(sw); + t.printStackTrace(pw); + sb.append(sw.toString()); + } finally { + if (pw != null) { + try { + pw.close(); + } catch (Exception e) { + // ignore + } + } + } + } + return sb.toString(); + } + + /** + * Left justify a string. + * + * @param s the string to justify + * @param width the field width to justify within + * @param fillChar the character to fill with + * + * @return the justified string. + */ + public static String left(String s, int width, char fillChar) { + if (s.length() >= width) { + return s; + } + StringBuffer sb = new StringBuffer(width); + sb.append(s); + for (int i = width - s.length(); --i >= 0;) { + sb.append(fillChar); + } + return sb.toString(); + } + +} diff --git a/src/main/java/org/eclipse/paho/mqttv5/client/logging/jsr47min.properties b/src/main/java/org/eclipse/paho/mqttv5/client/logging/jsr47min.properties new file mode 100644 index 0000000..4009555 --- /dev/null +++ b/src/main/java/org/eclipse/paho/mqttv5/client/logging/jsr47min.properties @@ -0,0 +1,83 @@ +# Properties file which configures the operation of the JDK logging facility. +# +# The configuration in this file is the suggesgted configuration +# for collecting trace for helping debug problems related to the +# Paho MQTT client. It configures trace to be continuosly collected +# in memory with minimal impact on performance. +# +# When the push trigger (by default a Severe level message) or a +# specific request is made to "push" the in memory trace then it +# is "pushed" to the configured target handler. By default +# this is the standard java.util.logging.FileHandler. The Paho Debug +# class can be used to push the memory trace to its target +# +# To enable trace either: +# - use this properties file as is and set the logging facility up +# to use it by configuring the util logging system property e.g. +# +# >java -Djava.util.logging.config.file=\jsr47min.properties +# +# - This contents of this file can also be merged with another +# java.util.logging config file to ensure provide wider logging +# and trace including Paho trace + +# Global logging properties. +# ------------------------------------------ +# The set of handlers to be loaded upon startup. +# Comma-separated list of class names. +# - Root handlers are not enabled by default - just handlers on the Paho packages. +#handlers=java.util.logging.MemoryHandler,java.util.logging.FileHandler, java.util.logging.ConsoleHandler + +# Default global logging level. +# Loggers and Handlers may override this level +#.level=INFO + +# Loggers +# ------------------------------------------ +# A memoryhandler is attached to the paho packages +# and the level specified to collected all trace related +# to paho packages. This will override any root/global +# level handlers if set. +org.eclipse.paho.mqttv5.client.handlers=java.util.logging.MemoryHandler +org.eclipse.paho.mqttv5.client.level=ALL +# It is possible to set more granular trace on a per class basis e.g. +#org.eclipse.paho.mqttv5.client.internal.ClientComms.level=ALL + +# Handlers +# ----------------------------------------- +# Note: the target handler that is associated with the MemoryHandler is not a root handler +# and hence not returned when getting the handlers from root. It appears accessing +# target handler programatically is not possible as target is a private variable in +# class MemoryHandler +java.util.logging.MemoryHandler.level=ALL +java.util.logging.MemoryHandler.size=10000 +java.util.logging.MemoryHandler.push=ALL +java.util.logging.MemoryHandler.target=java.util.logging.FileHandler +#java.util.logging.MemoryHandler.target=java.util.logging.ConsoleHandler + + +# --- FileHandler --- +# Override of global logging level +java.util.logging.FileHandler.level=ALL + +# Naming style for the output file: +# (The output file is placed in the directory +# defined by the "user.home" System property.) +# See java.util.logging for more options +java.util.logging.FileHandler.pattern=%h/ibm/paho/trace/paho%u.log + +# Limiting size of output file in bytes: +java.util.logging.FileHandler.limit=200000 + +# Number of output files to cycle through, by appending an +# integer to the base file name: +java.util.logging.FileHandler.count=3 + +# Style of output (Simple or XML): +java.util.logging.FileHandler.formatter=org.eclipse.paho.mqttv5.client.logging.SimpleLogFormatter + +# --- ConsoleHandler --- +# Override of global logging level +#java.util.logging.ConsoleHandler.level=INFO +#java.util.logging.ConsoleHandler.formatter=java.util.logging.SimpleFormatter +#java.util.logging.ConsoleHandler.formatter=org.eclipse.paho.mqttv5.client.logging.SimpleLogFormatter diff --git a/src/main/java/org/eclipse/paho/mqttv5/client/logging/package.html b/src/main/java/org/eclipse/paho/mqttv5/client/logging/package.html new file mode 100644 index 0000000..19c41d8 --- /dev/null +++ b/src/main/java/org/eclipse/paho/mqttv5/client/logging/package.html @@ -0,0 +1,5 @@ + +Provides helpers and utilities. + + + \ No newline at end of file diff --git a/src/main/java/org/eclipse/paho/mqttv5/client/persist/MemoryPersistence.java b/src/main/java/org/eclipse/paho/mqttv5/client/persist/MemoryPersistence.java new file mode 100644 index 0000000..8b09b5e --- /dev/null +++ b/src/main/java/org/eclipse/paho/mqttv5/client/persist/MemoryPersistence.java @@ -0,0 +1,111 @@ +/******************************************************************************* + * Copyright (c) 2009, 2014 IBM Corp. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * and Eclipse Distribution License v1.0 which accompany this distribution. + * + * The Eclipse Public License is available at + * https://www.eclipse.org/legal/epl-2.0 + * and the Eclipse Distribution License is available at + * https://www.eclipse.org/org/documents/edl-v10.php + * + * Contributors: + * Dave Locke - initial API and implementation and/or initial documentation + */ +package org.eclipse.paho.mqttv5.client.persist; + +import java.util.Enumeration; +import java.util.Hashtable; + +import org.eclipse.paho.mqttv5.client.MqttClientPersistence; +import org.eclipse.paho.mqttv5.common.MqttPersistable; +import org.eclipse.paho.mqttv5.common.MqttPersistenceException; + +/** + * Persistence that uses memory + * + * In cases where reliability is not required across client or device + * restarts memory this memory persistence can be used. In cases where + * reliability is required like when clean session is set to false + * then a non-volatile form of persistence should be used. + * + */ +public class MemoryPersistence implements MqttClientPersistence { + + private Hashtable data; + + /* (non-Javadoc) + * @see org.eclipse.paho.mqttv5.client.MqttClientPersistence#close() + */ + public void close() throws MqttPersistenceException { + if (data != null) { + data.clear(); + } + } + + /* (non-Javadoc) + * @see org.eclipse.paho.mqttv5.client.MqttClientPersistence#keys() + */ + public Enumeration keys() throws MqttPersistenceException { + checkIsOpen(); + return data.keys(); + } + + /* (non-Javadoc) + * @see org.eclipse.paho.mqttv5.client.MqttClientPersistence#get(java.lang.String) + */ + public MqttPersistable get(String key) throws MqttPersistenceException { + checkIsOpen(); + return (MqttPersistable)data.get(key); + } + + /* (non-Javadoc) + * @see org.eclipse.paho.mqttv5.client.MqttClientPersistence#open(java.lang.String, java.lang.String) + */ + public void open(String clientId) throws MqttPersistenceException { + this.data = new Hashtable(); + } + + /* (non-Javadoc) + * @see org.eclipse.paho.mqttv5.client.MqttClientPersistence#put(java.lang.String, org.eclipse.paho.mqttv5.client.MqttPersistable) + */ + public void put(String key, MqttPersistable persistable) throws MqttPersistenceException { + checkIsOpen(); + data.put(key, persistable); + } + + /* (non-Javadoc) + * @see org.eclipse.paho.mqttv5.client.MqttClientPersistence#remove(java.lang.String) + */ + public void remove(String key) throws MqttPersistenceException { + checkIsOpen(); + data.remove(key); + } + + /* (non-Javadoc) + * @see org.eclipse.paho.mqttv5.client.MqttClientPersistence#clear() + */ + public void clear() throws MqttPersistenceException { + checkIsOpen(); + data.clear(); + } + + /* (non-Javadoc) + * @see org.eclipse.paho.mqttv5.client.MqttClientPersistence#containsKey(java.lang.String) + */ + public boolean containsKey(String key) throws MqttPersistenceException { + checkIsOpen(); + return data.containsKey(key); + } + + /** + * Checks whether the persistence has been opened. + * @throws MqttPersistenceException if the persistence has not been opened. + */ + private void checkIsOpen() throws MqttPersistenceException { + if (data == null) { + throw new MqttPersistenceException(); + } + } +} diff --git a/src/main/java/org/eclipse/paho/mqttv5/client/persist/MqttDefaultFilePersistence.java b/src/main/java/org/eclipse/paho/mqttv5/client/persist/MqttDefaultFilePersistence.java new file mode 100644 index 0000000..b6482a2 --- /dev/null +++ b/src/main/java/org/eclipse/paho/mqttv5/client/persist/MqttDefaultFilePersistence.java @@ -0,0 +1,300 @@ +/******************************************************************************* + * Copyright (c) 2009, 2014 IBM Corp. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * and Eclipse Distribution License v1.0 which accompany this distribution. + * + * The Eclipse Public License is available at + * https://www.eclipse.org/legal/epl-2.0 + * and the Eclipse Distribution License is available at + * https://www.eclipse.org/org/documents/edl-v10.php + * + * Contributors: + * Dave Locke - initial API and implementation and/or initial documentation + */ +package org.eclipse.paho.mqttv5.client.persist; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.FilenameFilter; +import java.io.IOException; +import java.util.Enumeration; +import java.util.Vector; + +import org.eclipse.paho.mqttv5.client.MqttClientPersistence; +import org.eclipse.paho.mqttv5.client.internal.FileLock; +import org.eclipse.paho.mqttv5.client.internal.MqttPersistentData; +import org.eclipse.paho.mqttv5.common.MqttPersistable; +import org.eclipse.paho.mqttv5.common.MqttPersistenceException; + +/** + * An implementation of the {@link MqttClientPersistence} interface that provides + * file based persistence. + * + * A directory is specified when the Persistence object is created. When the persistence + * is then opened (see {@link #open(String)}), a sub-directory is made beneath the base + * for this client ID and connection key. This allows one persistence base directory + * to be shared by multiple clients. + * + * The sub-directory's name is created from a concatenation of the client ID and connection key + * with any instance of '/', '\\', ':' or ' ' removed. + */ +public class MqttDefaultFilePersistence implements MqttClientPersistence { + private static final String MESSAGE_FILE_EXTENSION = ".msg"; + private static final String MESSAGE_BACKUP_FILE_EXTENSION = ".bup"; + private static final String LOCK_FILENAME = ".lck"; + + private File dataDir; + private File clientDir = null; + private FileLock fileLock = null; + + //TODO + private static FilenameFilter FILENAME_FILTER; + + private static FilenameFilter getFilenameFilter(){ + if(FILENAME_FILTER == null){ + FILENAME_FILTER = new PersistenceFileNameFilter(MESSAGE_FILE_EXTENSION); + } + return FILENAME_FILTER; + } + + public MqttDefaultFilePersistence() { //throws MqttPersistenceException { + this(System.getProperty("user.dir")); + } + + /** + * Create an file-based persistent data store within the specified directory. + * @param directory the directory to use. + */ + public MqttDefaultFilePersistence(String directory) { //throws MqttPersistenceException { + dataDir = new File(directory); + } + + public void open(String clientId) throws MqttPersistenceException { + + if (dataDir.exists() && !dataDir.isDirectory()) { + throw new MqttPersistenceException(); + } else if (!dataDir.exists() ) { + if (!dataDir.mkdirs()) { + throw new MqttPersistenceException(); + } + } + if (!dataDir.canWrite()) { + throw new MqttPersistenceException(); + } + + + StringBuffer keyBuffer = new StringBuffer(); + for (int i=0;i keys() throws MqttPersistenceException { + checkIsOpen(); + File[] files = getFiles(); + Vector result = new Vector(files.length); + for (File file : files) { + String filename = file.getName(); + String key = filename.substring(0, filename.length() - MESSAGE_FILE_EXTENSION.length()); + result.addElement(key); + } + return result.elements(); + } + + private File[] getFiles() throws MqttPersistenceException { + checkIsOpen(); + File[] files = clientDir.listFiles(getFilenameFilter()); + if (files == null) { + throw new MqttPersistenceException(); + } + return files; + } + + private boolean isSafeChar(char c) { + return Character.isJavaIdentifierPart(c) || c=='-'; + } + + /** + * Identifies any backup files in the specified directory and restores them + * to their original file. This will overwrite any existing file of the same + * name. This is safe as a stray backup file will only exist if a problem + * occurred whilst writing to the original file. + * @param dir The directory in which to scan and restore backups + */ + private void restoreBackups(File dir) throws MqttPersistenceException { + File[] files = dir.listFiles(new PersistenceFileFilter(MESSAGE_BACKUP_FILE_EXTENSION)); + + if (files == null) { + throw new MqttPersistenceException(); + } + + for (File file : files) { + File originalFile = new File(dir, file.getName().substring(0, file.getName().length() - MESSAGE_BACKUP_FILE_EXTENSION.length())); + boolean result = file.renameTo(originalFile); + if (!result) { + originalFile.delete(); + file.renameTo(originalFile); + } + } + } + + public boolean containsKey(String key) throws MqttPersistenceException { + checkIsOpen(); + File file = new File(clientDir, key+MESSAGE_FILE_EXTENSION); + return file.exists(); + } + + public void clear() throws MqttPersistenceException { + checkIsOpen(); + File[] files = getFiles(); + for (File file : files) { + file.delete(); + } + clientDir.delete(); + } +} diff --git a/src/main/java/org/eclipse/paho/mqttv5/client/persist/PersistenceFileFilter.java b/src/main/java/org/eclipse/paho/mqttv5/client/persist/PersistenceFileFilter.java new file mode 100644 index 0000000..54f3e82 --- /dev/null +++ b/src/main/java/org/eclipse/paho/mqttv5/client/persist/PersistenceFileFilter.java @@ -0,0 +1,18 @@ +package org.eclipse.paho.mqttv5.client.persist; + +import java.io.File; +import java.io.FileFilter; + +public class PersistenceFileFilter implements FileFilter{ + + private final String fileExtension; + + public PersistenceFileFilter(String fileExtension){ + this.fileExtension = fileExtension; + } + + public boolean accept(File pathname) { + return pathname.getName().endsWith(fileExtension); + } + +} diff --git a/src/main/java/org/eclipse/paho/mqttv5/client/persist/PersistenceFileNameFilter.java b/src/main/java/org/eclipse/paho/mqttv5/client/persist/PersistenceFileNameFilter.java new file mode 100644 index 0000000..b982353 --- /dev/null +++ b/src/main/java/org/eclipse/paho/mqttv5/client/persist/PersistenceFileNameFilter.java @@ -0,0 +1,18 @@ +package org.eclipse.paho.mqttv5.client.persist; + +import java.io.File; +import java.io.FilenameFilter; + +public class PersistenceFileNameFilter implements FilenameFilter{ + + private final String fileExtension; + + public PersistenceFileNameFilter(String fileExtension){ + this.fileExtension = fileExtension; + } + + public boolean accept(File dir, String name) { + return name.endsWith(fileExtension); + } + +} diff --git a/src/main/java/org/eclipse/paho/mqttv5/client/security/SSLSocketFactoryFactory.java b/src/main/java/org/eclipse/paho/mqttv5/client/security/SSLSocketFactoryFactory.java new file mode 100644 index 0000000..d311e20 --- /dev/null +++ b/src/main/java/org/eclipse/paho/mqttv5/client/security/SSLSocketFactoryFactory.java @@ -0,0 +1,1358 @@ +/******************************************************************************* + * Copyright (c) 2009, 2014 IBM Corp. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * and Eclipse Distribution License v1.0 which accompany this distribution. + * + * The Eclipse Public License is available at + * https://www.eclipse.org/legal/epl-2.0 + * and the Eclipse Distribution License is available at + * https://www.eclipse.org/org/documents/edl-v10.php + * + * Contributors: + * Dave Locke - initial API and implementation and/or initial documentation + */ +package org.eclipse.paho.mqttv5.client.security; + +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.security.KeyManagementException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.UnrecoverableKeyException; +import java.security.cert.CertificateException; +import java.util.Hashtable; +import java.util.Iterator; +import java.util.Properties; +import java.util.Set; +import java.util.Vector; + +import javax.net.ssl.KeyManager; +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSocketFactory; +import javax.net.ssl.TrustManager; +import javax.net.ssl.TrustManagerFactory; + +import org.eclipse.paho.mqttv5.client.logging.Logger; +import org.eclipse.paho.mqttv5.common.MqttSecurityException; + + +/** + * An SSLSocketFactoryFactory provides a socket factory and a server socket + * factory that then can be used to create SSL client sockets or SSL server + * sockets. + *

+ * The SSLSocketFactoryFactory is configured using IBM SSL properties, i.e. + * properties of the format "com.ibm.ssl.propertyName", e.g. + * "com.ibm.ssl.keyStore". The class supports multiple configurations, each + * configuration is identified using a name or configuration ID. The + * configuration ID with "null" is used as a default configuration. When a + * socket factory is being created for a given configuration, properties of that + * configuration are first picked. If a property is not defined there, then that + * property is looked up in the default configuration. Finally, if a property + * element is still not found, then the corresponding system property is + * inspected, i.e. javax.net.ssl.keyStore. If the system property is not set + * either, then the system's default value is used (if available) or an + * exception is thrown. + *

+ * The SSLSocketFacotryFactory can be reconfigured at any time. A + * reconfiguration does not affect existing socket factories. + *

+ * All properties share the same key space; i.e. the configuration ID is not + * part of the property keys. + *

+ * The methods should be called in the following order: + *

    + *
  1. isSupportedOnJVM(): to check whether this class is supported on + * the runtime platform. Not all runtimes support SSL/TLS.
  2. + *
  3. SSLSocketFactoryFactory(): the constructor. Clients + * (in the same JVM) may share an SSLSocketFactoryFactory, or have one each.
  4. + *
  5. initialize(properties, configID): to initialize this object with + * the required SSL properties for a configuration. This may be called multiple + * times, once for each required configuration.It may be called again to change the required SSL + * properties for a particular configuration
  6. + *
  7. getEnabledCipherSuites(configID): to later set the enabled + * cipher suites on the socket [see below].
  8. + *
+ *
    + *
  • For an MQTT server: + + *
      + *
    1. getKeyStore(configID): Optionally, to check that if there is no + * keystore, then that all the enabled cipher suits are anonymous.
    2. + *
    3. createServerSocketFactory(configID): to create an + * SSLServerSocketFactory.
    4. + *
    5. getClientAuthentication(configID): to later set on the + * SSLServerSocket (itself created from the SSLServerSocketFactory) whether + * client authentication is needed.
    6. + *
    + *
  • + *
  • For an MQTT client: + *
      + *
    1. createSocketFactory(configID): to create an SSLSocketFactory.
    2. + *
    + *
  • + *
+ */ +public class SSLSocketFactoryFactory { + private static final String CLASS_NAME = "org.eclipse.paho.mqttv5.client.internal.security.SSLSocketFactoryFactory"; + /** + * Property keys specific to the client). + */ + public static final String SSLPROTOCOL="com.ibm.ssl.protocol"; + public static final String JSSEPROVIDER="com.ibm.ssl.contextProvider"; + public static final String KEYSTORE="com.ibm.ssl.keyStore"; + public static final String KEYSTOREPWD="com.ibm.ssl.keyStorePassword"; + public static final String KEYSTORETYPE="com.ibm.ssl.keyStoreType"; + public static final String KEYSTOREPROVIDER="com.ibm.ssl.keyStoreProvider"; + public static final String KEYSTOREMGR="com.ibm.ssl.keyManager"; + public static final String TRUSTSTORE="com.ibm.ssl.trustStore"; + public static final String TRUSTSTOREPWD="com.ibm.ssl.trustStorePassword"; + public static final String TRUSTSTORETYPE="com.ibm.ssl.trustStoreType"; + public static final String TRUSTSTOREPROVIDER="com.ibm.ssl.trustStoreProvider"; + public static final String TRUSTSTOREMGR="com.ibm.ssl.trustManager"; + public static final String CIPHERSUITES="com.ibm.ssl.enabledCipherSuites"; + public static final String CLIENTAUTH="com.ibm.ssl.clientAuthentication"; + + /** + * Property keys used for java system properties + */ + public static final String SYSKEYSTORE="javax.net.ssl.keyStore"; + public static final String SYSKEYSTORETYPE="javax.net.ssl.keyStoreType"; + public static final String SYSKEYSTOREPWD="javax.net.ssl.keyStorePassword"; + public static final String SYSTRUSTSTORE="javax.net.ssl.trustStore"; + public static final String SYSTRUSTSTORETYPE="javax.net.ssl.trustStoreType"; + public static final String SYSTRUSTSTOREPWD="javax.net.ssl.trustStorePassword"; + public static final String SYSKEYMGRALGO="ssl.KeyManagerFactory.algorithm"; + public static final String SYSTRUSTMGRALGO="ssl.TrustManagerFactory.algorithm"; + + + public static final String DEFAULT_PROTOCOL = "TLS"; // "SSL_TLS" is not supported by DesktopEE + + private static final String[] propertyKeys = {SSLPROTOCOL, JSSEPROVIDER, + KEYSTORE, KEYSTOREPWD, KEYSTORETYPE, KEYSTOREPROVIDER, KEYSTOREMGR, + TRUSTSTORE, TRUSTSTOREPWD, TRUSTSTORETYPE, TRUSTSTOREPROVIDER, + TRUSTSTOREMGR, CIPHERSUITES, CLIENTAUTH}; + + private Hashtable configs; // a hashtable that maps configIDs to properties. + + private Properties defaultProperties; + + private static final byte[] key = { (byte) 0x9d, (byte) 0xa7, (byte) 0xd9, + (byte) 0x80, (byte) 0x05, (byte) 0xb8, (byte) 0x89, (byte) 0x9c }; + + private static final String xorTag = "{xor}"; + + private Logger logger = null; + + + /** + * Not all of the JVM/Platforms support all of its + * security features. This method determines if is supported. + * + * @return whether dependent classes can be instantiated on the current + * JVM/platform. + * + * @throws Error + * if any unexpected error encountered whilst checking. Note + * this should not be a ClassNotFoundException, which should + * cause the method to return false. + */ + public static boolean isSupportedOnJVM() throws LinkageError, ExceptionInInitializerError { + String requiredClassname = "javax.net.ssl.SSLServerSocketFactory"; + try { + Class.forName(requiredClassname); + } catch (ClassNotFoundException e) { + return false; + } + return true; + } + + + /** + * Create new instance of class. + * Constructor used by clients. + */ + public SSLSocketFactoryFactory() { + configs = new Hashtable(); + } + + /** + * Create new instance of class. + * Constructor used by the broker. + * @param logger the {@link Logger} to be used + */ + public SSLSocketFactoryFactory(Logger logger) { + this(); + this.logger = logger; + } + + /** + * Checks whether a key belongs to the supported IBM SSL property keys. + * + * @param key + * @return whether a key belongs to the supported IBM SSL property keys. + */ + private boolean keyValid(String key) { + int i = 0; + while (i < propertyKeys.length) { + if (propertyKeys[i].equals(key)) { + break; + } + ++i; + } + return i < propertyKeys.length; + } + + /** + * Checks whether the property keys belong to the supported IBM SSL property + * key set. + * + * @param properties + * @throws IllegalArgumentException + * if any of the properties is not a valid IBM SSL property key. + */ + private void checkPropertyKeys(Properties properties) + throws IllegalArgumentException { + Set keys = properties.keySet(); + Iterator i = keys.iterator(); + while (i.hasNext()) { + String k = (String) i.next(); + if (!keyValid(k)) { + throw new IllegalArgumentException(k + " is not a valid IBM SSL property key."); + } + } + } + + /** + * Convert byte array to char array, where each char is constructed from two + * bytes. + * + * @param b + * byte array + * @return char array + */ + public static char[] toChar(byte[] b) { + if(b==null) return null; + char[] c= new char[b.length/2]; + int i=0; int j=0; + while(i> 8)& 0xFF); + } + return b; + } + + /** + * Obfuscates the password using a simple and not very secure XOR mechanism. + * This should not be used for cryptographical purpose, it's a simple + * scrambler to obfuscate clear-text passwords. + * + * @see org.eclipse.paho.mqttv5.client.security.SSLSocketFactoryFactory#deObfuscate(String) + * + * @param password + * The password to be encrypted, as a char[] array. + * @return An obfuscated password as a String. + */ + public static String obfuscate(char[] password) { + if (password == null) + return null; + byte[] bytes = toByte(password); + for (int i = 0; i < bytes.length; i++) { + bytes[i] = (byte) ((bytes[i] ^ key[i % key.length]) & 0x00ff); + } + String encryptedValue = xorTag + + new String(SimpleBase64Encoder.encode(bytes)); + return encryptedValue; + } + + /** + * The inverse operation of obfuscate: returns a cleartext password that was + * previously obfuscated using the XOR scrambler. + * + * @see org.eclipse.paho.mqttv5.client.security.SSLSocketFactoryFactory#obfuscate(char[]) + * + * @param ePassword + * An obfuscated password. + * @return An array of char, containing the clear text password. + */ + public static char[] deObfuscate(String ePassword) { + if (ePassword == null) + return null; + byte[] bytes = null; + try { + bytes = SimpleBase64Encoder.decode(ePassword.substring(xorTag + .length())); + } catch (Exception e) { + return null; + } + + for (int i = 0; i < bytes.length; i++) { + bytes[i] = (byte) ((bytes[i] ^ key[i % key.length]) & 0x00ff); + } + return toChar(bytes); + } + + /** + * Converts an array of ciphers into a single String. + * + * @param ciphers + * The array of cipher names. + * @return A string containing the name of the ciphers, separated by comma. + */ + public static String packCipherSuites(String[] ciphers) { + String cipherSet=null; + if (ciphers != null) { + StringBuffer buf = new StringBuffer(); + for (int i = 0; i < ciphers.length; i++) { + buf.append(ciphers[i]); + if (i < ciphers.length - 1) { + buf.append(','); + } + } + cipherSet = buf.toString(); + } + return cipherSet; + } + + /** + * Inverse operation of packCipherSuites: converts a string of cipher names + * into an array of cipher names + * + * @param ciphers + * A list of ciphers, separated by comma. + * @return An array of string, each string containing a single cipher name. + */ + public static String[] unpackCipherSuites(String ciphers) { + // can't use split as split is not available on all java platforms. + if(ciphers==null) return null; + Vector c=new Vector(); + int i=ciphers.indexOf(','); + int j=0; + // handle all commas. + while(i>-1) { + // add stuff before and up to (but not including) the comma. + c.add(ciphers.substring(j, i)); + j=i+1; // skip the comma. + i=ciphers.indexOf(',',j); + } + // add last element after the comma or only element if no comma is present. + c.add(ciphers.substring(j)); + String[] s = new String[c.size()]; + c.toArray(s); + return s; + } + + /** + * Obfuscate any key & trust store passwords within the given properties. + * + * @see org.eclipse.paho.mqttv5.client.internal.security.SSLSocketFactoryFactory#obfuscate + * + * @param p + * properties + */ + private void convertPassword(Properties p) { + String pw = p.getProperty(KEYSTOREPWD); + if (pw != null && !pw.startsWith(xorTag)) { + String epw = obfuscate(pw.toCharArray()); + p.put(KEYSTOREPWD, epw); + } + pw = p.getProperty(TRUSTSTOREPWD); + if (pw != null && !pw.startsWith(xorTag)) { + String epw = obfuscate(pw.toCharArray()); + p.put(TRUSTSTOREPWD, epw); + } + } + + /** + * Returns the properties object for configuration configID or creates a new + * one if required. + * + * @param configID + * The configuration identifier for selecting a configuration or + * null for the default configuration. + * @return the properties object for configuration configID + */ +// private Properties getOrCreate(String configID) { +// Properties res = null; +// if (configID == null) { +// if (this.defaultProperties == null) { +// this.defaultProperties = new Properties(); +// } +// res = this.defaultProperties; +// } else { +// res = (Properties) this.configs.get(configID); +// if (res == null) { +// res = new Properties(); +// this.configs.put(configID, res); +// } +// } +// return res; +// } + + /** + * Initializes the SSLSocketFactoryFactory with the provided properties for + * the provided configuration. + * + * @param props + * A properties object containing IBM SSL properties that are + * qualified by one or more configuration identifiers. + * @param configID + * The configuration identifier for selecting a configuration or + * null for the default configuration. + * @throws IllegalArgumentException + * if any of the properties is not a valid IBM SSL property key. + */ + public void initialize(Properties props, String configID) + throws IllegalArgumentException { + checkPropertyKeys(props); + // copy the properties. + Properties p = new Properties(); + p.putAll(props); + convertPassword(p); + if (configID != null) { + this.configs.put(configID, p); + } else { + this.defaultProperties = p; + } + } + + /** + * Merges the given IBM SSL properties into the existing configuration, + * overwriting existing properties. This method is used to selectively + * change properties for a given configuration. The method throws an + * IllegalArgumentException if any of the properties is not a valid IBM SSL + * property key. + * + * @param props + * A properties object containing IBM SSL properties + * @param configID + * The configuration identifier for selecting a configuration or + * null for the default configuration. + * @throws IllegalArgumentException + * if any of the properties is not a valid IBM SSL property key. + */ + public void merge(Properties props, String configID) + throws IllegalArgumentException { + checkPropertyKeys(props); + Properties p = this.defaultProperties; + if (configID != null) { + p = (Properties) this.configs.get(configID); + } + if (p == null) { + p = new Properties(); + } + convertPassword(props); + p.putAll(props); + if (configID != null) { + this.configs.put(configID, p); + } else { + this.defaultProperties = p; + } + + } + + /** + * Remove the configuration of a given configuration identifier. + * + * @param configID + * The configuration identifier for selecting a configuration or + * null for the default configuration. + * @return true, if the configuation could be removed. + */ + public boolean remove(String configID) { + boolean res = false; + if (configID != null) { + res = this.configs.remove(configID) != null; + } else { + if(null != this.defaultProperties) { + res = true; + this.defaultProperties = null; + } + } + return res; + } + + /** + * Returns the configuration of the SSLSocketFactoryFactory for a given + * configuration. Note that changes in the property are reflected in the + * SSLSocketFactoryFactory. + * + * @param configID + * The configuration identifier for selecting a configuration or + * null for the default configuration. + * @return A property object containing the current configuration of the + * SSLSocketFactoryFactory. Note that it could be null. + */ + public Properties getConfiguration(String configID) { + return (Properties) (configID == null ? this.defaultProperties + : this.configs.get(configID)); + } + + /** + * @return Returns the set of configuration IDs that exist in the SSLSocketFactoryFactory. + */ +// public String[] getConfigurationIDs() { +// Set s = this.configs.keySet(); +// String[] configs = new String[s.size()]; +// configs = (String[]) s.toArray(configs); +// return configs; +// } + + /** + * If the value is not null, then put it in the properties object using the + * key. If the value is null, then remove the entry in the properties object + * with the key. + * + * @param p + * @param key + * @param value + */ +// private final void putOrRemove(Properties p, String key, String value) { +// if (value == null) { +// p.remove(key); +// } else { +// p.put(key, value); +// } +// } + + /** + * Sets the SSL protocol variant. If protocol is NULL then an existing value + * will be removed. + * + * @param configID + * The configuration identifier for selecting a configuration or + * null for the default configuration. + * @param protocol + * One of SSL, SSLv3, TLS, TLSv1, SSL_TLS + */ +// public void setSSLProtocol(String configID, String protocol) { +// Properties p = getOrCreate(configID); +// putOrRemove(p, SSLPROTOCOL, protocol); +// } + + /** + * Sets the JSSE context provider. If provider is null, then an existing + * value will be removed. + * + * @param configID + * The configuration identifier for selecting a configuration or + * null for the default configuration. + * @param provider + * The JSSE provider. For example "IBMJSSE2" or "SunJSSE". + */ +// public void setJSSEProvider(String configID, String provider) { +// Properties p = getOrCreate(configID); +// putOrRemove(p, JSSEPROVIDER, provider); +// } + + /** + * Sets the filename of the keyStore object. A null value is ignored. + * + * @param configID + * The configuration identifier for selecting a configuration or + * null for the default configuration. + * @param keyStore + * A filename that points to a valid keystore. + */ +// public void setKeyStore(String configID, String keyStore) { +// if (keyStore == null) +// return; +// Properties p = getOrCreate(configID); +// putOrRemove(p, KEYSTORE, keyStore); +// } + + /** + * Sets the password that is used for the keystore. The password must be + * provided in plain text, but it will be stored internally in a scrambled + * XOR format. + * + * @see org.eclipse.paho.mqttv5.client.internal.security.SSLSocketFactoryFactory#obfuscate + * + * @param configID + * The configuration identifier for selecting a configuration or + * null for the default configuration. + * @param password + * The keystore password + */ +// public void setKeyStorePassword(String configID, char[] password) { +// if (password == null) +// return; +// Properties p = getOrCreate(configID); +// // convert password, using XOR-based scrambling. +// String ePasswd = obfuscate(password); +// for(int i=0;i=3){ + encoded.append(to64((((bytes[i] & 0xff) << 16) + | (int) ((bytes[i+1] & 0xff) << 8) | (int) (bytes[i+2] & 0xff)),4)); + i+=3; + j-=3; + } + // j==2 | j==1 | j==0 + if(j==2) { + // there is a rest of 2 bytes. This encodes into 3 chars. + encoded.append(to64(((bytes[i] &0xff)<<8) | ((bytes[i+1] & 0xff)),3)); + } + if(j==1) { + // there is a rest of 1 byte. This encodes into 1 char. + encoded.append(to64(((bytes[i] & 0xff)),2)); + } + return encoded.toString(); + } + + public static byte[] decode(String string) { + byte[] encoded=string.getBytes(); + int len=encoded.length; + byte[] decoded=new byte[len*3/4]; + int i=0; + int j=len; + int k=0; + while(j>=4) { + long d=from64(encoded, i, 4); + j-=4; + i+=4; + for(int l=2;l>=0;l--) { + decoded[k+l]=(byte) (d & 0xff); + d=d >>8; + } + k+=3; + } + // j==3 | j==2 + if(j==3) { + long d=from64(encoded, i, 3); + for(int l=1;l>=0;l--) { + decoded[k+l]=(byte) (d & 0xff); + d=d >>8; + } + } + if(j==2) { + long d=from64(encoded, i, 2); + decoded[k]=(byte) (d & 0xff); + } + return decoded; + } + + /* the core conding routine. Translates an input integer into + * a string of the given length.*/ + private final static String to64(long input, int size) { + final StringBuffer result = new StringBuffer(size); + while (size > 0) { + size--; + result.append(PWDCHARS_ARRAY[((int) (input & 0x3f))]); + input = input >> 6; + } + return result.toString(); + } + + /* + * The reverse operation of to64 + */ + private final static long from64(byte[] encoded, int idx, int size) { + long res=0; + int f=0; + while(size>0) { + size--; + long r=0; + // convert encoded[idx] back into a 6-bit value. + byte d=encoded[idx++]; + if(d=='/') { + r=1; + } + if(d>='0' && d<='9') { + r=2+d-'0'; + } + if(d>='A' && d<='Z') { + r=12+d-'A'; + } + if(d>='a' && d<='z') { + r=38+d-'a'; + } + res=res+((long)r << f); + f+=6; + } + return res; + } + +} diff --git a/src/main/java/org/eclipse/paho/mqttv5/client/spi/NetworkModuleFactory.java b/src/main/java/org/eclipse/paho/mqttv5/client/spi/NetworkModuleFactory.java new file mode 100644 index 0000000..b7275e2 --- /dev/null +++ b/src/main/java/org/eclipse/paho/mqttv5/client/spi/NetworkModuleFactory.java @@ -0,0 +1,56 @@ +/* + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * and Eclipse Distribution License v1.0 which accompany this distribution. + * + * The Eclipse Public License is available at + * https://www.eclipse.org/legal/epl-2.0 + * and the Eclipse Distribution License is available at + * https://www.eclipse.org/org/documents/edl-v10.php + */ +package org.eclipse.paho.mqttv5.client.spi; + +import java.net.URI; +import java.util.Set; + +import org.eclipse.paho.mqttv5.client.MqttConnectionOptions; +import org.eclipse.paho.mqttv5.client.internal.NetworkModule; +import org.eclipse.paho.mqttv5.common.MqttException; + +/** + * The NetworkModuleFactory provides a facility to discover and create {@link NetworkModule}s for URI schemes. + * + * @author Maik Scheibler + */ +public interface NetworkModuleFactory { + + /** + * Returns all URI schemes that are supported by the NetworkModules created from this factory. + * + * @return an unmodifiable set of all supported URI schemes (lower case letters) + * @see URI#getScheme() + */ + Set getSupportedUriSchemes(); + + /** + * This method validates the {@link URI#getSchemeSpecificPart() scheme specific part} of all by this factory + * supported URI schemes. If the provided URI does not fulfill the scheme requirements an + * {@link IllegalArgumentException} must be thrown with explanatory a message. + * + * @param brokerUri to be validated + * @throws IllegalArgumentException to signal an invalid URI; the exception transports the cause + */ + void validateURI(URI brokerUri) throws IllegalArgumentException; + + /** + * Creates a NetworkModule instance. + * + * @param brokerUri used to connect to the broker + * @param options used for the connection + * @param clientId a client identifier that is unique on the server being connected to + * @return a NetworkModule instance + * @throws MqttException in case of any error during instance creation + */ + NetworkModule createNetworkModule(URI brokerUri, MqttConnectionOptions options, String clientId) + throws MqttException; +} diff --git a/src/main/java/org/eclipse/paho/mqttv5/client/spi/package-info.java b/src/main/java/org/eclipse/paho/mqttv5/client/spi/package-info.java new file mode 100644 index 0000000..1d3fb58 --- /dev/null +++ b/src/main/java/org/eclipse/paho/mqttv5/client/spi/package-info.java @@ -0,0 +1,6 @@ +/** + * This package defines extensible service interfaces. + * + * @author Maik Scheibler + */ +package org.eclipse.paho.mqttv5.client.spi; diff --git a/src/main/java/org/eclipse/paho/mqttv5/client/util/Debug.java b/src/main/java/org/eclipse/paho/mqttv5/client/util/Debug.java new file mode 100644 index 0000000..a94d3af --- /dev/null +++ b/src/main/java/org/eclipse/paho/mqttv5/client/util/Debug.java @@ -0,0 +1,183 @@ +/******************************************************************************* + * Copyright (c) 2009, 2018 IBM Corp. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * and Eclipse Distribution License v1.0 which accompany this distribution. + * + * The Eclipse Public License is available at + * https://www.eclipse.org/legal/epl-2.0 + * and the Eclipse Distribution License is available at + * https://www.eclipse.org/org/documents/edl-v10.php + * + * Contributors: + * Dave Locke - initial API and implementation and/or initial documentation + */ +package org.eclipse.paho.mqttv5.client.util; + +import java.util.Enumeration; +import java.util.Properties; + +import org.eclipse.paho.mqttv5.client.internal.ClientComms; +import org.eclipse.paho.mqttv5.client.logging.Logger; +import org.eclipse.paho.mqttv5.client.logging.LoggerFactory; + +/** + * Utility to help debug problems with the Paho MQTT client + * Once initialised a call to dumpClientDebug will force any memory trace + * together with pertinent client and system state to the main log facility. + * + * No client wide lock is taken when the dump is progress. This means the + * set of client state may not be consistent as the client can still be + * processing work while the dump is in progress. + */ +public class Debug { + + private static final String CLASS_NAME = ClientComms.class.getName(); + private Logger log = LoggerFactory.getLogger(LoggerFactory.MQTT_CLIENT_MSG_CAT,CLASS_NAME); + private static final String separator = "=============="; + private static final String lineSep = System.getProperty("line.separator","\n"); + + private String clientID; + private ClientComms comms; + + /** + * Set the debug facility up for a specific client + * @param clientID the ID of the client being debugged + * @param comms the ClientComms object of the client being debugged + */ + public Debug(String clientID, ClientComms comms) { + this.clientID = clientID; + this.comms = comms; + log.setResourceName(clientID); + } + + /** + * Dump maximum debug info. + * This includes state specific to a client as well + * as debug that is JVM wide like trace and system properties. + * All state is written as debug log entries. + */ + public void dumpClientDebug() { + dumpClientComms(); + dumpConOptions(); + dumpClientState(); + dumpBaseDebug(); + } + + /** + * Dump of JVM wide debug info. + * This includes trace and system properties. + * Includes trace and system properties + */ + public void dumpBaseDebug() { + dumpVersion(); + dumpSystemProperties(); + dumpMemoryTrace(); + } + + /** + * If memory trace is being used a request is made to push it + * to the target handler. + */ + protected void dumpMemoryTrace() { + log.dumpTrace(); + } + + /** + * Dump information that show the version of the MQTT client being used. + */ + protected void dumpVersion() { + StringBuffer vInfo = new StringBuffer(); + vInfo.append(lineSep+separator+" Version Info "+ separator+lineSep); + vInfo.append(left("Version",20,' ') + ": "+ ClientComms.VERSION + lineSep); + vInfo.append(left("Build Level",20,' ') + ": "+ ClientComms.BUILD_LEVEL + lineSep); + vInfo.append(separator+separator+separator+lineSep); + log.fine(CLASS_NAME,"dumpVersion", vInfo.toString()); + } + + /** + * Dump the current set of system.properties to a log record + */ + public void dumpSystemProperties() { + + Properties sysProps = System.getProperties(); + log.fine(CLASS_NAME,"dumpSystemProperties", dumpProperties(sysProps, "SystemProperties").toString()); + } + + /** + * Dump interesting variables from ClientState + */ + public void dumpClientState() { + Properties props = null; + if (comms != null && comms.getClientState() != null ) { + props = comms.getClientState().getDebug(); + log.fine(CLASS_NAME,"dumpClientState", dumpProperties(props, clientID + " : ClientState").toString()); + } + } + + /** + * Dump interesting variables from ClientComms + */ + public void dumpClientComms() { + Properties props = null; + if (comms != null) { + props = comms.getDebug(); + log.fine(CLASS_NAME,"dumpClientComms", dumpProperties(props, clientID + " : ClientComms").toString()); + } + } + + /** + * Dump Connection options + */ + public void dumpConOptions() { + Properties props = null; + if (comms != null) { + props = comms.getConOptions().getDebug(); + log.fine(CLASS_NAME,"dumpConOptions", dumpProperties(props, clientID + " : Connect Options").toString()); + } + } + + + /** + * Return a set of properties as a formatted string + * @param props The Dump Properties + * @param name The associated name + * @return a set of properties as a formatted string + */ + public static String dumpProperties(Properties props, String name) { + + StringBuffer propStr = new StringBuffer(); + Enumeration propsE = props.propertyNames(); + propStr.append(lineSep+separator+" "+name+" "+ separator+lineSep); + while (propsE.hasMoreElements()) { + String key = (String)propsE.nextElement(); + propStr.append(left(key,28,' ') + ": "+ props.get(key)+lineSep); + } + propStr.append(separator+separator+separator+lineSep); + + return propStr.toString(); + } + + /** + * Left justify a string. + * + * @param s the string to justify + * @param width the field width to justify within + * @param fillChar the character to fill with + * + * @return the justified string. + */ + public static String left(String s, int width, char fillChar) { + if (s.length() >= width) { + return s; + } + StringBuffer sb = new StringBuffer(width); + sb.append(s); + for (int i = width - s.length(); --i >= 0;) { + sb.append(fillChar); + } + return sb.toString(); + } + +} diff --git a/src/main/java/org/eclipse/paho/mqttv5/client/websocket/Base64.java b/src/main/java/org/eclipse/paho/mqttv5/client/websocket/Base64.java new file mode 100644 index 0000000..8cc2d85 --- /dev/null +++ b/src/main/java/org/eclipse/paho/mqttv5/client/websocket/Base64.java @@ -0,0 +1,95 @@ +/******************************************************************************* + * Copyright (c) 2009, 2014 IBM Corp. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * and Eclipse Distribution License v1.0 which accompany this distribution. + * + * The Eclipse Public License is available at + * https://www.eclipse.org/legal/epl-2.0 + * and the Eclipse Distribution License is available at + * https://www.eclipse.org/org/documents/edl-v10.php + * + * Contributors: + * James Sutton - Bug 459142 - WebSocket support for the Java client. + */ +package org.eclipse.paho.mqttv5.client.websocket; + +import java.util.prefs.AbstractPreferences; +import java.util.prefs.BackingStoreException; + +public class Base64 { + + private static final Base64 instance = new Base64(); + private static final Base64Encoder encoder = instance.new Base64Encoder(); + + public static String encode (String s){ + encoder.putByteArray("akey", s.getBytes()); + return encoder.getBase64String(); + } + + public static String encodeBytes (byte[] b){ + encoder.putByteArray("aKey", b); + return encoder.getBase64String(); + + } + + public class Base64Encoder extends AbstractPreferences { + + private String base64String = null; + + public Base64Encoder() { + super(null, ""); + } + + + protected void putSpi(String key, String value) { + base64String = value; + } + + public String getBase64String() { + return base64String; + } + + + protected String getSpi(String key) { + return null; + } + + + protected void removeSpi(String key) { + } + + + protected void removeNodeSpi() throws BackingStoreException { + + } + + + protected String[] keysSpi() throws BackingStoreException { + return null; + } + + + protected String[] childrenNamesSpi() throws BackingStoreException { + return null; + } + + + protected AbstractPreferences childSpi(String name) { + return null; + } + + + protected void syncSpi() throws BackingStoreException { + + } + + + protected void flushSpi() throws BackingStoreException { + + } + + } + +} diff --git a/src/main/java/org/eclipse/paho/mqttv5/client/websocket/ExtendedByteArrayOutputStream.java b/src/main/java/org/eclipse/paho/mqttv5/client/websocket/ExtendedByteArrayOutputStream.java new file mode 100644 index 0000000..d6f5cc0 --- /dev/null +++ b/src/main/java/org/eclipse/paho/mqttv5/client/websocket/ExtendedByteArrayOutputStream.java @@ -0,0 +1,47 @@ +package org.eclipse.paho.mqttv5.client.websocket; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.ByteBuffer; + +class ExtendedByteArrayOutputStream extends ByteArrayOutputStream { + + final WebSocketNetworkModule webSocketNetworkModule; + final WebSocketSecureNetworkModule webSocketSecureNetworkModule; + + ExtendedByteArrayOutputStream(WebSocketNetworkModule module) { + this.webSocketNetworkModule = module; + this.webSocketSecureNetworkModule = null; + } + + ExtendedByteArrayOutputStream(WebSocketSecureNetworkModule module) { + this.webSocketNetworkModule = null; + this.webSocketSecureNetworkModule = module; + } + + public void flush() throws IOException { + final ByteBuffer byteBuffer; + synchronized (this) { + byteBuffer = ByteBuffer.wrap(toByteArray()); + reset(); + } + WebSocketFrame frame = new WebSocketFrame((byte)0x02, true, byteBuffer.array()); + byte[] rawFrame = frame.encodeFrame(); + getSocketOutputStream().write(rawFrame); + getSocketOutputStream().flush(); + + } + + OutputStream getSocketOutputStream() throws IOException { + + if(webSocketNetworkModule != null ){ + return webSocketNetworkModule.getSocketOutputStream(); + } + if(webSocketSecureNetworkModule != null){ + return webSocketSecureNetworkModule.getSocketOutputStream(); + } + return null; + } + +} \ No newline at end of file diff --git a/src/main/java/org/eclipse/paho/mqttv5/client/websocket/HandshakeFailedException.java b/src/main/java/org/eclipse/paho/mqttv5/client/websocket/HandshakeFailedException.java new file mode 100644 index 0000000..887ef4b --- /dev/null +++ b/src/main/java/org/eclipse/paho/mqttv5/client/websocket/HandshakeFailedException.java @@ -0,0 +1,22 @@ +/******************************************************************************* + * Copyright (c) 2009, 2014 IBM Corp. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * and Eclipse Distribution License v1.0 which accompany this distribution. + * + * The Eclipse Public License is available at + * https://www.eclipse.org/legal/epl-2.0 + * and the Eclipse Distribution License is available at + * https://www.eclipse.org/org/documents/edl-v10.php + * + * Contributors: + * James Sutton - Bug 459142 - WebSocket support for the Java client. + */ +package org.eclipse.paho.mqttv5.client.websocket; + +public class HandshakeFailedException extends Exception { + + private static final long serialVersionUID = 1L; + +} diff --git a/src/main/java/org/eclipse/paho/mqttv5/client/websocket/WebSocketFrame.java b/src/main/java/org/eclipse/paho/mqttv5/client/websocket/WebSocketFrame.java new file mode 100644 index 0000000..b078725 --- /dev/null +++ b/src/main/java/org/eclipse/paho/mqttv5/client/websocket/WebSocketFrame.java @@ -0,0 +1,306 @@ +/******************************************************************************* + * Copyright (c) 2009, 2014 IBM Corp. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * and Eclipse Distribution License v1.0 which accompany this distribution. + * + * The Eclipse Public License is available at + * https://www.eclipse.org/legal/epl-2.0 + * and the Eclipse Distribution License is available at + * https://www.eclipse.org/org/documents/edl-v10.php + * + * Contributors: + * James Sutton - Bug 459142 - WebSocket support for the Java client. + */ +package org.eclipse.paho.mqttv5.client.websocket; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.security.SecureRandom; + +public class WebSocketFrame { + + public static final int frameLengthOverhead = 6; + + private byte opcode; + private boolean fin; + private byte[] payload; + private boolean closeFlag = false; + + public byte getOpcode() { + return opcode; + } + + public boolean isFin() { + return fin; + } + + public byte[] getPayload() { + return payload; + } + + public boolean isCloseFlag() { + return closeFlag; + } + + + /** + * Initialise a new WebSocketFrame + * @param opcode WebSocket Opcode + * @param fin If it's final + * @param payload The payload + */ + public WebSocketFrame(byte opcode, boolean fin, byte[] payload){ + this.opcode = opcode; + this.fin = fin; + this.payload = payload.clone(); + } + + + /** + * Initialise WebSocketFrame from raw Data + * @param rawFrame The raw byte buffer + */ + public WebSocketFrame (byte[] rawFrame){ + + ByteBuffer buffer = ByteBuffer.wrap(rawFrame); + + // First Byte: Fin, Reserved, Opcode + byte b = buffer.get(); + setFinAndOpCode(b); + + // Second Byte Masked & Initial Length + b = buffer.get(); + boolean masked = ((b & 0x80) != 0); + int payloadLength = (byte)(0x7F & b); + int byteCount = 0; + if(payloadLength == 0X7F){ + // 8 Byte Extended payload length + byteCount = 8; + } else if (payloadLength == 0X7E){ + // 2 bytes extended payload length + byteCount = 2; + } + + // Decode the extended payload length + while (--byteCount > 0){ + b = buffer.get(); + payloadLength |= (b & 0xFF) << (8 * byteCount); + } + + // Get the Masking key if masked + byte[] maskingKey = null; + if(masked) { + maskingKey = new byte[4]; + buffer.get(maskingKey,0,4); + } + this.payload = new byte[payloadLength]; + buffer.get(this.payload,0,payloadLength); + + // Demask payload if needed + if(masked) + { + for(int i = 0; i < this.payload.length; i++){ + this.payload[i] ^= maskingKey[i % 4]; + } + } + return; + } + + + /** + * Sets the frames Fin flag and opcode. + * @param incomingByte + */ + private void setFinAndOpCode(byte incomingByte){ + this.fin = ((incomingByte & 0x80) !=0); + // Reserved bits, unused right now. + // boolean rsv1 = ((incomingByte & 0x40) != 0); + // boolean rsv2 = ((incomingByte & 0x20) != 0); + // boolean rsv3 = ((incomingByte & 0x10) != 0); + this.opcode = (byte)(incomingByte & 0x0F); + + } + + /** + * Takes an input stream and parses it into a Websocket frame. + * @param input The incoming {@link InputStream} + * @throws IOException if an exception occurs whilst reading the input stream + */ + public WebSocketFrame(InputStream input) throws IOException { + byte firstByte = (byte) input.read(); + setFinAndOpCode(firstByte); + if(this.opcode == 2){ + byte maskLengthByte = (byte) input.read(); + boolean masked = ((maskLengthByte & 0x80) != 0); + int payloadLength = (byte)(0x7F & maskLengthByte); + int byteCount = 0; + if(payloadLength == 0X7F){ + // 8 Byte Extended payload length + byteCount = 8; + } else if (payloadLength == 0X7E){ + // 2 bytes extended payload length + byteCount = 2; + } + + // Decode the payload length + if(byteCount > 0){ + payloadLength = 0; + } + while (--byteCount >= 0){ + maskLengthByte = (byte) input.read(); + payloadLength |= (maskLengthByte & 0xFF) << (8 * byteCount); + } + + // Get the masking key + byte[] maskingKey = null; + if(masked) { + maskingKey = new byte[4]; + input.read(maskingKey,0,4); + } + + this.payload = new byte[payloadLength]; + int offsetIndex = 0; + int tempLength = payloadLength; + int bytesRead = 0; + while (offsetIndex != payloadLength){ + bytesRead = input.read(this.payload,offsetIndex,tempLength); + offsetIndex += bytesRead; + tempLength -= bytesRead; + } + + + // Demask if needed + if(masked) + { + for(int i = 0; i < this.payload.length; i++){ + this.payload[i] ^= maskingKey[i % 4]; + } + } + return; + } else if(this.opcode == 8){ + // Closing connection with server + closeFlag = true; + } else { + throw new IOException("Invalid Frame: Opcode: " +this.opcode); + } + + + } + + + /** + * Encodes the this WebSocketFrame into a byte array. + * @return byte array + */ + public byte[] encodeFrame(){ + int length = this.payload.length + frameLengthOverhead; + // Calculating overhead + if(this.payload.length > 65535){ + length += 8; + } else if(this.payload.length >= 126) { + length += 2; + } + + ByteBuffer buffer = ByteBuffer.allocate(length); + appendFinAndOpCode(buffer, this.opcode, this.fin); + byte[] mask = generateMaskingKey(); + appendLengthAndMask(buffer, this.payload.length, mask); + + for(int i = 0; i < this.payload.length; i ++){ + buffer.put((byte)(this.payload[i] ^=mask[i % 4])); + } + + buffer.flip(); + return buffer.array(); + } + + /** + * Appends the Length and Mask to the buffer + * @param buffer the outgoing {@link ByteBuffer} + * @param length the length of the frame + * @param mask The WebSocket Mask + */ + public static void appendLengthAndMask(ByteBuffer buffer, int length, byte[] mask){ + if(mask != null){ + appendLength(buffer, length, true); + buffer.put(mask); + } else { + appendLength(buffer, length, false); + } + } + + + /** + * Appends the Length of the payload to the buffer + * @param buffer + * @param length + * @param b + */ + private static void appendLength(ByteBuffer buffer, int length, boolean masked) { + + if(length < 0){ + throw new IllegalArgumentException("Length cannot be negative"); + } + + byte b = (masked?(byte)0x80:0x00); + if(length > 0xFFFF){ + buffer.put((byte) (b | 0x7F)); + buffer.put((byte)0x00); + buffer.put((byte)0x00); + buffer.put((byte)0x00); + buffer.put((byte)0x00); + buffer.put((byte)((length >> 24) & 0xFF)); + buffer.put((byte)((length >> 16) & 0xFF)); + buffer.put((byte)((length >> 8) & 0xFF)); + buffer.put((byte)(length & 0xFF)); + } else if(length >= 0x7E){ + buffer.put((byte)(b | 0x7E)); + buffer.put((byte)(length >> 8)); + buffer.put((byte)(length & 0xFF)); + } else { + buffer.put((byte)(b | length)); + } + } + + /** + * Appends the Fin flag and the OpCode + * @param buffer The outgoing buffer + * @param opcode The Websocket OpCode + * @param fin if this is a final frame + */ + public static void appendFinAndOpCode(ByteBuffer buffer, byte opcode, boolean fin){ + byte b = 0x00; + // Add Fin flag + if(fin){ + b |= 0x80; + } + //RSV 1,2,3 aren't important + + // Add opcode + b |= opcode & 0x0F; + buffer.put(b); + } + + /** + * Generates a random masking key + * Nothing super secure, but enough + * for websockets. + * @return ByteArray containing the key; + */ + public static byte[] generateMaskingKey(){ + SecureRandom secureRandomGenerator = new SecureRandom(); + int a = secureRandomGenerator.nextInt(255); + int b = secureRandomGenerator.nextInt(255); + int c = secureRandomGenerator.nextInt(255); + int d = secureRandomGenerator.nextInt(255); + return new byte[] {(byte) a,(byte) b,(byte) c,(byte) d}; + } + + + + + +} diff --git a/src/main/java/org/eclipse/paho/mqttv5/client/websocket/WebSocketHandshake.java b/src/main/java/org/eclipse/paho/mqttv5/client/websocket/WebSocketHandshake.java new file mode 100644 index 0000000..92126e2 --- /dev/null +++ b/src/main/java/org/eclipse/paho/mqttv5/client/websocket/WebSocketHandshake.java @@ -0,0 +1,230 @@ +/******************************************************************************* + * Copyright (c) 2009, 2014 IBM Corp. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * and Eclipse Distribution License v1.0 which accompany this distribution. + * + * The Eclipse Public License is available at + * https://www.eclipse.org/legal/epl-2.0 + * and the Eclipse Distribution License is available at + * https://www.eclipse.org/org/documents/edl-v10.php + * + * Contributors: + * James Sutton - Bug 459142 - WebSocket support for the Java client. + */ +package org.eclipse.paho.mqttv5.client.websocket; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.PrintWriter; +import java.net.URI; +import java.net.URISyntaxException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +/** + * Helper class to execute a WebSocket Handshake. + */ +public class WebSocketHandshake { + + // Do not change: https://tools.ietf.org/html/rfc6455#section-1.3 + private static final String ACCEPT_SALT = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; + private static final String SHA1_PROTOCOL = "SHA1"; + private static final String HTTP_HEADER_SEC_WEBSOCKET_ACCEPT = "sec-websocket-accept"; + private static final String HTTP_HEADER_UPGRADE = "upgrade"; + private static final String HTTP_HEADER_UPGRADE_WEBSOCKET = "websocket"; + private static final String EMPTY = ""; + private static final String LINE_SEPARATOR = "\r\n"; + private static final String HTTP_HEADER_CONNECTION = "connection"; + private static final String HTTP_HEADER_CONNECTION_VALUE = "upgrade"; + private static final String HTTP_HEADER_SEC_WEBSOCKET_PROTOCOL = "sec-websocket-protocol"; + + InputStream input; + OutputStream output; + String uri; + String host; + int port; + Map customWebSocketHeaders; + + public WebSocketHandshake(InputStream input, OutputStream output, String uri, String host, int port, Map customWebSocketHeaders) { + this.input = input; + this.output = output; + this.uri = uri; + this.host = host; + this.port = port; + this.customWebSocketHeaders = customWebSocketHeaders; + } + + /** + * Executes a Websocket Handshake. Will throw an IOException if the handshake + * fails + * + * @throws IOException + * thrown if an exception occurs during the handshake + */ + public void execute() throws IOException { + byte[] key = new byte[16]; + System.arraycopy(UUID.randomUUID().toString().getBytes(), 0, key, 0, 16); + String b64Key = Base64.encodeBytes(key); + sendHandshakeRequest(b64Key); + receiveHandshakeResponse(b64Key); + } + + /** + * Builds and sends the HTTP Header GET Request for the socket. + * + * @param key + * Base64 encoded key + * @throws IOException + */ + private void sendHandshakeRequest(String key) { + try { + String path = "/mqtt"; + URI srvUri = new URI(uri); + if (srvUri.getRawPath() != null && !srvUri.getRawPath().isEmpty()) { + path = srvUri.getRawPath(); + if (srvUri.getRawQuery() != null && !srvUri.getRawQuery().isEmpty()) { + path += "?" + srvUri.getRawQuery(); + } + } + + PrintWriter pw = new PrintWriter(output); + pw.print("GET " + path + " HTTP/1.1" + LINE_SEPARATOR); + if (port != 80 && port != 443) { + pw.print("Host: " + host + ":" + port + LINE_SEPARATOR); + } else { + pw.print("Host: " + host + LINE_SEPARATOR); + } + + pw.print("Upgrade: websocket" + LINE_SEPARATOR); + pw.print("Connection: Upgrade" + LINE_SEPARATOR); + pw.print("Sec-WebSocket-Key: " + key + LINE_SEPARATOR); + pw.print("Sec-WebSocket-Protocol: mqtt" + LINE_SEPARATOR); + pw.print("Sec-WebSocket-Version: 13" + LINE_SEPARATOR); + + if (customWebSocketHeaders != null) { + customWebSocketHeaders.entrySet().forEach(entry -> + pw.print(entry.getKey() + ": " + entry.getValue() + LINE_SEPARATOR) + ); + } + + String userInfo = srvUri.getUserInfo(); + if (userInfo != null) { + pw.print("Authorization: Basic " + Base64.encode(userInfo) + LINE_SEPARATOR); + } + + pw.print(LINE_SEPARATOR); + pw.flush(); + } catch (URISyntaxException e) { + throw new IllegalStateException(e.getMessage()); + } + } + + /** + * Receives the Handshake response and verifies that it is valid. + * + * @param key + * Base64 encoded key + * @throws IOException + */ + private void receiveHandshakeResponse(String key) throws IOException { + BufferedReader in = new BufferedReader(new InputStreamReader(input)); + ArrayList responseLines = new ArrayList<>(); + String line = in.readLine(); + if (line == null) { + throw new IOException( + "WebSocket Response header: Invalid response from Server, It may not support WebSockets."); + } + while (!line.equals(EMPTY)) { + responseLines.add(line); + line = in.readLine(); + } + Map headerMap = getHeaders(responseLines); + + String connectionHeader = headerMap.get(HTTP_HEADER_CONNECTION); + if (connectionHeader == null || connectionHeader.equalsIgnoreCase(HTTP_HEADER_CONNECTION_VALUE)) { + throw new IOException("WebSocket Response header: Incorrect connection header"); + } + + String upgradeHeader = headerMap.get(HTTP_HEADER_UPGRADE); + if (upgradeHeader == null || !upgradeHeader.toLowerCase().contains(HTTP_HEADER_UPGRADE_WEBSOCKET)) { + throw new IOException("WebSocket Response header: Incorrect upgrade."); + } + + String secWebsocketProtocolHeader = headerMap.get(HTTP_HEADER_SEC_WEBSOCKET_PROTOCOL); + if (secWebsocketProtocolHeader == null) { + throw new IOException("WebSocket Response header: empty sec-websocket-protocol"); + } + + if (!headerMap.containsKey(HTTP_HEADER_SEC_WEBSOCKET_ACCEPT)) { + throw new IOException("WebSocket Response header: Missing Sec-WebSocket-Accept"); + } + + try { + verifyWebSocketKey(key, (String) headerMap.get(HTTP_HEADER_SEC_WEBSOCKET_ACCEPT)); + } catch (NoSuchAlgorithmException e) { + throw new IOException(e.getMessage()); + } catch (HandshakeFailedException e) { + throw new IOException("WebSocket Response header: Incorrect Sec-WebSocket-Key"); + } + + } + + /** + * Returns a Hashmap of HTTP headers + * + * @param headers + * ArrayList of headers + * @return A Hashmap of the headers + */ + private Map getHeaders(ArrayList headers) { + Map headerMap = new HashMap<>(); + for (int i = 1; i < headers.size(); i++) { + String headerPre = headers.get(i); + String[] header = headerPre.split(":"); + headerMap.put(header[0].toLowerCase(), header[1]); + } + return headerMap; + } + + /** + * Verifies that the Accept key provided is correctly built from the original + * key sent. + * + * @param key + * @param accept + * @throws NoSuchAlgorithmException + * @throws HandshakeFailedException + */ + private void verifyWebSocketKey(String key, String accept) + throws NoSuchAlgorithmException, HandshakeFailedException { + // We build up the accept in the same way the server should + // then we check that the response is the same. + byte[] sha1Bytes = sha1(key + ACCEPT_SALT); + String encodedSha1Bytes = Base64.encodeBytes(sha1Bytes).trim(); + if (!encodedSha1Bytes.equals(accept.trim())) { + throw new HandshakeFailedException(); + } + } + + /** + * Returns the sha1 byte array of the provided string. + * + * @param input + * @return + * @throws NoSuchAlgorithmException + */ + private byte[] sha1(String input) throws NoSuchAlgorithmException { + MessageDigest mDigest = MessageDigest.getInstance(SHA1_PROTOCOL); + return mDigest.digest(input.getBytes()); + } + +} diff --git a/src/main/java/org/eclipse/paho/mqttv5/client/websocket/WebSocketNetworkModule.java b/src/main/java/org/eclipse/paho/mqttv5/client/websocket/WebSocketNetworkModule.java new file mode 100644 index 0000000..4b2c447 --- /dev/null +++ b/src/main/java/org/eclipse/paho/mqttv5/client/websocket/WebSocketNetworkModule.java @@ -0,0 +1,111 @@ +/******************************************************************************* + * Copyright (c) 2009, 2014 IBM Corp. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * and Eclipse Distribution License v1.0 which accompany this distribution. + * + * The Eclipse Public License is available at + * https://www.eclipse.org/legal/epl-2.0 + * and the Eclipse Distribution License is available at + * https://www.eclipse.org/org/documents/edl-v10.php + * + * Contributors: + * James Sutton - Bug 459142 - WebSocket support for the Java client. + */ +package org.eclipse.paho.mqttv5.client.websocket; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.PipedInputStream; +import java.nio.ByteBuffer; +import java.util.Collections; +import java.util.Map; +import javax.net.SocketFactory; + +import org.eclipse.paho.mqttv5.client.internal.TCPNetworkModule; +import org.eclipse.paho.mqttv5.client.logging.Logger; +import org.eclipse.paho.mqttv5.client.logging.LoggerFactory; +import org.eclipse.paho.mqttv5.common.MqttException; + +public class WebSocketNetworkModule extends TCPNetworkModule { + + private static final String CLASS_NAME = WebSocketNetworkModule.class.getName(); + private Logger log = LoggerFactory.getLogger(LoggerFactory.MQTT_CLIENT_MSG_CAT, CLASS_NAME); + + private String uri; + private String host; + private int port; + private PipedInputStream pipedInputStream; + private WebSocketReceiver webSocketReceiver; + ByteBuffer recievedPayload; + Map customWebSocketHeaders; + + /** + * Overrides the flush method. + * This allows us to encode the MQTT payload into a WebSocket + * Frame before passing it through to the real socket. + */ + private ByteArrayOutputStream outputStream = new ExtendedByteArrayOutputStream(this); + + public WebSocketNetworkModule(SocketFactory factory, String uri, String host, int port, String resourceContext){ + super(factory, host, port, resourceContext); + this.uri = uri; + this.host = host; + this.port = port; + this.pipedInputStream = new PipedInputStream(); + + log.setResourceName(resourceContext); + } + + public void start() throws IOException, MqttException { + super.start(); + WebSocketHandshake handshake = new WebSocketHandshake(getSocketInputStream(), getSocketOutputStream(), uri, host, port, customWebSocketHeaders); + handshake.execute(); + this.webSocketReceiver = new WebSocketReceiver(getSocketInputStream(), pipedInputStream); + webSocketReceiver.start("webSocketReceiver"); + } + + OutputStream getSocketOutputStream() throws IOException { + return super.getOutputStream(); + } + + InputStream getSocketInputStream() throws IOException { + return super.getInputStream(); + } + + public InputStream getInputStream() throws IOException { + return pipedInputStream; + } + + public OutputStream getOutputStream() throws IOException { + return outputStream; + } + + public void setCustomWebSocketHeaders(Map customWebSocketHeaders) { + this.customWebSocketHeaders = customWebSocketHeaders; + } + + /** + * Stops the module, by closing the TCP socket. + */ + public void stop() throws IOException { + // Creating Close Frame + WebSocketFrame frame = new WebSocketFrame((byte)0x08, true, "1000".getBytes()); + byte[] rawFrame = frame.encodeFrame(); + getSocketOutputStream().write(rawFrame); + getSocketOutputStream().flush(); + + if(webSocketReceiver != null){ + webSocketReceiver.stop(); + } + super.stop(); + } + + public String getServerURI() { + return "ws://" + host + ":" + port; + } + +} diff --git a/src/main/java/org/eclipse/paho/mqttv5/client/websocket/WebSocketNetworkModuleFactory.java b/src/main/java/org/eclipse/paho/mqttv5/client/websocket/WebSocketNetworkModuleFactory.java new file mode 100644 index 0000000..1203d8b --- /dev/null +++ b/src/main/java/org/eclipse/paho/mqttv5/client/websocket/WebSocketNetworkModuleFactory.java @@ -0,0 +1,61 @@ +/* + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * and Eclipse Distribution License v1.0 which accompany this distribution. + * + * The Eclipse Public License is available at + * https://www.eclipse.org/legal/epl-2.0 + * and the Eclipse Distribution License is available at + * https://www.eclipse.org/org/documents/edl-v10.php + */ +package org.eclipse.paho.mqttv5.client.websocket; + +import java.net.URI; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +import javax.net.SocketFactory; +import javax.net.ssl.SSLSocketFactory; +import org.eclipse.paho.mqttv5.client.MqttClientException; +import org.eclipse.paho.mqttv5.client.MqttConnectionOptions; +import org.eclipse.paho.mqttv5.client.internal.NetworkModule; +import org.eclipse.paho.mqttv5.client.spi.NetworkModuleFactory; +import org.eclipse.paho.mqttv5.common.ExceptionHelper; +import org.eclipse.paho.mqttv5.common.MqttException; + +public class WebSocketNetworkModuleFactory implements NetworkModuleFactory { + + @Override + public Set getSupportedUriSchemes() { + return Collections.unmodifiableSet(new HashSet<>(Arrays.asList("ws"))); + } + + @Override + public void validateURI(URI brokerUri) throws IllegalArgumentException { + // so specific requirements so far + } + + @Override + public NetworkModule createNetworkModule(URI brokerUri, MqttConnectionOptions options, String clientId) + throws MqttException + { + String host = brokerUri.getHost(); + int port = brokerUri.getPort(); // -1 if not defined + if (port == -1) { + port = 80; + } + SocketFactory factory = options.getSocketFactory(); + if (factory == null) { + factory = SocketFactory.getDefault(); + } else if (factory instanceof SSLSocketFactory) { + throw ExceptionHelper.createMqttException(MqttClientException.REASON_CODE_SOCKET_FACTORY_MISMATCH); + } + WebSocketNetworkModule netModule = new WebSocketNetworkModule(factory, brokerUri.toString(), host, port, + clientId); + netModule.setConnectTimeout(options.getConnectionTimeout()); + netModule.setCustomWebSocketHeaders(options.getCustomWebSocketHeaders()); + return netModule; + } +} diff --git a/src/main/java/org/eclipse/paho/mqttv5/client/websocket/WebSocketReceiver.java b/src/main/java/org/eclipse/paho/mqttv5/client/websocket/WebSocketReceiver.java new file mode 100644 index 0000000..94a96e8 --- /dev/null +++ b/src/main/java/org/eclipse/paho/mqttv5/client/websocket/WebSocketReceiver.java @@ -0,0 +1,149 @@ +/******************************************************************************* + * Copyright (c) 2009, 2014 IBM Corp. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * and Eclipse Distribution License v1.0 which accompany this distribution. + * + * The Eclipse Public License is available at + * https://www.eclipse.org/legal/epl-2.0 + * and the Eclipse Distribution License is available at + * https://www.eclipse.org/org/documents/edl-v10.php + * + * Contributors: + * James Sutton - Bug 459142 - WebSocket support for the Java client. + */ +package org.eclipse.paho.mqttv5.client.websocket; + +import java.io.IOException; +import java.io.InputStream; +import java.io.PipedInputStream; +import java.io.PipedOutputStream; +import java.net.SocketTimeoutException; + +import org.eclipse.paho.mqttv5.client.logging.Logger; +import org.eclipse.paho.mqttv5.client.logging.LoggerFactory; + +public class WebSocketReceiver implements Runnable{ + + private static final String CLASS_NAME = WebSocketReceiver.class.getName(); + private Logger log = LoggerFactory.getLogger(LoggerFactory.MQTT_CLIENT_MSG_CAT, CLASS_NAME); + + private boolean running = false; + private boolean stopping = false; + private final Object lifecycle = new Object(); + private InputStream input; + private Thread receiverThread = null; + private volatile boolean receiving; + private PipedOutputStream pipedOutputStream; + + public WebSocketReceiver(InputStream input, PipedInputStream pipedInputStream) throws IOException{ + this.input = input; + this.pipedOutputStream = new PipedOutputStream(); + pipedInputStream.connect(pipedOutputStream); + } + + /** + * Starts up the WebSocketReceiver's thread + * @param threadName The name of the thread + */ + public void start(String threadName){ + final String methodName = "start"; + //@TRACE 855=starting + log.fine(CLASS_NAME, methodName, "855"); + synchronized (lifecycle) { + if(!running) { + running = true; + receiverThread = new Thread(this, threadName); + receiverThread.start(); + } + } + } + + /** + * Stops this WebSocketReceiver's thread. + * This call will block. + */ + public void stop() { + final String methodName = "stop"; + stopping = true; + boolean closed = false; + synchronized (lifecycle) { + //@TRACE 850=stopping + log.fine(CLASS_NAME,methodName, "850"); + if(running) { + running = false; + receiving = false; + closed = true; + closeOutputStream(); + + } + } + if(closed && !Thread.currentThread().equals(receiverThread) && (receiverThread != null)) { + try { + // Wait for the thread to finish + //This must not happen in the synchronized block, otherwise we can deadlock ourselves! + receiverThread.join(); + } catch (InterruptedException ex) { + // Interrupted Exception + } + } + receiverThread = null; + //@TRACE 851=stopped + log.fine(CLASS_NAME, methodName, "851"); + } + + public void run() { + final String methodName = "run"; + + while (running && (input != null)) { + try { + //@TRACE 852=network read message + log.fine(CLASS_NAME, methodName, "852"); + receiving = input.available() > 0; + WebSocketFrame incomingFrame = new WebSocketFrame(input); + if(!incomingFrame.isCloseFlag()){ + for(int i = 0; i < incomingFrame.getPayload().length; i++){ + pipedOutputStream.write(incomingFrame.getPayload()[i]); + } + + pipedOutputStream.flush(); + } else { + if(!stopping){ + throw new IOException("Server sent a WebSocket Frame with the Stop OpCode"); + } + } + + receiving = false; + + } catch (SocketTimeoutException ex) { + // Ignore SocketTimeoutException + } catch (IOException ex) { + // Exception occurred whilst reading the stream. + this.stop(); + } + } + } + + private void closeOutputStream(){ + try { + pipedOutputStream.close(); + } catch (IOException e) { + } + } + + + public boolean isRunning() { + return running; + } + + /** + * Returns the receiving state. + * + * @return true if the receiver is receiving data, false otherwise. + */ + public boolean isReceiving(){ + return receiving; + } + +} diff --git a/src/main/java/org/eclipse/paho/mqttv5/client/websocket/WebSocketSecureNetworkModule.java b/src/main/java/org/eclipse/paho/mqttv5/client/websocket/WebSocketSecureNetworkModule.java new file mode 100644 index 0000000..877a59c --- /dev/null +++ b/src/main/java/org/eclipse/paho/mqttv5/client/websocket/WebSocketSecureNetworkModule.java @@ -0,0 +1,111 @@ +/******************************************************************************* + * Copyright (c) 2009, 2014 IBM Corp. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * and Eclipse Distribution License v1.0 which accompany this distribution. + * + * The Eclipse Public License is available at + * https://www.eclipse.org/legal/epl-2.0 + * and the Eclipse Distribution License is available at + * https://www.eclipse.org/org/documents/edl-v10.php + * + * Contributors: + * James Sutton - Bug 459142 - WebSocket support for the Java client. + */ +package org.eclipse.paho.mqttv5.client.websocket; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.PipedInputStream; +import java.nio.ByteBuffer; +import java.util.Map; + +import javax.net.ssl.SSLSocketFactory; + +import org.eclipse.paho.mqttv5.client.internal.SSLNetworkModule; +import org.eclipse.paho.mqttv5.client.logging.Logger; +import org.eclipse.paho.mqttv5.client.logging.LoggerFactory; +import org.eclipse.paho.mqttv5.common.MqttException; + +public class WebSocketSecureNetworkModule extends SSLNetworkModule{ + + private static final String CLASS_NAME = WebSocketSecureNetworkModule.class.getName(); + private Logger log = LoggerFactory.getLogger(LoggerFactory.MQTT_CLIENT_MSG_CAT, CLASS_NAME); + + private PipedInputStream pipedInputStream; + private WebSocketReceiver webSocketReceiver; + private String uri; + private String host; + private int port; + ByteBuffer recievedPayload; + Map customWebSocketHeaders; + + /** + * Overrides the flush method. + * This allows us to encode the MQTT payload into a WebSocket + * Frame before passing it through to the real socket. + */ + private ByteArrayOutputStream outputStream = new ExtendedByteArrayOutputStream(this); + + public WebSocketSecureNetworkModule(SSLSocketFactory factory, String uri, String host, int port, String clientId) { + super(factory, host, port, clientId); + this.uri = uri; + this.host = host; + this.port = port; + this.pipedInputStream = new PipedInputStream(); + log.setResourceName(clientId); + } + + public void start() throws IOException, MqttException { + super.start(); + WebSocketHandshake handshake = new WebSocketHandshake(super.getInputStream(), super.getOutputStream(), uri, host, port, customWebSocketHeaders); + handshake.execute(); + this.webSocketReceiver = new WebSocketReceiver(getSocketInputStream(), pipedInputStream); + webSocketReceiver.start("WssSocketReceiver"); + + } + + OutputStream getSocketOutputStream() throws IOException { + return super.getOutputStream(); + } + + InputStream getSocketInputStream() throws IOException { + return super.getInputStream(); + } + + public InputStream getInputStream() throws IOException { + return pipedInputStream; + } + + public OutputStream getOutputStream() throws IOException { + return outputStream; + } + + public void setCustomWebSocketHeaders(Map customWebSocketHeaders) { + this.customWebSocketHeaders = customWebSocketHeaders; + } + + public void stop() throws IOException { + // Creating Close Frame + WebSocketFrame frame = new WebSocketFrame((byte)0x08, true, "1000".getBytes()); + byte[] rawFrame = frame.encodeFrame(); + getSocketOutputStream().write(rawFrame); + getSocketOutputStream().flush(); + + if(webSocketReceiver != null){ + webSocketReceiver.stop(); + } + super.stop(); + } + + public String getServerURI() { + return "wss://" + host + ":" + port; + } + + + + +} diff --git a/src/main/java/org/eclipse/paho/mqttv5/client/websocket/WebSocketSecureNetworkModuleFactory.java b/src/main/java/org/eclipse/paho/mqttv5/client/websocket/WebSocketSecureNetworkModuleFactory.java new file mode 100644 index 0000000..85b052a --- /dev/null +++ b/src/main/java/org/eclipse/paho/mqttv5/client/websocket/WebSocketSecureNetworkModuleFactory.java @@ -0,0 +1,82 @@ +/* + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * and Eclipse Distribution License v1.0 which accompany this distribution. + * + * The Eclipse Public License is available at + * https://www.eclipse.org/legal/epl-2.0 + * and the Eclipse Distribution License is available at + * https://www.eclipse.org/org/documents/edl-v10.php + */ +package org.eclipse.paho.mqttv5.client.websocket; + +import java.net.URI; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Properties; +import java.util.Set; + +import javax.net.SocketFactory; +import javax.net.ssl.SSLSocketFactory; +import org.eclipse.paho.mqttv5.client.MqttClientException; +import org.eclipse.paho.mqttv5.client.MqttConnectionOptions; +import org.eclipse.paho.mqttv5.client.internal.NetworkModule; +import org.eclipse.paho.mqttv5.client.internal.SSLNetworkModule; +import org.eclipse.paho.mqttv5.client.security.SSLSocketFactoryFactory; +import org.eclipse.paho.mqttv5.client.spi.NetworkModuleFactory; +import org.eclipse.paho.mqttv5.common.ExceptionHelper; +import org.eclipse.paho.mqttv5.common.MqttException; + +public class WebSocketSecureNetworkModuleFactory implements NetworkModuleFactory { + + @Override + public Set getSupportedUriSchemes() { + return Collections.unmodifiableSet(new HashSet<>(Arrays.asList("wss"))); + } + + @Override + public void validateURI(URI brokerUri) throws IllegalArgumentException { + // so specific requirements so far + } + + @Override + public NetworkModule createNetworkModule(URI brokerUri, MqttConnectionOptions options, String clientId) + throws MqttException + { + String host = brokerUri.getHost(); + int port = brokerUri.getPort(); // -1 if not defined + if (port == -1) { + port = 443; + } + SocketFactory factory = options.getSocketFactory(); + SSLSocketFactoryFactory wSSFactoryFactory = null; + if (factory == null) { + wSSFactoryFactory = new SSLSocketFactoryFactory(); + Properties sslClientProps = options.getSSLProperties(); + if (null != sslClientProps) { + wSSFactoryFactory.initialize(sslClientProps, null); + } + factory = wSSFactoryFactory.createSocketFactory(null); + + } else if ((factory instanceof SSLSocketFactory) == false) { + throw ExceptionHelper.createMqttException(MqttClientException.REASON_CODE_SOCKET_FACTORY_MISMATCH); + } + + // Create the network module... + WebSocketSecureNetworkModule netModule = new WebSocketSecureNetworkModule((SSLSocketFactory) factory, + brokerUri.toString(), host, port, clientId); + netModule.setSSLhandshakeTimeout(options.getConnectionTimeout()); + netModule.setSSLHostnameVerifier(options.getSSLHostnameVerifier()); + netModule.setHttpsHostnameVerificationEnabled(options.isHttpsHostnameVerificationEnabled()); + netModule.setCustomWebSocketHeaders(options.getCustomWebSocketHeaders()); + // Ciphers suites need to be set, if they are available + if (wSSFactoryFactory != null) { + String[] enabledCiphers = wSSFactoryFactory.getEnabledCipherSuites(null); + if (enabledCiphers != null) { + ((SSLNetworkModule) netModule).setEnabledCiphers(enabledCiphers); + } + } + return netModule; + } +} diff --git a/src/main/java/org/eclipse/paho/mqttv5/client/wire/MqttInputStream.java b/src/main/java/org/eclipse/paho/mqttv5/client/wire/MqttInputStream.java new file mode 100644 index 0000000..28afe96 --- /dev/null +++ b/src/main/java/org/eclipse/paho/mqttv5/client/wire/MqttInputStream.java @@ -0,0 +1,166 @@ +/******************************************************************************* + * Copyright (c) 2009, 2014 IBM Corp. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * and Eclipse Distribution License v1.0 which accompany this distribution. + * + * The Eclipse Public License is available at + * https://www.eclipse.org/legal/epl-2.0 + * and the Eclipse Distribution License is available at + * https://www.eclipse.org/org/documents/edl-v10.php + * + * Contributors: + * Dave Locke - initial API and implementation and/or initial documentation + */ +package org.eclipse.paho.mqttv5.client.wire; + +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.EOFException; +import java.io.IOException; +import java.io.InputStream; +import java.net.SocketTimeoutException; + +import org.eclipse.paho.mqttv5.client.MqttClientException; +import org.eclipse.paho.mqttv5.client.internal.MqttState; +import org.eclipse.paho.mqttv5.client.logging.Logger; +import org.eclipse.paho.mqttv5.client.logging.LoggerFactory; +import org.eclipse.paho.mqttv5.common.ExceptionHelper; +import org.eclipse.paho.mqttv5.common.MqttException; +import org.eclipse.paho.mqttv5.common.packet.MqttDataTypes; +import org.eclipse.paho.mqttv5.common.packet.MqttWireMessage; + + +/** + * An MqttInputStream lets applications read instances of + * MqttWireMessage. + */ +public class MqttInputStream extends InputStream { + private static final String CLASS_NAME = MqttInputStream.class.getName(); + private Logger log = LoggerFactory.getLogger(LoggerFactory.MQTT_CLIENT_MSG_CAT, CLASS_NAME); + + private MqttState clientState = null; + private DataInputStream in; + private ByteArrayOutputStream bais; + private int remLen; + private int packetLen; + private byte[] packet; + + public MqttInputStream(MqttState clientState, InputStream in, String clientId) { + this.clientState = clientState; + this.in = new DataInputStream(in); + this.bais = new ByteArrayOutputStream(); + this.remLen = -1; + log.setResourceName(clientId); + } + + public int read() throws IOException { + return in.read(); + } + + public int available() throws IOException { + return in.available(); + } + + public void close() throws IOException { + in.close(); + } + + /** + * Reads an MqttWireMessage from the stream. + * If the message cannot be fully read within the socket read timeout, + * a null message is returned and the method can be called again until + * the message is fully read. + * @return The {@link MqttWireMessage} + * @throws IOException if an exception is thrown when reading from the stream + * @throws MqttException if the message is invalid + */ + public MqttWireMessage readMqttWireMessage() throws IOException, MqttException { + final String methodName ="readMqttWireMessage"; + + MqttWireMessage message = null; + try { + // read header + if (remLen < 0) { + // Assume we can read the whole header at once. + // The header is very small so it's likely we + // are able to read it fully or not at all. + // This keeps the parser lean since we don't + // need to cope with a partial header. + // Should we lose synch with the stream, + // the keepalive mechanism would kick in + // closing the connection. + bais.reset(); + + byte first = in.readByte(); + clientState.notifyReceivedBytes(1); + + byte type = (byte) ((first >>> 4) & 0x0F); + if ((type < MqttWireMessage.MESSAGE_TYPE_CONNECT) || + (type > MqttWireMessage.MESSAGE_TYPE_AUTH)) { + // Invalid MQTT message type... + throw ExceptionHelper.createMqttException(MqttClientException.REASON_CODE_INVALID_MESSAGE); + } + + byte reserved = (byte) (first & 0x0F); + MqttWireMessage.validateReservedBits(type, reserved); + + remLen = MqttDataTypes.readVariableByteInteger(in).getValue(); + bais.write(first); + bais.write(MqttWireMessage.encodeVariableByteInteger((int)remLen)); + packet = new byte[(int)(bais.size()+remLen)]; + if(this.clientState.getIncomingMaximumPacketSize() != null && + bais.size()+remLen > this.clientState.getIncomingMaximumPacketSize() ) { + // Incoming packet is too large + throw ExceptionHelper.createMqttException(MqttClientException.REASON_CODE_INCOMING_PACKET_TOO_LARGE); + } + packetLen = 0; + } + + // read remaining packet + if (remLen >= 0) { + // the remaining packet can be read with timeouts + readFully(); + + // reset packet parsing state + remLen = -1; + + byte[] header = bais.toByteArray(); + System.arraycopy(header,0,packet,0, header.length); + message = MqttWireMessage.createWireMessage(packet); + // @TRACE 530= Received {0} + log.fine(CLASS_NAME, methodName, "530",new Object[] {message}); + } + } catch (SocketTimeoutException e) { + // ignore socket read timeout + } + + return message; + } + + private void readFully() throws IOException { + int off = bais.size() + (int) packetLen; + int len = (int) (remLen - packetLen); + if (len < 0) + throw new IndexOutOfBoundsException(); + int n = 0; + while (n < len) { + int count = -1; + try { + count = in.read(packet, off + n, len - n); + } catch (SocketTimeoutException e) { + // remember the packet read so far + packetLen += n; + throw e; + } + + + if (count < 0) { + throw new EOFException(); + } + clientState.notifyReceivedBytes(count); + n += count; + } + } +} diff --git a/src/main/java/org/eclipse/paho/mqttv5/client/wire/MqttOutputStream.java b/src/main/java/org/eclipse/paho/mqttv5/client/wire/MqttOutputStream.java new file mode 100644 index 0000000..8c2d1b2 --- /dev/null +++ b/src/main/java/org/eclipse/paho/mqttv5/client/wire/MqttOutputStream.java @@ -0,0 +1,101 @@ +/******************************************************************************* + * Copyright (c) 2009, 2014 IBM Corp. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * and Eclipse Distribution License v1.0 which accompany this distribution. + * + * The Eclipse Public License is available at + * https://www.eclipse.org/legal/epl-2.0 + * and the Eclipse Distribution License is available at + * https://www.eclipse.org/org/documents/edl-v10.php + * + * Contributors: + * Dave Locke - initial API and implementation and/or initial documentation + */ +package org.eclipse.paho.mqttv5.client.wire; + +import java.io.BufferedOutputStream; +import java.io.IOException; +import java.io.OutputStream; + +import org.eclipse.paho.mqttv5.client.MqttClientException; +import org.eclipse.paho.mqttv5.client.internal.MqttState; +import org.eclipse.paho.mqttv5.client.logging.Logger; +import org.eclipse.paho.mqttv5.client.logging.LoggerFactory; +import org.eclipse.paho.mqttv5.common.ExceptionHelper; +import org.eclipse.paho.mqttv5.common.MqttException; +import org.eclipse.paho.mqttv5.common.packet.MqttWireMessage; + + +/** + * An MqttOutputStream lets applications write instances of + * MqttWireMessage. + */ +public class MqttOutputStream extends OutputStream { + private static final String CLASS_NAME = MqttOutputStream.class.getName(); + private Logger log = LoggerFactory.getLogger(LoggerFactory.MQTT_CLIENT_MSG_CAT, CLASS_NAME); + + private MqttState clientState = null; + private BufferedOutputStream out; + + public MqttOutputStream(MqttState clientState, OutputStream out, String clientId) { + this.clientState = clientState; + this.out = new BufferedOutputStream(out); + log.setResourceName(clientId); + } + + public void close() throws IOException { + out.close(); + } + + public void flush() throws IOException { + out.flush(); + } + + public void write(byte[] b) throws IOException { + out.write(b); + clientState.notifySentBytes(b.length); + } + + public void write(byte[] b, int off, int len) throws IOException { + out.write(b, off, len); + clientState.notifySentBytes(len); + } + + public void write(int b) throws IOException { + out.write(b); + } + + /** + * Writes an MqttWireMessage to the stream. + * @param message The {@link MqttWireMessage} to send + * @throws IOException if an exception is thrown when writing to the output stream. + * @throws MqttException if an exception is thrown when getting the header or payload + */ + public void write(MqttWireMessage message) throws IOException, MqttException { + final String methodName = "write"; + byte[] bytes = message.getHeader(); + byte[] pl = message.getPayload(); + if(this.clientState.getOutgoingMaximumPacketSize() != null && + bytes.length+pl.length > this.clientState.getOutgoingMaximumPacketSize() ) { + // Outgoing packet is too large + throw ExceptionHelper.createMqttException(MqttClientException.REASON_CODE_OUTGOING_PACKET_TOO_LARGE); + } + out.write(bytes,0,bytes.length); + clientState.notifySentBytes(bytes.length); + + int offset = 0; + int chunckSize = 1024; + while (offset < pl.length) { + int length = Math.min(chunckSize, pl.length - offset); + out.write(pl, offset, length); + offset += chunckSize; + clientState.notifySentBytes(length); + } + + // @TRACE 529= sent {0} + log.fine(CLASS_NAME, methodName, "529", new Object[]{message}); + } +} + diff --git a/src/main/java/org/eclipse/paho/mqttv5/common/ExceptionHelper.java b/src/main/java/org/eclipse/paho/mqttv5/common/ExceptionHelper.java new file mode 100644 index 0000000..a0039ba --- /dev/null +++ b/src/main/java/org/eclipse/paho/mqttv5/common/ExceptionHelper.java @@ -0,0 +1,54 @@ +/******************************************************************************* + * Copyright (c) 2016 IBM Corp. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * and Eclipse Distribution License v1.0 which accompany this distribution. + * + * The Eclipse Public License is available at + * https://www.eclipse.org/legal/epl-2.0 + * and the Eclipse Distribution License is available at + * https://www.eclipse.org/org/documents/edl-v10.php + * + * Contributors: + * Dave Locke - Original MQTTv3 implementation + * James Sutton - Initial MQTTv5 implementation + */ +package org.eclipse.paho.mqttv5.common; + +/** + * Utility class to help create exceptions of the correct type. + */ +public class ExceptionHelper { + public static MqttException createMqttException(int reasonCode) { + + return new MqttException(reasonCode); + } + + public static MqttException createMqttException(Throwable cause) { + + return new MqttException(cause); + } + + /** + * Returns whether or not the specified class is available to the current + * class loader. This is used to protect the code against using Java SE + * APIs on Java ME. + * @param className The ClassName + * @return if the class is available + */ + public static boolean isClassAvailable(String className) { + boolean result = false; + try { + Class.forName(className); + result = true; + } + catch (ClassNotFoundException ex) { + } + return result; + } + + // Utility classes should not have a public or default constructor. + private ExceptionHelper() { + } +} diff --git a/src/main/java/org/eclipse/paho/mqttv5/common/MqttException.java b/src/main/java/org/eclipse/paho/mqttv5/common/MqttException.java new file mode 100644 index 0000000..460e072 --- /dev/null +++ b/src/main/java/org/eclipse/paho/mqttv5/common/MqttException.java @@ -0,0 +1,158 @@ +package org.eclipse.paho.mqttv5.common; + +import java.util.MissingResourceException; +import java.util.ResourceBundle; + +import org.eclipse.paho.mqttv5.common.packet.MqttDisconnect; + +public class MqttException extends Exception { + + private static final long serialVersionUID = 1L; + + /** + * Client encountered an exception. Use the {@link #getCause()} method to get + * the underlying reason. + */ + public static final short REASON_CODE_CLIENT_EXCEPTION = 0x00; + + // New MQTTv5 Packet Errors + public static final int REASON_CODE_INVALID_IDENTIFIER = 50000; // Invalid Identifier in the IV fields + public static final int REASON_CODE_INVALID_RETURN_CODE = 50001; // Invalid Return code + public static final int REASON_CODE_MALFORMED_PACKET = 50002; // Packet was somehow malformed and did not comply to the MQTTv5 specification + public static final int REASON_CODE_UNSUPPORTED_PROTOCOL_VERSION = 50003; // The CONNECT packet did not contain the correct protocol name or version + + /** + * The Server sent a publish message with an invalid topic alias. + */ + public static final int REASON_CODE_INVALID_TOPIC_ALAS = 50004; + + /** + * The client attempted to decode a property that had already been decoded, and can only be included once. + */ + public static final int REASON_CODE_DUPLICATE_PROPERTY = 50005; + + private int reasonCode; + private Throwable cause; + private String disconnectReasonString; + private int disconnectReasonCode = 0; + + /** + * Constructs a new MqttException with the specified code as the + * underlying reason. + * + * @param reasonCode + * the reason code for the exception. + */ + public MqttException(int reasonCode) { + super(); + this.reasonCode = reasonCode; + } + + /** + * Constructs a new MqttException with the specified code as the + * underlying reason, with the disconnect reason if available. This is only + * meant as a hint for the developer, as the + * MqttCallback.disconnected callback is the intended disconnect + * notification mechanism. + * + * @param reasonCode the reason code for the exception. + * @param disconnect diconnect flag + */ + public MqttException(int reasonCode, MqttDisconnect disconnect) { + super(); + this.reasonCode = reasonCode; + if (disconnect != null) { + this.disconnectReasonCode = disconnect.getReturnCode(); + if (disconnect.getProperties() != null) { + this.disconnectReasonString = disconnect.getProperties().getReasonString(); + } + } + + } + + /** + * Constructs a new MqttException with the specified + * Throwable as the underlying reason. + * + * @param cause + * the underlying cause of the exception. + */ + public MqttException(Throwable cause) { + super(); + this.reasonCode = REASON_CODE_CLIENT_EXCEPTION; + this.cause = cause; + } + + /** + * Constructs a new MqttException with the specified + * Throwable as the underlying reason. + * + * @param reason + * the reason code for the exception. + * @param cause + * the underlying cause of the exception. + */ + public MqttException(int reason, Throwable cause) { + super(); + this.reasonCode = reason; + this.cause = cause; + } + + /** + * Returns the reason code for this exception. + * + * @return the code representing the reason for this exception. + */ + public int getReasonCode() { + return reasonCode; + } + + /** + * Returns the underlying cause of this exception, if available. + * + * @return the Throwable that was the root cause of this exception, which may be + * null. + */ + @Override + public Throwable getCause() { + return cause; + } + + /** + * Returns the detail message for this exception. + * + * @return the detail message, which may be null. + */ + @Override + public String getMessage() { + ResourceBundle bundle = ResourceBundle.getBundle("org.eclipse.paho.mqttv5.common.nls.messages"); + String message; + try { + message = bundle.getString(Integer.toString(reasonCode)); + } catch (MissingResourceException mre) { + message = "Untranslated MqttException - RC: " + reasonCode; + } + if(this.disconnectReasonCode != 0) { + message += " Disconnect RC: " + disconnectReasonCode; + } + if(this.disconnectReasonString != null) { + message += " Disconnect Reason: " + disconnectReasonString; + } + return message; + } + + /** + * Returns a String representation of this exception. + * + * @return a String representation of this exception. + */ + @Override + public String toString() { + String result = getMessage() + " (" + reasonCode + ")"; + if (cause != null) { + result = result + " - " + cause.toString(); + } + return result; + } + +} diff --git a/src/main/java/org/eclipse/paho/mqttv5/common/MqttMessage.java b/src/main/java/org/eclipse/paho/mqttv5/common/MqttMessage.java new file mode 100644 index 0000000..e31f8f1 --- /dev/null +++ b/src/main/java/org/eclipse/paho/mqttv5/common/MqttMessage.java @@ -0,0 +1,298 @@ +/******************************************************************************* + * Copyright (c) 2016 IBM Corp. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * and Eclipse Distribution License v1.0 which accompany this distribution. + * + * The Eclipse Public License is available at + * https://www.eclipse.org/legal/epl-2.0 + * and the Eclipse Distribution License is available at + * https://www.eclipse.org/org/documents/edl-v10.php + * + * Contributors: + * Dave Locke - Original MQTTv3 implementation + * James Sutton - Initial MQTTv5 implementation + */ +package org.eclipse.paho.mqttv5.common; + +import org.eclipse.paho.mqttv5.common.packet.MqttProperties; + +/** + * An MQTT message holds the application payload and options specifying how the + * message is to be delivered The message includes a "payload" (the body of the + * message) represented as a byte[]. + */ +public class MqttMessage { + + private boolean mutable = true; + private byte[] payload; + private int qos = 1; + private boolean retained = false; + private boolean dup = false; + private int messageId; + private MqttProperties properties; + + /** + * Utility method to validate the supplied QoS value. + * + * @param qos + * The Quality Of Service Level to validate + * @throws IllegalArgumentException + * if value of QoS is not 0, 1 or 2. + * + */ + public static void validateQos(int qos) { + if ((qos < 0) || (qos > 2)) { + throw new IllegalArgumentException(); + } + } + + /** + * Constructs a message with an empty payload, and all other values set to + * defaults. + * + * The defaults are: + *
    + *
  • Message QoS set to 1
  • + *
  • Message will not be "retained" by the server
  • + *
+ */ + public MqttMessage() { + setPayload(new byte[] {}); + } + + /** + * Constructs a message with the specified byte array as a payload, and all + * other values set to defaults. + * + * @param payload + * the payload + */ + public MqttMessage(byte[] payload) { + setPayload(payload); + } + + /** + * Contructs an message with the specified payload, qos and retained flag. + * + * @param payload + * The Message Payload. + * @param qos + * The Message QoS. + * @param retained + * If the message is retained. + * @param properties + * The Message {@link MqttProperties} + */ + public MqttMessage(byte[] payload, int qos, boolean retained, MqttProperties properties) { + setPayload(payload); + setQos(qos); + setRetained(retained); + setProperties(properties); + } + + /** + * Returns the payload as a byte array. + * + * @return the payload as a byte array. + */ + public byte[] getPayload() { + return payload; + } + + /** + * Clears the payload, resetting it to be empty. + * + * @throws IllegalStateException + * if this message cannot be edited + */ + public void clearPayload() { + checkMutable(); + this.payload = new byte[] {}; + } + + /** + * Sets the payload of this message to be the specified byte array. + * + * @param payload + * the payload for this message. + * @throws IllegalStateException + * if this message cannot be edited + * @throws NullPointerException + * if no payload is provided + */ + public void setPayload(byte[] payload) { + checkMutable(); + if (payload == null) { + throw new NullPointerException(); + } + this.payload = payload; + } + + /** + * Returns whether or not this message should be/was retained by the server. For + * messages received from the server, this method returns whether or not the + * message was from a current publisher, or was "retained" by the server as the + * last message published on the topic. + * + * @return true if the message should be, or was, retained by the + * server. + * @see #setRetained(boolean) + */ + public boolean isRetained() { + return retained; + } + + /** + * Whether or not the publish message should be retained by the messaging + * engine. Sending a message with retained set to true and with an + * empty byte array as the payload e.g. new byte[0] will clear the + * retained message from the server. The default value is false + * + * @param retained + * whether or not the messaging engine should retain the message. + * @throws IllegalStateException + * if this message cannot be edited + */ + public void setRetained(boolean retained) { + checkMutable(); + this.retained = retained; + } + + /** + * Returns the quality of service for this message. + * + * @return the quality of service to use, either 0, 1, or 2. + * @see #setQos(int) + */ + public int getQos() { + return qos; + } + + /** + * Sets the quality of service for this message. + *
    + *
  • Quality of Service 0 - indicates that a message should be delivered at + * most once (zero or one times). The message will not be persisted to disk, and + * will not be acknowledged across the network. This QoS is the fastest, but + * should only be used for messages which are not valuable - note that if the + * server cannot process the message (for example, there is an authorization + * problem). Also known as "fire and forget".
  • + * + *
  • Quality of Service 1 - indicates that a message should be delivered at + * least once (one or more times). The message can only be delivered safely if + * it can be persisted, so the application must supply a means of persistence + * using MqttConnectOptions. If a persistence mechanism is not + * specified, the message will not be delivered in the event of a client + * failure. The message will be acknowledged across the network. This is the + * default QoS.
  • + * + *
  • Quality of Service 2 - indicates that a message should be delivered once. + * The message will be persisted to disk, and will be subject to a two-phase + * acknowledgement across the network. The message can only be delivered safely + * if it can be persisted, so the application must supply a means of persistence + * using MqttConnectOptions. If a persistence mechanism is not + * specified, the message will not be delivered in the event of a client + * failure.
  • + *
+ * + * If persistence is not configured, QoS 1 and 2 messages will still be + * delivered in the event of a network or server problem as the client will hold + * state in memory. If the MQTT client is shutdown or fails and persistence is + * not configured then delivery of QoS 1 and 2 messages can not be maintained as + * client-side state will be lost. + * + * @param qos + * the "quality of service" to use. Set to 0, 1, 2. + * @throws IllegalArgumentException + * if value of QoS is not 0, 1 or 2. + * @throws IllegalStateException + * if this message cannot be edited + */ + public void setQos(int qos) { + checkMutable(); + validateQos(qos); + this.qos = qos; + } + + /** + * Sets the mutability of this object (whether or not its values can be changed. + * + * @param mutable + * true if the values can be changed, false + * to prevent them from being changed. + */ + public void setMutable(boolean mutable) { + this.mutable = mutable; + } + + protected void checkMutable() throws IllegalStateException { + if (!mutable) { + throw new IllegalStateException(); + } + } + + public void setDuplicate(boolean dup) { + this.dup = dup; + } + + /** + * Returns whether or not this message might be a duplicate of one which has + * already been received. This will only be set on messages received from the + * server. + * + * @return true if the message might be a duplicate. + */ + public boolean isDuplicate() { + return this.dup; + } + + /** + * This is only to be used internally to provide the MQTT id of a message + * received from the server. Has no effect when publishing messages. + * + * @param messageId + * the Message Identifier + */ + public void setId(int messageId) { + this.messageId = messageId; + } + + /** + * Returns the MQTT id of the message. This is only applicable to messages + * received from the server. + * + * @return the MQTT id of the message + */ + public int getId() { + return this.messageId; + } + + + + public MqttProperties getProperties() { + return properties; + } + + public void setProperties(MqttProperties properties) { + this.properties = properties; + } + + /** + * Returns a string representation of this message's payload. Makes an attempt + * to return the payload as a string. As the MQTT client has no control over the + * content of the payload it may fail. + * + * @return a string representation of this message. + */ + public String toString() { + return new String(payload); + } + + public String toDebugString() { + return "MqttMessage [mutable=" + mutable + ", payload=" + new String(payload) + ", qos=" + qos + ", retained=" + + retained + ", dup=" + dup + ", messageId=" + messageId + "]"; + } + +} diff --git a/src/main/java/org/eclipse/paho/mqttv5/common/MqttPersistable.java b/src/main/java/org/eclipse/paho/mqttv5/common/MqttPersistable.java new file mode 100644 index 0000000..81ac522 --- /dev/null +++ b/src/main/java/org/eclipse/paho/mqttv5/common/MqttPersistable.java @@ -0,0 +1,111 @@ +/******************************************************************************* + * Copyright (c) 2009, 2014 IBM Corp. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * and Eclipse Distribution License v1.0 which accompany this distribution. + * + * The Eclipse Public License is available at + * https://www.eclipse.org/legal/epl-2.0 + * and the Eclipse Distribution License is available at + * https://www.eclipse.org/org/documents/edl-v10.php + * + * Contributors: + * Dave Locke - initial API and implementation and/or initial documentation + */ +package org.eclipse.paho.mqttv5.common; + +/** + * Represents an object used to pass data to be persisted across the + * MqttClientPersistence interface. + *

+ * When data is passed across the interface the header and payload are + * separated, so that unnecessary message copies may be avoided. For example, if + * a 10 MB payload was published it would be inefficient to create a byte array + * a few bytes larger than 10 MB and copy the MQTT message header and payload + * into a contiguous byte array. + *

+ *

+ * When the request to persist data is made a separate byte array and offset is + * passed for the header and payload. Only the data between offset and length + * need be persisted. So for example, a message to be persisted consists of a + * header byte array starting at offset 1 and length 4, plus a payload byte + * array starting at offset 30 and length 40000. There are three ways in which + * the persistence implementation may return data to the client on recovery: + *

+ *
    + *
  • It could return the data as it was passed in originally, with the same + * byte arrays and offsets.
  • + *
  • It could safely just persist and return the bytes from the offset for the + * specified length. For example, return a header byte array with offset 0 and + * length 4, plus a payload byte array with offset 0 and length 40000
  • + *
  • It could return the header and payload as a contiguous byte array with + * the header bytes preceeding the payload. The contiguous byte array should be + * set as the header byte array, with the payload byte array being null. For + * example, return a single byte array with offset 0 and length 40004. This is + * useful when recovering from a file where the header and payload could be + * written as a contiguous stream of bytes.
  • + *
+ */ +public interface MqttPersistable { + + /** + * Returns the header bytes in an array. The bytes start at + * {@link #getHeaderOffset()} and continue for {@link #getHeaderLength()}. + * + * @return the header bytes. + * @throws MqttPersistenceException + * if an error occurs getting the Header Bytes + */ + byte[] getHeaderBytes() throws MqttPersistenceException; + + /** + * Returns the length of the header. + * + * @return the header length + * @throws MqttPersistenceException + * if an error occurs getting the Header length + */ + int getHeaderLength() throws MqttPersistenceException; + + /** + * Returns the offset of the header within the byte array returned by + * {@link #getHeaderBytes()}. + * + * @return the header offset. + * @throws MqttPersistenceException + * if an error occurs getting the Header offset + * + */ + int getHeaderOffset() throws MqttPersistenceException; + + /** + * Returns the payload bytes in an array. The bytes start at + * {@link #getPayloadOffset()} and continue for {@link #getPayloadLength()}. + * + * @return the payload bytes. + * @throws MqttPersistenceException + * if an error occurs getting the Payload Bytes + */ + byte[] getPayloadBytes() throws MqttPersistenceException; + + /** + * Returns the length of the payload. + * + * @return the payload length. + * @throws MqttPersistenceException + * if an error occurs getting the Payload length + */ + int getPayloadLength() throws MqttPersistenceException; + + /** + * Returns the offset of the payload within the byte array returned by + * {@link #getPayloadBytes()}. + * + * @return the payload offset. + * @throws MqttPersistenceException + * if an error occurs getting the Payload Offset + * + */ + int getPayloadOffset() throws MqttPersistenceException; +} diff --git a/src/main/java/org/eclipse/paho/mqttv5/common/MqttPersistenceException.java b/src/main/java/org/eclipse/paho/mqttv5/common/MqttPersistenceException.java new file mode 100644 index 0000000..bb65c34 --- /dev/null +++ b/src/main/java/org/eclipse/paho/mqttv5/common/MqttPersistenceException.java @@ -0,0 +1,45 @@ +package org.eclipse.paho.mqttv5.common; + +/** + * This exception is thrown by the implementor of the persistence + * interface if there is a problem reading or writing persistent data. + */ +public class MqttPersistenceException extends MqttException { + private static final long serialVersionUID = 300L; + + /** Persistence is already being used by another client. */ + public static final short REASON_CODE_PERSISTENCE_IN_USE = 32200; + + /** + * Constructs a new MqttPersistenceException + */ + public MqttPersistenceException() { + super(REASON_CODE_CLIENT_EXCEPTION); + } + + /** + * Constructs a new MqttPersistenceException with the specified code + * as the underlying reason. + * @param reasonCode the reason code for the exception. + */ + public MqttPersistenceException(int reasonCode) { + super(reasonCode); + } + /** + * Constructs a new MqttPersistenceException with the specified + * Throwable as the underlying reason. + * @param cause the underlying cause of the exception. + */ + public MqttPersistenceException(Throwable cause) { + super(cause); + } + /** + * Constructs a new MqttPersistenceException with the specified + * Throwable as the underlying reason. + * @param reason the reason code for the exception. + * @param cause the underlying cause of the exception. + */ + public MqttPersistenceException(int reason, Throwable cause) { + super(reason, cause); + } +} \ No newline at end of file diff --git a/src/main/java/org/eclipse/paho/mqttv5/common/MqttSecurityException.java b/src/main/java/org/eclipse/paho/mqttv5/common/MqttSecurityException.java new file mode 100644 index 0000000..9f73c37 --- /dev/null +++ b/src/main/java/org/eclipse/paho/mqttv5/common/MqttSecurityException.java @@ -0,0 +1,52 @@ +/******************************************************************************* + * Copyright (c) 2016 IBM Corp. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * and Eclipse Distribution License v1.0 which accompany this distribution. + * + * The Eclipse Public License is available at + * https://www.eclipse.org/legal/epl-2.0 + * and the Eclipse Distribution License is available at + * https://www.eclipse.org/org/documents/edl-v10.php + * + * Contributors: + * Dave Locke - Original MQTTv3 implementation + * James Sutton - Initial MQTTv5 implementation + */ +package org.eclipse.paho.mqttv5.common; + +/** + * Thrown when a client is not authorized to perform an operation, or + * if there is a problem with the security configuration. + */ +public class MqttSecurityException extends MqttException { + private static final long serialVersionUID = 300L; + + /** + * Constructs a new MqttSecurityException with the specified code + * as the underlying reason. + * @param reasonCode the reason code for the exception. + */ + public MqttSecurityException(int reasonCode) { + super(reasonCode); + } + + /** + * Constructs a new MqttSecurityException with the specified + * Throwable as the underlying reason. + * @param cause the underlying cause of the exception. + */ + public MqttSecurityException(Throwable cause) { + super(cause); + } + /** + * Constructs a new MqttSecurityException with the specified + * code and Throwable as the underlying reason. + * @param reasonCode the reason code for the exception. + * @param cause the underlying cause of the exception. + */ + public MqttSecurityException(int reasonCode, Throwable cause) { + super(reasonCode, cause); + } +} diff --git a/src/main/java/org/eclipse/paho/mqttv5/common/MqttSubscription.java b/src/main/java/org/eclipse/paho/mqttv5/common/MqttSubscription.java new file mode 100644 index 0000000..8ba7b2f --- /dev/null +++ b/src/main/java/org/eclipse/paho/mqttv5/common/MqttSubscription.java @@ -0,0 +1,166 @@ +/******************************************************************************* + * Copyright (c) 2016 IBM Corp. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * and Eclipse Distribution License v1.0 which accompany this distribution. + * + * The Eclipse Public License is available at + * https://www.eclipse.org/legal/epl-2.0 + * and the Eclipse Distribution License is available at + * https://www.eclipse.org/org/documents/edl-v10.php + * + * Contributors: + * Dave Locke - Original MQTTv3 implementation + * James Sutton - Initial MQTTv5 implementation + */ +package org.eclipse.paho.mqttv5.common; + +public class MqttSubscription { + + private boolean mutable = true; + private String topic; + private int qos = 1; + private boolean noLocal = false; + private boolean retainAsPublished = false; + private int retainHandling = 0; + private int messageId; + + + + /** + * Constructs a subscription with the specified topic with + * all other values set to defaults. + * + * The defaults are: + *
    + *
  • Subscription QoS is set to 1
  • + *
  • Messages published to this topic by the same client will also be received.
  • + *
  • Messages received by this subscription will keep the retain flag (if it is set).
  • + *
  • Retained messages on this topic will be delivered once the subscription has been made.
  • + *
+ * @param topic The Topic + */ + public MqttSubscription(String topic){ + setTopic(topic); + } + + public MqttSubscription(String topic, int qos) { + setTopic(topic); + setQos(qos); + } + + /** + * Utility method to validate the supplied QoS value. + * @throws IllegalArgumentException if value of QoS is not 0, 1 or 2. + * @param qos The QoS level to validate. + */ + public static void validateQos(int qos) { + if ((qos < 0) || (qos > 2)) { + throw new IllegalArgumentException(); + } + } + + /** + * Utility method to validate the supplied Retain handling value. + * @throws IllegalArgumentException if value of QoS is not 0, 1 or 2. + * @param retainHandling the retain value to validate. + */ + public static void validateRetainHandling(int retainHandling) { + if ((retainHandling < 0) || (retainHandling > 2)) { + throw new IllegalArgumentException(); + } + } + + /** + * Sets the mutability of this object (whether or not its values can be + * changed. + * @param mutable true if the values can be changed, + * false to prevent them from being changed. + */ + protected void setMutable(boolean mutable) { + this.mutable = mutable; + } + + protected void checkMutable() throws IllegalStateException { + if (!mutable) { + throw new IllegalStateException(); + } + } + + public String getTopic() { + return topic; + } + + public void setTopic(String topic) { + checkMutable(); + if(topic == null){ + throw new NullPointerException(); + } + this.topic = topic; + } + + public int getQos() { + return qos; + } + + public void setQos(int qos) { + checkMutable(); + validateQos(qos); + this.qos = qos; + } + + public boolean isNoLocal() { + return noLocal; + } + + public void setNoLocal(boolean noLocal) { + checkMutable(); + this.noLocal = noLocal; + } + + public boolean isRetainAsPublished() { + return retainAsPublished; + } + + public void setRetainAsPublished(boolean retainAsPublished) { + checkMutable(); + this.retainAsPublished = retainAsPublished; + } + + public int getRetainHandling() { + return retainHandling; + } + + public void setRetainHandling(int retainHandling) { + checkMutable(); + validateRetainHandling(retainHandling); + this.retainHandling = retainHandling; + } + + @Override + public String toString() { + return "MqttSubscription [mutable=" + mutable + ", topic=" + topic + ", qos=" + qos + ", noLocal=" + noLocal + + ", retainAsPublished=" + retainAsPublished + ", retainHandling=" + retainHandling + "]"; + } + + /** + * This is only to be used internally to provide the MQTT id of a message + * received from the server. Has no effect when publishing messages. + * @param messageId The Message Identifier + */ + public void setId(int messageId) { + this.messageId = messageId; + } + + /** + * Returns the MQTT id of the message. This is only applicable to messages + * received from the server. + * @return the MQTT id of the message + */ + public int getId() { + return this.messageId; + } + + +} diff --git a/src/main/java/org/eclipse/paho/mqttv5/common/Validators.java b/src/main/java/org/eclipse/paho/mqttv5/common/Validators.java new file mode 100644 index 0000000..0e2854d --- /dev/null +++ b/src/main/java/org/eclipse/paho/mqttv5/common/Validators.java @@ -0,0 +1,28 @@ +package org.eclipse.paho.mqttv5.common; + +public class Validators { + + public boolean validateClientId(String clientId) { + // Count characters, surrogate pairs count as one character. + int clientIdLength = 0; + for (int i = 0; i < clientId.length() - 1; i++) { + if (isCharacterHighSurrogate(clientId.charAt(i))) { + i++; + } + clientIdLength++; + } + return clientIdLength < 65535; + } + + /** + * @param ch + * the character to check. + * @return returns 'true' if the character is a high-surrogate code unit + */ + protected static boolean isCharacterHighSurrogate(char ch) { + final char minHighSurrogate = '\uD800'; + final char maxHighSurrogate = '\uDBFF'; + return (ch >= minHighSurrogate) && (ch <= maxHighSurrogate); + } + +} diff --git a/src/main/java/org/eclipse/paho/mqttv5/common/packet/MqttAck.java b/src/main/java/org/eclipse/paho/mqttv5/common/packet/MqttAck.java new file mode 100644 index 0000000..e7ab7f8 --- /dev/null +++ b/src/main/java/org/eclipse/paho/mqttv5/common/packet/MqttAck.java @@ -0,0 +1,31 @@ +/******************************************************************************* + * Copyright (c) 2016, 2019 IBM Corp. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * and Eclipse Distribution License v1.0 which accompany this distribution. + * + * The Eclipse Public License is available at + * https://www.eclipse.org/legal/epl-2.0 + * and the Eclipse Distribution License is available at + * https://www.eclipse.org/org/documents/edl-v10.php + * + * Contributors: + * Dave Locke - Original MQTTv3 implementation + * James Sutton - Initial MQTTv5 implementation + */ +package org.eclipse.paho.mqttv5.common.packet; + +/** + * Abstract super-class of all acknowledgement messages. + */ +public abstract class MqttAck extends MqttPersistableWireMessage { + public MqttAck(byte type) { + super(type); + } + + @Override + protected byte getMessageInfo() { + return 0; + } +} \ No newline at end of file diff --git a/src/main/java/org/eclipse/paho/mqttv5/common/packet/MqttAuth.java b/src/main/java/org/eclipse/paho/mqttv5/common/packet/MqttAuth.java new file mode 100644 index 0000000..5bb8d9a --- /dev/null +++ b/src/main/java/org/eclipse/paho/mqttv5/common/packet/MqttAuth.java @@ -0,0 +1,123 @@ +/******************************************************************************* + * Copyright (c) 2016 IBM Corp. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * and Eclipse Distribution License v1.0 which accompany this distribution. + * + * The Eclipse Public License is available at + * https://www.eclipse.org/legal/epl-2.0 + * and the Eclipse Distribution License is available at + * https://www.eclipse.org/org/documents/edl-v10.php + * + * Contributors: + * Dave Locke - Original MQTTv3 implementation + * James Sutton - Initial MQTTv5 implementation + */ +package org.eclipse.paho.mqttv5.common.packet; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; + +import org.eclipse.paho.mqttv5.common.MqttException; + +/** + * An on-the-wire representation of an MQTT AUTH message. MQTTv5 - 3.15 + */ +public class MqttAuth extends MqttWireMessage { + + // Return codes + private static final int[] validReturnCodes = { MqttReturnCode.RETURN_CODE_SUCCESS, + MqttReturnCode.RETURN_CODE_CONTINUE_AUTHENTICATION, MqttReturnCode.RETURN_CODE_RE_AUTHENTICATE }; + + private static final Byte[] validProperties = { MqttProperties.AUTH_METHOD_IDENTIFIER, + MqttProperties.AUTH_DATA_IDENTIFIER, MqttProperties.REASON_STRING_IDENTIFIER, + MqttProperties.USER_DEFINED_PAIR_IDENTIFIER }; + + // Fields + private MqttProperties properties; + + /** + * Constructs an Auth message from a raw byte array + * + * @param data + * - The variable header and payload bytes. + * @throws IOException + * - if an exception occurs when decoding an input stream + * @throws MqttException + * - If an exception occurs decoding this packet + */ + public MqttAuth(byte[] data) throws IOException, MqttException { + super(MqttWireMessage.MESSAGE_TYPE_AUTH); + properties = new MqttProperties(validProperties); + ByteArrayInputStream bais = new ByteArrayInputStream(data); + DataInputStream inputStream = new DataInputStream(bais); + reasonCode = inputStream.readUnsignedByte(); + validateReturnCode(reasonCode, validReturnCodes); + this.properties.decodeProperties(inputStream); + inputStream.close(); + } + + /** + * Constructs an Auth message from the return code + * + * @param returnCode + * - The Auth Return Code + * @param properties + * - The {@link MqttProperties} for the packet. + * @throws MqttException + * - If an exception occurs encoding this packet + */ + public MqttAuth(int returnCode, MqttProperties properties) throws MqttException { + super(MqttWireMessage.MESSAGE_TYPE_AUTH); + if (properties != null) { + this.properties = properties; + } else { + this.properties = new MqttProperties(); + } + this.properties.setValidProperties(validProperties); + validateReturnCode(returnCode, validReturnCodes); + this.reasonCode = returnCode; + } + + @Override + protected byte[] getVariableHeader() throws MqttException { + try { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + DataOutputStream outputStream = new DataOutputStream(baos); + + // Encode the Return Code + outputStream.writeByte(reasonCode); + + // Write Identifier / Value Fields + byte[] identifierValueFieldsByteArray = this.properties.encodeProperties(); + outputStream.write(identifierValueFieldsByteArray); + outputStream.flush(); + return baos.toByteArray(); + } catch (IOException ioe) { + throw new MqttException(ioe); + } + } + + @Override + protected byte getMessageInfo() { + return (byte) (1); + } + + public int getReturnCode() { + return reasonCode; + } + + @Override + public MqttProperties getProperties() { + return this.properties; + } + + @Override + public String toString() { + return "MqttAuth [returnCode=" + reasonCode + ", properties=" + properties + "]"; + } +} diff --git a/src/main/java/org/eclipse/paho/mqttv5/common/packet/MqttConnAck.java b/src/main/java/org/eclipse/paho/mqttv5/common/packet/MqttConnAck.java new file mode 100644 index 0000000..c260aeb --- /dev/null +++ b/src/main/java/org/eclipse/paho/mqttv5/common/packet/MqttConnAck.java @@ -0,0 +1,154 @@ +/******************************************************************************* + * Copyright (c) 2016 IBM Corp. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * and Eclipse Distribution License v1.0 which accompany this distribution. + * + * The Eclipse Public License is available at + * https://www.eclipse.org/legal/epl-2.0 + * and the Eclipse Distribution License is available at + * https://www.eclipse.org/org/documents/edl-v10.php + * + * Contributors: + * Dave Locke - Original MQTTv3 implementation + * James Sutton - Initial MQTTv5 implementation + */ +package org.eclipse.paho.mqttv5.common.packet; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; + +import org.eclipse.paho.mqttv5.common.MqttException; + +/** + * An on-the-wire representation of an MQTT CONNACK. + */ +public class MqttConnAck extends MqttAck { + public static final String KEY = "Con"; + + private static final int[] validReturnCodes = { MqttReturnCode.RETURN_CODE_SUCCESS, + MqttReturnCode.RETURN_CODE_UNSPECIFIED_ERROR, MqttReturnCode.RETURN_CODE_MALFORMED_CONTROL_PACKET, + MqttReturnCode.RETURN_CODE_PROTOCOL_ERROR, MqttReturnCode.RETURN_CODE_IMPLEMENTATION_SPECIFIC_ERROR, + MqttReturnCode.RETURN_CODE_UNSUPPORTED_PROTOCOL_VERSION, MqttReturnCode.RETURN_CODE_IDENTIFIER_NOT_VALID, + MqttReturnCode.RETURN_CODE_BAD_USERNAME_OR_PASSWORD, MqttReturnCode.RETURN_CODE_NOT_AUTHORIZED, + MqttReturnCode.RETURN_CODE_SERVER_UNAVAILABLE, MqttReturnCode.RETURN_CODE_SERVER_BUSY, + MqttReturnCode.RETURN_CODE_BANNED, MqttReturnCode.RETURN_CODE_BAD_AUTHENTICATION, + MqttReturnCode.RETURN_CODE_TOPIC_NAME_INVALID, MqttReturnCode.RETURN_CODE_PACKET_TOO_LARGE, + MqttReturnCode.RETURN_CODE_QUOTA_EXCEEDED, MqttReturnCode.RETURN_CODE_RETAIN_NOT_SUPPORTED, + MqttReturnCode.RETURN_CODE_USE_ANOTHER_SERVER, MqttReturnCode.RETURN_CODE_SERVER_MOVED, + MqttReturnCode.RETURN_CODE_CONNECTION_RATE_EXCEEDED }; + + private static final Byte[] validProperties = { MqttProperties.SESSION_EXPIRY_INTERVAL_IDENTIFIER, + MqttProperties.RECEIVE_MAXIMUM_IDENTIFIER, MqttProperties.MAXIMUM_QOS_IDENTIFIER, + MqttProperties.RETAIN_AVAILABLE_IDENTIFIER, MqttProperties.MAXIMUM_PACKET_SIZE_IDENTIFIER, + MqttProperties.ASSIGNED_CLIENT_IDENTIFIER_IDENTIFIER, MqttProperties.TOPIC_ALIAS_MAXIMUM_IDENTIFIER, + MqttProperties.WILDCARD_SUB_AVAILABLE_IDENTIFIER, MqttProperties.SUBSCRIPTION_AVAILABLE_IDENTIFIER, + MqttProperties.SHARED_SUBSCRIPTION_AVAILABLE_IDENTIFIER, MqttProperties.SERVER_KEEP_ALIVE_IDENTIFIER, + MqttProperties.RESPONSE_INFO_IDENTIFIER, MqttProperties.SERVER_REFERENCE_IDENTIFIER, + MqttProperties.AUTH_METHOD_IDENTIFIER, MqttProperties.AUTH_DATA_IDENTIFIER, + MqttProperties.REASON_STRING_IDENTIFIER, MqttProperties.USER_DEFINED_PAIR_IDENTIFIER }; + + private boolean sessionPresent; + private MqttProperties properties; + + public MqttConnAck(byte[] variableHeader) throws IOException, MqttException { + super(MqttWireMessage.MESSAGE_TYPE_CONNACK); + this.properties = new MqttProperties(validProperties); + ByteArrayInputStream bais = new ByteArrayInputStream(variableHeader); + DataInputStream dis = new DataInputStream(bais); + sessionPresent = (dis.readUnsignedByte() & 0x01) == 0x01; + reasonCode = dis.readUnsignedByte(); + validateReturnCode(reasonCode, validReturnCodes); + this.properties.decodeProperties(dis); + dis.close(); + } + + public MqttConnAck(boolean sessionPresent, int returnCode, MqttProperties properties) throws MqttException { + super(MqttWireMessage.MESSAGE_TYPE_CONNACK); + if (properties != null) { + this.properties = properties; + } else { + this.properties = new MqttProperties(); + } + this.properties.setValidProperties(validProperties); + this.sessionPresent = sessionPresent; + validateReturnCode(returnCode, validReturnCodes); + this.reasonCode = returnCode; + } + + @Override + protected byte[] getVariableHeader() throws MqttException { + try { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + DataOutputStream dos = new DataOutputStream(baos); + + // Encode the Session Present Flag + byte connectAchnowledgeFlag = 0; + if (sessionPresent) { + connectAchnowledgeFlag |= 0x01; + } + dos.write(connectAchnowledgeFlag); + + // Encode the Connect Return Code + dos.write((byte) reasonCode); + + // Write Identifier / Value Fields + byte[] identifierValueFieldsByteArray = this.properties.encodeProperties(); + dos.write(identifierValueFieldsByteArray); + dos.flush(); + return baos.toByteArray(); + } catch (IOException ioe) { + throw new MqttException(ioe); + } + + } + + /** + * Returns whether or not this message needs to include a message ID. + */ + @Override + public boolean isMessageIdRequired() { + return false; + } + + @Override + public String getKey() { + return KEY; + } + + public boolean getSessionPresent() { + return sessionPresent; + } + + public void setSessionPresent(boolean sessionPresent) { + this.sessionPresent = sessionPresent; + } + + public int getReturnCode() { + return reasonCode; + } + + public void setReturnCode(int returnCode) { + this.reasonCode = returnCode; + } + + @Override + public MqttProperties getProperties() { + return properties; + } + + public static int[] getValidreturncodes() { + return validReturnCodes; + } + + @Override + public String toString() { + return "MqttConnAck [returnCode=" + reasonCode + ", sessionPresent=" + sessionPresent + ", properties=" + + properties + "]"; + } + +} diff --git a/src/main/java/org/eclipse/paho/mqttv5/common/packet/MqttConnect.java b/src/main/java/org/eclipse/paho/mqttv5/common/packet/MqttConnect.java new file mode 100644 index 0000000..9a49aa3 --- /dev/null +++ b/src/main/java/org/eclipse/paho/mqttv5/common/packet/MqttConnect.java @@ -0,0 +1,337 @@ +/******************************************************************************* + * Copyright (c) 2016 IBM Corp. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * and Eclipse Distribution License v1.0 which accompany this distribution. + * + * The Eclipse Public License is available at + * https://www.eclipse.org/legal/epl-2.0 + * and the Eclipse Distribution License is available at + * https://www.eclipse.org/org/documents/edl-v10.php + * + * Contributors: + * Dave Locke - Original MQTTv3 implementation + * James Sutton - Initial MQTTv5 implementation + */ +package org.eclipse.paho.mqttv5.common.packet; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.util.Arrays; + +import org.eclipse.paho.mqttv5.common.MqttException; +import org.eclipse.paho.mqttv5.common.MqttMessage; + +public class MqttConnect extends MqttWireMessage { + + public static final String KEY = "Con"; + + private MqttProperties properties; + + private MqttProperties willProperties; + + // Fields + private byte info; + private String clientId; + private boolean reservedByte; + private boolean cleanStart; + private MqttMessage willMessage; + private String userName; + private byte[] password; + private int keepAliveInterval; + private String willDestination; + private int mqttVersion = DEFAULT_PROTOCOL_VERSION; + + private static final Byte[] validProperties = { MqttProperties.SESSION_EXPIRY_INTERVAL_IDENTIFIER, + MqttProperties.WILL_DELAY_INTERVAL_IDENTIFIER, MqttProperties.RECEIVE_MAXIMUM_IDENTIFIER, + MqttProperties.MAXIMUM_PACKET_SIZE_IDENTIFIER, MqttProperties.TOPIC_ALIAS_MAXIMUM_IDENTIFIER, + MqttProperties.REQUEST_RESPONSE_INFO_IDENTIFIER, MqttProperties.REQUEST_PROBLEM_INFO_IDENTIFIER, + MqttProperties.USER_DEFINED_PAIR_IDENTIFIER, MqttProperties.AUTH_METHOD_IDENTIFIER, + MqttProperties.AUTH_DATA_IDENTIFIER }; + + private static final Byte[] validWillProperties = { MqttProperties.WILL_DELAY_INTERVAL_IDENTIFIER, + MqttProperties.PAYLOAD_FORMAT_INDICATOR_IDENTIFIER, MqttProperties.MESSAGE_EXPIRY_INTERVAL_IDENTIFIER, + MqttProperties.RESPONSE_TOPIC_IDENTIFIER, MqttProperties.CORRELATION_DATA_IDENTIFIER, + MqttProperties.USER_DEFINED_PAIR_IDENTIFIER, MqttProperties.CONTENT_TYPE_IDENTIFIER }; + + /** + * Constructor for an on the wire MQTT Connect message + * + * @param info + * - Info Byte + * @param data + * - The variable header and payload bytes. + * @throws IOException + * - if an exception occurs when decoding an input stream + * @throws MqttException + * - If an exception occurs decoding this packet + */ + public MqttConnect(byte info, byte[] data) throws IOException, MqttException { + super(MqttWireMessage.MESSAGE_TYPE_CONNECT); + this.info = info; + this.properties = new MqttProperties(validProperties); + this.willProperties = new MqttProperties(validWillProperties); + ByteArrayInputStream bais = new ByteArrayInputStream(data); + DataInputStream dis = new DataInputStream(bais); + + // Verify the Protocol name and version + String protocolName = MqttDataTypes.decodeUTF8(dis); + if (!protocolName.equalsIgnoreCase(DEFAULT_PROTOCOL_NAME)) { + throw new MqttPacketException(MqttPacketException.PACKET_CONNECT_ERROR_UNSUPPORTED_PROTOCOL_NAME); + } + mqttVersion = dis.readByte(); + if (mqttVersion != DEFAULT_PROTOCOL_VERSION) { + throw new MqttPacketException(MqttPacketException.PACKET_CONNECT_ERROR_UNSUPPORTED_PROTOCOL_VERSION); + } + + byte connectFlags = dis.readByte(); + reservedByte = (connectFlags & 0x01) != 0; + cleanStart = (connectFlags & 0x02) != 0; + boolean willFlag = (connectFlags & 0x04) != 0; + int willQoS = (connectFlags >> 3) & 0x03; + boolean willRetain = (connectFlags & 0x20) != 0; + boolean passwordFlag = (connectFlags & 0x40) != 0; + boolean usernameFlag = (connectFlags & 0x80) != 0; + + if (reservedByte) { + throw new MqttPacketException(MqttPacketException.PACKET_CONNECT_ERROR_INVALID_RESERVE_FLAG); + } + + keepAliveInterval = dis.readUnsignedShort(); + properties.decodeProperties(dis); + clientId = MqttDataTypes.decodeUTF8(dis); + + if (willFlag) { + willProperties.decodeProperties(dis); + if (willQoS == 3) { + throw new MqttPacketException(MqttPacketException.PACKET_CONNECT_ERROR_INVALID_WILL_QOS); + } + willDestination = MqttDataTypes.decodeUTF8(dis); + int willMessageLength = dis.readShort(); + byte[] willMessageBytes = new byte[willMessageLength]; + dis.read(willMessageBytes, 0, willMessageLength); + willMessage = new MqttMessage(willMessageBytes); + willMessage.setQos(willQoS); + willMessage.setRetained(willRetain); + } + if (usernameFlag) { + userName = MqttDataTypes.decodeUTF8(dis); + } + if (passwordFlag) { + int passwordLength = dis.readShort(); + password = new byte[passwordLength]; + dis.read(password, 0, passwordLength); + } + + dis.close(); + } + + /** + * Constructor for a new MQTT Connect Message + * + * @param clientId + * - The Client Identifier + * @param mqttVersion + * - The MQTT Protocol version + * @param cleanStart + * - The Clean Session Identifier + * @param keepAliveInterval + * - The Keep Alive Interval + * @param properties + * - The {@link MqttProperties} for the packet. + * @param willProperties + * - The {@link MqttProperties} for the will message. + * + */ + public MqttConnect(String clientId, int mqttVersion, boolean cleanStart, int keepAliveInterval, + MqttProperties properties, MqttProperties willProperties) { + super(MqttWireMessage.MESSAGE_TYPE_CONNECT); + this.clientId = clientId; + this.mqttVersion = mqttVersion; + this.cleanStart = cleanStart; + this.keepAliveInterval = keepAliveInterval; + if (properties != null) { + this.properties = properties; + } else { + this.properties = new MqttProperties(); + } + this.properties.setValidProperties(validProperties); + this.willProperties = willProperties; + this.willProperties.setValidProperties(validWillProperties); + + } + + @Override + protected byte getMessageInfo() { + return (byte) 0; + } + + public boolean isCleanStart() { + return cleanStart; + } + + @Override + protected byte[] getVariableHeader() throws MqttException { + try { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + DataOutputStream dos = new DataOutputStream(baos); + + // Encode the Protocol Name + MqttDataTypes.encodeUTF8(dos, "MQTT"); + + // Encode the MQTT Version + dos.write(mqttVersion); + + byte connectFlags = 0; + + if (cleanStart) { + connectFlags |= 0x02; + } + + if (willMessage != null) { + connectFlags |= 0x04; + connectFlags |= (willMessage.getQos() << 3); + if (willMessage.isRetained()) { + connectFlags |= 0x20; + } + } + + if (userName != null) { + connectFlags |= 0x80; + } + + if (password != null) { + connectFlags |= 0x40; + } + + dos.write(connectFlags); + dos.writeShort(keepAliveInterval); + + // Write Identifier / Value Fields + byte[] identifierValueFieldsByteArray = this.properties.encodeProperties(); + dos.write(identifierValueFieldsByteArray); + dos.flush(); + + return baos.toByteArray(); + } catch (IOException ioe) { + throw new MqttException(ioe); + } + } + + @Override + public byte[] getPayload() throws MqttException { + try { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + DataOutputStream dos = new DataOutputStream(baos); + MqttDataTypes.encodeUTF8(dos, clientId); + + + + if (willMessage != null) { + // Encode Will properties here + byte[] willIdentifierValueFieldsByteArray = willProperties.encodeProperties(); + dos.write(willIdentifierValueFieldsByteArray); + + MqttDataTypes.encodeUTF8(dos, willDestination); + dos.writeShort(willMessage.getPayload().length); + dos.write(willMessage.getPayload()); + } + + if (userName != null) { + MqttDataTypes.encodeUTF8(dos, userName); + } + + if (password != null) { + dos.writeShort(password.length); + dos.write(password); + } + dos.flush(); + return baos.toByteArray(); + } catch (IOException ioe) { + throw new MqttException(ioe); + } + } + + /** + * Returns whether or not this message needs to include a message ID. + */ + @Override + public boolean isMessageIdRequired() { + return false; + } + + @Override + public String getKey() { + return KEY; + } + + public void setWillMessage(MqttMessage willMessage) { + this.willMessage = willMessage; + } + + public void setUserName(String userName) { + this.userName = userName; + } + + public void setPassword(byte[] password) { + this.password = password; + } + + public void setWillDestination(String willDestination) { + this.willDestination = willDestination; + } + + public byte getInfo() { + return info; + } + + public String getClientId() { + return clientId; + } + + public MqttMessage getWillMessage() { + return willMessage; + } + + public String getUserName() { + return userName; + } + + public byte[] getPassword() { + return password; + } + + public int getKeepAliveInterval() { + return keepAliveInterval; + } + + public String getWillDestination() { + return willDestination; + } + + public int getMqttVersion() { + return mqttVersion; + } + + @Override + public MqttProperties getProperties() { + return properties; + } + + public MqttProperties getWillProperties() { + return willProperties; + } + + @Override + public String toString() { + return "MqttConnect [properties=" + properties + ", willProperties=" + willProperties + ", info=" + info + + ", clientId=" + clientId + ", reservedByte=" + reservedByte + ", cleanStart=" + cleanStart + + ", willMessage=" + willMessage + ", userName=" + userName + ", password=" + Arrays.toString(password) + + ", keepAliveInterval=" + keepAliveInterval + ", willDestination=" + willDestination + ", mqttVersion=" + + mqttVersion + "]"; + } +} diff --git a/src/main/java/org/eclipse/paho/mqttv5/common/packet/MqttDataTypes.java b/src/main/java/org/eclipse/paho/mqttv5/common/packet/MqttDataTypes.java new file mode 100644 index 0000000..eda54e9 --- /dev/null +++ b/src/main/java/org/eclipse/paho/mqttv5/common/packet/MqttDataTypes.java @@ -0,0 +1,253 @@ +package org.eclipse.paho.mqttv5.common.packet; + +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.EOFException; +import java.io.IOException; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; + +import org.eclipse.paho.mqttv5.common.MqttException; +import org.eclipse.paho.mqttv5.common.packet.util.VariableByteInteger; + +public class MqttDataTypes { + + private static final int TWO_BYTE_INT_MAX = 65535; + private static final long FOUR_BYTE_INT_MAX = 4294967295L; + public static final int VARIABLE_BYTE_INT_MAX = 268435455; + + public MqttDataTypes() throws IllegalAccessException { + throw new IllegalAccessException("Utility Class"); + } + + public static void validateTwoByteInt(Integer value) throws IllegalArgumentException { + if (value == null) { + return; + } + if (value >= 0 && value <= TWO_BYTE_INT_MAX) { + return; + } else { + throw new IllegalArgumentException("This property must be a number between 0 and " + TWO_BYTE_INT_MAX); + } + } + + public static void validateFourByteInt(Long value) throws IllegalArgumentException { + if (value == null) { + return; + } + if (value >= 0 && value <= FOUR_BYTE_INT_MAX) { + return; + } else { + throw new IllegalArgumentException("This property must be a number between 0 and " + FOUR_BYTE_INT_MAX); + } + } + + public static void validateVariableByteInt(int value) throws IllegalArgumentException { + if (value >= 0 && value <= VARIABLE_BYTE_INT_MAX) { + return; + } else { + throw new IllegalArgumentException("This property must be a number between 0 and " + VARIABLE_BYTE_INT_MAX); + } + + } + + public static void writeUnsignedFourByteInt(long value, DataOutputStream stream) throws IOException { + stream.writeByte((byte) (value >>> 24)); + stream.writeByte((byte) (value >>> 16)); + stream.writeByte((byte) (value >>> 8)); + stream.writeByte((byte) (value >>> 0)); + } + + /** + * Reads a Four Byte Integer, then converts it to a float. This is because Java + * doesn't have Unsigned Integers. + * + * @param inputStream + * The input stream to read from. + * @return a {@link Long} containing the value of the Four Byte int (Between 0 + * and 4294967295) + * @throws IOException + * if an exception occurs whilst reading from the Input Stream + */ + public static Long readUnsignedFourByteInt(DataInputStream inputStream) throws IOException { + byte[] readBuffer = {0, 0, 0, 0, 0, 0, 0, 0}; + inputStream.readFully(readBuffer, 4, 4); + return (((long) readBuffer[0] << 56) + ((long) (readBuffer[1] & 255) << 48) + + ((long) (readBuffer[2] & 255) << 40) + ((long) (readBuffer[3] & 255) << 32) + + ((long) (readBuffer[4] & 255) << 24) + ((readBuffer[5] & 255) << 16) + ((readBuffer[6] & 255) << 8) + + ((readBuffer[7] & 255) << 0)); + } + + /** + * Reads a Two Byte Integer, this is because Java does not have unsigned + * integers. + * + * @param inputStream + * The input stream to read from. + * @return a {@link int} containing the value of the Two Byte int (Between 0 and + * 65535) + * @throws IOException + * if an exception occurs whilst reading from the Input Stream + * + */ + public static int readUnsignedTwoByteInt(DataInputStream inputStream) throws IOException { + // byte readBuffer[] = {0,0} + int ch1 = inputStream.read(); + int ch2 = inputStream.read(); + if ((ch1 | ch2) < 0) + throw new EOFException(); + return (int) ((ch1 << 8) + (ch2 << 0)); + } + + /** + * Encodes a String given into UTF-8, before writing this to the + * {@link DataOutputStream} the length of the encoded string is encoded into two + * bytes and then written to the {@link DataOutputStream}. + * {@link DataOutputStream#writeUTF(String)} should be no longer used. + * {@link DataOutputStream#writeUTF(String)} does not correctly encode UTF-16 + * surrogate characters. + * + * @param dos + * The stream to write the encoded UTF-8 string to. + * @param stringToEncode + * The string to be encoded + * @throws MqttException + * Thrown when an error occurs with either the encoding or writing + * the data to the stream. + */ + public static void encodeUTF8(DataOutputStream dos, String stringToEncode) throws MqttException { + validateUTF8String(stringToEncode); + try { + byte[] encodedString = stringToEncode.getBytes(STRING_ENCODING); + byte byte1 = (byte) ((encodedString.length >>> 8) & 0xFF); + byte byte2 = (byte) ((encodedString.length >>> 0) & 0xFF); + + dos.write(byte1); + dos.write(byte2); + dos.write(encodedString); + } catch (IOException ex) { + throw new MqttException(ex); + } + } + + protected static final Charset STRING_ENCODING = StandardCharsets.UTF_8; + + /** + * Decodes a UTF-8 string from the {@link DataInputStream} provided. + * {@link DataInputStream#readUTF()} should be no longer used, because + * {@link DataInputStream#readUTF()} does not decode UTF-16 surrogate characters + * correctly. + * + * @param input + * The input stream from which to read the encoded string. + * @return a decoded String from the {@link DataInputStream}. + * @throws MqttException + * thrown when an error occurs with either reading from the stream + * or decoding the encoding string. + */ + public static String decodeUTF8(DataInputStream input) throws MqttException { + int encodedLength; + try { + encodedLength = input.readUnsignedShort(); + + byte[] encodedString = new byte[encodedLength]; + input.readFully(encodedString); + String output = new String(encodedString, STRING_ENCODING); + validateUTF8String(output); + + return output; + } catch (IOException ioe) { + throw new MqttException(MqttException.REASON_CODE_MALFORMED_PACKET, ioe); + } + } + + /** + * Validate a UTF-8 String for suitability for MQTT. + * + * @param input + * - The Input String + * @throws IllegalArgumentException + */ + private static void validateUTF8String(String input) throws IllegalArgumentException { + for (int i = 0; i < input.length(); i++) { + boolean isBad = false; + char c = input.charAt(i); + /* Check for mismatched surrogates */ + if (Character.isHighSurrogate(c)) { + if (++i == input.length()) { + isBad = true; /* Trailing high surrogate */ + } else { + char c2 = input.charAt(i); + if (!Character.isLowSurrogate(c2)) { + isBad = true; /* No low surrogate */ + } else { + int ch = ((((int) c) & 0x3ff) << 10) | (c2 & 0x3ff); + if ((ch & 0xffff) == 0xffff || (ch & 0xffff) == 0xfffe) { + isBad = true; /* Noncharacter in base plane */ + } + } + } + } else { + if (Character.isISOControl(c) || Character.isLowSurrogate(c)) { + isBad = true; /* Control character or no high surrogate */ + } else if (c >= 0xfdd0 && (c <= 0xfddf || c >= 0xfffe)) { + isBad = true; /* Noncharacter in other nonbase plane */ + } + } + if (isBad) { + throw new IllegalArgumentException(String.format("Invalid UTF-8 char: [%04x]", (int) c)); + } + } + } + + /** + * Decodes an MQTT Multi-Byte Integer from the given stream + * + * @param in + * the DataInputStream to decode a Variable Byte Integer From + * @return a new VariableByteInteger + * @throws IOException + * if an error occurred whilst decoding the VBI + */ + public static VariableByteInteger readVariableByteInteger(DataInputStream in) throws IOException { + byte digit; + int value = 0; + int multiplier = 1; + int count = 0; + + do { + digit = in.readByte(); + count++; + value += ((digit & 0x7F) * multiplier); + multiplier *= 128; + } while ((digit & 0x80) != 0); + + if (value < 0 || value > VARIABLE_BYTE_INT_MAX) { + throw new IOException("This property must be a number between 0 and " + VARIABLE_BYTE_INT_MAX + + ". Read value was: " + value); + } + + return new VariableByteInteger(value, count); + + } + + public static byte[] encodeVariableByteInteger(int number) throws IllegalArgumentException { + validateVariableByteInt(number); + int numBytes = 0; + long no = number; + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + // Encode the remaining length fields in the four bytes + do { + byte digit = (byte) (no % 128); + no = no / 128; + if (no > 0) { + digit |= 0x80; + } + baos.write(digit); + numBytes++; + } while ((no > 0) && (numBytes < 4)); + return baos.toByteArray(); + } + +} diff --git a/src/main/java/org/eclipse/paho/mqttv5/common/packet/MqttDisconnect.java b/src/main/java/org/eclipse/paho/mqttv5/common/packet/MqttDisconnect.java new file mode 100644 index 0000000..5f02a4b --- /dev/null +++ b/src/main/java/org/eclipse/paho/mqttv5/common/packet/MqttDisconnect.java @@ -0,0 +1,129 @@ +/******************************************************************************* + * Copyright (c) 2016 IBM Corp. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * and Eclipse Distribution License v1.0 which accompany this distribution. + * + * The Eclipse Public License is available at + * https://www.eclipse.org/legal/epl-2.0 + * and the Eclipse Distribution License is available at + * https://www.eclipse.org/org/documents/edl-v10.php + * + * Contributors: + * Dave Locke - Original MQTTv3 implementation + * James Sutton - Initial MQTTv5 implementation + */ +package org.eclipse.paho.mqttv5.common.packet; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; + +import org.eclipse.paho.mqttv5.common.MqttException; +import org.eclipse.paho.mqttv5.common.packet.util.CountingInputStream; + +public class MqttDisconnect extends MqttWireMessage { + + public static final String KEY = "Disc"; + + private static final int[] validReturnCodes = { MqttReturnCode.RETURN_CODE_SUCCESS, + MqttReturnCode.RETURN_CODE_DISCONNECT_WITH_WILL_MESSAGE, MqttReturnCode.RETURN_CODE_UNSPECIFIED_ERROR, + MqttReturnCode.RETURN_CODE_MALFORMED_CONTROL_PACKET, MqttReturnCode.RETURN_CODE_PROTOCOL_ERROR, + MqttReturnCode.RETURN_CODE_IMPLEMENTATION_SPECIFIC_ERROR, MqttReturnCode.RETURN_CODE_NOT_AUTHORIZED, + MqttReturnCode.RETURN_CODE_SERVER_BUSY, MqttReturnCode.RETURN_CODE_SERVER_SHUTTING_DOWN, + MqttReturnCode.RETURN_CODE_KEEP_ALIVE_TIMEOUT, MqttReturnCode.RETURN_CODE_SESSION_TAKEN_OVER, + MqttReturnCode.RETURN_CODE_TOPIC_FILTER_NOT_VALID, MqttReturnCode.RETURN_CODE_TOPIC_NAME_INVALID, + MqttReturnCode.RETURN_CODE_RECEIVE_MAXIMUM_EXCEEDED, MqttReturnCode.RETURN_CODE_TOPIC_ALIAS_NOT_ACCEPTED, + MqttReturnCode.RETURN_CODE_PACKET_TOO_LARGE, MqttReturnCode.RETURN_CODE_MESSAGE_RATE_TOO_HIGH, + MqttReturnCode.RETURN_CODE_QUOTA_EXCEEDED, MqttReturnCode.RETURN_CODE_ADMINISTRITIVE_ACTION, + MqttReturnCode.RETURN_CODE_PAYLOAD_FORMAT_INVALID, MqttReturnCode.RETURN_CODE_RETAIN_NOT_SUPPORTED, + MqttReturnCode.RETURN_CODE_QOS_NOT_SUPPORTED, MqttReturnCode.RETURN_CODE_USE_ANOTHER_SERVER, + MqttReturnCode.RETURN_CODE_SERVER_MOVED, MqttReturnCode.RETURN_CODE_SHARED_SUB_NOT_SUPPORTED, + MqttReturnCode.RETURN_CODE_CONNECTION_RATE_EXCEEDED, MqttReturnCode.RETURN_CODE_MAXIMUM_CONNECT_TIME, + MqttReturnCode.RETURN_CODE_SUB_IDENTIFIERS_NOT_SUPPORTED, + MqttReturnCode.RETURN_CODE_WILDCARD_SUB_NOT_SUPPORTED }; + + // Fields + private int returnCode = MqttReturnCode.RETURN_CODE_SUCCESS; + + private static final Byte[] validProperties = { MqttProperties.SESSION_EXPIRY_INTERVAL_IDENTIFIER, + MqttProperties.SERVER_REFERENCE_IDENTIFIER, MqttProperties.REASON_STRING_IDENTIFIER, + MqttProperties.USER_DEFINED_PAIR_IDENTIFIER }; + + private MqttProperties properties; + + public MqttDisconnect(byte[] data) throws IOException, MqttException { + super(MqttWireMessage.MESSAGE_TYPE_DISCONNECT); + this.properties = new MqttProperties(validProperties); + ByteArrayInputStream bais = new ByteArrayInputStream(data); + CountingInputStream counter = new CountingInputStream(bais); + DataInputStream inputStream = new DataInputStream(counter); + if(data.length - counter.getCounter() >= 1) { + returnCode = inputStream.readUnsignedByte(); + validateReturnCode(returnCode, validReturnCodes); + } + + long remainder = (long) data.length - counter.getCounter(); + if (remainder >= 2) { + this.properties.decodeProperties(inputStream); + } + + inputStream.close(); + } + + public MqttDisconnect(int returnCode, MqttProperties properties) throws MqttException { + super(MqttWireMessage.MESSAGE_TYPE_DISCONNECT); + validateReturnCode(returnCode, validReturnCodes); + this.returnCode = returnCode; + if (properties != null) { + this.properties = properties; + } else { + this.properties = new MqttProperties(); + } + this.properties.setValidProperties(validProperties); + } + + @Override + protected byte[] getVariableHeader() throws MqttException { + try { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + DataOutputStream outputStream = new DataOutputStream(baos); + + // Encode the Return Code + outputStream.writeByte(returnCode); + + // Write Identifier / Value Fields + byte[] identifierValueFieldsByteArray = this.properties.encodeProperties(); + if (identifierValueFieldsByteArray.length != 0) { + outputStream.write(identifierValueFieldsByteArray); + outputStream.flush(); + } + return baos.toByteArray(); + } catch (IOException ioe) { + throw new MqttException(ioe); + } + } + + public int getReturnCode() { + return returnCode; + } + + @Override + protected byte getMessageInfo() { + return (byte) 0; + } + + @Override + public MqttProperties getProperties() { + return this.properties; + } + + @Override + public String toString() { + return "MqttDisconnect [returnCode=" + returnCode + ", properties=" + properties + "]"; + } + +} diff --git a/src/main/java/org/eclipse/paho/mqttv5/common/packet/MqttPacketException.java b/src/main/java/org/eclipse/paho/mqttv5/common/packet/MqttPacketException.java new file mode 100644 index 0000000..4aba77f --- /dev/null +++ b/src/main/java/org/eclipse/paho/mqttv5/common/packet/MqttPacketException.java @@ -0,0 +1,43 @@ +/******************************************************************************* + * Copyright (c) 2016 IBM Corp. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * and Eclipse Distribution License v1.0 which accompany this distribution. + * + * The Eclipse Public License is available at + * https://www.eclipse.org/legal/epl-2.0 + * and the Eclipse Distribution License is available at + * https://www.eclipse.org/org/documents/edl-v10.php + * + * Contributors: + * James Sutton - Initial MQTTv5 implementation + */ +package org.eclipse.paho.mqttv5.common.packet; + +import org.eclipse.paho.mqttv5.common.MqttException; + +/** + * This is a Class containing all of the low level packet exceptions that may be useful in identifying the cause of protocol errors. + * @author jamessutton + */ +public class MqttPacketException extends MqttException { + + private static final long serialVersionUID = 1L; + + + /** + * Protocol Exceptions for the CONNECT Packet + */ + public static final int PACKET_CONNECT_ERROR_UNSUPPORTED_PROTOCOL_NAME = 51000; // 3.1.2.1 - If Protocol name is not 'MQTT', return Unsupported Protocol Version Error. + public static final int PACKET_CONNECT_ERROR_UNSUPPORTED_PROTOCOL_VERSION = 51001; // 3.1.2.2 - If Protocol version is not 5, return Unsupported Protocol Version Error. + public static final int PACKET_CONNECT_ERROR_INVALID_RESERVE_FLAG = 51002; // 3.1.2.3 - If Reserved flag is not 0, return Malformed Packet Error. + public static final int PACKET_CONNECT_ERROR_INVALID_WILL_QOS = 51003; // 3.1.2.6 - If Will QoS is 0x03, return Malformed Packet Error. + + + public MqttPacketException(int reasonCode) { + super(reasonCode); + } + + +} diff --git a/src/main/java/org/eclipse/paho/mqttv5/common/packet/MqttPersistableWireMessage.java b/src/main/java/org/eclipse/paho/mqttv5/common/packet/MqttPersistableWireMessage.java new file mode 100644 index 0000000..99de2f6 --- /dev/null +++ b/src/main/java/org/eclipse/paho/mqttv5/common/packet/MqttPersistableWireMessage.java @@ -0,0 +1,76 @@ +/******************************************************************************* + * Copyright (c) 2009, 2014 IBM Corp. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * and Eclipse Distribution License v1.0 which accompany this distribution. + * + * The Eclipse Public License is available at + * https://www.eclipse.org/legal/epl-2.0 + * and the Eclipse Distribution License is available at + * https://www.eclipse.org/org/documents/edl-v10.php + * + * Contributors: + * Dave Locke - initial API and implementation and/or initial documentation + */ +package org.eclipse.paho.mqttv5.common.packet; + +import org.eclipse.paho.mqttv5.common.MqttException; +import org.eclipse.paho.mqttv5.common.MqttPersistable; +import org.eclipse.paho.mqttv5.common.MqttPersistenceException; + +public abstract class MqttPersistableWireMessage extends MqttWireMessage + implements MqttPersistable { + + public MqttPersistableWireMessage(byte type) { + super(type); + } + + public byte[] getHeaderBytes() throws MqttPersistenceException { + byte[] headerBytes = null; + try { + if(this.getClass() == MqttPublish.class && this.getProperties().getTopicAlias() != null) { + // Remove the Topic Alias temporarily. + MqttProperties props = this.getProperties(); + Integer topicAlias = props.getTopicAlias(); + props.setTopicAlias(null); + headerBytes = getHeader(); + // Re Set Topic Alias + props.setTopicAlias(topicAlias); + this.properties = props; + } else { + headerBytes = getHeader(); + } + return headerBytes; + } + catch (MqttException ex) { + throw new MqttPersistenceException(ex.getCause()); + } + } + + public int getHeaderLength() throws MqttPersistenceException { + return getHeaderBytes().length; + } + + public int getHeaderOffset() throws MqttPersistenceException{ + return 0; + } + + public byte[] getPayloadBytes() throws MqttPersistenceException { + try { + return getPayload(); + } + catch (MqttException ex) { + throw new MqttPersistenceException(ex.getCause()); + } + } + + public int getPayloadLength() throws MqttPersistenceException { + return 0; + } + + public int getPayloadOffset() throws MqttPersistenceException { + return 0; + } + +} diff --git a/src/main/java/org/eclipse/paho/mqttv5/common/packet/MqttPingReq.java b/src/main/java/org/eclipse/paho/mqttv5/common/packet/MqttPingReq.java new file mode 100644 index 0000000..9b06f89 --- /dev/null +++ b/src/main/java/org/eclipse/paho/mqttv5/common/packet/MqttPingReq.java @@ -0,0 +1,52 @@ +/******************************************************************************* + * Copyright (c) 2016 IBM Corp. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * and Eclipse Distribution License v1.0 which accompany this distribution. + * + * The Eclipse Public License is available at + * https://www.eclipse.org/legal/epl-2.0 + * and the Eclipse Distribution License is available at + * https://www.eclipse.org/org/documents/edl-v10.php + * + * Contributors: + * Dave Locke - Original MQTTv3 implementation + * James Sutton - Initial MQTTv5 implementation + */ +package org.eclipse.paho.mqttv5.common.packet; + +import org.eclipse.paho.mqttv5.common.MqttException; + +public class MqttPingReq extends MqttWireMessage{ + + public static final String KEY = "Ping"; + + public MqttPingReq(){ + super(MqttWireMessage.MESSAGE_TYPE_PINGREQ); + } + + /** + * Returns false as message IDs are not required for MQTT + * PINGREQ messages. + */ + @Override + public boolean isMessageIdRequired() { + return false; + } + + @Override + protected byte[] getVariableHeader() throws MqttException { + return new byte[0]; + } + + @Override + protected byte getMessageInfo() { + return 0; + } + + @Override + public String getKey() { + return KEY; + } +} diff --git a/src/main/java/org/eclipse/paho/mqttv5/common/packet/MqttPingResp.java b/src/main/java/org/eclipse/paho/mqttv5/common/packet/MqttPingResp.java new file mode 100644 index 0000000..cfac651 --- /dev/null +++ b/src/main/java/org/eclipse/paho/mqttv5/common/packet/MqttPingResp.java @@ -0,0 +1,52 @@ +/******************************************************************************* + * Copyright (c) 2016 IBM Corp. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * and Eclipse Distribution License v1.0 which accompany this distribution. + * + * The Eclipse Public License is available at + * https://www.eclipse.org/legal/epl-2.0 + * and the Eclipse Distribution License is available at + * https://www.eclipse.org/org/documents/edl-v10.php + * + * Contributors: + * Dave Locke - Original MQTTv3 implementation + * James Sutton - Initial MQTTv5 implementation + */ +package org.eclipse.paho.mqttv5.common.packet; + +import org.eclipse.paho.mqttv5.common.MqttException; + +public class MqttPingResp extends MqttAck { + + public static final String KEY = "Ping"; + + public MqttPingResp() { + super(MqttWireMessage.MESSAGE_TYPE_PINGRESP); + } + + /** + * Returns false as message IDs are not required for MQTT PINGREQ + * messages. + */ + @Override + public boolean isMessageIdRequired() { + return false; + } + + @Override + protected byte[] getVariableHeader() throws MqttException { + return new byte[0]; + } + + @Override + protected byte getMessageInfo() { + return 0; + } + + @Override + public String getKey() { + return KEY; + } +} diff --git a/src/main/java/org/eclipse/paho/mqttv5/common/packet/MqttProperties.java b/src/main/java/org/eclipse/paho/mqttv5/common/packet/MqttProperties.java new file mode 100644 index 0000000..5b2c09e --- /dev/null +++ b/src/main/java/org/eclipse/paho/mqttv5/common/packet/MqttProperties.java @@ -0,0 +1,1419 @@ +package org.eclipse.paho.mqttv5.common.packet; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.eclipse.paho.mqttv5.common.MqttException; +import org.eclipse.paho.mqttv5.common.util.MqttTopicValidator; + +/** + * MQTT v5 Properties Class. This Class contains all of the available MQTTv5 + * Properties accessible through getters and setters. + * + * It also contains the logic to encode and decode the properties into the + * appropriate byte arrays that can then be appended to the main MQTT packets. + * + * This class will only encode valid properties for a packet, so although you + * may set a value on a property, it will only be sent if it is valid for that + * message type. + * + * When a property has not yet been set, or was not included in a packet being + * decoded, it will remain as {@code null}. + * + * @author James Sutton + * + */ +public class MqttProperties { + + /** 1 - Payload format indicator (Byte) */ + public static final byte PAYLOAD_FORMAT_INDICATOR_IDENTIFIER = 0x01; + + /** 2 - Message Expiry Interval. (Four Byte Int). */ + public static final byte MESSAGE_EXPIRY_INTERVAL_IDENTIFIER = 0x02; + + /** 3 - Content Type (UTF-8). */ + public static final byte CONTENT_TYPE_IDENTIFIER = 0x03; + + /** 8 - Response Topic (UTF-8). */ + public static final byte RESPONSE_TOPIC_IDENTIFIER = 0x08; + + /** 9 - Correlation Data. (UTF-8). */ + public static final byte CORRELATION_DATA_IDENTIFIER = 0x09; + + /** 11 - Subscription Identifier (Variable Byte Int). */ + public static final byte SUBSCRIPTION_IDENTIFIER = 0x0B; + + /** 126 - Subscription Identifier Multi Flag (NOT ACTUAL PROPERTY). */ + public static final byte SUBSCRIPTION_IDENTIFIER_MULTI = 0x7E; + + /** 127 - Subscription Identifier Single Flag (NOT ACTUAL PROPERTY). */ + public static final byte SUBSCRIPTION_IDENTIFIER_SINGLE = 0x7F; + + /** 17 - Session Expiry Interval (Four Byte Int). */ + public static final byte SESSION_EXPIRY_INTERVAL_IDENTIFIER = 0x11; + + /** 18 - Assigned Client Identifier (UTF-8). */ + public static final byte ASSIGNED_CLIENT_IDENTIFIER_IDENTIFIER = 0x12; + + /** 19 - Server Keep Alive (Two Byte Int). */ + public static final byte SERVER_KEEP_ALIVE_IDENTIFIER = 0x13; + + /** 21 - Authentication Method (UTF-8). */ + public static final byte AUTH_METHOD_IDENTIFIER = 0x15; + + /** 22 - Authentication Data (Binary Data). */ + public static final byte AUTH_DATA_IDENTIFIER = 0x16; + + /** 23 - Request Problem Information (Byte). */ + public static final byte REQUEST_PROBLEM_INFO_IDENTIFIER = 0x17; + + /** 24 - Will Delay Interval (Four Byte Int). */ + public static final byte WILL_DELAY_INTERVAL_IDENTIFIER = 0x18; + + /** 25 - Request Response Information (Byte). */ + public static final byte REQUEST_RESPONSE_INFO_IDENTIFIER = 0x19; + + /** 26 - Response Information (UTF-8). */ + public static final byte RESPONSE_INFO_IDENTIFIER = 0x1A; + + /** 28 - Server Reference (UTF-8). */ + public static final byte SERVER_REFERENCE_IDENTIFIER = 0x1C; + + /** 31 - Reason String (UTF-8). */ + public static final byte REASON_STRING_IDENTIFIER = 0x1F; + + /** 33 - Receive Maximum (Two Byte Int). */ + public static final byte RECEIVE_MAXIMUM_IDENTIFIER = 0x21; + + /** 34 - Topic Alias Maximum (Two Byte Int). */ + public static final byte TOPIC_ALIAS_MAXIMUM_IDENTIFIER = 0x22; + + /** 35 - Topic Alias (Two Byte Int). */ + public static final byte TOPIC_ALIAS_IDENTIFIER = 0x23; + + /** 36 - Maximum QOS (Byte). */ + public static final byte MAXIMUM_QOS_IDENTIFIER = 0x24; + + /** 37 - Retain Available (Byte). */ + public static final byte RETAIN_AVAILABLE_IDENTIFIER = 0x25; + + /** 38 - User Defined Pair (UTF-8 key value). */ + public static final byte USER_DEFINED_PAIR_IDENTIFIER = 0x26; + + /** 39 - Maximum Packet Size (Four Byte Int). */ + public static final byte MAXIMUM_PACKET_SIZE_IDENTIFIER = 0x27; + + /** 40 - Wildcard Subscriptions available (Byte). */ + public static final byte WILDCARD_SUB_AVAILABLE_IDENTIFIER = 0x28; + + /** 41 - Subscription Identifier Available (Byte). */ + public static final byte SUBSCRIPTION_AVAILABLE_IDENTIFIER = 0x29; + + /** 42 - Shared Subscription Available (Byte). */ + public static final byte SHARED_SUBSCRIPTION_AVAILABLE_IDENTIFIER = 0x2A; + + private List validProperties; + + // Properties, sorted by Type + // Byte + private Boolean payloadFormat = false; + private Boolean requestProblemInfo; + private Boolean requestResponseInfo; + private Integer maximumQoS; + private Boolean retainAvailable = null; + private Boolean wildcardSubscriptionsAvailable = null; + private Boolean subscriptionIdentifiersAvailable = null; + private Boolean sharedSubscriptionAvailable = null; + + // Two Byte Integer + private Integer serverKeepAlive; + private Integer receiveMaximum; + private Integer topicAliasMaximum; + private Integer topicAlias; + + // Four Byte Integer + private Long messageExpiryInterval; + private Long sessionExpiryInterval; + private Long willDelayInterval; + private Long maximumPacketSize; + + // UTF-8 encoded String + private String contentType; + private String responseTopic; + private String assignedClientIdentifier; + private String authenticationMethod; + private String responseInfo; + private String serverReference; + private String reasonString; + private List userProperties = new ArrayList<>(); + + // Binary Data + private byte[] correlationData; + private byte[] authenticationData; + + // Variable Byte Integer + private List publishSubscriptionIdentifiers = new ArrayList<>(); + private Integer subscribeSubscriptionIdentifier; + + /** + * Initialises this MqttProperties Object. + */ + public MqttProperties() { + } + + /** + * Initialises this MqttProperties Object with a list of valid properties. + * + * @param validProperties + * the valid properties for the associated packet. + */ + public MqttProperties(Byte[] validProperties) { + this.validProperties = Arrays.asList(validProperties); + + } + + /** + * Sets the list of Valid MQTT Properties that are allowed for the associated + * packet. + * + * @param validProperties + * The valid properties for this packet. + */ + public void setValidProperties(Byte[] validProperties) { + this.validProperties = Arrays.asList(validProperties); + + } + + /** + * Encodes Non-Null Properties that are in the list of valid properties into a + * byte array. + * + * @return a byte array containing encoded properties. + * @throws MqttException + * if an exception occurs whilst encoding the properties. + */ + public byte[] encodeProperties() throws MqttException { + try { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + DataOutputStream outputStream = new DataOutputStream(baos); + + // Payload Format Indicator + if (payloadFormat && validProperties.contains(PAYLOAD_FORMAT_INDICATOR_IDENTIFIER)) { + outputStream.write(PAYLOAD_FORMAT_INDICATOR_IDENTIFIER); + outputStream.writeByte(0x01); + } + + // Message Expiry Interval + if (messageExpiryInterval != null && validProperties.contains(MESSAGE_EXPIRY_INTERVAL_IDENTIFIER)) { + outputStream.write(MESSAGE_EXPIRY_INTERVAL_IDENTIFIER); + MqttDataTypes.writeUnsignedFourByteInt(messageExpiryInterval, outputStream); + } + + // Content Type + if (contentType != null && validProperties.contains(CONTENT_TYPE_IDENTIFIER)) { + outputStream.write(CONTENT_TYPE_IDENTIFIER); + MqttDataTypes.encodeUTF8(outputStream, contentType); + } + + // Response Topic + if (responseTopic != null && validProperties.contains(RESPONSE_TOPIC_IDENTIFIER)) { + outputStream.write(RESPONSE_TOPIC_IDENTIFIER); + MqttDataTypes.encodeUTF8(outputStream, responseTopic); + } + + // Correlation Data + if (correlationData != null && validProperties.contains(CORRELATION_DATA_IDENTIFIER)) { + outputStream.write(CORRELATION_DATA_IDENTIFIER); + outputStream.writeShort(correlationData.length); + outputStream.write(correlationData); + } + + // Subscription Identifier + if (!publishSubscriptionIdentifiers.isEmpty() && validProperties.contains(SUBSCRIPTION_IDENTIFIER_MULTI)) { + for (Integer subscriptionIdentifier : publishSubscriptionIdentifiers) { + outputStream.write(SUBSCRIPTION_IDENTIFIER); + outputStream.write(MqttDataTypes.encodeVariableByteInteger(subscriptionIdentifier)); + } + } + if (subscribeSubscriptionIdentifier != null && validProperties.contains(SUBSCRIPTION_IDENTIFIER_SINGLE)) { + outputStream.write(SUBSCRIPTION_IDENTIFIER); + outputStream.write(MqttDataTypes.encodeVariableByteInteger(subscribeSubscriptionIdentifier)); + } + + // Session Expiry Interval + if (sessionExpiryInterval != null && validProperties.contains(SESSION_EXPIRY_INTERVAL_IDENTIFIER)) { + outputStream.write(SESSION_EXPIRY_INTERVAL_IDENTIFIER); + MqttDataTypes.writeUnsignedFourByteInt(sessionExpiryInterval, outputStream); + + } + + // Assigned Client Identifier + if (assignedClientIdentifier != null && validProperties.contains(ASSIGNED_CLIENT_IDENTIFIER_IDENTIFIER)) { + outputStream.write(ASSIGNED_CLIENT_IDENTIFIER_IDENTIFIER); + MqttDataTypes.encodeUTF8(outputStream, assignedClientIdentifier); + } + + // Server Keep Alive + if (serverKeepAlive != null && validProperties.contains(SERVER_KEEP_ALIVE_IDENTIFIER)) { + outputStream.write(SERVER_KEEP_ALIVE_IDENTIFIER); + outputStream.writeShort(serverKeepAlive); + } + + // Auth Method + if (authenticationMethod != null && validProperties.contains(AUTH_METHOD_IDENTIFIER)) { + outputStream.write(AUTH_METHOD_IDENTIFIER); + MqttDataTypes.encodeUTF8(outputStream, authenticationMethod); + } + + // Auth Data + if (authenticationData != null && validProperties.contains(AUTH_DATA_IDENTIFIER)) { + outputStream.write(AUTH_DATA_IDENTIFIER); + outputStream.writeShort(authenticationData.length); + outputStream.write(authenticationData); + } + + // Request Problem Info + if (requestProblemInfo != null && validProperties.contains(REQUEST_PROBLEM_INFO_IDENTIFIER)) { + outputStream.write(REQUEST_PROBLEM_INFO_IDENTIFIER); + outputStream.write(requestProblemInfo ? 1 : 0); + } + + // Will Delay Interval + if (willDelayInterval != null && validProperties.contains(WILL_DELAY_INTERVAL_IDENTIFIER)) { + outputStream.write(WILL_DELAY_INTERVAL_IDENTIFIER); + MqttDataTypes.writeUnsignedFourByteInt(willDelayInterval, outputStream); + // outputStream.writeInt(willDelayInterval); + } + + // Request Response Info + if (requestResponseInfo != null && validProperties.contains(REQUEST_RESPONSE_INFO_IDENTIFIER)) { + outputStream.write(REQUEST_RESPONSE_INFO_IDENTIFIER); + outputStream.write(requestResponseInfo ? 1 : 0); + } + + // Response Info + if (responseInfo != null && validProperties.contains(RESPONSE_INFO_IDENTIFIER)) { + outputStream.write(RESPONSE_INFO_IDENTIFIER); + MqttDataTypes.encodeUTF8(outputStream, responseInfo); + } + + // Server Reference + if (serverReference != null && validProperties.contains(SERVER_REFERENCE_IDENTIFIER)) { + outputStream.write(SERVER_REFERENCE_IDENTIFIER); + MqttDataTypes.encodeUTF8(outputStream, serverReference); + } + + // Reason String + if (reasonString != null && validProperties.contains(REASON_STRING_IDENTIFIER)) { + outputStream.write(REASON_STRING_IDENTIFIER); + MqttDataTypes.encodeUTF8(outputStream, reasonString); + } + + // Receive Maximum + if (receiveMaximum != null && validProperties.contains(RECEIVE_MAXIMUM_IDENTIFIER)) { + outputStream.write(RECEIVE_MAXIMUM_IDENTIFIER); + outputStream.writeShort(receiveMaximum); + } + + // Topic Alias Maximum + if (topicAliasMaximum != null && validProperties.contains(TOPIC_ALIAS_MAXIMUM_IDENTIFIER)) { + outputStream.write(TOPIC_ALIAS_MAXIMUM_IDENTIFIER); + outputStream.writeShort(topicAliasMaximum); + } + + // Topic Alias + if (topicAlias != null && validProperties.contains(TOPIC_ALIAS_IDENTIFIER)) { + outputStream.write(TOPIC_ALIAS_IDENTIFIER); + outputStream.writeShort(topicAlias); + } + + // Maximum QoS + if (maximumQoS != null && validProperties.contains(MAXIMUM_QOS_IDENTIFIER)) { + outputStream.write(MAXIMUM_QOS_IDENTIFIER); + outputStream.writeByte(maximumQoS); + } + + // Retain Available + if (retainAvailable != null && validProperties.contains(RETAIN_AVAILABLE_IDENTIFIER)) { + outputStream.write(RETAIN_AVAILABLE_IDENTIFIER); + outputStream.writeBoolean(retainAvailable); + } + + // User Defined Properties + if (userProperties != null && !userProperties.isEmpty() && validProperties.contains(USER_DEFINED_PAIR_IDENTIFIER)) { + for (UserProperty property : userProperties) { + // outputStream.write(USER_DEFINED_PAIR_IDENTIFIER); + outputStream.writeByte(USER_DEFINED_PAIR_IDENTIFIER); + MqttDataTypes.encodeUTF8(outputStream, property.getKey()); + MqttDataTypes.encodeUTF8(outputStream, property.getValue()); + } + } + + // Maximum Packet Size + if (maximumPacketSize != null && validProperties.contains(MAXIMUM_PACKET_SIZE_IDENTIFIER)) { + outputStream.write(MAXIMUM_PACKET_SIZE_IDENTIFIER); + MqttDataTypes.writeUnsignedFourByteInt(maximumPacketSize, outputStream); + + } + + // Wildcard Subscription Available flag + if (wildcardSubscriptionsAvailable != null && validProperties.contains(WILDCARD_SUB_AVAILABLE_IDENTIFIER)) { + outputStream.write(WILDCARD_SUB_AVAILABLE_IDENTIFIER); + outputStream.writeBoolean(wildcardSubscriptionsAvailable); + } + + // Subscription Identifiers Available flag + if (subscriptionIdentifiersAvailable != null + && validProperties.contains(SUBSCRIPTION_AVAILABLE_IDENTIFIER)) { + outputStream.write(SUBSCRIPTION_AVAILABLE_IDENTIFIER); + outputStream.writeBoolean(subscriptionIdentifiersAvailable); + } + + // Shared Subscription Available flag + if (sharedSubscriptionAvailable != null + && validProperties.contains(SHARED_SUBSCRIPTION_AVAILABLE_IDENTIFIER)) { + outputStream.write(SHARED_SUBSCRIPTION_AVAILABLE_IDENTIFIER); + outputStream.writeBoolean(sharedSubscriptionAvailable); + } + + int length = outputStream.size(); + outputStream.flush(); + ByteArrayOutputStream finalOutput = new ByteArrayOutputStream(); + finalOutput.write(MqttDataTypes.encodeVariableByteInteger(length)); + finalOutput.write(baos.toByteArray()); + + return finalOutput.toByteArray(); + } catch (IOException ioe) { + throw new MqttException(ioe); + } + } + + /** + * Decodes a Byte array of MQTT properties and sets them on this object. + * + * @param dis + * the {@link DataInputStream} containing the encoded Properties. + * @throws IOException + * if an exception occurs whilst reading the Byte Array + * @throws MqttException + * if an invalid MQTT Property Identifier is present. + */ + public void decodeProperties(DataInputStream dis) throws IOException, MqttException { + + // First get the length of the IV fields + int length = MqttDataTypes.readVariableByteInteger(dis).getValue(); + if (length > 0) { + byte[] identifierValueByteArray = new byte[length]; + dis.read(identifierValueByteArray, 0, length); + ByteArrayInputStream bais = new ByteArrayInputStream(identifierValueByteArray); + ArrayList decodedProperties = new ArrayList(); + DataInputStream inputStream = new DataInputStream(bais); + while (inputStream.available() > 0) { + // Get the first Byte + byte identifier = inputStream.readByte(); + if (validProperties.contains(identifier)) { + + // Verify that certain properties are not included more than once + if(!decodedProperties.contains(identifier)) { + decodedProperties.add(identifier); + } else if(identifier!= SUBSCRIPTION_IDENTIFIER && identifier != USER_DEFINED_PAIR_IDENTIFIER) { + // This property can only be included once + throw new MqttException(MqttException.REASON_CODE_DUPLICATE_PROPERTY); + } + + if (identifier == PAYLOAD_FORMAT_INDICATOR_IDENTIFIER) { + payloadFormat = (boolean) inputStream.readBoolean(); + } else if (identifier == MESSAGE_EXPIRY_INTERVAL_IDENTIFIER) { + messageExpiryInterval = MqttDataTypes.readUnsignedFourByteInt(inputStream); + } else if (identifier == CONTENT_TYPE_IDENTIFIER) { + contentType = MqttDataTypes.decodeUTF8(inputStream); + } else if (identifier == RESPONSE_TOPIC_IDENTIFIER) { + responseTopic = MqttDataTypes.decodeUTF8(inputStream); + } else if (identifier == CORRELATION_DATA_IDENTIFIER) { + int correlationDataLength = (int) inputStream.readShort(); + correlationData = new byte[correlationDataLength]; + inputStream.read(correlationData, 0, correlationDataLength); + } else if (identifier == SUBSCRIPTION_IDENTIFIER) { + int subscriptionIdentifier = MqttDataTypes.readVariableByteInteger(inputStream).getValue(); + publishSubscriptionIdentifiers.add(subscriptionIdentifier); + // Bit of a hack, where we potentially write this many times, users should make + // sure they read the JavaDoc. + subscribeSubscriptionIdentifier = subscriptionIdentifier; + } else if (identifier == SESSION_EXPIRY_INTERVAL_IDENTIFIER) { + sessionExpiryInterval = MqttDataTypes.readUnsignedFourByteInt(inputStream); + } else if (identifier == ASSIGNED_CLIENT_IDENTIFIER_IDENTIFIER) { + assignedClientIdentifier = MqttDataTypes.decodeUTF8(inputStream); + } else if (identifier == SERVER_KEEP_ALIVE_IDENTIFIER) { + serverKeepAlive = MqttDataTypes.readUnsignedTwoByteInt(inputStream); + } else if (identifier == AUTH_METHOD_IDENTIFIER) { + authenticationMethod = MqttDataTypes.decodeUTF8(inputStream); + } else if (identifier == AUTH_DATA_IDENTIFIER) { + int authDataLength = inputStream.readShort(); + authenticationData = new byte[authDataLength]; + inputStream.read(this.authenticationData, 0, authDataLength); + } else if (identifier == REQUEST_PROBLEM_INFO_IDENTIFIER) { + requestProblemInfo = inputStream.read() != 0; + } else if (identifier == WILL_DELAY_INTERVAL_IDENTIFIER) { + willDelayInterval = MqttDataTypes.readUnsignedFourByteInt(inputStream); + } else if (identifier == REQUEST_RESPONSE_INFO_IDENTIFIER) { + requestResponseInfo = inputStream.read() != 0; + } else if (identifier == RESPONSE_INFO_IDENTIFIER) { + responseInfo = MqttDataTypes.decodeUTF8(inputStream); + } else if (identifier == SERVER_REFERENCE_IDENTIFIER) { + serverReference = MqttDataTypes.decodeUTF8(inputStream); + } else if (identifier == REASON_STRING_IDENTIFIER) { + reasonString = MqttDataTypes.decodeUTF8(inputStream); + } else if (identifier == RECEIVE_MAXIMUM_IDENTIFIER) { + receiveMaximum = (int) inputStream.readShort(); + } else if (identifier == TOPIC_ALIAS_MAXIMUM_IDENTIFIER) { + topicAliasMaximum = (int) inputStream.readShort(); + } else if (identifier == TOPIC_ALIAS_IDENTIFIER) { + topicAlias = (int) inputStream.readShort(); + } else if (identifier == MAXIMUM_QOS_IDENTIFIER) { + maximumQoS = inputStream.read(); + } else if (identifier == RETAIN_AVAILABLE_IDENTIFIER) { + retainAvailable = inputStream.readBoolean(); + } else if (identifier == USER_DEFINED_PAIR_IDENTIFIER) { + String key = MqttDataTypes.decodeUTF8(inputStream); + String value = MqttDataTypes.decodeUTF8(inputStream); + userProperties.add(new UserProperty(key, value)); + } else if (identifier == MAXIMUM_PACKET_SIZE_IDENTIFIER) { + maximumPacketSize = MqttDataTypes.readUnsignedFourByteInt(inputStream); + } else if (identifier == WILDCARD_SUB_AVAILABLE_IDENTIFIER) { + wildcardSubscriptionsAvailable = inputStream.readBoolean(); + } else if (identifier == SUBSCRIPTION_AVAILABLE_IDENTIFIER) { + subscriptionIdentifiersAvailable = inputStream.readBoolean(); + } else if (identifier == SHARED_SUBSCRIPTION_AVAILABLE_IDENTIFIER) { + sharedSubscriptionAvailable = inputStream.readBoolean(); + } else { + + // Unidentified Identifier + inputStream.close(); + throw new MqttException(MqttException.REASON_CODE_INVALID_IDENTIFIER); + } + } else { + // Unidentified Identifier + inputStream.close(); + throw new MqttException(MqttException.REASON_CODE_INVALID_IDENTIFIER); + } + + } + } + + } + + /** + * Get a list of valid Properties for the associated MQTT Packet. + * + * @return a {@link List} of valid properties. + */ + public List getValidProperties() { + return validProperties; + } + + /** + * Request Response Information. + * + *

+ * The Client uses this value to request the Server to return Response + * Information in the CONNACK. A value of false indicates that the Sever MUST + * NOT return Response Information. If the value is true, the server MAY return + * Response Information in the CONNACK packet. If absent, the default value is + * false. The Server can choose not to include Response Information in the + * CONNACK, even if the Client requested it. + *

+ * + * @return the Request Response Information flag. May be null. + */ + public Boolean requestResponseInfo() { + return requestResponseInfo; + } + + /** + * Request Response Information. See + * {@link MqttProperties#requestResponseInfo()} + * + * @param requestResponseInfo + * - whether to request response information. + */ + public void setRequestResponseInfo(Boolean requestResponseInfo) { + this.requestResponseInfo = requestResponseInfo; + } + + /** + * Request Problem Information. + * + *

+ * The Client uses this value to indicate whether the Reason String or User + * Properties are sent in the case of failures. + *

+ *

+ * If the value of Request Problem Information is false, the Server MAY return a + * Reason String or User Properties on a CONNACK or DISCONNECT packet, but MUST + * NOT send a Reason String or User Properties on any packet other than PUBLISH, + * CONNACK, or DISCONNECT. If the value is false and the Client receives a + * Reason String or User Properties in a packet other than PUBLISH, CONNACK, or + * DISCONNECT, it uses a Disconnect Packet with a Reason Code 0x82 (Protocol + * Error).If absent, the default value is true. If this value is true, the + * Server MAY return a Reason String or User Properties on any Packet where it + * is allowed. + *

+ * + * @return the Request Problem Information flag. May be null. + */ + public Boolean requestProblemInfo() { + return requestProblemInfo; + } + + /** + * Request Problem Information. See {@link MqttProperties#requestProblemInfo()} + * + * @param requestProblemInfo + * - whether to request problem information. + */ + public void setRequestProblemInfo(Boolean requestProblemInfo) { + this.requestProblemInfo = requestProblemInfo; + } + + /** + * Will Delay Interval. + * + *

+ * The Server delays publishing the Client's Will Message until the Will Delay + * Interval has passed or the Session ends, whichever happens first. If a new + * Network Connection to this Session is made before the Will Delay Interval has + * passed, the Server MUST NOT send the Will Message. If this value is absent, + * the default value is 0 and there is no delay before the Will Message is + * published. + *

+ * + * @return The Will Delay Interval in seconds. May be null. + */ + public Long getWillDelayInterval() { + return willDelayInterval; + } + + /** + * Will Delay Interval. See {@link MqttProperties#getWillDelayInterval()} + * + * @param willDelayInterval + * - The Will Delay Interval in seconds. + */ + public void setWillDelayInterval(Long willDelayInterval) { + MqttDataTypes.validateFourByteInt(willDelayInterval); + this.willDelayInterval = willDelayInterval; + } + + /** + * The Receive Maximum. + * + *

+ * The Receive Maximum is used be the server to limit the number of QoS 1 and + * QoS 2 publications that it is willing to process concurrently for the Client. + * It does not provide a mechanism to limit the QoS 0 messages that the client + * might try to send. + *

+ *

+ * Valid values for this property range between 0 and 65,535. If this property + * is absent, the value defaults to 65,535. + *

+ * + * @return the Receive maximum. May be null if it has not been set. + */ + public Integer getReceiveMaximum() { + return receiveMaximum; + } + + /** + * The Receive Maximum. See {@link MqttProperties#getReceiveMaximum()} + * + * @param receiveMaximum + * - The Receive Maximum. May be null. + */ + public void setReceiveMaximum(Integer receiveMaximum) { + MqttDataTypes.validateTwoByteInt(receiveMaximum); + this.receiveMaximum = receiveMaximum; + } + + /** + * Maximum QoS + *

+ * If a Server does not support QoS 1 or QoS 2 PUBLISH packets it MUST send a + * Maximum Qos in the CONNACK packet specifying the highest QoS it supports. A + * Server that does not support QoS 1 or QoS 2 PUBLISH packets MUST still accept + * SUBSCRIBE packets containing a requested QoS of 0, 1 or 2. + *

+ * + *

+ * If a Server receives a CONNECT packet containing a Will QoS that exceeds its + * capabilities, it MUST reject the connection. It SHOULD use a CONNACK packet + * with a Reason Code 0x9B (QoS not supported) and MUST close the network + * connection. + *

+ * + *

+ * If the Maximum QoS is absent, the Client uses a Maximum QoS of 2. + *

+ * + * @return the maximum QoS. May be null. + */ + public Integer getMaximumQoS() { + return maximumQoS; + } + + /** + * Maximum QoS. See {@link MqttProperties#getMaximumQoS()} + * + * @param maximumQoS + * The Maximum QoS + */ + public void setMaximumQoS(Integer maximumQoS) { + this.maximumQoS = maximumQoS; + } + + /** + * Maximum Packet Size. + * + *

+ * The packet size is the total number of bytes in an MQTT Control Packet. The + * Server uses the Maximum Packet Size to inform the Client that it will not + * process packets whose size exceeds this limit. If this value is absent, there + * is no limit on the packet size imposed beyond the limitations in the protocol + * as a result of the remaining length encoding and the protocol header sizes. + *

+ * + * @return the Maximum Packet Size. May be null. + */ + public Long getMaximumPacketSize() { + return maximumPacketSize; + } + + /** + * Maximum Packet Size. See {@link MqttProperties#getMaximumPacketSize()} + * + * @param maximumPacketSize + * the Maximum Packet Size in bytes. May be null. + */ + public void setMaximumPacketSize(Long maximumPacketSize) { + MqttDataTypes.validateFourByteInt(maximumPacketSize); + this.maximumPacketSize = maximumPacketSize; + } + + /** + * Retain Available. + * + *

+ * If present, this property declares whether ther Server supports retained + * messages. If not present, then retained messages are supported. + *

+ * + *

+ * If a Server receives a CONNECT packet containing a Will Message with the Will + * Retain set to true, and it does not support retained messages, the Server + * MUST reject the connection request. It SHOULD send a CONNACK with the Reason + * Code 0x9A (Retain not supported) and then it MUST close the network + * connection. A client receiving Retain Available set to false from the Server + * MUST NOT send a PUBLISH packet with the Retain flag set to true. If the + * Server receives such a packet, this is a Protocol Error. The Server SHOULD + * send a DISCONNECT with the Reason code of 0x9A (Retain not supported). + *

+ * + * @return Retain Available Flag. May be Null. + */ + public Boolean isRetainAvailable() { + if(retainAvailable == null || retainAvailable == true) { + return true; + } else { + return false; + } + } + + /** + * Retain Available. See {@link MqttProperties#isRetainAvailable()} + * + * @param retainAvailable + * - Whether the server supports retained messages. May be null. + */ + public void setRetainAvailable(boolean retainAvailable) { + this.retainAvailable = retainAvailable; + } + + /** + * Assigned Client Identifier. + * + *

+ * The Client Identifier which was assigned by the Server because a zero length + * Client Identifier was found in the CONNECT packet. + *

+ * + *

+ * If the Client connects using a zero length Client Identifier, the Server MUST + * respond with a CONNACK containing an Assigned Client Identifier. The Assigned + * Client Identifier MUSE be a new Client Identifier not used by any other + * Session currently in the Server. + *

+ * + * @return The Assigned Client Identifier. May be null. + */ + public String getAssignedClientIdentifier() { + return assignedClientIdentifier; + } + + /** + * Assigned Client Identifier. See + * {@link MqttProperties#getAssignedClientIdentifier()} + * + * @param assignedClientIdentifier + * The Assigned Client Identifier. + */ + public void setAssignedClientIdentifier(String assignedClientIdentifier) { + this.assignedClientIdentifier = assignedClientIdentifier; + } + + /** + * Topic Alias Maximum. + * + *

+ * This value indicates the highest value that the Server will accept as a Topic + * Alias sent by the Client. The Server uses this value to limit the number of + * Topic Aliases that it is willing to hold on this Connection. The Client MUST + * NOT send a Topic Alias in a PUBLISH packet to the Server greater than this + * value. A value of 0 indicates that the Server does not accept any Topic + * Aliases on this connection. If Topic Alias Maximum is absent or 0, the Client + * MUST NOT send any Topic Aliases on to the Server. + *

+ * + * @return The Topic Alias Maximum. May be null. + */ + public Integer getTopicAliasMaximum() { + return topicAliasMaximum; + } + + /** + * Topic Alias Maximum. See {@link MqttProperties#getTopicAliasMaximum()} + * + * @param topicAliasMaximum + * The Topic Alias Maximum. + */ + public void setTopicAliasMaximum(Integer topicAliasMaximum) { + MqttDataTypes.validateTwoByteInt(topicAliasMaximum); + this.topicAliasMaximum = topicAliasMaximum; + } + + /** + * Topic Alias. + * + *

+ * A Topic Alias is an integer value that is used to identify the Topic instead + * of using the Topic Name. This reduces the size of the PUBLISH packet, and is + * useful when the Topic Names are long and the same Topic Names are used + * repetitively within a network connection. + *

+ * + *

+ * If enabled, this client will automatically assign Topic Aliases to outgoing + * messages. + *

+ * + * @return The Topic Alias. + */ + public Integer getTopicAlias() { + return topicAlias; + } + + /** + * Topic Alias. See {@link MqttProperties#getTopicAlias()} + * + * @param topicAlias + * The Topic Alias to set. + */ + public void setTopicAlias(Integer topicAlias) { + MqttDataTypes.validateTwoByteInt(topicAlias); + this.topicAlias = topicAlias; + } + + /** + * Server Keep Alive. + * + *

+ * The Keep Alive time Assigned by the Server. If the Server sends a Server Keep + * Alive on the CONNACK packet, the Client MUST use this value instead of the + * Keep Alive value the Client send on CONNECT. The Paho Client will + * automatically adjust the Keep Alive Value if this property is present in the + * CONNACK packet. + *

+ * + * @return The Server Keep Alive Value + */ + public Integer getServerKeepAlive() { + return serverKeepAlive; + } + + /** + * Server Keep Alive. See {@link MqttProperties#getServerKeepAlive()} + * + * @param serverKeepAlive + * The Server Keep Alive Value. + */ + public void setServerKeepAlive(Integer serverKeepAlive) { + MqttDataTypes.validateTwoByteInt(serverKeepAlive); + this.serverKeepAlive = serverKeepAlive; + + } + + /** + * Response Information. + * + *

+ * A String that is used as the basis for creating a Response Topic . The way in + * which the Client creates a Response Topic from the Response Information is + * not defined by the MQTT v5 specification. + *

+ * + * @return The Response Information + */ + public String getResponseInfo() { + return responseInfo; + } + + /** + * Response Information. See {@link MqttProperties#getResponseInfo()} + * + * @param responseInfo + * The Response Information. + */ + public void setResponseInfo(String responseInfo) { + this.responseInfo = responseInfo; + } + + /** + * Server Reference. + * + *

+ * A String that can be used by the Client to identifier another Server to use. + *

+ * + * @return The Server Reference. + */ + public String getServerReference() { + return serverReference; + } + + /** + * Server Reference. See {@link MqttProperties#getServerReference()} + * + * @param serverReference + * The Server Reference. + */ + public void setServerReference(String serverReference) { + this.serverReference = serverReference; + } + + /** + * Wildcard Subscription Available. + * + *

+ * If present, this flag declares whether the Server supports Wildcard + * Subscriptions. False means that Wildcard Subscriptions are not supported, + * True means that they are supported. If not present, then Wildcard + * Subscriptions are supported. + *

+ * + * @return A boolean defining whether Wildcard Subscriptions are supported. + */ + public boolean isWildcardSubscriptionsAvailable() { + if(wildcardSubscriptionsAvailable == null || wildcardSubscriptionsAvailable == true) { + return true; + } else { + return false; + } + } + + /** + * Wildcard Subscription Available. See + * {@link MqttProperties#isWildcardSubscriptionsAvailable()} + * + * @param wildcardSubscriptionsAvailable + * A boolean defining whether Wildcard Subscriptions are supported. + */ + public void setWildcardSubscriptionsAvailable(boolean wildcardSubscriptionsAvailable) { + this.wildcardSubscriptionsAvailable = wildcardSubscriptionsAvailable; + } + + /** + * Subscription Identifiers Available. + * + *

+ * If present, this flag declares whether the Server supports Subscription + * Identifiers. False means that Subscription Identifiers are not supported, + * True means that they are supported. If not present, then Subscription + * Identifiers are supported. The Paho Client will automatically attempt to use + * Subscription Identifiers if they are available. + *

+ * + * @return A boolean defining whether Subscription Identifiers are supported. + */ + public boolean isSubscriptionIdentifiersAvailable() { + if(subscriptionIdentifiersAvailable == null || subscriptionIdentifiersAvailable == true) { + return true; + } else { + return false; + } + } + + /** + * Subscription Identifiers Available. See + * {@link MqttProperties#isSubscriptionIdentifiersAvailable()} + * + * @param subscriptionIdentifiersAvailable + * A boolean defining whether Subscription Identifiers are supported. + */ + public void setSubscriptionIdentifiersAvailable(boolean subscriptionIdentifiersAvailable) { + this.subscriptionIdentifiersAvailable = subscriptionIdentifiersAvailable; + } + + /** + * Shared Subscription Available. + * + *

+ * If present, this flag declares whether the Server supports Shared + * Subscriptions. False means that Shared Subscriptions are not supported, True + * means that they are supported. If not present, then Shared subscriptions are + * supported. + *

+ * + * @return A boolean defining whether Shared Subscriptions are supported. + */ + public boolean isSharedSubscriptionAvailable() { + if(sharedSubscriptionAvailable == null || sharedSubscriptionAvailable == true) { + return true; + } else { + return false; + } + } + + /** + * Shared Subscription Available. See + * {@link MqttProperties#isSharedSubscriptionAvailable()} + * + * @param sharedSubscriptionAvailable + * A boolean defining whether Shared Subscriptions are supported. + */ + public void setSharedSubscriptionAvailable(boolean sharedSubscriptionAvailable) { + this.sharedSubscriptionAvailable = sharedSubscriptionAvailable; + } + + /** + * Session Expiry Interval. + * + *

+ * The Session Expiry Interval in seconds. If the Session Expiry interval is + * absent in the CONNECT packet, the default value of 0 is used, however the + * Server MAY set this value in the CONNACK packet if it is not present in the + * CONNECT. + *

+ * + * @return The Session Expiry Interval in seconds. + */ + public Long getSessionExpiryInterval() { + return sessionExpiryInterval; + } + + /** + * Session Expiry Interval. See + * {@link MqttProperties#getSessionExpiryInterval()} + * + * @param sessionExpiryInterval + * The Session Expiry Interval in seconds. + */ + public void setSessionExpiryInterval(Long sessionExpiryInterval) { + MqttDataTypes.validateFourByteInt(sessionExpiryInterval); + this.sessionExpiryInterval = sessionExpiryInterval; + } + + /** + * Authentication Method. + * + *

+ * A String containing the name of the authentication method. + *

+ * + * @return The Authentication Method + */ + public String getAuthenticationMethod() { + return authenticationMethod; + } + + /** + * Authentication Method. See {@link MqttProperties#getAuthenticationMethod()} + * + * @param authenticationMethod + * The Authentication Method + */ + public void setAuthenticationMethod(String authenticationMethod) { + this.authenticationMethod = authenticationMethod; + } + + /** + * Authentication Data. + * + *

+ * Binary Data containing the Authentication data defined by the + * {@link MqttProperties#getAuthenticationMethod()} + *

+ * + * @return The Authentication Data. + */ + public byte[] getAuthenticationData() { + return authenticationData; + } + + /** + * Authentication Data. See {@link MqttProperties#getAuthenticationData()} + * + * @param authenticationData + * The Authentication Data. + */ + public void setAuthenticationData(byte[] authenticationData) { + this.authenticationData = authenticationData; + } + + /** + * Reason String. + * + *

+ * A String representing the reason associated with this response. This Reason + * String is a human readable string designed for diagnostics and SHOULD NOT be + * parsed by the client. + *

+ * + * @return The Reason String + */ + public String getReasonString() { + return reasonString; + } + + /** + * Reason String. See {@link MqttProperties#getReasonString()} + * + * @param reasonString + * The Reason String + */ + public void setReasonString(String reasonString) { + this.reasonString = reasonString; + } + + /** + * User Properties. + * + *

+ * A {@link List} of {@link UserProperty} which are String Key Value Pairs. + * These properties can be used to provide additional information to the Server + * or Client including diagnostic information. + *

+ * + * @return a {@link List} of {@link UserProperty} Objects. + */ + public List getUserProperties() { + return userProperties; + } + + /** + * User Properties. See {@link MqttProperties#getUserProperties()} + * + * @param userProperties + * a {@link List} of {@link UserProperty} Objects. + */ + public void setUserProperties(List userProperties) { + this.userProperties = userProperties; + } + + /** + * Payload Format. + * + *

+ * A flag defining the payload format: + *

+ *
    + *
  • False: Unspecified bytes, which is equivalent to not sending the payload + * format.
  • + *
  • True: A UTF-8 Encoded Character Data.
  • + *
+ * + * + * @return The Payload Format flag. + */ + public boolean getPayloadFormat() { + return payloadFormat; + } + + /** + * Payload Format. See {@link MqttProperties#getPayloadFormat()} + * + * @param payloadFormat + * true if payload format is set, otherwise false + */ + public void setPayloadFormat(boolean payloadFormat) { + this.payloadFormat = payloadFormat; + } + + /** + * Message Expiry Interval. + * + *

+ * If present, this value is the lifetime of the Application Message in seconds. + * If the Message Expiry Interval has passed and the Server has not managed to + * start onward delivery to a matching subscriber, then it MUST delete the copy + * of the message for that subscriber. + *

+ * + * @return The Message Expiry Interval in seconds. + */ + public Long getMessageExpiryInterval() { + return messageExpiryInterval; + } + + /** + * Message Expiry Interval. See + * {@link MqttProperties#getMessageExpiryInterval()} + * + * @param messageExpiryInterval + * The Message Expiry Interval in seconds. + */ + public void setMessageExpiryInterval(Long messageExpiryInterval) { + MqttDataTypes.validateFourByteInt(messageExpiryInterval); + this.messageExpiryInterval = messageExpiryInterval; + } + + /** + * Content Type. + * + *

+ * A UTF-8 Encoded String describing the content of the Application Message. + *

+ * + * @return The Content Type. + */ + public String getContentType() { + return contentType; + } + + /** + * Content Type. See {@link MqttProperties#getContentType()} + * + * @param contentType + * The Content Type. + */ + public void setContentType(String contentType) { + this.contentType = contentType; + } + + /** + * Response Topic. + * + *

+ * A UTF-8 Encoded String used as the Topic Name for a response message. + *

+ * + * @return The Response Topic + */ + public String getResponseTopic() { + return responseTopic; + } + + /** + * Response Topic. See {@link MqttProperties#getResponseTopic()} + * + * @param responseTopic + * The Response Topic. + */ + public void setResponseTopic(String responseTopic) { + if(responseTopic != null) { + MqttTopicValidator.validate(responseTopic, false, true); + } + this.responseTopic = responseTopic; + } + + /** + * Correlation Data. + * + *

+ * Binary data used by the Sender of the Request Message to identify which + * request the Response Message is for when it is received. + *

+ * + * @return The Correlation Data. + */ + public byte[] getCorrelationData() { + return correlationData; + } + + /** + * Correlation Data. See {@link MqttProperties#getCorrelationData()} + * + * @param correlationData + * The Correlation Data. + */ + public void setCorrelationData(byte[] correlationData) { + this.correlationData = correlationData; + } + + /** + * Subscription Identifiers. (Publish Only) + * + *

+ * The Subscription Identifiers are associated with any subscription created or + * modified as the result of a SUBSCRIBE packet. If a subscription was made with + * a Subscription Identifier, then any incoming messages that match that + * subscription will contain the associated subscription identifier, if the + * incoming message matches multiple subscriptions made by the same client, then + * it will contain a list of all associated subscription identifiers. This + * property is ONLY for PUBLISH packets. For a Subscription Identifier sent in a + * SUBSCRIBE packet, see {@link MqttProperties#getSubscriptionIdentifier()} + *

+ * + * @return A {@link List} of Subscription Identifiers. + */ + public List getSubscriptionIdentifiers() { + return publishSubscriptionIdentifiers; + } + + /** + * Subscription Identifiers. (Publish Only) See + * {@link MqttProperties#getSubscriptionIdentifiers()} + * + * @param subscriptionIdentifiers + * A {@link List} of Subscription Identifiers. + */ + public void setSubscriptionIdentifiers(List subscriptionIdentifiers) { + for (Integer subId : subscriptionIdentifiers) { + MqttDataTypes.validateVariableByteInt(subId); + } + this.publishSubscriptionIdentifiers = subscriptionIdentifiers; + } + + /** + * Subscription Identifier. (Subscribe Only) + * + *

+ * The Subscription identifier field can be set on a SUBSCRIBE packet and will + * be returned with any incoming PUBLISH packets that match the associated + * subscription. This property is ONLY for SUBSCRIBE packets. For Subscription + * Identifier(s) sent in a PUBLISH packet, see + * {@link MqttProperties#getSubscriptionIdentifiers()} + *

+ * + * @return The Subscription Identifier. + */ + public Integer getSubscriptionIdentifier() { + return subscribeSubscriptionIdentifier; + } + + /** + * Subscription Identifier. (Subscribe Only) See + * {@link MqttProperties#getSubscriptionIdentifier()} + * + * @param subscriptionIdentifier + * The Subscription Identifier. + */ + public void setSubscriptionIdentifier(Integer subscriptionIdentifier) { + MqttDataTypes.validateVariableByteInt(subscriptionIdentifier); + this.subscribeSubscriptionIdentifier = subscriptionIdentifier; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("MqttProperties [validProperties=" + validProperties); + if (requestResponseInfo != null) { + sb.append(", requestResponseInfo=" + requestResponseInfo); + } + if (requestProblemInfo != null) { + sb.append(", requestProblemInfo=" + requestProblemInfo); + } + if (willDelayInterval != null) { + sb.append(", willDelayInterval=" + willDelayInterval); + } + if (receiveMaximum != null) { + sb.append(", receiveMaximum=" + receiveMaximum); + } + if (maximumQoS != null) { + sb.append(", maximumQoS=" + maximumQoS); + } + if (maximumPacketSize != null) { + sb.append(", maximumPacketSize=" + maximumPacketSize); + } + if (retainAvailable != null) { + sb.append(", retainAvailable=" + retainAvailable); + } + if (assignedClientIdentifier != null) { + sb.append(", assignedClientIdentifier=" + assignedClientIdentifier); + } + if (topicAliasMaximum != null) { + sb.append(", topicAliasMaximum=" + topicAliasMaximum); + } + if (topicAlias != null) { + sb.append(", topicAlias=" + topicAlias); + } + if (serverKeepAlive != null) { + sb.append(", serverKeepAlive=" + serverKeepAlive); + } + if (responseInfo != null) { + sb.append(", responseInfo=" + responseInfo); + } + if (serverReference != null) { + sb.append(", serverReference=" + serverReference); + } + if (wildcardSubscriptionsAvailable != null) { + sb.append(", wildcardSubscriptionsAvailable=" + wildcardSubscriptionsAvailable); + } + if (subscriptionIdentifiersAvailable != null) { + sb.append(", subscriptionIdentifiersAvailable=" + subscriptionIdentifiersAvailable); + } + if (sharedSubscriptionAvailable != null) { + sb.append(", sharedSubscriptionAvailable=" + sharedSubscriptionAvailable); + } + if (sessionExpiryInterval != null) { + sb.append(", sessionExpiryInterval=" + sessionExpiryInterval); + } + if (authenticationMethod != null) { + sb.append(", authenticationMethod=" + authenticationMethod); + } + if (authenticationData != null) { + sb.append(", authenticationData=" + Arrays.toString(authenticationData)); + } + if (reasonString != null) { + sb.append(", reasonString=" + reasonString); + } + if (userProperties != null && userProperties.size() != 0) { + sb.append(", userProperties=" + userProperties); + } + if (payloadFormat) { + sb.append(", isUTF8=" + payloadFormat); + } + if (messageExpiryInterval != null) { + sb.append(", messageExpiryInterval=" + messageExpiryInterval); + } + if (contentType != null) { + sb.append(", contentType=" + contentType); + } + if (responseTopic != null) { + sb.append(", responseTopic=" + responseTopic); + } + if (correlationData != null) { + sb.append(", correlationData=" + Arrays.toString(correlationData)); + } + if (publishSubscriptionIdentifiers.size() != 0) { + sb.append(", subscriptionIdentifiers=" + publishSubscriptionIdentifiers); + } + sb.append("]"); + return (sb.toString()); + } + +} diff --git a/src/main/java/org/eclipse/paho/mqttv5/common/packet/MqttPubAck.java b/src/main/java/org/eclipse/paho/mqttv5/common/packet/MqttPubAck.java new file mode 100644 index 0000000..c5fc1e3 --- /dev/null +++ b/src/main/java/org/eclipse/paho/mqttv5/common/packet/MqttPubAck.java @@ -0,0 +1,116 @@ +/******************************************************************************* + * Copyright (c) 2016 IBM Corp. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * and Eclipse Distribution License v1.0 which accompany this distribution. + * + * The Eclipse Public License is available at + * https://www.eclipse.org/legal/epl-2.0 + * and the Eclipse Distribution License is available at + * https://www.eclipse.org/org/documents/edl-v10.php + * + * Contributors: + * Dave Locke - Original MQTTv3 implementation + * James Sutton - Initial MQTTv5 implementation + */ +package org.eclipse.paho.mqttv5.common.packet; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; + +import org.eclipse.paho.mqttv5.common.MqttException; +import org.eclipse.paho.mqttv5.common.packet.util.CountingInputStream; + +public class MqttPubAck extends MqttAck { + + private static final int[] validReturnCodes = { MqttReturnCode.RETURN_CODE_SUCCESS, + MqttReturnCode.RETURN_CODE_NO_MATCHING_SUBSCRIBERS, MqttReturnCode.RETURN_CODE_UNSPECIFIED_ERROR, + MqttReturnCode.RETURN_CODE_IMPLEMENTATION_SPECIFIC_ERROR, MqttReturnCode.RETURN_CODE_NOT_AUTHORIZED, + MqttReturnCode.RETURN_CODE_TOPIC_NAME_INVALID, MqttReturnCode.RETURN_CODE_QUOTA_EXCEEDED, + MqttReturnCode.RETURN_CODE_PAYLOAD_FORMAT_INVALID }; + + private static final Byte[] validProperties = { MqttProperties.REASON_STRING_IDENTIFIER, + MqttProperties.USER_DEFINED_PAIR_IDENTIFIER }; + + private MqttProperties properties; + + public MqttPubAck(byte[] data) throws IOException, MqttException { + super(MqttWireMessage.MESSAGE_TYPE_PUBACK); + properties = new MqttProperties(validProperties); + ByteArrayInputStream bais = new ByteArrayInputStream(data); + CountingInputStream counter = new CountingInputStream(bais); + DataInputStream dis = new DataInputStream(counter); + msgId = dis.readUnsignedShort(); + long remainder = (long) data.length - counter.getCounter(); + if (remainder >= 1) { + reasonCode = dis.readUnsignedByte(); + validateReturnCode(reasonCode, validReturnCodes); + } else { + reasonCode = 0; + } + if (remainder >= 4) { + this.properties.decodeProperties(dis); + } + dis.close(); + } + + public MqttPubAck(int returnCode, int msgId, MqttProperties properties) throws MqttException { + super(MqttWireMessage.MESSAGE_TYPE_PUBACK); + this.reasonCode = returnCode; + this.msgId = msgId; + if (properties != null) { + this.properties = properties; + } else { + this.properties = new MqttProperties(); + } + this.properties.setValidProperties(validProperties); + validateReturnCode(returnCode, validReturnCodes); + } + + @Override + protected byte[] getVariableHeader() throws MqttException { + try { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + DataOutputStream outputStream = new DataOutputStream(baos); + + // Encode the Message ID + outputStream.writeShort(msgId); + + byte[] identifierValueFieldsByteArray = this.properties.encodeProperties(); + + if (reasonCode != MqttReturnCode.RETURN_CODE_SUCCESS && identifierValueFieldsByteArray.length == 1) { + // Encode the Return Code + outputStream.write((byte) reasonCode); + } else if (reasonCode != MqttReturnCode.RETURN_CODE_SUCCESS || identifierValueFieldsByteArray.length > 1) { + // Encode the Return Code + outputStream.write((byte) reasonCode); + // Write Identifier / Value Fields + outputStream.write(identifierValueFieldsByteArray); + } + + outputStream.flush(); + return baos.toByteArray(); + } catch (IOException ioe) { + throw new MqttException(ioe); + } + } + + public int getReturnCode() { + return reasonCode; + } + + @Override + public MqttProperties getProperties() { + return this.properties; + } + + @Override + public String toString() { + return "MqttPubAck [returnCode=" + reasonCode + ", properties=" + properties + "]"; + } + +} diff --git a/src/main/java/org/eclipse/paho/mqttv5/common/packet/MqttPubComp.java b/src/main/java/org/eclipse/paho/mqttv5/common/packet/MqttPubComp.java new file mode 100644 index 0000000..de568ce --- /dev/null +++ b/src/main/java/org/eclipse/paho/mqttv5/common/packet/MqttPubComp.java @@ -0,0 +1,118 @@ +/******************************************************************************* + * Copyright (c) 2016 IBM Corp. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * and Eclipse Distribution License v1.0 which accompany this distribution. + * + * The Eclipse Public License is available at + * https://www.eclipse.org/legal/epl-2.0 + * and the Eclipse Distribution License is available at + * https://www.eclipse.org/org/documents/edl-v10.php + * + * Contributors: + * Dave Locke - Original MQTTv3 implementation + * James Sutton - Initial MQTTv5 implementation + */ +package org.eclipse.paho.mqttv5.common.packet; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; + +import org.eclipse.paho.mqttv5.common.MqttException; +import org.eclipse.paho.mqttv5.common.packet.util.CountingInputStream; + +public class MqttPubComp extends MqttAck { + + private static final int[] validReturnCodes = { MqttReturnCode.RETURN_CODE_SUCCESS, + MqttReturnCode.RETURN_CODE_PACKET_ID_NOT_FOUND }; + + private static final Byte[] validProperties = { MqttProperties.REASON_STRING_IDENTIFIER, + MqttProperties.USER_DEFINED_PAIR_IDENTIFIER }; + + // Fields + private MqttProperties properties; + + public MqttPubComp(byte[] data) throws IOException, MqttException { + super(MqttWireMessage.MESSAGE_TYPE_PUBCOMP); + properties = new MqttProperties(validProperties); + ByteArrayInputStream bais = new ByteArrayInputStream(data); + CountingInputStream counter = new CountingInputStream(bais); + DataInputStream dis = new DataInputStream(counter); + msgId = dis.readUnsignedShort(); + long remainder = (long) data.length - counter.getCounter(); + if (remainder >= 1) { + reasonCode = dis.readUnsignedByte(); + validateReturnCode(reasonCode, validReturnCodes); + } else { + reasonCode = 0; + } + if (remainder >= 4) { + this.properties.decodeProperties(dis); + } + dis.close(); + } + + public MqttPubComp(int returnCode, int msgId, MqttProperties properties) throws MqttException { + super(MqttWireMessage.MESSAGE_TYPE_PUBCOMP); + validateReturnCode(returnCode, validReturnCodes); + this.reasonCode = returnCode; + this.msgId = msgId; + if (properties != null) { + this.properties = properties; + } else { + this.properties = new MqttProperties(); + } + this.properties.setValidProperties(validProperties); + } + + @Override + protected byte[] getVariableHeader() throws MqttException { + try { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + DataOutputStream outputStream = new DataOutputStream(baos); + + // Encode the Message ID + outputStream.writeShort(msgId); + + byte[] identifierValueFieldsByteArray = this.properties.encodeProperties(); + + if (reasonCode != MqttReturnCode.RETURN_CODE_SUCCESS && identifierValueFieldsByteArray.length == 1) { + // Encode the Return Code + outputStream.write((byte) reasonCode); + } else if (reasonCode != MqttReturnCode.RETURN_CODE_SUCCESS || identifierValueFieldsByteArray.length > 1) { + // Encode the Return Code + outputStream.write((byte) reasonCode); + // Write Identifier / Value Fields + outputStream.write(identifierValueFieldsByteArray); + } + + outputStream.flush(); + return baos.toByteArray(); + } catch (IOException ioe) { + throw new MqttException(ioe); + } + } + + public int getReturnCode() { + return reasonCode; + } + + public void setReturnCode(int returnCode) { + this.reasonCode = returnCode; + } + + @Override + public MqttProperties getProperties() { + return this.properties; + } + + @Override + public String toString() { + return "MqttPubComp [returnCode=" + reasonCode + ", properties=" + properties + "]"; + } + +} diff --git a/src/main/java/org/eclipse/paho/mqttv5/common/packet/MqttPubRec.java b/src/main/java/org/eclipse/paho/mqttv5/common/packet/MqttPubRec.java new file mode 100644 index 0000000..88353ac --- /dev/null +++ b/src/main/java/org/eclipse/paho/mqttv5/common/packet/MqttPubRec.java @@ -0,0 +1,121 @@ +/******************************************************************************* + * Copyright (c) 2016 IBM Corp. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * and Eclipse Distribution License v1.0 which accompany this distribution. + * + * The Eclipse Public License is available at + * https://www.eclipse.org/legal/epl-2.0 + * and the Eclipse Distribution License is available at + * https://www.eclipse.org/org/documents/edl-v10.php + * + * Contributors: + * Dave Locke - Original MQTTv3 implementation + * James Sutton - Initial MQTTv5 implementation + */ +package org.eclipse.paho.mqttv5.common.packet; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; + +import org.eclipse.paho.mqttv5.common.MqttException; +import org.eclipse.paho.mqttv5.common.packet.util.CountingInputStream; + +public class MqttPubRec extends MqttAck { + + private static final int[] validReturnCodes = { MqttReturnCode.RETURN_CODE_SUCCESS, + MqttReturnCode.RETURN_CODE_NO_MATCHING_SUBSCRIBERS, MqttReturnCode.RETURN_CODE_UNSPECIFIED_ERROR, + MqttReturnCode.RETURN_CODE_IMPLEMENTATION_SPECIFIC_ERROR, MqttReturnCode.RETURN_CODE_NOT_AUTHORIZED, + MqttReturnCode.RETURN_CODE_TOPIC_NAME_INVALID, MqttReturnCode.RETURN_CODE_PACKET_ID_IN_USE, + MqttReturnCode.RETURN_CODE_QUOTA_EXCEEDED, MqttReturnCode.RETURN_CODE_PAYLOAD_FORMAT_INVALID }; + + private static final Byte[] validProperties = { MqttProperties.REASON_STRING_IDENTIFIER, + MqttProperties.USER_DEFINED_PAIR_IDENTIFIER }; + + // Fields + private MqttProperties properties; + + public MqttPubRec(byte[] data) throws IOException, MqttException { + super(MqttWireMessage.MESSAGE_TYPE_PUBREC); + properties = new MqttProperties(validProperties); + ByteArrayInputStream bais = new ByteArrayInputStream(data); + CountingInputStream counter = new CountingInputStream(bais); + DataInputStream dis = new DataInputStream(counter); + msgId = dis.readUnsignedShort(); + long remainder = (long) data.length - counter.getCounter(); + if (remainder >= 1) { + reasonCode = dis.readUnsignedByte(); + validateReturnCode(reasonCode, validReturnCodes); + } else { + reasonCode = 0; + } + if (remainder >= 4) { + this.properties.decodeProperties(dis); + } + dis.close(); + } + + public MqttPubRec(int returnCode, int msgId, MqttProperties properties) throws MqttException { + super(MqttWireMessage.MESSAGE_TYPE_PUBREC); + validateReturnCode(returnCode, validReturnCodes); + this.reasonCode = returnCode; + this.msgId = msgId; + if (properties != null) { + this.properties = properties; + } else { + this.properties = new MqttProperties(); + } + this.properties.setValidProperties(validProperties); + } + + @Override + protected byte[] getVariableHeader() throws MqttException { + try { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + DataOutputStream outputStream = new DataOutputStream(baos); + + // Encode the Message ID + outputStream.writeShort(msgId); + + byte[] identifierValueFieldsByteArray = this.properties.encodeProperties(); + + if (reasonCode != MqttReturnCode.RETURN_CODE_SUCCESS && identifierValueFieldsByteArray.length == 1) { + // Encode the Return Code + outputStream.write((byte) reasonCode); + } else if (reasonCode != MqttReturnCode.RETURN_CODE_SUCCESS || identifierValueFieldsByteArray.length > 1) { + // Encode the Return Code + outputStream.write((byte) reasonCode); + // Write Identifier / Value Fields + outputStream.write(identifierValueFieldsByteArray); + } + + outputStream.flush(); + return baos.toByteArray(); + } catch (IOException ioe) { + throw new MqttException(ioe); + } + } + + public int getReturnCode() { + return reasonCode; + } + + public void setReturnCode(int returnCode) { + this.reasonCode = returnCode; + } + + @Override + public MqttProperties getProperties() { + return this.properties; + } + + @Override + public String toString() { + return "MqttPubRec [returnCode=" + reasonCode + ", properties=" + properties + "]"; + } + +} diff --git a/src/main/java/org/eclipse/paho/mqttv5/common/packet/MqttPubRel.java b/src/main/java/org/eclipse/paho/mqttv5/common/packet/MqttPubRel.java new file mode 100644 index 0000000..b45176d --- /dev/null +++ b/src/main/java/org/eclipse/paho/mqttv5/common/packet/MqttPubRel.java @@ -0,0 +1,123 @@ +/******************************************************************************* + * Copyright (c) 2016 IBM Corp. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * and Eclipse Distribution License v1.0 which accompany this distribution. + * + * The Eclipse Public License is available at + * https://www.eclipse.org/legal/epl-2.0 + * and the Eclipse Distribution License is available at + * https://www.eclipse.org/org/documents/edl-v10.php + * + * Contributors: + * Dave Locke - Original MQTTv3 implementation + * James Sutton - Initial MQTTv5 implementation + */ +package org.eclipse.paho.mqttv5.common.packet; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; + +import org.eclipse.paho.mqttv5.common.MqttException; +import org.eclipse.paho.mqttv5.common.packet.util.CountingInputStream; + +public class MqttPubRel extends MqttPersistableWireMessage { + + private static final int[] validReturnCodes = { MqttReturnCode.RETURN_CODE_SUCCESS, + MqttReturnCode.RETURN_CODE_PACKET_ID_NOT_FOUND }; + + private static final Byte[] validProperties = { MqttProperties.REASON_STRING_IDENTIFIER, + MqttProperties.USER_DEFINED_PAIR_IDENTIFIER }; + + // Fields + private MqttProperties properties; + + public MqttPubRel(byte[] data) throws IOException, MqttException { + super(MqttWireMessage.MESSAGE_TYPE_PUBREL); + properties = new MqttProperties(validProperties); + ByteArrayInputStream bais = new ByteArrayInputStream(data); + CountingInputStream counter = new CountingInputStream(bais); + DataInputStream dis = new DataInputStream(counter); + msgId = dis.readUnsignedShort(); + long remainder = (long) data.length - counter.getCounter(); + if (remainder >= 1) { + reasonCode = dis.readUnsignedByte(); + validateReturnCode(reasonCode, validReturnCodes); + } else { + reasonCode = 0; + } + if (remainder >= 4) { + this.properties.decodeProperties(dis); + } + dis.close(); + } + + public MqttPubRel(int returnCode, int msgId, MqttProperties properties) throws MqttException { + super(MqttWireMessage.MESSAGE_TYPE_PUBREL); + validateReturnCode(returnCode, validReturnCodes); + this.reasonCode = returnCode; + this.msgId = msgId; + if (properties != null) { + this.properties = properties; + } else { + this.properties = new MqttProperties(); + } + this.properties.setValidProperties(validProperties); + } + + @Override + protected byte[] getVariableHeader() throws MqttException { + try { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + DataOutputStream outputStream = new DataOutputStream(baos); + + // Encode the Message ID + outputStream.writeShort(msgId); + + byte[] identifierValueFieldsByteArray = this.properties.encodeProperties(); + + if (reasonCode != MqttReturnCode.RETURN_CODE_SUCCESS && identifierValueFieldsByteArray.length == 1) { + // Encode the Return Code + outputStream.write((byte) reasonCode); + } else if (reasonCode != MqttReturnCode.RETURN_CODE_SUCCESS || identifierValueFieldsByteArray.length > 1) { + // Encode the Return Code + outputStream.write((byte) reasonCode); + // Write Identifier / Value Fields + outputStream.write(identifierValueFieldsByteArray); + } + + outputStream.flush(); + return baos.toByteArray(); + } catch (IOException ioe) { + throw new MqttException(ioe); + } + } + + @Override + protected byte getMessageInfo() { + return (byte) (2 | (this.duplicate ? 8 : 0)); + } + + public int getReturnCode() { + return reasonCode; + } + + public void setReturnCode(int returnCode) { + this.reasonCode = returnCode; + } + + @Override + public MqttProperties getProperties() { + return this.properties; + } + + @Override + public String toString() { + return "MqttPubRel [returnCode=" + reasonCode + ", properties=" + properties + "]"; + } + +} diff --git a/src/main/java/org/eclipse/paho/mqttv5/common/packet/MqttPublish.java b/src/main/java/org/eclipse/paho/mqttv5/common/packet/MqttPublish.java new file mode 100644 index 0000000..bd2d70e --- /dev/null +++ b/src/main/java/org/eclipse/paho/mqttv5/common/packet/MqttPublish.java @@ -0,0 +1,242 @@ +/******************************************************************************* + * Copyright (c) 2016 IBM Corp. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * and Eclipse Distribution License v1.0 which accompany this distribution. + * + * The Eclipse Public License is available at + * https://www.eclipse.org/legal/epl-2.0 + * and the Eclipse Distribution License is available at + * https://www.eclipse.org/org/documents/edl-v10.php + * + * Contributors: + * Dave Locke - Original MQTTv3 implementation + * James Sutton - Initial MQTTv5 implementation + */ +package org.eclipse.paho.mqttv5.common.packet; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.io.UnsupportedEncodingException; + +import org.eclipse.paho.mqttv5.common.MqttException; +import org.eclipse.paho.mqttv5.common.MqttMessage; +import org.eclipse.paho.mqttv5.common.packet.util.CountingInputStream; + +/** + * An on-the-wire representation of an MQTT Publish message. + */ +public class MqttPublish extends MqttPersistableWireMessage { + + private static final Byte[] validProperties = { MqttProperties.PAYLOAD_FORMAT_INDICATOR_IDENTIFIER, + MqttProperties.MESSAGE_EXPIRY_INTERVAL_IDENTIFIER, MqttProperties.TOPIC_ALIAS_IDENTIFIER, + MqttProperties.RESPONSE_TOPIC_IDENTIFIER, MqttProperties.CORRELATION_DATA_IDENTIFIER, + MqttProperties.USER_DEFINED_PAIR_IDENTIFIER, MqttProperties.CONTENT_TYPE_IDENTIFIER, + MqttProperties.SUBSCRIPTION_IDENTIFIER_MULTI, MqttProperties.SUBSCRIPTION_IDENTIFIER }; + + private MqttProperties properties; + + // Fields + private byte[] payload; + private int qos = 1; + private boolean retained = false; + private boolean dup = false; + private String topicName; + + /** + * Constructs a new MqttPublish message + * + * @param topic + * - The Destination Topic. + * @param message + * - The Message being sent. + * @param properties + * - The {@link MqttProperties} for the packet. + */ + public MqttPublish(String topic, MqttMessage message, MqttProperties properties) { + super(MqttWireMessage.MESSAGE_TYPE_PUBLISH); + this.topicName = topic; + this.payload = message.getPayload(); + this.qos = message.getQos(); + this.dup = message.isDuplicate(); + this.retained = message.isRetained(); + if (properties != null) { + this.properties = properties; + } else { + this.properties = new MqttProperties(); + } + this.properties.setValidProperties(validProperties); + } + + /** + * Constructs a new MqttPublish message from a byte array + * + * @param info + * - Info Byte + * @param data + * - The variable header and payload bytes. + * @throws IOException + * - if an exception occurs when decoding an input stream + * @throws MqttException + * - If an exception occurs decoding this packet + */ + public MqttPublish(byte info, byte[] data) throws MqttException, IOException { + super(MqttWireMessage.MESSAGE_TYPE_PUBLISH); + this.properties = new MqttProperties(validProperties); + this.qos = (info >> 1) & 0x03; + if ((info & 0x01) == 0x01) { + this.retained = true; + } + + if ((info & 0x08) == 0x08) { + this.dup = true; + } + + ByteArrayInputStream bais = new ByteArrayInputStream(data); + CountingInputStream counter = new CountingInputStream(bais); + DataInputStream dis = new DataInputStream(counter); + + topicName = MqttDataTypes.decodeUTF8(dis); + if (this.qos > 0) { + msgId = dis.readUnsignedShort(); + } + this.properties.decodeProperties(dis); + this.payload = new byte[data.length - counter.getCounter()]; + dis.readFully(this.payload); + dis.close(); + } + + @Override + protected byte[] getVariableHeader() throws MqttException { + try { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + DataOutputStream dos = new DataOutputStream(baos); + + // If we are using a Topic Alias, then the topic should be empty + if (topicName != null) { + MqttDataTypes.encodeUTF8(dos, topicName); + } else { + MqttDataTypes.encodeUTF8(dos, ""); + } + + if (this.qos > 0) { + dos.writeShort(msgId); + } + // Write Identifier / Value Fields + byte[] identifierValueFieldsArray = this.properties.encodeProperties(); + dos.write(identifierValueFieldsArray); + dos.flush(); + return baos.toByteArray(); + } catch (IOException ioe) { + throw new MqttException(ioe); + } + } + + @Override + protected byte getMessageInfo() { + byte info = (byte) (this.qos << 1); + if (this.retained) { + info |= 0x01; + } + if (this.dup || duplicate) { + info |= 0x08; + } + return info; + } + + @Override + public byte[] getPayload() { + return this.payload; + } + + @Override + public int getPayloadLength() { + if (this.payload != null) { + return this.payload.length; + } else { + return 0; + } + } + + @Override + public boolean isMessageIdRequired() { + // all publishes require a message ID as it's used as the key to the + // token store + return true; + } + + public MqttMessage getMessage() { + MqttMessage message = new MqttMessage(payload, qos, retained, properties); + return message; + } + + public void setMessage(MqttMessage message) { + this.payload = message.getPayload(); + this.qos = message.getQos(); + this.dup = message.isDuplicate(); + this.retained = message.isRetained(); + } + + public String getTopicName() { + return topicName; + } + + public int getQoS() { + return qos; + } + + public void setTopicName(String topicName) { + this.topicName = topicName; + } + + @Override + public MqttProperties getProperties() { + return this.properties; + } + + @Override + public String toString() { + // Convert the first few bytes of the payload into a hex string + StringBuilder hex = new StringBuilder(); + int limit = Math.min(payload.length, 20); + for (int i = 0; i < limit; i++) { + byte b = payload[i]; + String ch = Integer.toHexString(b); + if (ch.length() == 1) { + ch = "0" + ch; + } + hex.append(ch); + } + + // It will not always be possible to convert the binary payload into + // characters, but never-the-less we attempt to do this as it is often + // useful. + String string = null; + try { + string = new String(payload, 0, limit, "UTF-8"); + } catch (UnsupportedEncodingException uee) { + string = "?"; + } + + StringBuilder sb = new StringBuilder(); + sb.append("MqttPublish ["); + sb.append(", qos=").append(this.qos); + if (this.qos > 0) { + sb.append(", messageId=").append(msgId); + } + sb.append(", retained=").append(this.retained); + sb.append(", duplicate=").append(duplicate); + sb.append(", topic=").append(topicName); + sb.append(", payload=[hex=").append(hex); + sb.append(", utf8=").append(string); + sb.append(", length=").append(payload.length).append("]"); + sb.append(", properties=").append(this.properties.toString()); + + return sb.toString(); + } + +} diff --git a/src/main/java/org/eclipse/paho/mqttv5/common/packet/MqttReceivedMessage.java b/src/main/java/org/eclipse/paho/mqttv5/common/packet/MqttReceivedMessage.java new file mode 100644 index 0000000..c874d50 --- /dev/null +++ b/src/main/java/org/eclipse/paho/mqttv5/common/packet/MqttReceivedMessage.java @@ -0,0 +1,38 @@ +/******************************************************************************* + * Copyright (c) 2016 IBM Corp. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * and Eclipse Distribution License v1.0 which accompany this distribution. + * + * The Eclipse Public License is available at + * https://www.eclipse.org/legal/epl-2.0 + * and the Eclipse Distribution License is available at + * https://www.eclipse.org/org/documents/edl-v10.php + * + * Contributors: + * Dave Locke - Original MQTTv3 implementation + * James Sutton - Initial MQTTv5 implementation + */ +package org.eclipse.paho.mqttv5.common.packet; + +import org.eclipse.paho.mqttv5.common.MqttMessage; + +public class MqttReceivedMessage extends MqttMessage { + + public void setMessageId(int messageId){ + super.setId(messageId); + } + + public int getMessageId(){ + return super.getId(); + } + + // This method exists here to get around the protected visibility of the + // super class method. + @Override + public void setDuplicate(boolean value) { + super.setDuplicate(value); + } + +} diff --git a/src/main/java/org/eclipse/paho/mqttv5/common/packet/MqttReturnCode.java b/src/main/java/org/eclipse/paho/mqttv5/common/packet/MqttReturnCode.java new file mode 100644 index 0000000..679a625 --- /dev/null +++ b/src/main/java/org/eclipse/paho/mqttv5/common/packet/MqttReturnCode.java @@ -0,0 +1,77 @@ +/******************************************************************************* + * Copyright (c) 2016 IBM Corp. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * and Eclipse Distribution License v1.0 which accompany this distribution. + * + * The Eclipse Public License is available at + * https://www.eclipse.org/legal/epl-2.0 + * and the Eclipse Distribution License is available at + * https://www.eclipse.org/org/documents/edl-v10.php + * + * Contributors: + * James Sutton - Initial MQTTv5 implementation + */ +package org.eclipse.paho.mqttv5.common.packet; + +/** + * Reference of MQTT v5 Return Codes - 2.4 + */ +public class MqttReturnCode { + + /** Success and general Return Codes **/ + public static final int RETURN_CODE_SUCCESS = 0x00; // 0 + public static final int RETURN_CODE_MAX_QOS_0 = 0x00; // 0 + public static final int RETURN_CODE_MAX_QOS_1 = 0x01; // 1 + public static final int RETURN_CODE_MAX_QOS_2 = 0x02; // 2 + public static final int RETURN_CODE_DISCONNECT_WITH_WILL_MESSAGE = 0x04; // 4 + public static final int RETURN_CODE_NO_MATCHING_SUBSCRIBERS = 0x10; // 16 + public static final int RETURN_CODE_NO_SUBSCRIPTION_EXISTED = 0x11; // 17 + public static final int RETURN_CODE_CONTINUE_AUTHENTICATION = 0x18; // 24 + public static final int RETURN_CODE_RE_AUTHENTICATE = 0x19; // 25 + + /** Error Return Codes **/ + public static final int RETURN_CODE_UNSPECIFIED_ERROR = 0x80; // 128 + public static final int RETURN_CODE_MALFORMED_CONTROL_PACKET = 0x81; // 129 + public static final int RETURN_CODE_PROTOCOL_ERROR = 0x82; // 130 + public static final int RETURN_CODE_IMPLEMENTATION_SPECIFIC_ERROR = 0x83; // 131 + public static final int RETURN_CODE_UNSUPPORTED_PROTOCOL_VERSION = 0x84; // 132 + public static final int RETURN_CODE_IDENTIFIER_NOT_VALID = 0x85; // 133 + public static final int RETURN_CODE_BAD_USERNAME_OR_PASSWORD = 0x86; // 134 + public static final int RETURN_CODE_NOT_AUTHORIZED = 0x87; // 135 + public static final int RETURN_CODE_SERVER_UNAVAILABLE = 0x88; // 136 + public static final int RETURN_CODE_SERVER_BUSY = 0x89; // 137 + public static final int RETURN_CODE_BANNED = 0x8A; // 138 + public static final int RETURN_CODE_SERVER_SHUTTING_DOWN = 0x8B; // 139 + public static final int RETURN_CODE_BAD_AUTHENTICATION = 0x8C; // 140 + public static final int RETURN_CODE_KEEP_ALIVE_TIMEOUT = 0x8D; // 141 + public static final int RETURN_CODE_SESSION_TAKEN_OVER = 0x8E; // 142 + public static final int RETURN_CODE_TOPIC_FILTER_NOT_VALID = 0x8F; // 143 + public static final int RETURN_CODE_TOPIC_NAME_INVALID = 0x90; // 144 + public static final int RETURN_CODE_PACKET_ID_IN_USE = 0x91; // 145 + public static final int RETURN_CODE_PACKET_ID_NOT_FOUND = 0x92; // 146 + public static final int RETURN_CODE_RECEIVE_MAXIMUM_EXCEEDED = 0x93; // 147 + public static final int RETURN_CODE_TOPIC_ALIAS_NOT_ACCEPTED = 0x94; // 148 + public static final int RETURN_CODE_PACKET_TOO_LARGE = 0x95; // 149 + public static final int RETURN_CODE_MESSAGE_RATE_TOO_HIGH = 0x96; // 150 + public static final int RETURN_CODE_QUOTA_EXCEEDED = 0x97; // 151 + public static final int RETURN_CODE_ADMINISTRITIVE_ACTION = 0x98; // 152 + public static final int RETURN_CODE_PAYLOAD_FORMAT_INVALID = 0x99; // 153 + public static final int RETURN_CODE_RETAIN_NOT_SUPPORTED = 0x9A; // 154 + public static final int RETURN_CODE_QOS_NOT_SUPPORTED = 0x9B; // 155 + public static final int RETURN_CODE_USE_ANOTHER_SERVER = 0x9C; // 156 + public static final int RETURN_CODE_SERVER_MOVED = 0x9D; // 157 + public static final int RETURN_CODE_SHARED_SUB_NOT_SUPPORTED = 0x9E; // 158 + public static final int RETURN_CODE_CONNECTION_RATE_EXCEEDED = 0x9F; // 159 + public static final int RETURN_CODE_MAXIMUM_CONNECT_TIME = 0xA0; // 160 + public static final int RETURN_CODE_SUB_IDENTIFIERS_NOT_SUPPORTED = 0xA1; // 161 + public static final int RETURN_CODE_WILDCARD_SUB_NOT_SUPPORTED = 0xA2; // 162 + + + private MqttReturnCode() { + throw new IllegalAccessError("Utility class"); + } + + +} diff --git a/src/main/java/org/eclipse/paho/mqttv5/common/packet/MqttSubAck.java b/src/main/java/org/eclipse/paho/mqttv5/common/packet/MqttSubAck.java new file mode 100644 index 0000000..f64ba8f --- /dev/null +++ b/src/main/java/org/eclipse/paho/mqttv5/common/packet/MqttSubAck.java @@ -0,0 +1,134 @@ +/******************************************************************************* + * Copyright (c) 2016 IBM Corp. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * and Eclipse Distribution License v1.0 which accompany this distribution. + * + * The Eclipse Public License is available at + * https://www.eclipse.org/legal/epl-2.0 + * and the Eclipse Distribution License is available at + * https://www.eclipse.org/org/documents/edl-v10.php + * + * Contributors: + * Dave Locke - Original MQTTv3 implementation + * James Sutton - Initial MQTTv5 implementation + */ +package org.eclipse.paho.mqttv5.common.packet; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.util.Arrays; + +import org.eclipse.paho.mqttv5.common.MqttException; +import org.eclipse.paho.mqttv5.common.packet.util.CountingInputStream; + +public class MqttSubAck extends MqttAck { + + private static final int[] validReturnCodes = { MqttReturnCode.RETURN_CODE_MAX_QOS_0, + MqttReturnCode.RETURN_CODE_MAX_QOS_1, MqttReturnCode.RETURN_CODE_MAX_QOS_2, + MqttReturnCode.RETURN_CODE_UNSPECIFIED_ERROR, MqttReturnCode.RETURN_CODE_IMPLEMENTATION_SPECIFIC_ERROR, + MqttReturnCode.RETURN_CODE_NOT_AUTHORIZED, MqttReturnCode.RETURN_CODE_TOPIC_FILTER_NOT_VALID, + MqttReturnCode.RETURN_CODE_PACKET_ID_IN_USE, MqttReturnCode.RETURN_CODE_SHARED_SUB_NOT_SUPPORTED }; + + private static final Byte[] validProperties = { MqttProperties.REASON_STRING_IDENTIFIER, + MqttProperties.USER_DEFINED_PAIR_IDENTIFIER }; + + // Fields + private MqttProperties properties; + + public MqttSubAck(byte[] data) throws IOException, MqttException { + super(MqttWireMessage.MESSAGE_TYPE_SUBACK); + properties = new MqttProperties(validProperties); + ByteArrayInputStream bais = new ByteArrayInputStream(data); + CountingInputStream counter = new CountingInputStream(bais); + DataInputStream inputStream = new DataInputStream(counter); + msgId = inputStream.readUnsignedShort(); + this.properties.decodeProperties(inputStream); + + int remainingLength = data.length - counter.getCounter(); + reasonCodes = new int[remainingLength]; + + for (int i = 0; i < remainingLength; i++) { + int returnCode = inputStream.readUnsignedByte(); + validateReturnCode(returnCode, validReturnCodes); + reasonCodes[i] = returnCode; + } + + inputStream.close(); + } + + public MqttSubAck(int[] returnCodes, MqttProperties properties) throws MqttException { + super(MqttWireMessage.MESSAGE_TYPE_SUBACK); + for (int returnCode : returnCodes) { + validateReturnCode(returnCode, validReturnCodes); + } + this.reasonCodes = returnCodes; + if (properties != null) { + this.properties = properties; + } else { + this.properties = new MqttProperties(); + } + this.properties.setValidProperties(validProperties); + } + + @Override + protected byte[] getVariableHeader() throws MqttException { + try { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + DataOutputStream outputStream = new DataOutputStream(baos); + + // Encode the Message ID + outputStream.writeShort(msgId); + + // Write Identifier / Value Fields + byte[] identifierValueFieldsByteArray = this.properties.encodeProperties(); + // Write Identifier / Value Fields + outputStream.write(identifierValueFieldsByteArray); + + outputStream.flush(); + return baos.toByteArray(); + } catch (IOException ioe) { + throw new MqttException(ioe); + } + } + + @Override + public byte[] getPayload() throws MqttException { + try { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + DataOutputStream outputStream = new DataOutputStream(baos); + + for (int returnCode : reasonCodes) { + outputStream.writeByte(returnCode); + } + + outputStream.flush(); + return baos.toByteArray(); + } catch (IOException ioe) { + throw new MqttException(ioe); + } + } + + public int[] getReturnCodes() { + return reasonCodes; + } + + public void setReturnCodes(int[] returnCodes) { + this.reasonCodes = returnCodes; + } + + @Override + public MqttProperties getProperties() { + return this.properties; + } + + @Override + public String toString() { + return "MqttSubAck [returnCodes=" + Arrays.toString(reasonCodes) + ", properties=" + properties + "]"; + } + +} diff --git a/src/main/java/org/eclipse/paho/mqttv5/common/packet/MqttSubscribe.java b/src/main/java/org/eclipse/paho/mqttv5/common/packet/MqttSubscribe.java new file mode 100644 index 0000000..103a4a4 --- /dev/null +++ b/src/main/java/org/eclipse/paho/mqttv5/common/packet/MqttSubscribe.java @@ -0,0 +1,217 @@ +/******************************************************************************* + * Copyright (c) 2016, 2019 IBM Corp. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * and Eclipse Distribution License v1.0 which accompany this distribution. + * + * The Eclipse Public License is available at + * https://www.eclipse.org/legal/epl-2.0 + * and the Eclipse Distribution License is available at + * https://www.eclipse.org/org/documents/edl-v10.php + * + * Contributors: + * Dave Locke - Original MQTTv3 implementation + * James Sutton - Initial MQTTv5 implementation + */ +package org.eclipse.paho.mqttv5.common.packet; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; + +import org.eclipse.paho.mqttv5.common.MqttException; +import org.eclipse.paho.mqttv5.common.MqttSubscription; +import org.eclipse.paho.mqttv5.common.packet.util.CountingInputStream; + +public class MqttSubscribe extends MqttPersistableWireMessage { + + private static final Byte[] validProperties = { MqttProperties.SUBSCRIPTION_IDENTIFIER, + MqttProperties.SUBSCRIPTION_IDENTIFIER_SINGLE, + MqttProperties.USER_DEFINED_PAIR_IDENTIFIER }; + + // Fields + private MqttProperties properties; + private MqttSubscription[] subscriptions; + + /** + * Constructor for an on the Wire MQTT Subscribe message + * + * @param data + * - The variable header and payload bytes. + * @throws IOException + * - if an exception occurs when decoding an input stream + * @throws MqttException + * - If an exception occurs decoding this packet + */ + public MqttSubscribe(byte[] data) throws IOException, MqttException { + super(MqttWireMessage.MESSAGE_TYPE_SUBSCRIBE); + this.properties = new MqttProperties(validProperties); + ByteArrayInputStream bais = new ByteArrayInputStream(data); + CountingInputStream counter = new CountingInputStream(bais); + DataInputStream inputStream = new DataInputStream(counter); + msgId = inputStream.readUnsignedShort(); + + this.properties.decodeProperties(inputStream); + + ArrayList subscriptionList = new ArrayList<>(); + // Whilst we are reading data + while (counter.getCounter() < data.length) { + String topic = MqttDataTypes.decodeUTF8(inputStream); + byte subscriptionOptions = inputStream.readByte(); + subscriptionList.add(decodeSubscription(topic, subscriptionOptions)); + } + subscriptions = subscriptionList.toArray(new MqttSubscription[subscriptionList.size()]); + inputStream.close(); + } + + /** + * Constructor for an on the Wire MQTT Subscribe message + * + * @param subscriptions + * - An Array of {@link MqttSubscription} subscriptions. + * @param properties + * - The {@link MqttProperties} for the packet. + */ + public MqttSubscribe(MqttSubscription[] subscriptions, MqttProperties properties) { + super(MqttWireMessage.MESSAGE_TYPE_SUBSCRIBE); + this.subscriptions = subscriptions; + if (properties != null) { + this.properties = properties; + } else { + this.properties = new MqttProperties(); + } + this.properties.setValidProperties(validProperties); + } + + /** + * Constructor for an on the Wire MQTT Subscribe message + * + * @param subscription + * - An {@link MqttSubscription} + * @param properties + * - The {@link MqttProperties} for the packet. + */ + public MqttSubscribe(MqttSubscription subscription, MqttProperties properties) { + super(MqttWireMessage.MESSAGE_TYPE_SUBSCRIBE); + this.subscriptions = new MqttSubscription[] { subscription }; + this.properties = properties; + this.properties.setValidProperties(validProperties); + } + + @Override + protected byte[] getVariableHeader() throws MqttException { + try { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + DataOutputStream outputStream = new DataOutputStream(baos); + + // Encode the Message ID + outputStream.writeShort(msgId); + + // Write Identifier / Value Fields + byte[] identifierValueFieldsByteArray = this.properties.encodeProperties(); + outputStream.write(identifierValueFieldsByteArray); + + outputStream.flush(); + return baos.toByteArray(); + } catch (IOException ioe) { + throw new MqttException(ioe); + } + } + + @Override + public byte[] getPayload() throws MqttException { + try { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + DataOutputStream outputStream = new DataOutputStream(baos); + + for (MqttSubscription subscription : subscriptions) { + outputStream.write(encodeSubscription(subscription)); + } + + outputStream.flush(); + return baos.toByteArray(); + } catch (IOException ioe) { + throw new MqttException(ioe); + } + } + + @Override + public boolean isRetryable() { + return true; + } + + /** + * Encodes an {@link MqttSubscription} into it's on-the-wire representation. + * Assumes that the Subscription topic is valid. + * + * @param subscription + * - The {@link MqttSubscription} to encode. + * @return A byte array containing the encoded subscription. + * @throws MqttException + */ + private byte[] encodeSubscription(MqttSubscription subscription) throws MqttException { + try { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + DataOutputStream outputStream = new DataOutputStream(baos); + + MqttDataTypes.encodeUTF8(outputStream, subscription.getTopic()); + + // Encode Subscription QoS + byte subscriptionOptions = (byte) subscription.getQos(); + + // Encode NoLocal Option + if (subscription.isNoLocal()) { + subscriptionOptions |= 0x04; + } + + // Encode Retain As Published Option + if (subscription.isRetainAsPublished()) { + subscriptionOptions |= 0x08; + } + + // Encode Retain Handling Level + subscriptionOptions |= (subscription.getRetainHandling() << 4); + + outputStream.write(subscriptionOptions); + + outputStream.flush(); + return baos.toByteArray(); + } catch (IOException ioe) { + throw new MqttException(ioe); + } + } + + private MqttSubscription decodeSubscription(String topic, byte subscriptionOptions) { + MqttSubscription subscription = new MqttSubscription(topic); + subscription.setQos(subscriptionOptions & 0x03); + subscription.setNoLocal((subscriptionOptions & 0x04) != 0); + subscription.setRetainAsPublished((subscriptionOptions & 0x08) != 0); + subscription.setRetainHandling((subscriptionOptions >> 4) & 0x03); + return subscription; + } + + @Override + protected byte getMessageInfo() { + return (byte) (2 | (duplicate ? 8 : 0)); + } + + public MqttSubscription[] getSubscriptions() { + return subscriptions; + } + + @Override + public MqttProperties getProperties() { + return this.properties; + } + + @Override + public String toString() { + return "MqttSubscribe [properties=" + properties + ", subscriptions=" + Arrays.toString(subscriptions) + "]"; + } + +} diff --git a/src/main/java/org/eclipse/paho/mqttv5/common/packet/MqttUnsubAck.java b/src/main/java/org/eclipse/paho/mqttv5/common/packet/MqttUnsubAck.java new file mode 100644 index 0000000..70004da --- /dev/null +++ b/src/main/java/org/eclipse/paho/mqttv5/common/packet/MqttUnsubAck.java @@ -0,0 +1,130 @@ +/******************************************************************************* + * Copyright (c) 2016 IBM Corp. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * and Eclipse Distribution License v1.0 which accompany this distribution. + * + * The Eclipse Public License is available at + * https://www.eclipse.org/legal/epl-2.0 + * and the Eclipse Distribution License is available at + * https://www.eclipse.org/org/documents/edl-v10.php + * + * Contributors: + * Dave Locke - Original MQTTv3 implementation + * James Sutton - Initial MQTTv5 implementation + */ +package org.eclipse.paho.mqttv5.common.packet; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.util.Arrays; + +import org.eclipse.paho.mqttv5.common.MqttException; +import org.eclipse.paho.mqttv5.common.packet.util.CountingInputStream; + +public class MqttUnsubAck extends MqttAck { + + private static final int[] validReturnCodes = { MqttReturnCode.RETURN_CODE_SUCCESS, + MqttReturnCode.RETURN_CODE_NO_SUBSCRIPTION_EXISTED, MqttReturnCode.RETURN_CODE_UNSPECIFIED_ERROR, + MqttReturnCode.RETURN_CODE_IMPLEMENTATION_SPECIFIC_ERROR, MqttReturnCode.RETURN_CODE_NOT_AUTHORIZED, + MqttReturnCode.RETURN_CODE_TOPIC_FILTER_NOT_VALID, MqttReturnCode.RETURN_CODE_PACKET_ID_IN_USE }; + + private static final Byte[] validProperties = { MqttProperties.REASON_STRING_IDENTIFIER, + MqttProperties.USER_DEFINED_PAIR_IDENTIFIER }; + + // Fields + private MqttProperties properties; + + public MqttUnsubAck(byte[] data) throws IOException, MqttException { + super(MqttWireMessage.MESSAGE_TYPE_UNSUBACK); + properties = new MqttProperties(validProperties); + ByteArrayInputStream bais = new ByteArrayInputStream(data); + CountingInputStream counter = new CountingInputStream(bais); + DataInputStream inputStream = new DataInputStream(counter); + + msgId = inputStream.readUnsignedShort(); + + this.properties.decodeProperties(inputStream); + + int remainingLengh = data.length - counter.getCounter(); + reasonCodes = new int[remainingLengh]; + + for (int i = 0; i < remainingLengh; i++) { + reasonCodes[i] = inputStream.readUnsignedByte(); + validateReturnCode(reasonCodes[i], validReturnCodes); + } + + inputStream.close(); + } + + public MqttUnsubAck(int[] returnCodes, MqttProperties properties) throws MqttException { + super(MqttWireMessage.MESSAGE_TYPE_UNSUBACK); + for (int returnCode : returnCodes) { + validateReturnCode(returnCode, validReturnCodes); + } + this.reasonCodes = returnCodes; + if (properties != null) { + this.properties = properties; + } else { + this.properties = new MqttProperties(); + } + this.properties.setValidProperties(validProperties); + } + + @Override + protected byte[] getVariableHeader() throws MqttException { + try { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + DataOutputStream outputStream = new DataOutputStream(baos); + + // Encode the msgId + outputStream.writeShort(msgId); + + // Write Identifier / Value Fields + byte[] identifierValueFieldsByteArray = this.properties.encodeProperties(); + // Write Identifier / Value Fields + outputStream.write(identifierValueFieldsByteArray); + outputStream.flush(); + return baos.toByteArray(); + } catch (IOException ioe) { + throw new MqttException(ioe); + } + } + + @Override + public byte[] getPayload() throws MqttException { + try { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + DataOutputStream outputStream = new DataOutputStream(baos); + + for (int returnCode : reasonCodes) { + outputStream.writeByte(returnCode); + } + + outputStream.flush(); + return baos.toByteArray(); + } catch (IOException ioe) { + throw new MqttException(ioe); + } + + } + + public int[] getReturnCodes() { + return reasonCodes; + } + + @Override + public MqttProperties getProperties() { + return this.properties; + } + + @Override + public String toString() { + return "MqttUnsubAck [returnCodes=" + Arrays.toString(reasonCodes) + ", properties=" + properties + "]"; + } + +} diff --git a/src/main/java/org/eclipse/paho/mqttv5/common/packet/MqttUnsubscribe.java b/src/main/java/org/eclipse/paho/mqttv5/common/packet/MqttUnsubscribe.java new file mode 100644 index 0000000..07a284f --- /dev/null +++ b/src/main/java/org/eclipse/paho/mqttv5/common/packet/MqttUnsubscribe.java @@ -0,0 +1,138 @@ +/******************************************************************************* + * Copyright (c) 2016, 2019 IBM Corp. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * and Eclipse Distribution License v1.0 which accompany this distribution. + * + * The Eclipse Public License is available at + * https://www.eclipse.org/legal/epl-2.0 + * and the Eclipse Distribution License is available at + * https://www.eclipse.org/org/documents/edl-v10.php + * + * Contributors: + * Dave Locke - Original MQTTv3 implementation + * James Sutton - Initial MQTTv5 implementation + */ +package org.eclipse.paho.mqttv5.common.packet; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; + +import org.eclipse.paho.mqttv5.common.MqttException; +import org.eclipse.paho.mqttv5.common.packet.util.CountingInputStream; + +public class MqttUnsubscribe extends MqttPersistableWireMessage{ + + private static final Byte[] validProperties = { MqttProperties.USER_DEFINED_PAIR_IDENTIFIER }; + + + // Fields + private String[] topics; + private MqttProperties properties; + + + + public MqttUnsubscribe(byte[] data) throws IOException, MqttException { + super(MqttWireMessage.MESSAGE_TYPE_UNSUBSCRIBE); + this.properties = new MqttProperties(validProperties); + ByteArrayInputStream bais = new ByteArrayInputStream(data); + CountingInputStream counter = new CountingInputStream(bais); + DataInputStream inputStream = new DataInputStream(counter); + msgId = inputStream.readUnsignedShort(); + + this.properties.decodeProperties(inputStream); + + ArrayList topicList = new ArrayList<>(); + // Whilst we are reading data + while(counter.getCounter() < data.length){ + topicList.add( MqttDataTypes.decodeUTF8(inputStream)); + } + topics = topicList.toArray(new String[topicList.size()]); + + + + inputStream.close(); + } + + public MqttUnsubscribe(String[] topics, MqttProperties properties){ + super(MqttWireMessage.MESSAGE_TYPE_UNSUBSCRIBE); + this.topics = topics; + if (properties != null) { + this.properties = properties; + } else { + this.properties = new MqttProperties(); + } + this.properties.setValidProperties(validProperties); + } + + @Override + protected byte[] getVariableHeader() throws MqttException { + try { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + DataOutputStream outputStream = new DataOutputStream(baos); + + // Encode the Message ID + outputStream.writeShort(msgId); + + + // Write Identifier / Value Fields + byte[] identifierValueFieldsByteArray = this.properties.encodeProperties(); + outputStream.write(identifierValueFieldsByteArray); + outputStream.flush(); + return baos.toByteArray(); + } catch (IOException ioe) { + throw new MqttException(ioe); + } + } + + @Override + public byte[] getPayload() throws MqttException { + try { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + DataOutputStream outputStream = new DataOutputStream(baos); + + for(String topic : topics){ + MqttDataTypes.encodeUTF8(outputStream, topic); + } + + outputStream.flush(); + return baos.toByteArray(); + } catch (IOException ioe){ + throw new MqttException(ioe); + } + } + + + @Override + protected byte getMessageInfo() { + return (byte)( 2 | (this.duplicate ? 8 : 0)); + } + + public String[] getTopics() { + return topics; + } + + public void setTopics(String[] topics) { + this.topics = topics; + } + + @Override + public MqttProperties getProperties() { + return this.properties; + } + + @Override + public String toString() { + return "MqttUnsubscribe [topics=" + Arrays.toString(topics) + ", properties=" + properties + "]"; + } + + + + +} diff --git a/src/main/java/org/eclipse/paho/mqttv5/common/packet/MqttWireMessage.java b/src/main/java/org/eclipse/paho/mqttv5/common/packet/MqttWireMessage.java new file mode 100644 index 0000000..9ae17e7 --- /dev/null +++ b/src/main/java/org/eclipse/paho/mqttv5/common/packet/MqttWireMessage.java @@ -0,0 +1,417 @@ +/******************************************************************************* + * Copyright (c) 2016 IBM Corp. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * and Eclipse Distribution License v1.0 which accompany this distribution. + * + * The Eclipse Public License is available at + * https://www.eclipse.org/legal/epl-2.0 + * and the Eclipse Distribution License is available at + * https://www.eclipse.org/org/documents/edl-v10.php + * + * Contributors: + * Dave Locke - Original MQTTv3 implementation + * James Sutton - Initial MQTTv5 implementation + */ +package org.eclipse.paho.mqttv5.common.packet; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.io.InputStream; + +import org.eclipse.paho.mqttv5.common.ExceptionHelper; +import org.eclipse.paho.mqttv5.common.MqttException; +import org.eclipse.paho.mqttv5.common.MqttPersistable; +import org.eclipse.paho.mqttv5.common.packet.util.CountingInputStream; +import org.eclipse.paho.mqttv5.common.packet.util.MultiByteArrayInputStream; + +/** + * An on-the wire representation of an MQTTv5 Message + */ +public abstract class MqttWireMessage { + + public static final byte MESSAGE_TYPE_RESERVED = 0; + public static final byte MESSAGE_TYPE_CONNECT = 1; + public static final byte MESSAGE_TYPE_CONNACK = 2; + public static final byte MESSAGE_TYPE_PUBLISH = 3; + public static final byte MESSAGE_TYPE_PUBACK = 4; + public static final byte MESSAGE_TYPE_PUBREC = 5; + public static final byte MESSAGE_TYPE_PUBREL = 6; + public static final byte MESSAGE_TYPE_PUBCOMP = 7; + public static final byte MESSAGE_TYPE_SUBSCRIBE = 8; + public static final byte MESSAGE_TYPE_SUBACK = 9; + public static final byte MESSAGE_TYPE_UNSUBSCRIBE = 10; + public static final byte MESSAGE_TYPE_UNSUBACK = 11; + public static final byte MESSAGE_TYPE_PINGREQ = 12; + public static final byte MESSAGE_TYPE_PINGRESP = 13; + public static final byte MESSAGE_TYPE_DISCONNECT = 14; + public static final byte MESSAGE_TYPE_AUTH = 15; + + protected static final String STRING_ENCODING = "UTF-8"; + protected static final String DEFAULT_PROTOCOL_NAME = "MQTT"; + protected static final int DEFAULT_PROTOCOL_VERSION = 5; + + private static final String[] PACKET_NAMES = { "reserved", "CONNECT", "CONNACK", "PUBLISH", "PUBACK", "PUBREC", + "PUBREL", "PUBCOMP", "SUBSCRIBE", "SUBACK", "UNSUBSCRIBE", "UNSUBACK", "PINGREQ", "PINGRESP", "DISCONNECT", + "AUTH" }; + + private static final byte[] PACKET_RESERVED_MASKS = { 0, 0, 0, 0, 0, 0, 2, 0, 2, 0, 2, 0, 0, 0, 0, 0 }; + + // The type of the message (e.g CONNECT, PUBLISH, SUBSCRIBE) + private byte type; + + MqttProperties properties = new MqttProperties(); + + // The MQTT Message ID + protected int msgId; + protected int[] reasonCodes = null; // Multiple Reason Codes (SUBACK, UNSUBACK) + protected int reasonCode = -1; // Single Reason Code, init with -1 as that's an invalid RC + protected boolean duplicate = false; + + public MqttWireMessage(byte type) { + this.type = type; + // Use zero as the default message ID. Can't use -1, as that is serialized + // as 65535, which would be a valid ID. + this.msgId = 0; + } + + /** + * Sub-classes should override this to encode the message info. Only the + * least-significant four bits will be used. + * + * @return The Message information byte. + */ + protected abstract byte getMessageInfo(); + + /** + * Sub-classes should override this method to supply the payload bytes. + * + * @return The payload byte array + * @throws MqttException + * if an exception occurs whilst getting the payload. + */ + public byte[] getPayload() throws MqttException { + return new byte[0]; + } + + /** + * @return the type of the message + */ + public byte getType() { + return type; + } + + /** + * @return the MQTT message ID + */ + public int getMessageId() { + return msgId; + } + + /** + * Sets the MQTT message ID. + * + * @param msgId + * the MQTT message ID + */ + public void setMessageId(int msgId) { + this.msgId = msgId; + } + + /** + * Returns a key associated with the message. For most message types this will + * be unique. For connect, disconnect and ping only one message of this type is + * allowed so a fixed key will be returned. + * + * @return The key associated with the message + */ + public String getKey() { + return Integer.toString(getMessageId()); + } + + /** + * Returns a byte array containing the MQTT header for the message. + * + * @return The MQTT Message Header + * @throws MqttException + * if there was an issue encoding the header + */ + public byte[] getHeader() throws MqttException { + try { + int first = ((getType() & 0x0f) << 4) ^ (getMessageInfo() & 0x0f); + byte[] varHeader = getVariableHeader(); + int remLen = varHeader.length + getPayload().length; + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + DataOutputStream dos = new DataOutputStream(baos); + dos.writeByte(first); + dos.write(encodeVariableByteInteger(remLen)); + dos.write(varHeader); + dos.flush(); + return baos.toByteArray(); + } catch (IOException ioe) { + throw new MqttException(ioe); + } + } + + protected abstract byte[] getVariableHeader() throws MqttException; + + /** + * @return whether or not this message needs to include a message ID. + */ + public boolean isMessageIdRequired() { + return true; + } + + /** + * Create an MQTT Wire Message + * + * @throws MqttException + * if an error occurred whilst creating the WireMessage + * @param data + * the MqttPersistable to create the message from + * @return MqttWireMessage the de-persisted message + */ + public static MqttWireMessage createWireMessage(MqttPersistable data) throws MqttException { + byte[] payload = data.getPayloadBytes(); + + // The persistable interface allows a message to be restored entirely in the + // header array. + // We need to treat these two arrays as a single array of bytes and use the + // decoding + // logic to identify the true header / payload split. + + if (payload == null) { + payload = new byte[0]; + } + MultiByteArrayInputStream mbais = new MultiByteArrayInputStream(data.getHeaderBytes(), data.getHeaderOffset(), + data.getHeaderLength(), payload, data.getPayloadOffset(), data.getPayloadLength()); + return createWireMessage(mbais); + } + + public static MqttWireMessage createWireMessage(byte[] bytes) throws MqttException { + ByteArrayInputStream bais = new ByteArrayInputStream(bytes); + return createWireMessage(bais); + } + + private static MqttWireMessage createWireMessage(InputStream inputStream) throws MqttException { + try { + CountingInputStream counter = new CountingInputStream(inputStream); + DataInputStream in = new DataInputStream(counter); + int first = in.readUnsignedByte(); + byte type = (byte) (first >> 4); + byte info = (byte) (first &= 0x0f); + long remLen = MqttDataTypes.readVariableByteInteger(in).getValue(); + long totalToRead = counter.getCounter() + remLen; + + MqttWireMessage result; + long remainder = totalToRead - counter.getCounter(); + byte[] data = new byte[0]; + + // The remaining bytes must be the payload + if (remainder > 0) { + data = new byte[(int) remainder]; + in.readFully(data, 0, data.length); + } + + switch (type) { + case MqttWireMessage.MESSAGE_TYPE_CONNECT: + result = new MqttConnect(info, data); + break; + case MqttWireMessage.MESSAGE_TYPE_CONNACK: + result = new MqttConnAck(data); + break; + case MqttWireMessage.MESSAGE_TYPE_PUBLISH: + result = new MqttPublish(info, data); + break; + case MqttWireMessage.MESSAGE_TYPE_PUBACK: + result = new MqttPubAck(data); + break; + case MqttWireMessage.MESSAGE_TYPE_PUBREC: + result = new MqttPubRec(data); + break; + case MqttWireMessage.MESSAGE_TYPE_PUBREL: + result = new MqttPubRel(data); + break; + case MqttWireMessage.MESSAGE_TYPE_PUBCOMP: + result = new MqttPubComp(data); + break; + case MqttWireMessage.MESSAGE_TYPE_SUBSCRIBE: + result = new MqttSubscribe(data); + break; + case MqttWireMessage.MESSAGE_TYPE_SUBACK: + result = new MqttSubAck(data); + break; + case MqttWireMessage.MESSAGE_TYPE_UNSUBSCRIBE: + result = new MqttUnsubscribe(data); + break; + case MqttWireMessage.MESSAGE_TYPE_UNSUBACK: + result = new MqttUnsubAck(data); + break; + case MqttWireMessage.MESSAGE_TYPE_PINGREQ: + result = new MqttPingReq(); + break; + case MqttWireMessage.MESSAGE_TYPE_PINGRESP: + result = new MqttPingResp(); + break; + case MqttWireMessage.MESSAGE_TYPE_DISCONNECT: + result = new MqttDisconnect(data); + break; + case MqttWireMessage.MESSAGE_TYPE_AUTH: + result = new MqttAuth(data); + break; + default: + throw ExceptionHelper.createMqttException(MqttException.REASON_CODE_MALFORMED_PACKET); + } + return result; + + } catch (IOException ioe) { + throw new MqttException(ioe); + } + } + + public static byte[] encodeVariableByteInteger(int number) { + int numBytes = 0; + long no = number; + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + // Encode the remaining length fields in the four bytes + do { + byte digit = (byte) (no % 128); + no = no / 128; + if (no > 0) { + digit |= 0x80; + } + baos.write(digit); + numBytes++; + } while ((no > 0) && (numBytes < 4)); + return baos.toByteArray(); + } + + /** + * Validates that the reserved bits set on an MQTT packet conform to the MQTT + * specification. + * + * @param type + * - The Message Type + * @param reserved + * - The Reserved Bits + * @throws MqttException + * If the set reserved bits do not match the specification. + * @throws IllegalArgumentException + * If the message type does not exist. + */ + public static void validateReservedBits(byte type, byte reserved) throws MqttException, IllegalArgumentException { + if (type == MESSAGE_TYPE_PUBLISH) { + // Publish can vary, but will be parsed separately. + return; + } + if (type > MESSAGE_TYPE_AUTH) { + throw new IllegalArgumentException("Unrecognised Message Type."); + } + if (reserved != PACKET_RESERVED_MASKS[type]) { + throw new MqttException(MqttException.REASON_CODE_MALFORMED_PACKET); + } + } + + protected byte[] encodeMessageId() throws MqttException { + try { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + DataOutputStream dos = new DataOutputStream(baos); + dos.writeShort(msgId); + dos.flush(); + return baos.toByteArray(); + } catch (IOException ioe) { + throw new MqttException(ioe); + } + } + + public boolean isRetryable() { + return false; + } + + public void setDuplicate(boolean duplicate) { + this.duplicate = duplicate; + } + + public boolean isDuplicate() { + return this.duplicate; + } + + public MqttProperties getProperties() { + return properties; + } + + public void setProperties(MqttProperties properties) { + this.properties = properties; + } + + @Override + public String toString() { + return PACKET_NAMES[type]; + } + + /** + * Validates that a return code is valid for this Packet + * + * @param returnCode + * - The return code to validate + * @param validReturnCodes + * - The list of valid return codes + * @throws MqttException + * - Thrown if the return code is not valid + */ + protected void validateReturnCode(int returnCode, int[] validReturnCodes) throws MqttException { + for (int validReturnCode : validReturnCodes) { + if (returnCode == validReturnCode) { + return; + } + } + throw new MqttException(MqttException.REASON_CODE_INVALID_RETURN_CODE); + } + + /** + * + * Returns the reason codes from the MqttWireMessage. These will be present if + * the messages is of the following types: + *
    + *
  • CONNACK - 1 Reason Code Max.
  • + *
  • PUBACK - 1 Reason Code Max.
  • + *
  • PUBREC - 1 Reason Code Max.
  • + *
  • PUBCOMP - 1 Reason Code Max.
  • + *
  • PUBREL - 1 Reason Code Max.
  • + *
  • SUBACK - 1 or more Reason Codes.
  • + *
  • UNSUBACK - 1 or more Reason Codes.
  • + *
  • AUTH - 1 Reason Code Max.
  • + *
+ * + * Warning: This method may be removed in favour of Token.getReasonCodes() + * + * May be null if this message does not contain any Reason Codes. + * + * @return An array of return codes, or null. + */ + public int[] getReasonCodes() { + if (this.reasonCodes != null) { + return this.reasonCodes; + } else if (this.reasonCode != -1) { + return new int[] { this.reasonCode }; + } else { + return null; + } + } + + public byte[] serialize() throws MqttException { + byte[] a = getHeader(); + byte[] b = getPayload(); + + byte[] c = new byte[a.length + b.length]; + System.arraycopy(a, 0, c, 0, a.length); + System.arraycopy(b, 0, c, a.length, b.length); + return c; + } + +} diff --git a/src/main/java/org/eclipse/paho/mqttv5/common/packet/UserProperty.java b/src/main/java/org/eclipse/paho/mqttv5/common/packet/UserProperty.java new file mode 100644 index 0000000..dfec1a8 --- /dev/null +++ b/src/main/java/org/eclipse/paho/mqttv5/common/packet/UserProperty.java @@ -0,0 +1,40 @@ +package org.eclipse.paho.mqttv5.common.packet; + +public class UserProperty { + private final String key; + private final String value; + + public UserProperty(String key, String value) { + this.key = key; + this.value = value; + } + + public String getKey() { + return key; + } + + public String getValue() { + return value; + } + + @Override + public int hashCode() { + return key.hashCode() ^ value.hashCode(); + } + + @Override + public boolean equals(Object o) { + if (o instanceof UserProperty) { + UserProperty property = (UserProperty) o; + return this.key.equals(property.getKey()) && this.value.equals(property.getValue()); + } else { + return false; + } + } + + @Override + public String toString() { + return "UserProperty [key=" + key + ", value=" + value + "]"; + } + +} diff --git a/src/main/java/org/eclipse/paho/mqttv5/common/packet/util/CountingInputStream.java b/src/main/java/org/eclipse/paho/mqttv5/common/packet/util/CountingInputStream.java new file mode 100644 index 0000000..ef7ecc3 --- /dev/null +++ b/src/main/java/org/eclipse/paho/mqttv5/common/packet/util/CountingInputStream.java @@ -0,0 +1,58 @@ +/******************************************************************************* + * Copyright (c) 2016 IBM Corp. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * and Eclipse Distribution License v1.0 which accompany this distribution. + * + * The Eclipse Public License is available at + * https://www.eclipse.org/legal/epl-2.0 + * and the Eclipse Distribution License is available at + * https://www.eclipse.org/org/documents/edl-v10.php + * + * Contributors: + * Dave Locke - Original MQTTv3 implementation + * James Sutton - Initial MQTTv5 implementation + */ +package org.eclipse.paho.mqttv5.common.packet.util; + +import java.io.IOException; +import java.io.InputStream; + +public class CountingInputStream extends InputStream{ + private InputStream inputStream; + private int counter; + + + /**Constructs a new CountingInputStream wrapping the supplied + * input stream. + * @param inputStream The inputStream to count and provide + */ + public CountingInputStream(InputStream inputStream){ + this.inputStream = inputStream; + this.counter = 0; + } + + public int read() throws IOException { + int i = inputStream.read(); + if (i != -1){ + counter++; + } + return i; + } + + /** + * Returns the number of bytes read since last reset + * @return the counter + */ + public int getCounter() { + return counter; + } + /** + * Resets the counter to zero + */ + public void resetCounter() { + counter = 0; + } + +} diff --git a/src/main/java/org/eclipse/paho/mqttv5/common/packet/util/MultiByteArrayInputStream.java b/src/main/java/org/eclipse/paho/mqttv5/common/packet/util/MultiByteArrayInputStream.java new file mode 100644 index 0000000..9b8edba --- /dev/null +++ b/src/main/java/org/eclipse/paho/mqttv5/common/packet/util/MultiByteArrayInputStream.java @@ -0,0 +1,57 @@ +/******************************************************************************* + * Copyright (c) 2016 IBM Corp. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * and Eclipse Distribution License v1.0 which accompany this distribution. + * + * The Eclipse Public License is available at + * https://www.eclipse.org/legal/epl-2.0 + * and the Eclipse Distribution License is available at + * https://www.eclipse.org/org/documents/edl-v10.php + * + * Contributors: + * Dave Locke - Original MQTTv3 implementation + * James Sutton - Initial MQTTv5 implementation + */ +package org.eclipse.paho.mqttv5.common.packet.util; + +import java.io.IOException; +import java.io.InputStream; + +public class MultiByteArrayInputStream extends InputStream { + + private byte[] bytesA; + private int offsetA; + private int lengthA; + private byte[] bytesB; + private int offsetB; + private int lengthB; + + private int pos = 0; + + public MultiByteArrayInputStream(byte[] bytesA, int offsetA, int lengthA, byte[] bytesB, int offsetB, int lengthB) { + this.bytesA = bytesA; + this.bytesB = bytesB; + this.offsetA = offsetA; + this.offsetB = offsetB; + this.lengthA = lengthA; + this.lengthB = lengthB; + } + public int read() throws IOException { + int result = -1; + if (pos MAX_TOPIC_LEN) { + throw new IllegalArgumentException(String.format("Invalid topic length, should be in range[%d, %d]!", + new Object[] { Integer.valueOf(MIN_TOPIC_LEN), Integer.valueOf(MAX_TOPIC_LEN) })); + } + + // ******************************************************************************* + // 1) This is a topic filter string that can contain wildcard characters + // ******************************************************************************* + if (wildcardAllowed) { + // Only # or + + if (Strings.equalsAny(topicString, new String[] { MULTI_LEVEL_WILDCARD, SINGLE_LEVEL_WILDCARD })) { + return; + } + + // 1) Check multi-level wildcard + // Rule: + // The multi-level wildcard can be specified only on its own or next + // to the topic level separator character. + + // - Can only contains one multi-level wildcard character + // - The multi-level wildcard must be the last character used within + // the topic tree + if (Strings.countMatches(topicString, MULTI_LEVEL_WILDCARD) > 1 + || (topicString.contains(MULTI_LEVEL_WILDCARD) && !topicString.endsWith(MULTI_LEVEL_WILDCARD_PATTERN))) { + throw new IllegalArgumentException("Invalid usage of multi-level wildcard in topic string: " + topicString); + } + + // 2) Check single-level wildcard + // Rule: + // The single-level wildcard can be used at any level in the topic + // tree, and in conjunction with the + // multilevel wildcard. It must be used next to the topic level + // separator, except when it is specified on + // its own. + validateSingleLevelWildcard(topicString); + + return; + } + + // Validate Shared Subscriptions + if (!sharedSubAllowed && topicString.startsWith("$share/")) { + throw new IllegalArgumentException("Shared Subscriptions are not allowed."); + } + + // ******************************************************************************* + // 2) This is a topic name string that MUST NOT contains any wildcard characters + // ******************************************************************************* + if (Strings.containsAny(topicString, TOPIC_WILDCARDS)) { + throw new IllegalArgumentException("The topic name MUST NOT contain any wildcard characters (#+)"); + } + + } + + private static void validateSingleLevelWildcard(String topicString) { + char singleLevelWildcardChar = SINGLE_LEVEL_WILDCARD.charAt(0); + char topicLevelSeparatorChar = TOPIC_LEVEL_SEPARATOR.charAt(0); + + char[] chars = topicString.toCharArray(); + int length = chars.length; + char prev = NUL, next = NUL; + for (int i = 0; i < length; i++) { + prev = (i - 1 >= 0) ? chars[i - 1] : NUL; + next = (i + 1 < length) ? chars[i + 1] : NUL; + + if (chars[i] == singleLevelWildcardChar) { + // prev and next can be only '/' or none + if (prev != topicLevelSeparatorChar && prev != NUL || next != topicLevelSeparatorChar && next != NUL) { + throw new IllegalArgumentException(String + .format("Invalid usage of single-level wildcard in topic string '%s'!", new Object[] { topicString })); + + } + } + } + } + + /** + * Check the supplied topic name and filter match + * + * @param topicFilter + * topic filter: wildcards allowed + * @param topicName + * topic name: wildcards not allowed + * @return true if the topic matches the filter + * @throws IllegalArgumentException + * if the topic name or filter is invalid + */ + public static boolean isMatched(String topicFilter, String topicName) throws IllegalArgumentException { + int topicPos = 0; + int filterPos = 0; + int topicLen = topicName.length(); + int filterLen = topicFilter.length(); + + MqttTopicValidator.validate(topicFilter, true, true); + MqttTopicValidator.validate(topicName, false, true); + + if (topicFilter.equals(topicName)) { + return true; + } + + while (filterPos < filterLen && topicPos < topicLen) { + if (topicFilter.charAt(filterPos) == '#') { + /* + * next 'if' will break when topicFilter = topic/# and topicName topic/A/, but they are matched + */ + topicPos = topicLen; + filterPos = filterLen; + break; + } + if (topicName.charAt(topicPos) == '/' && topicFilter.charAt(filterPos) != '/') + break; + if (topicFilter.charAt(filterPos) != '+' && topicFilter.charAt(filterPos) != '#' + && topicFilter.charAt(filterPos) != topicName.charAt(topicPos)) + break; + if (topicFilter.charAt(filterPos) == '+') { // skip until we meet the next separator, or end of string + int nextpos = topicPos + 1; + while (nextpos < topicLen && topicName.charAt(nextpos) != '/') + nextpos = ++topicPos + 1; + } else if (topicFilter.charAt(filterPos) == '#') + topicPos = topicLen - 1; // skip until end of string + filterPos++; + topicPos++; + } + + if ((topicPos == topicLen) && (filterPos == filterLen)) { + return true; + } else { + /* + * https://github.com/eclipse/paho.mqtt.java/issues/418 Covers edge case to match sport/# to sport + */ + if ((topicFilter.length() - filterPos > 0) && (topicPos == topicLen)) { + if (topicName.charAt(topicPos - 1) == '/' && topicFilter.charAt(filterPos) == '#') + return true; + if (topicFilter.length() - filterPos > 1 && topicFilter.substring(filterPos, filterPos + 2).equals("/#")) { + if ((topicFilter.length() - topicName.length()) == 2 + && topicFilter.substring(topicFilter.length() - 2, topicFilter.length()).equals("/#")) { + return true; + } + } + } + /* + * https://github.com/eclipse/paho.mqtt.java/issues/918 + * covers cases that include more then one wildcard + * sport/+/tennis/# + */ + String[] topicFilterParts = topicFilter.split(TOPIC_LEVEL_SEPARATOR); + String[] topicParts = topicName.split(TOPIC_LEVEL_SEPARATOR); + if(topicFilterParts.length -1 == topicParts.length && + topicFilterParts[topicFilterParts.length-1].equals( MULTI_LEVEL_WILDCARD)) { + for (int i = 0; i ClientID={0} ServerURI={1} PersistenceType={2} +103=cleanStart={0} connectionTimeout={1} TimekeepAlive={2} userName={3} password={4} will={5} userContext={6} callback={7} +104=> quiesceTimeout={0} userContext={1} callback={2} +105=< exception +106=Subscribe topicFilter={0} userContext={1} callback={2} +107=Unsubscribe topic={0} userContext={1} callback={2} +108=< +109=< +110=< +111=< topic={0} message={1}userContext={1} callback={2} +112=< +113=< +114=> +115=URI={0} +116=URI={0} +117=> +118=<200=internalSend key={0} message={1} token={2} +119=Invalid URI Provided that could not be used to create a NetworkModule: {0} +204=connect failed: rc={0} +207=connect failed: not disconnected {0} +208=failed: not connected +209=connect failed: unexpected exception +210=failed: called on callback thread +211=failed: already disconnected +212=connect failed: unexpected exception +213=fail: token in use: key={0} message={1} token={2} +214=state=CONNECTING +215=state=CONNECTED +216=state=DISCONNECTING +217=state=DISCONNECTED +218=state=DISCONNECTING +219=failed: already disconnecting +220=> +221=> +222=> +223=failed: in closed state +224=failed: not disconnected +250=Failed to create TCP socket +252=connect to host {0} port {1} timeout {2} +260=setEnabledCiphers ciphers={0} +300=key={0} message={1} +302=existing key={0} message={1} token={2} +303=creating new token key={0} message={1} token={2} +305=> {0} tokens +306=key={0} +307=key={0} token={1} +308=<> +309=resp={0} +310=> +311=> +312=> +400=>key={0} timeout={1} sent={2} completed={3} hasException={4} response={5} token={6} +401=failed with exception +402=key={0} response={1} +403=> key={0} +404=>key={0} response={1} excep={2} +406=key={0} timed out token={1} +407=key={0} wait max={1} token={2} +408=key={0} wait max={1} +409=wait key={0} +410=> key={0} +411=>key={0} response={1} excep={2} +500=Attempting to reconnect client: {0} +501=Automatic Reconnect Successful: {0} +502=Automatic Reconnect failed, rescheduling: {0} +503=Start reconnect timer for client: {0}, delay: {1} +504=Stop reconnect timer for client: {0} +505=Rescheduling reconnect timer for client: {0}, delay: {1} +506=Triggering Automatic Reconnect attempt. +507=Client Connected, Offline Buffer available, but not empty. Adding message to buffer. message={0} +508=Client Resting, Offline Buffer available. Adding message to buffer. message={0} +509=Client Reconnected, Offline Buffer Available. Sending Buffered Messages. +510=Publising Buffered message message={0} +511=outbound QoS 0 publish key={0} message={1} +512=QoS 0 publish key={0} +513=Persisted Buffered Message key={0} +514=Failed to persist buffered message key={0} +515=Could not Persist, attempting to Re-Open Persistence Store +516=Restoring all buffered messages. +517=Un-Persisting Buffered message key={0} +518=Failed to Un-Persist Buffered message key={0} +519=Error occurred attempting to publish buffered message due to disconnect. Exception: {0}. +529=Sent {0} +530=Received {0} +600=> +601=key={0} message={1} +602=key={0} exception +603=clearState +604=inbound QoS 2 publish key={0} message={1} +605=outbound QoS 2 pubrel key={0} message={1} +606=outbound QoS 2 completed key={0} message={1} +607=outbound QoS 2 publish key={0} message={1} +608=outbound QoS 1 publish key={0} message={1} +609=removing orphaned pubrel key={0} +610=QoS 2 publish key={0} +611=QoS 2 pubrel key={0} +612=QoS 1 publish key={0} +613= sending {0} msgs at max inflight window +615=pending send key={0} message {1} +616=checkForActivity entered +617=+1 inflightpubrels={0} +618=key={0} QoS={1} +619=Timed out as no activity, keepAlive={0} lastOutboundActivity={1} lastInboundActivity={2} time={3} lastPing={4} +620=ping needed. keepAlive={0} lastOutboundActivity={1} lastInboundActivity={2} +621=no outstanding flows and not connected +622=inflight window full +623=+1 actualInFlight={0} +624=Schedule next ping at {0} +625=key={0} +626=quiescing={0} actualInFlight={1} pendingFlows={2} inFlightPubRels={3} callbackQuiesce={4} tokens={5} +627=received key={0} message={1} +628=pending publish key={0} qos={1} message={2} +629=received key={0} token={1} message={2} +630=received bytes count={0} +631=connected +632=reason {0} +633=disconnected +634=ping not needed yet. Schedule next ping +635=ping sent. pingOutstanding: {0} +636=ping response received. pingOutstanding: {0} +637=timeout={0} +638=notifying queueLock holders +639=wait for outstanding: actualInFlight={0} pendingFlows={1} inFlightPubRels={2} tokens={3} +640=finished +641=remove publish from persistence. key={0} +642=Timed out as no write activity, keepAlive={0} lastOutboundActivity={1} lastInboundActivity={2} time={3} lastPing={4} +643=sent bytes count={0} +644=wait for new work or for space in the inflight window +645=removed QoS 2 publish/pubrel. key={0}, -1 inFlightPubRels={1} +646=-1 actualInFlight={0} +647=new work or ping arrived +648=key{0}, msg={1}, excep={2} +649=key={0},excep={1} +650=removed Qos 1 publish. key={0} +651=received key={0} message={1} +652=Setting Incoming New Topic Alias alias={0}, topicName={1} +653=Invalid Topic Alias: topicAliasMax={0}, publishTopicAlias={1} +654=Unknown Topic Alias: Incoming Alias={1} +659=start timer for client:{0} +660=Check schedule at {0} +661=stop +662=no message found for ack id={0} +663=Disconnect message received from Server. Details={0} +664=[MQTT-4.3.3-4] - A Reason code greater than 0x80 (128) was received in an incoming PUBREC id={0} rc={1}, halting QoS 2 flow. +665=Clearing Connection State (Topic Aliases) +666=Orphaned Ack key={0} message={1} +667=MqttPubRel was received with an error code: key={0} message={1}, Reason Code= {2} +668=Creating MqttPubComp: {0} +700=stopping +701=notify workAvailable and wait for run +703=stopped +704=wait for workAvailable +705=callback and notify for key={0} +706=notify spaceAvailable +708=call connectionLost +709=wait for spaceAvailable +710=new msg avail, notify workAvailable +711=quiesce notify spaceAvailable +713=call messageArrived key={0} topic={1} +714=callback threw exception +715=new workAvailable. key={0} +716=call onSuccess key={0} +717=call onFailure key {0} +719=callback threw ex: +720=Ignoring Exception thrown from connectionLost {0} +725=Ignoring Exception thrown from messageArrived: {0} +726=726=Ignoring Exception thrown from deliveryComplete {0} +721=Non-Critical MQTT error thrown, passing back to application={0} +722=Server initiated disconnect, connection closed. Disconnect={0} +723=Creating MqttPubComp due to manual ACK: {0} +724=Ignoring Exception thrown from mqttErrorOccurred: {0} +725=Ignoring Exception thrown from messageArrived: {0} +726=Ignoring Exception thrown from deliveryComplete {0} +727=Ignoring Exception thrown from authPacketArrived {0} +800=stopping sender +801=stopped +802=network send key={0} msg={1} +803=get message returned null, stopping} +804=exception +805=< +850=stopping +851=stopped +852=network read message +853=Stopping due to IOException +854=< +855=starting +856=Stopping, MQttException +857=Unknown PubAck, PubComp or PubRec received. Ignoring. diff --git a/src/main/resources/org/eclipse/paho/mqttv5/common/nls/logcat.properties b/src/main/resources/org/eclipse/paho/mqttv5/common/nls/logcat.properties new file mode 100644 index 0000000..63f0a9f --- /dev/null +++ b/src/main/resources/org/eclipse/paho/mqttv5/common/nls/logcat.properties @@ -0,0 +1 @@ +0=MQTT Catalog \ No newline at end of file diff --git a/src/main/resources/org/eclipse/paho/mqttv5/common/nls/messages.properties b/src/main/resources/org/eclipse/paho/mqttv5/common/nls/messages.properties new file mode 100644 index 0000000..5507838 --- /dev/null +++ b/src/main/resources/org/eclipse/paho/mqttv5/common/nls/messages.properties @@ -0,0 +1,88 @@ +#/* +# * Copyright (c) 2017 IBM Corp. +# * +# * All rights reserved. This program and the accompanying materials +# * are made available under the terms of the Eclipse Public License v1.0 +# * which accompanies this distribution, and is available at +# * http://www.eclipse.org/legal/epl-v10.html +# * +# * Contributors: +# * James Sutton - Initial MQTTv5 implementation +# */ +# NLS_MESSAGEFORMAT_VAR +# NLS_ENCODING=UNICODE +1=Invalid protocol version +2=Invalid client ID +3=Broker unavailable +4=Bad user name or password +5=Not authorized to connect +6=Unexpected error +32000=Timed out waiting for a response from the server +32001=Internal error, caused by no new message IDs being available +32002=Timed out while waiting to write messages to the server +32100=Client is connected +32101=Client is disconnected +32102=Client is currently disconnecting +32103=Unable to connect to server +32104=Client is not connected +32105=The specified SocketFactory type does not match the broker URI +32106=SSL configuration error +32107=Disconnecting is not allowed from a callback method +32108=Unrecognized packet +32109=Connection lost +32110=Connect already in progress +32111=Client is closed +32200=Persistence already in use +32201=Token already in use +32202=Too many publishes in progress +32204=The Server Disconnected the client. +32301=An invalid topic alias combination was received. +50000=Invalid Message Property Identifier +50001=Invalid Return code +50002=Malformed Packet +50003=Unsupported Protocol +50004=An invalid topic alias combination was received in an incoming message. +50005=Duplicate property in Packet +51001=Incoming packet too large. +51002=Outgoing packet too large. + +### - MQTTv5 Return Codes +16=No matching subscribers. +17=No subscription existed. +24=Continue authentication. +25=Re-authenticate. +128=Unspecified error. +129=Malformed packet. +130=Protocol error. +131=Implementation specific error. +132=Unsupported protocol version. +133=Client identifier not valid. +134=Bad User Name or Password. +135=Not authorized. +136=Server unavailable. +137=Server busy. +138=Banned. +139=Server shutting down. +140=Bad authentication method. +141=Keep Alive timeout. +142=Session taken over. +143=Topic Filter invalid. +144=Topic Name invalid. +145=Packet identifier in use. +146=Packet identifier not found. +147=Receive Maximum exceeded. +148=Topic Alias invalid. +149=Packet too large. +150=Message rate too high. +151=Quota exceeded. +152=Administrative action. +153=Payload format invalid. +154=Retain not supported. +155=QoS not supported +156=Use another server. +157=Server moved. +158=Shared Subscriptions not supported. +159=Connection rate exceeded. +160=Maximum connect time. +161=Subscription Identifiers not supported. +162-Wildcard Subscriptions no supported. \ No newline at end of file diff --git a/src/main/resources/org/eclipse/paho/mqttv5/common/nls/messages_cs.properties b/src/main/resources/org/eclipse/paho/mqttv5/common/nls/messages_cs.properties new file mode 100644 index 0000000..c746dd1 --- /dev/null +++ b/src/main/resources/org/eclipse/paho/mqttv5/common/nls/messages_cs.properties @@ -0,0 +1,35 @@ +#/* +# * Copyright (c) 2009, 2012 IBM Corp. +# * +# * All rights reserved. This program and the accompanying materials +# * are made available under the terms of the Eclipse Public License v1.0 +# * which accompanies this distribution, and is available at +# * http://www.eclipse.org/legal/epl-v10.html +# * +# * Contributors: +# * Dave Locke - initial API and implementation and/or initial documentation +# */ +# NLS_MESSAGEFORMAT_VAR +# NLS_ENCODING=UNICODE +1=Neplatn\u00e1 verze protokolu +2=Neplatn\u00e9 ID klienta +3=Nedostupn\u00fd zprost\u0159edkovatel +4=Chybn\u00e9 jm\u00e9no u\u017eivatele nebo heslo +5=Chyb\u00ed autorizace pro p\u0159ipojen\u00ed +6=Neo\u010dek\u00e1van\u00e1 chyba +32000=Vypr\u0161en\u00ed \u010dasov\u00e9ho limitu pro odpov\u011b\u010f ze serveru +32100=Klient je p\u0159ipojen +32101=Klient je odpojen +32102=Klient se aktu\u00e1ln\u011b odpojuje +32103=Nelze se p\u0159ipojit k serveru +32104=Klient nen\u00ed p\u0159ipojen +32105=Ur\u010den\u00fd typ polo\u017eky SocketFactory neodpov\u00edd\u00e1 identifik\u00e1toru URI zprost\u0159edkovatele. +32106=Chyba konfigurace zabezpe\u010den\u00ed SSL +32107=Z metody zp\u011btn\u00e9ho vol\u00e1n\u00ed nen\u00ed povoleno odpojen\u00ed +32108=Nerozpoznan\u00fd paket +32109=P\u0159ipojen\u00ed bylo ztraceno. +32110=P\u0159ipojen\u00ed ji\u017e prob\u00edh\u00e1 +32111=Klient je zav\u0159en +32200=Perzistence je ji\u017e pou\u017e\u00edv\u00e1na. +32201=Token se ji\u017e pou\u017e\u00edv\u00e1 +32202=Prob\u00edh\u00e1 p\u0159\u00edli\u0161 mnoho publikov\u00e1n\u00ed diff --git a/src/main/resources/org/eclipse/paho/mqttv5/common/nls/messages_de.properties b/src/main/resources/org/eclipse/paho/mqttv5/common/nls/messages_de.properties new file mode 100644 index 0000000..bdd4d48 --- /dev/null +++ b/src/main/resources/org/eclipse/paho/mqttv5/common/nls/messages_de.properties @@ -0,0 +1,35 @@ +#/* +# * Copyright (c) 2009, 2012 IBM Corp. +# * +# * All rights reserved. This program and the accompanying materials +# * are made available under the terms of the Eclipse Public License v1.0 +# * which accompanies this distribution, and is available at +# * http://www.eclipse.org/legal/epl-v10.html +# * +# * Contributors: +# * Dave Locke - initial API and implementation and/or initial documentation +# */ +# NLS_MESSAGEFORMAT_VAR +# NLS_ENCODING=UNICODE +1=Protokollversion ung\u00fcltig +2=Client-ID ung\u00fcltig +3=Broker nicht verf\u00fcgbar +4=Benutzername oder Kennwort falsch +5=Keine Berechtigung f\u00fcr Verbindung +6=Unerwarteter Fehler +32000=Zeitlimit\u00fcberschreitung beim Warten auf eine Antwort vom Server +32100=Verbindung zu Client ist hergestellt +32101=Verbindung zu Client ist getrennt +32102=Verbindung zu Client wird derzeit getrennt +32103=Verbindung zu Server kann nicht hergestellt werden +32104=Keine Verbindung zu Client +32105=Der angegebene Socket-Factorytyp entspricht nicht der Broker-URI +32106=SSL-Konfigurationsfehler +32107=Trennung einer Verbindung \u00fcber eine Callback-Methode ist nicht zul\u00e4ssig +32108=Paket nicht erkannt +32109=Verbindung wurde getrennt +32110=Verbindungsherstellung wird ausgef\u00fchrt +32111=Client ist geschlossen +32200=Persistenz wird bereits verwendet +32201=Token wird bereits verwendet +32202=Zu viele Ver\u00f6ffentlichungen werden ausgef\u00fchrt diff --git a/src/main/resources/org/eclipse/paho/mqttv5/common/nls/messages_es.properties b/src/main/resources/org/eclipse/paho/mqttv5/common/nls/messages_es.properties new file mode 100644 index 0000000..7517e09 --- /dev/null +++ b/src/main/resources/org/eclipse/paho/mqttv5/common/nls/messages_es.properties @@ -0,0 +1,35 @@ +#/* +# * Copyright (c) 2009, 2012 IBM Corp. +# * +# * All rights reserved. This program and the accompanying materials +# * are made available under the terms of the Eclipse Public License v1.0 +# * which accompanies this distribution, and is available at +# * http://www.eclipse.org/legal/epl-v10.html +# * +# * Contributors: +# * Dave Locke - initial API and implementation and/or initial documentation +# */ +# NLS_MESSAGEFORMAT_VAR +# NLS_ENCODING=UNICODE +1=Versi\u00f3n de protocolo incorrecta +2=Identificador de cliente incorrecto +3=Intermediario no disponible +4=Nombre de usuario o contrase\u00f1a incorrecto +5=No autorizado a conectarse +6=Error inesperado +32000=Tiempo de espera excedido al esperar una respuesta del servidor +32100=El cliente est\u00e1 conectado +32101=El cliente est\u00e1 desconectado +32102=El cliente se est\u00e1 desconectando +32103=No es posible conectarse al servidor +32104=El cliente no est\u00e1 conectado +32105=El tipo SocketFactory especificado no coincide con el URI del intermediario +32106=Error de configuraci\u00f3n SSL +32107=No se permite la desconexi\u00f3n desde un m\u00e9todo de devoluci\u00f3n de llamada +32108=Paquete no reconocido +32109=Se ha perdido la conexi\u00f3n +32110=Conexi\u00f3n ya en curso +32111=El cliente est\u00e1 cerrado +32200=La persistencia ya se est\u00e1 utilizando +32201=La se\u00f1al ya se est\u00e1 utilizando +32202=Demasiadas publicaciones en curso diff --git a/src/main/resources/org/eclipse/paho/mqttv5/common/nls/messages_fr.properties b/src/main/resources/org/eclipse/paho/mqttv5/common/nls/messages_fr.properties new file mode 100644 index 0000000..d7b42e4 --- /dev/null +++ b/src/main/resources/org/eclipse/paho/mqttv5/common/nls/messages_fr.properties @@ -0,0 +1,35 @@ +#/* +# * Copyright (c) 2009, 2012 IBM Corp. +# * +# * All rights reserved. This program and the accompanying materials +# * are made available under the terms of the Eclipse Public License v1.0 +# * which accompanies this distribution, and is available at +# * http://www.eclipse.org/legal/epl-v10.html +# * +# * Contributors: +# * Dave Locke - initial API and implementation and/or initial documentation +# */ +# NLS_MESSAGEFORMAT_VAR +# NLS_ENCODING=UNICODE +1=Version de protocole incorrecte +2=ID client incorrect +3=Courtier indisponible +4=Nom d'utilisateur ou mot de passe incorrect +5=L'utilisateur n'est pas autoris\u00e9 \u00e0 se connecter +6=Erreur inattendue. +32000=Expiration du d\u00e9lai d'attente d'une r\u00e9ponse du serveur +32100=Client connect\u00e9 +32101=Client d\u00e9connect\u00e9 +32102=Client en cours de d\u00e9connexion +32103=Impossible de se connecter au serveur +32104=Client non connect\u00e9 +32105=Le type SocketFactory sp\u00e9cifi\u00e9 ne correspond pas \u00e0 l'URI de courtier +32106=Erreur de configuration SSL +32107=D\u00e9connexion non autoris\u00e9e pour une m\u00e9thode de rappel +32108=Paquet non reconnu +32109=Connexion perdue +32110=Connexion d\u00e9j\u00e0 en cours +32111=Client ferm\u00e9 +32200=La persistance est d\u00e9j\u00e0 en cours d'utilisation +32201=Jeton d\u00e9j\u00e0 en cours d'utilisation +32202=Trop de publications en cours diff --git a/src/main/resources/org/eclipse/paho/mqttv5/common/nls/messages_hu.properties b/src/main/resources/org/eclipse/paho/mqttv5/common/nls/messages_hu.properties new file mode 100644 index 0000000..dc4cfe6 --- /dev/null +++ b/src/main/resources/org/eclipse/paho/mqttv5/common/nls/messages_hu.properties @@ -0,0 +1,35 @@ +#/* +# * Copyright (c) 2009, 2012 IBM Corp. +# * +# * All rights reserved. This program and the accompanying materials +# * are made available under the terms of the Eclipse Public License v1.0 +# * which accompanies this distribution, and is available at +# * http://www.eclipse.org/legal/epl-v10.html +# * +# * Contributors: +# * Dave Locke - initial API and implementation and/or initial documentation +# */ +# NLS_MESSAGEFORMAT_VAR +# NLS_ENCODING=UNICODE +1=\u00c9rv\u00e9nytelen protokoll v\u00e1ltozat +2=\u00c9rv\u00e9nytelen \u00fcgyf\u00e9lazonos\u00edt\u00f3 +3=K\u00f6zvet\u00edt\u0151 nem el\u00e9rhet\u0151 +4=Rossz felhaszn\u00e1l\u00f3i n\u00e9v vagy jelsz\u00f3 +5=Nem jogosult csatlakozni +6=V\u00e1ratlan hiba +32000=T\u00fall\u00e9pte a megengedett id\u0151t a kiszolg\u00e1l\u00f3 v\u00e1lasz\u00e1ra v\u00e1rva +32100=Az \u00fcgyf\u00e9l csatlakoztatva van +32101=Az \u00fcgyf\u00e9l sz\u00e9tkapcsolt +32102=Az \u00fcgyf\u00e9l \u00e9pp megszak\u00edtja a kapcsolatot +32103=Nem lehet kapcsol\u00f3dni a kiszolg\u00e1l\u00f3hoz +32104=Az \u00fcgyf\u00e9l nincs csatlakoztatva +32105=A megadott SocketFactory t\u00edpus nem illeszkedik a k\u00f6zvet\u00edt\u0151 URI azonos\u00edt\u00f3hoz +32106=SSL konfigur\u00e1ci\u00f3s hiba +32107=A megszak\u00edt\u00e1s visszah\u00edv\u00e1s met\u00f3dusb\u00f3l nem enged\u00e9lyezett +32108=Ismeretlen csomag +32109=Kapcsolat elveszett +32110=A csatlakoz\u00e1s m\u00e1r folyamatban van +32111=Az \u00fcgyf\u00e9l bez\u00e1r\u00e1sra ker\u00fclt +32200=A megmarad\u00f3 \u00e1llapot m\u00e1r haszn\u00e1latban van +32201=A token m\u00e1r haszn\u00e1latban van. +32202=T\u00fal sok k\u00f6zz\u00e9t\u00e9tel van folyamatban diff --git a/src/main/resources/org/eclipse/paho/mqttv5/common/nls/messages_it.properties b/src/main/resources/org/eclipse/paho/mqttv5/common/nls/messages_it.properties new file mode 100644 index 0000000..e0dcfcc --- /dev/null +++ b/src/main/resources/org/eclipse/paho/mqttv5/common/nls/messages_it.properties @@ -0,0 +1,35 @@ +#/* +# * Copyright (c) 2009, 2012 IBM Corp. +# * +# * All rights reserved. This program and the accompanying materials +# * are made available under the terms of the Eclipse Public License v1.0 +# * which accompanies this distribution, and is available at +# * http://www.eclipse.org/legal/epl-v10.html +# * +# * Contributors: +# * Dave Locke - initial API and implementation and/or initial documentation +# */ +# NLS_MESSAGEFORMAT_VAR +# NLS_ENCODING=UNICODE +1=Versione di protocollo non valida +2=ID client non valido +3=Broker non disponibile +4=Nome utente o password non validi +5=Non autorizzato per la connessione +6=Errore imprevisto +32000=Scaduto in attesa di una risposta dal server +32100=Client connesso +32101=Client disconnesso +32102=Client in fase di disconnessione +32103=Impossibile effettuare la connessione al server +32104=Client non connesso +32105=Il tipo SocketFactory specificato non corrisponde all'URI del broker +32106=Errore di configurazione SSL +32107=Disconnessione non consentita da un metodo callback +32108=Pacchetto non riconosciuto +32109=Connessione persa +32110=Connessione gi\u00e0 in corso +32111=Client chiuso +32200=Persistenza gi\u00e0 in uso +32201=Token gi\u00e0 in uso +32202=Numero eccessivo di pubblicazioni in corso diff --git a/src/main/resources/org/eclipse/paho/mqttv5/common/nls/messages_ja.properties b/src/main/resources/org/eclipse/paho/mqttv5/common/nls/messages_ja.properties new file mode 100644 index 0000000..9f78d2a --- /dev/null +++ b/src/main/resources/org/eclipse/paho/mqttv5/common/nls/messages_ja.properties @@ -0,0 +1,35 @@ +#/* +# * Copyright (c) 2009, 2012 IBM Corp. +# * +# * All rights reserved. This program and the accompanying materials +# * are made available under the terms of the Eclipse Public License v1.0 +# * which accompanies this distribution, and is available at +# * http://www.eclipse.org/legal/epl-v10.html +# * +# * Contributors: +# * Dave Locke - initial API and implementation and/or initial documentation +# */ +# NLS_MESSAGEFORMAT_VAR +# NLS_ENCODING=UNICODE +1=\u7121\u52b9\u306a\u30d7\u30ed\u30c8\u30b3\u30eb\u30fb\u30d0\u30fc\u30b8\u30e7\u30f3\u3067\u3059 +2=\u7121\u52b9\u306a\u30af\u30e9\u30a4\u30a2\u30f3\u30c8 ID \u3067\u3059 +3=\u30d6\u30ed\u30fc\u30ab\u30fc\u304c\u4f7f\u7528\u4e0d\u53ef\u3067\u3059 +4=\u9593\u9055\u3063\u305f\u30e6\u30fc\u30b6\u30fc\u540d\u307e\u305f\u306f\u30d1\u30b9\u30ef\u30fc\u30c9\u3067\u3059 +5=\u63a5\u7d9a\u3059\u308b\u6a29\u9650\u304c\u3042\u308a\u307e\u305b\u3093 +6=\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc +32000=\u30b5\u30fc\u30d0\u30fc\u304b\u3089\u306e\u5fdc\u7b54\u5f85\u6a5f\u304c\u30bf\u30a4\u30e0\u30a2\u30a6\u30c8\u306b\u306a\u308a\u307e\u3057\u305f +32100=\u30af\u30e9\u30a4\u30a2\u30f3\u30c8\u306b\u63a5\u7d9a\u3057\u307e\u3057\u305f +32101=\u30af\u30e9\u30a4\u30a2\u30f3\u30c8\u3092\u5207\u65ad\u3057\u307e\u3057\u305f +32102=\u30af\u30e9\u30a4\u30a2\u30f3\u30c8\u306f\u73fe\u5728\u5207\u65ad\u4e2d\u3067\u3059 +32103=\u30b5\u30fc\u30d0\u30fc\u306b\u63a5\u7d9a\u3067\u304d\u307e\u305b\u3093 +32104=\u30af\u30e9\u30a4\u30a2\u30f3\u30c8\u306f\u63a5\u7d9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093 +32105=\u6307\u5b9a\u3055\u308c\u305f SocketFactory \u30bf\u30a4\u30d7\u306f\u30d6\u30ed\u30fc\u30ab\u30fc URI \u3068\u4e00\u81f4\u3057\u307e\u305b\u3093 +32106=SSL \u69cb\u6210\u30a8\u30e9\u30fc\u3067\u3059 +32107=\u30b3\u30fc\u30eb\u30d0\u30c3\u30af\u30fb\u30e1\u30bd\u30c3\u30c9\u304b\u3089\u306e\u5207\u65ad\u306f\u8a31\u53ef\u3055\u308c\u307e\u305b\u3093 +32108=\u8b58\u5225\u3055\u308c\u3066\u3044\u306a\u3044\u30d1\u30b1\u30c3\u30c8\u3067\u3059 +32109=\u63a5\u7d9a\u55aa\u5931 +32110=\u63a5\u7d9a\u51e6\u7406\u306f\u65e2\u306b\u9032\u884c\u4e2d\u3067\u3059 +32111=\u30af\u30e9\u30a4\u30a2\u30f3\u30c8\u304c\u30af\u30ed\u30fc\u30ba\u3055\u308c\u307e\u3057\u305f +32200=\u30d1\u30fc\u30b7\u30b9\u30bf\u30f3\u30b9\u306f\u3059\u3067\u306b\u4f7f\u7528\u4e2d\u3067\u3059\u3002 +32201=\u30c8\u30fc\u30af\u30f3\u306f\u65e2\u306b\u4f7f\u7528\u4e2d\u3067\u3059 +32202=\u51e6\u7406\u4e2d\u306e\u30d1\u30d6\u30ea\u30c3\u30b7\u30e5\u304c\u591a\u3059\u304e\u307e\u3059 diff --git a/src/main/resources/org/eclipse/paho/mqttv5/common/nls/messages_ko.properties b/src/main/resources/org/eclipse/paho/mqttv5/common/nls/messages_ko.properties new file mode 100644 index 0000000..2209928 --- /dev/null +++ b/src/main/resources/org/eclipse/paho/mqttv5/common/nls/messages_ko.properties @@ -0,0 +1,35 @@ +#/* +# * Copyright (c) 2009, 2012 IBM Corp. +# * +# * All rights reserved. This program and the accompanying materials +# * are made available under the terms of the Eclipse Public License v1.0 +# * which accompanies this distribution, and is available at +# * http://www.eclipse.org/legal/epl-v10.html +# * +# * Contributors: +# * Dave Locke - initial API and implementation and/or initial documentation +# */ +# NLS_MESSAGEFORMAT_VAR +# NLS_ENCODING=UNICODE +1=\uc62c\ubc14\ub974\uc9c0 \uc54a\uc740 \ud504\ub85c\ud1a0\ucf5c \ubc84\uc804 +2=\uc62c\ubc14\ub974\uc9c0 \uc54a\uc740 \ud074\ub77c\uc774\uc5b8\ud2b8 ID +3=\ube0c\ub85c\ucee4 \uc0ac\uc6a9 \ubd88\uac00\ub2a5 +4=\uc798\ubabb\ub41c \uc0ac\uc6a9\uc790 \uc774\ub984 \ub610\ub294 \ube44\ubc00\ubc88\ud638 +5=\uc5f0\uacb0\ud560 \uc218 \uc788\ub294 \uad8c\ud55c\uc774 \ubd80\uc5ec\ub418\uc9c0 \uc54a\uc74c +6=\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958 +32000=\uc11c\ubc84\uc5d0\uc11c \uc751\ub2f5\uc744 \uae30\ub2e4\ub9ac\ub294 \uc911 \uc81c\ud55c\uc2dc\uac04 \ucd08\uacfc +32100=\ud074\ub77c\uc774\uc5b8\ud2b8\uac00 \uc5f0\uacb0\ub428 +32101=\ud074\ub77c\uc774\uc5b8\ud2b8\uac00 \uc5f0\uacb0\uc774 \ub04a\uae40 +32102=\ud604\uc7ac \ud074\ub77c\uc774\uc5b8\ud2b8\uac00 \uc5f0\uacb0\uc744 \ub04a\ub294 \uc911 +32103=\uc11c\ubc84\uc5d0 \uc5f0\uacb0\ud560 \uc218 \uc5c6\uc74c +32104=\ud074\ub77c\uc774\uc5b8\ud2b8\uac00 \uc5f0\uacb0\ub418\uc9c0 \uc54a\uc74c +32105=\uc9c0\uc815\ub41c SocketFactory \uc720\ud615\uc774 \ube0c\ub85c\ucee4 URI\uc640 \uc77c\uce58\ud558\uc9c0 \uc54a\uc74c +32106=SSL \uad6c\uc131 \uc624\ub958 +32107=\ucf5c\ubc31 \uba54\uc18c\ub4dc\ub85c\ubd80\ud130 \uc5f0\uacb0\uc744 \ub04a\ub294 \uac83\uc774 \ud5c8\uc6a9\ub418\uc9c0 \uc54a\uc74c +32108=\uc778\uc2dd\ub418\uc9c0 \uc54a\uc740 \ud328\ud0b7 +32109=\uc5f0\uacb0 \uc720\uc2e4 +32110=\uc5f0\uacb0\uc774 \uc774\ubbf8 \uc9c4\ud589 \uc911\uc784 +32111=\ud074\ub77c\uc774\uc5b8\ud2b8\uac00 \ub2eb\ud798 +32200=\uc9c0\uc18d \ud30c\uc77c\uc744 \uc774\ubbf8 \uc0ac\uc6a9 \uc911 +32201=\ud1a0\ud070\uc774 \uc774\ubbf8 \uc0ac\uc6a9 \uc911\uc784 +32202=\ub108\ubb34 \ub9ce\uc740 \ubc1c\ud589\uc774 \uc9c4\ud589 \uc911\uc784 diff --git a/src/main/resources/org/eclipse/paho/mqttv5/common/nls/messages_pl.properties b/src/main/resources/org/eclipse/paho/mqttv5/common/nls/messages_pl.properties new file mode 100644 index 0000000..255175c --- /dev/null +++ b/src/main/resources/org/eclipse/paho/mqttv5/common/nls/messages_pl.properties @@ -0,0 +1,35 @@ +#/* +# * Copyright (c) 2009, 2012 IBM Corp. +# * +# * All rights reserved. This program and the accompanying materials +# * are made available under the terms of the Eclipse Public License v1.0 +# * which accompanies this distribution, and is available at +# * http://www.eclipse.org/legal/epl-v10.html +# * +# * Contributors: +# * Dave Locke - initial API and implementation and/or initial documentation +# */ +# NLS_MESSAGEFORMAT_VAR +# NLS_ENCODING=UNICODE +1=Niepoprawna wersja protoko\u0142u +2=Niepoprawny identyfikator klienta +3=Broker niedost\u0119pny +4=Niepoprawna nazwa u\u017cytkownika lub has\u0142o +5=Brak autoryzacji do nawi\u0105zania po\u0142\u0105czenia +6=Nieoczekiwany b\u0142\u0105d +32000=Przekroczono limit czasu oczekiwania na odpowied\u017a z serwera +32100=Po\u0142\u0105czenie z klientem zosta\u0142o nawi\u0105zane +32101=Po\u0142\u0105czenie z klientem zosta\u0142o roz\u0142\u0105czone +32102=Klient roz\u0142\u0105cza si\u0119 +32103=Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia z serwerem +32104=Po\u0142\u0105czenie z klientem nie jest nawi\u0105zane +32105=Podany typ fabryki SocketFactory nie jest zgodny z identyfikatorem URI brokera +32106=B\u0142\u0105d konfiguracji protoko\u0142u SSL +32107=Roz\u0142\u0105czenie nie jest dozwolone w metodzie procedury zwrotnej +32108=Nierozpoznany pakiet +32109=Utracono po\u0142\u0105czenie +32110=Operacja nawi\u0105zywania po\u0142\u0105czenia jest ju\u017c w toku +32111=Klient zosta\u0142 zamkni\u0119ty +32200=Trwa\u0142o\u015b\u0107 jest ju\u017c w u\u017cyciu +32201=Znacznik jest ju\u017c w u\u017cyciu +32202=Zbyt wiele operacji publikowania jest w toku diff --git a/src/main/resources/org/eclipse/paho/mqttv5/common/nls/messages_pt_BR.properties b/src/main/resources/org/eclipse/paho/mqttv5/common/nls/messages_pt_BR.properties new file mode 100644 index 0000000..f9345ad --- /dev/null +++ b/src/main/resources/org/eclipse/paho/mqttv5/common/nls/messages_pt_BR.properties @@ -0,0 +1,35 @@ +#/* +# * Copyright (c) 2009, 2012 IBM Corp. +# * +# * All rights reserved. This program and the accompanying materials +# * are made available under the terms of the Eclipse Public License v1.0 +# * which accompanies this distribution, and is available at +# * http://www.eclipse.org/legal/epl-v10.html +# * +# * Contributors: +# * Dave Locke - initial API and implementation and/or initial documentation +# */ +# NLS_MESSAGEFORMAT_VAR +# NLS_ENCODING=UNICODE +1=Vers\u00e3o de protocolo inv\u00e1lida +2=ID de cliente inv\u00e1lido +3=Broker indispon\u00edvel +4=Nome de usu\u00e1rio ou senha inv\u00e1lidos +5=N\u00e3o autorizado a conectar +6=Erro inesperado +32000=Tempo limite atingido ao aguardar por uma resposta do servidor +32100=O cliente est\u00e1 conectado +32101=O cliente est\u00e1 desconectado +32102=Cliente desconectando atualmente +32103=N\u00e3o \u00e9 poss\u00edvel se conectar ao servidor +32104=O cliente n\u00e3o est\u00e1 conectado +32105=O tipo SocketFactory especificado n\u00e3o corresponde ao URI do broker +32106=Erro de configura\u00e7\u00e3o de SSL +32107=A desconex\u00e3o n\u00e3o \u00e9 permitida a partir de um m\u00e9todo de retorno de chamada +32108=Pacote n\u00e3o reconhecido +32109=Conex\u00e3o perdida +32110=A conex\u00e3o j\u00e1 est\u00e1 em andamento +32111=O cliente foi encerrado +32200=Persist\u00eancia j\u00e1 em uso +32201=O token j\u00e1 est\u00e1 em uso +32202=Muitas publica\u00e7\u00f5es em andamento diff --git a/src/main/resources/org/eclipse/paho/mqttv5/common/nls/messages_ru.properties b/src/main/resources/org/eclipse/paho/mqttv5/common/nls/messages_ru.properties new file mode 100644 index 0000000..39fe6b6 --- /dev/null +++ b/src/main/resources/org/eclipse/paho/mqttv5/common/nls/messages_ru.properties @@ -0,0 +1,35 @@ +#/* +# * Copyright (c) 2009, 2012 IBM Corp. +# * +# * All rights reserved. This program and the accompanying materials +# * are made available under the terms of the Eclipse Public License v1.0 +# * which accompanies this distribution, and is available at +# * http://www.eclipse.org/legal/epl-v10.html +# * +# * Contributors: +# * Dave Locke - initial API and implementation and/or initial documentation +# */ +# NLS_MESSAGEFORMAT_VAR +# NLS_ENCODING=UNICODE +1=\u041d\u0435\u0434\u043e\u043f\u0443\u0441\u0442\u0438\u043c\u0430\u044f \u0432\u0435\u0440\u0441\u0438\u044f \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b\u0430 +2=\u041d\u0435\u0434\u043e\u043f\u0443\u0441\u0442\u0438\u043c\u044b\u0439 \u0418\u0414 \u043a\u043b\u0438\u0435\u043d\u0442\u0430 +3=\u041f\u043e\u0441\u0440\u0435\u0434\u043d\u0438\u043a \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u0435\u043d +4=\u041e\u0448\u0438\u0431\u043e\u0447\u043d\u043e\u0435 \u0438\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f \u0438\u043b\u0438 \u043f\u0430\u0440\u043e\u043b\u044c +5=\u041d\u0435\u0442 \u043f\u0440\u0430\u0432 \u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u043d\u0430 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 +6=\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430 +32000=\u0422\u0430\u043c-\u0430\u0443\u0442 \u043e\u0436\u0438\u0434\u0430\u043d\u0438\u044f \u043e\u0442\u0432\u0435\u0442\u0430 \u0441\u0435\u0440\u0432\u0435\u0440\u0430 +32100=\u041a\u043b\u0438\u0435\u043d\u0442 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d +32101=\u041a\u043b\u0438\u0435\u043d\u0442 \u043e\u0442\u043a\u043b\u044e\u0447\u0435\u043d +32102=\u041a\u043b\u0438\u0435\u043d\u0442 \u043e\u0442\u043a\u043b\u044e\u0447\u0430\u0435\u0442\u0441\u044f +32103=\u041d\u0435 \u0443\u0434\u0430\u0435\u0442\u0441\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0441\u0435\u0440\u0432\u0435\u0440\u0443 +32104=\u041a\u043b\u0438\u0435\u043d\u0442 \u043d\u0435 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d +32105=\u0422\u0438\u043f SocketFactory \u043d\u0435 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u0435\u0442 URI \u043f\u043e\u0441\u0440\u0435\u0434\u043d\u0438\u043a\u0430 +32106=\u041e\u0448\u0438\u0431\u043a\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 SSL +32107=\u041e\u0442\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043d\u0435 \u0440\u0430\u0437\u0440\u0435\u0448\u0435\u043d\u043e \u0432 \u043c\u0435\u0442\u043e\u0434\u0435 \u043e\u0431\u0440\u0430\u0442\u043d\u043e\u0433\u043e \u0432\u044b\u0437\u043e\u0432\u0430 +32108=\u041d\u0435\u0440\u0430\u0441\u043f\u043e\u0437\u043d\u0430\u043d\u043d\u044b\u0439 \u043f\u0430\u043a\u0435\u0442 +32109=\u0421\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u0435 \u043f\u043e\u0442\u0435\u0440\u044f\u043d\u043e +32110=\u0412\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442\u0441\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 +32111=\u041a\u043b\u0438\u0435\u043d\u0442 \u0437\u0430\u043a\u0440\u044b\u0442 +32200=\u0425\u0440\u0430\u043d\u0438\u043b\u0438\u0449\u0435 \u0443\u0436\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f +32201=\u041c\u0430\u0440\u043a\u0435\u0440 \u0443\u0436\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f +32202=\u0412\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442\u0441\u044f \u0441\u043b\u0438\u0448\u043a\u043e\u043c \u043c\u043d\u043e\u0433\u043e \u043f\u0443\u0431\u043b\u0438\u043a\u0430\u0446\u0438\u0439 diff --git a/src/main/resources/org/eclipse/paho/mqttv5/common/nls/messages_zh_CN.properties b/src/main/resources/org/eclipse/paho/mqttv5/common/nls/messages_zh_CN.properties new file mode 100644 index 0000000..8483ef8 --- /dev/null +++ b/src/main/resources/org/eclipse/paho/mqttv5/common/nls/messages_zh_CN.properties @@ -0,0 +1,35 @@ +#/* +# * Copyright (c) 2009, 2012 IBM Corp. +# * +# * All rights reserved. This program and the accompanying materials +# * are made available under the terms of the Eclipse Public License v1.0 +# * which accompanies this distribution, and is available at +# * http://www.eclipse.org/legal/epl-v10.html +# * +# * Contributors: +# * Dave Locke - initial API and implementation and/or initial documentation +# */ +# NLS_MESSAGEFORMAT_VAR +# NLS_ENCODING=UNICODE +1=\u65e0\u6548\u534f\u8bae\u7248\u672c +2=\u65e0\u6548\u5ba2\u6237\u673a\u6807\u8bc6 +3=\u4ee3\u7406\u7a0b\u5e8f\u4e0d\u53ef\u7528 +4=\u9519\u8bef\u7684\u7528\u6237\u540d\u6216\u5bc6\u7801 +5=\u65e0\u6743\u8fde\u63a5 +6=\u610f\u5916\u9519\u8bef +32000=\u7b49\u5f85\u6765\u81ea\u670d\u52a1\u5668\u7684\u54cd\u5e94\u65f6\u8d85\u65f6 +32100=\u5df2\u8fde\u63a5\u5ba2\u6237\u673a +32101=\u5df2\u65ad\u5f00\u5ba2\u6237\u673a\u8fde\u63a5 +32102=\u5ba2\u6237\u673a\u6b63\u5728\u65ad\u5f00\u8fde\u63a5 +32103=\u65e0\u6cd5\u8fde\u63a5\u81f3\u670d\u52a1\u5668 +32104=\u5ba2\u6237\u673a\u672a\u8fde\u63a5 +32105=\u6307\u5b9a\u7684 SocketFactory \u7c7b\u578b\u4e0e\u4ee3\u7406\u7a0b\u5e8f URI \u4e0d\u5339\u914d +32106=SSL \u914d\u7f6e\u9519\u8bef +32107=\u4e0d\u5141\u8bb8\u901a\u8fc7\u56de\u8c03\u65b9\u6cd5\u65ad\u5f00\u8fde\u63a5 +32108=\u4e0d\u53ef\u8bc6\u522b\u7684\u5305 +32109=\u5df2\u65ad\u5f00\u8fde\u63a5 +32110=\u5df2\u5728\u8fdb\u884c\u8fde\u63a5 +32111=\u5ba2\u6237\u673a\u5df2\u5173\u95ed +32200=\u6301\u4e45\u6027\u5df2\u5728\u4f7f\u7528\u4e2d +32201=\u4ee4\u724c\u5df2\u5728\u4f7f\u7528\u4e2d +32202=\u6b63\u5728\u8fdb\u884c\u8fc7\u591a\u7684\u53d1\u5e03 diff --git a/src/main/resources/org/eclipse/paho/mqttv5/common/nls/messages_zh_TW.properties b/src/main/resources/org/eclipse/paho/mqttv5/common/nls/messages_zh_TW.properties new file mode 100644 index 0000000..846f1b6 --- /dev/null +++ b/src/main/resources/org/eclipse/paho/mqttv5/common/nls/messages_zh_TW.properties @@ -0,0 +1,35 @@ +#/* +# * Copyright (c) 2009, 2012 IBM Corp. +# * +# * All rights reserved. This program and the accompanying materials +# * are made available under the terms of the Eclipse Public License v1.0 +# * which accompanies this distribution, and is available at +# * http://www.eclipse.org/legal/epl-v10.html +# * +# * Contributors: +# * Dave Locke - initial API and implementation and/or initial documentation +# */ +# NLS_MESSAGEFORMAT_VAR +# NLS_ENCODING=UNICODE +1=\u901a\u8a0a\u5354\u5b9a\u7248\u672c\u7121\u6548 +2=\u7528\u6236\u7aef ID \u7121\u6548 +3=\u5206\u914d\u7ba1\u7406\u7cfb\u7d71\u7121\u6cd5\u4f7f\u7528 +4=\u4f7f\u7528\u8005\u540d\u7a31\u6216\u5bc6\u78bc\u4e0d\u7576 +5=\u672a\u7372\u6388\u6b0a\u9023\u63a5 +6=\u975e\u9810\u671f\u7684\u932f\u8aa4 +32000=\u7b49\u5f85\u4f3a\u670d\u5668\u7684\u56de\u61c9\u6642\u903e\u6642 +32100=\u5df2\u9023\u63a5\u7528\u6236\u7aef +32101=\u5df2\u4e2d\u65b7\u7528\u6236\u7aef\u7684\u9023\u63a5 +32102=\u7528\u6236\u7aef\u76ee\u524d\u6b63\u5728\u4e2d\u65b7\u9023\u7dda +32103=\u7121\u6cd5\u9023\u63a5\u5230\u4f3a\u670d\u5668 +32104=\u7528\u6236\u7aef\u672a\u9023\u63a5 +32105=\u6307\u5b9a\u7684 SocketFactory \u985e\u578b\u8207\u5206\u914d\u7ba1\u7406\u7cfb\u7d71 URI \u4e0d\u7b26 +32106=SSL \u914d\u7f6e\u932f\u8aa4 +32107=\u4e0d\u5bb9\u8a31\u8207\u56de\u547c\u65b9\u6cd5\u4e2d\u65b7\u9023\u7dda +32108=\u5c01\u5305\u7121\u6cd5\u8fa8\u8b58 +32109=\u9023\u7dda\u907a\u5931 +32110=\u9023\u63a5\u5df2\u5728\u9032\u884c\u4e2d +32111=\u5df2\u95dc\u9589\u7528\u6236\u7aef +32200=\u6301\u7e8c\u6027\u5df2\u5728\u4f7f\u7528\u4e2d +32201=\u8a18\u865f\u5df2\u5728\u4f7f\u7528\u4e2d +32202=\u592a\u591a\u767c\u4f48\u9032\u884c\u4e2d